diff --git a/.ci/Jenkinsfile_baseline_capture b/.ci/Jenkinsfile_baseline_capture index 33ecfcd84fd3e..791cacf7abb4c 100644 --- a/.ci/Jenkinsfile_baseline_capture +++ b/.ci/Jenkinsfile_baseline_capture @@ -12,12 +12,12 @@ kibanaPipeline(timeoutMinutes: 120) { ]) { parallel([ 'oss-baseline': { - workers.ci(name: 'oss-baseline', size: 's-highmem', ramDisk: true, runErrorReporter: false) { + workers.ci(name: 'oss-baseline', size: 'l', ramDisk: true, runErrorReporter: false) { kibanaPipeline.functionalTestProcess('oss-baseline', './test/scripts/jenkins_baseline.sh')() } }, 'xpack-baseline': { - workers.ci(name: 'xpack-baseline', size: 's-highmem', ramDisk: true, runErrorReporter: false) { + workers.ci(name: 'xpack-baseline', size: 'l', ramDisk: true, runErrorReporter: false) { kibanaPipeline.functionalTestProcess('xpack-baseline', './test/scripts/jenkins_xpack_baseline.sh')() } }, diff --git a/.eslintrc.js b/.eslintrc.js index 3161a25b70870..3778bd374da61 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -17,9 +17,6 @@ * under the License. */ -const { readdirSync } = require('fs'); -const { resolve } = require('path'); - const APACHE_2_0_LICENSE_HEADER = ` /* * Licensed to Elasticsearch B.V. under one or more contributor @@ -288,7 +285,7 @@ module.exports = { }, { target: [ - '(src|x-pack)/legacy/**/*', + 'src/legacy/**/*', '(src|x-pack)/plugins/**/(public|server)/**/*', 'examples/**/*', ], @@ -319,14 +316,11 @@ module.exports = { }, { target: [ - '(src|x-pack)/legacy/**/*', + 'src/legacy/**/*', '(src|x-pack)/plugins/**/(public|server)/**/*', 'examples/**/*', '!(src|x-pack)/**/*.test.*', '!(x-pack/)?test/**/*', - // next folder contains legacy browser tests which can't be migrated to jest - // which import np files - '!src/legacy/core_plugins/kibana/public/__tests__/**/*', ], from: [ '(src|x-pack)/plugins/**/(public|server)/**/*', @@ -341,14 +335,6 @@ module.exports = { '(src|x-pack)/plugins/**/*', '!(src|x-pack)/plugins/**/server/**/*', - 'src/legacy/core_plugins/**/*', - '!src/legacy/core_plugins/**/server/**/*', - '!src/legacy/core_plugins/**/index.{js,mjs,ts,tsx}', - - 'x-pack/legacy/plugins/**/*', - '!x-pack/legacy/plugins/**/server/**/*', - '!x-pack/legacy/plugins/**/index.{js,mjs,ts,tsx}', - 'examples/**/*', '!examples/**/server/**/*', ], @@ -370,12 +356,7 @@ module.exports = { }, { target: ['src/core/**/*'], - from: [ - 'plugins/**/*', - 'src/plugins/**/*', - 'src/legacy/core_plugins/**/*', - 'src/legacy/ui/**/*', - ], + from: ['plugins/**/*', 'src/plugins/**/*', 'src/legacy/ui/**/*'], errorMessage: 'The core cannot depend on any plugins.', }, { @@ -388,12 +369,6 @@ module.exports = { target: [ 'test/plugin_functional/plugins/**/public/np_ready/**/*', 'test/plugin_functional/plugins/**/server/np_ready/**/*', - 'src/legacy/core_plugins/**/public/np_ready/**/*', - 'src/legacy/core_plugins/vis_type_*/public/**/*', - '!src/legacy/core_plugins/vis_type_*/public/legacy*', - 'src/legacy/core_plugins/**/server/np_ready/**/*', - 'x-pack/legacy/plugins/**/public/np_ready/**/*', - 'x-pack/legacy/plugins/**/server/np_ready/**/*', ], allowSameFolder: true, errorMessage: @@ -443,22 +418,14 @@ module.exports = { settings: { // instructs import/no-extraneous-dependencies to treat certain modules // as core modules, even if they aren't listed in package.json - 'import/core-modules': ['plugins', 'legacy/ui'], + 'import/core-modules': ['plugins'], 'import/resolver': { '@kbn/eslint-import-resolver-kibana': { forceNode: false, rootPackageName: 'kibana', kibanaPath: '.', - pluginMap: readdirSync(resolve(__dirname, 'x-pack/legacy/plugins')).reduce( - (acc, name) => { - if (!name.startsWith('_')) { - acc[name] = `x-pack/legacy/plugins/${name}`; - } - return acc; - }, - {} - ), + pluginMap: {}, }, }, }, @@ -764,16 +731,6 @@ module.exports = { }, }, - /** - * GIS overrides - */ - { - files: ['x-pack/legacy/plugins/maps/**/*.js'], - rules: { - 'react/prefer-stateless-function': [0, { ignorePureComponents: false }], - }, - }, - /** * ML overrides */ @@ -812,7 +769,7 @@ module.exports = { }, { // typescript only for front and back end - files: ['x-pack/{,legacy/}plugins/security_solution/**/*.{ts,tsx}'], + files: ['x-pack/plugins/security_solution/**/*.{ts,tsx}'], rules: { // This will be turned on after bug fixes are complete // '@typescript-eslint/explicit-member-accessibility': 'warn', @@ -858,7 +815,7 @@ module.exports = { // }, { // typescript and javascript for front and back end - files: ['x-pack/{,legacy/}plugins/security_solution/**/*.{js,mjs,ts,tsx}'], + files: ['x-pack/plugins/security_solution/**/*.{js,mjs,ts,tsx}'], plugins: ['eslint-plugin-node', 'react'], env: { mocha: true, @@ -1089,7 +1046,7 @@ module.exports = { { // typescript only for front and back end files: [ - 'x-pack/{,legacy/}plugins/{alerts,alerting_builtins,actions,task_manager,event_log}/**/*.{ts,tsx}', + 'x-pack/plugins/{alerts,alerting_builtins,actions,task_manager,event_log}/**/*.{ts,tsx}', ], rules: { '@typescript-eslint/no-explicit-any': 'error', @@ -1238,10 +1195,7 @@ module.exports = { * TSVB overrides */ { - files: [ - 'src/plugins/vis_type_timeseries/**/*.{js,mjs,ts,tsx}', - 'src/legacy/core_plugins/vis_type_timeseries/**/*.{js,mjs,ts,tsx}', - ], + files: ['src/plugins/vis_type_timeseries/**/*.{js,mjs,ts,tsx}'], rules: { 'import/no-default-export': 'error', }, diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0bdddddab8de5..8a8cc5c5e448c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,10 +7,12 @@ /x-pack/plugins/discover_enhanced/ @elastic/kibana-app /x-pack/plugins/lens/ @elastic/kibana-app /x-pack/plugins/graph/ @elastic/kibana-app +/src/plugins/advanced_settings/ @elastic/kibana-app /src/plugins/charts/ @elastic/kibana-app /src/plugins/dashboard/ @elastic/kibana-app /src/plugins/discover/ @elastic/kibana-app /src/plugins/input_control_vis/ @elastic/kibana-app +/src/plugins/management/ @elastic/kibana-app /src/plugins/kibana_legacy/ @elastic/kibana-app /src/plugins/vis_default_editor/ @elastic/kibana-app /src/plugins/vis_type_markdown/ @elastic/kibana-app @@ -38,7 +40,6 @@ /examples/url_generators_explorer/ @elastic/kibana-app-arch /packages/elastic-datemath/ @elastic/kibana-app-arch /packages/kbn-interpreter/ @elastic/kibana-app-arch -/src/plugins/advanced_settings/ @elastic/kibana-app-arch /src/plugins/bfetch/ @elastic/kibana-app-arch /src/plugins/data/ @elastic/kibana-app-arch /src/plugins/embeddable/ @elastic/kibana-app-arch @@ -47,7 +48,6 @@ /src/plugins/kibana_react/ @elastic/kibana-app-arch /src/plugins/kibana_react/public/code_editor @elastic/kibana-canvas /src/plugins/kibana_utils/ @elastic/kibana-app-arch -/src/plugins/management/ @elastic/kibana-app-arch /src/plugins/navigation/ @elastic/kibana-app-arch /src/plugins/share/ @elastic/kibana-app-arch /src/plugins/ui_actions/ @elastic/kibana-app-arch @@ -65,14 +65,15 @@ # Client Side Monitoring (lives in APM directories but owned by Uptime) /x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm @elastic/uptime +/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature @elastic/uptime /x-pack/plugins/apm/public/application/csmApp.tsx @elastic/uptime /x-pack/plugins/apm/public/components/app/RumDashboard @elastic/uptime /x-pack/plugins/apm/server/lib/rum_client @elastic/uptime /x-pack/plugins/apm/server/routes/rum_client.ts @elastic/uptime -/x-pack/plugins/apm/server/projections/rum_overview.ts @elastic/uptime +/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts @elastic/uptime # Beats -/x-pack/legacy/plugins/beats_management/ @elastic/beats +/x-pack/plugins/beats_management/ @elastic/beats # Canvas /x-pack/plugins/canvas/ @elastic/kibana-canvas @@ -86,16 +87,13 @@ /x-pack/plugins/global_search_bar/ @elastic/kibana-core-ui # Observability UIs -/x-pack/legacy/plugins/infra/ @elastic/logs-metrics-ui /x-pack/plugins/infra/ @elastic/logs-metrics-ui /x-pack/plugins/ingest_manager/ @elastic/ingest-management -/x-pack/legacy/plugins/ingest_manager/ @elastic/ingest-management /x-pack/plugins/observability/ @elastic/observability-ui /x-pack/plugins/monitoring/ @elastic/stack-monitoring-ui /x-pack/plugins/uptime @elastic/uptime # Machine Learning -/x-pack/legacy/plugins/ml/ @elastic/ml-ui /x-pack/plugins/ml/ @elastic/ml-ui /x-pack/test/functional/apps/machine_learning/ @elastic/ml-ui /x-pack/test/functional/services/machine_learning/ @elastic/ml-ui @@ -107,7 +105,6 @@ /x-pack/test/functional/services/transform.ts @elastic/ml-ui # Maps -/x-pack/legacy/plugins/maps/ @elastic/kibana-gis /x-pack/plugins/maps/ @elastic/kibana-gis /x-pack/test/api_integration/apis/maps/ @elastic/kibana-gis /x-pack/test/functional/apps/maps/ @elastic/kibana-gis @@ -234,13 +231,8 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /src/plugins/dev_tools/ @elastic/es-ui /src/plugins/console/ @elastic/es-ui /src/plugins/es_ui_shared/ @elastic/es-ui -/x-pack/legacy/plugins/cross_cluster_replication/ @elastic/es-ui +/x-pack/plugins/cross_cluster_replication/ @elastic/es-ui /x-pack/plugins/index_lifecycle_management/ @elastic/es-ui -/x-pack/legacy/plugins/index_management/ @elastic/es-ui -/x-pack/legacy/plugins/license_management/ @elastic/es-ui -/x-pack/legacy/plugins/rollup/ @elastic/es-ui -/x-pack/legacy/plugins/snapshot_restore/ @elastic/es-ui -/x-pack/legacy/plugins/upgrade_assistant/ @elastic/es-ui /x-pack/plugins/console_extensions/ @elastic/es-ui /x-pack/plugins/es_ui_shared/ @elastic/es-ui /x-pack/plugins/grokdebugger/ @elastic/es-ui diff --git a/docs/apm/service-maps.asciidoc b/docs/apm/service-maps.asciidoc index db2f85c54c762..d629a95073a74 100644 --- a/docs/apm/service-maps.asciidoc +++ b/docs/apm/service-maps.asciidoc @@ -2,11 +2,6 @@ [[service-maps]] === Service maps -beta::[] - -WARNING: Service map support for Internet Explorer 11 is extremely limited. -Please use Chrome or Firefox if available. - A service map is a real-time visual representation of the instrumented services in your application's architecture. It shows you how these services are connected, along with high-level metrics like average transaction duration, requests per minute, and errors per minute. diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index b426621fed296..5a4a60c2e628e 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -504,6 +504,10 @@ in their infrastructure. |Contains HTTP endpoints and UiSettings that are slated for removal. +|{kib-repo}blob/{branch}/x-pack/plugins/drilldowns/url_drilldown/README.md[urlDrilldown] +|NOTE: This plugin contains implementation of URL drilldown. For drilldowns infrastructure code refer to ui_actions_enhanced plugin. + + |=== include::{kibana-root}/src/plugins/dashboard/README.asciidoc[leveloffset=+1] diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md index 903462ac3039d..470a41f30afbf 100644 --- a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.md @@ -29,4 +29,5 @@ export interface SavedObjectsFindOptions | [sortField](./kibana-plugin-core-public.savedobjectsfindoptions.sortfield.md) | string | | | [sortOrder](./kibana-plugin-core-public.savedobjectsfindoptions.sortorder.md) | string | | | [type](./kibana-plugin-core-public.savedobjectsfindoptions.type.md) | string | string[] | | +| [typeToNamespacesMap](./kibana-plugin-core-public.savedobjectsfindoptions.typetonamespacesmap.md) | Map<string, string[] | undefined> | This map defines each type to search for, and the namespace(s) to search for the type in; this is only intended to be used by a saved object client wrapper. If this is defined, it supersedes the type and namespaces fields when building the Elasticsearch query. Any types that are not included in this map will be excluded entirely. If a type is included but its value is undefined, the operation will search for that type in the Default namespace. | diff --git a/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.typetonamespacesmap.md b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.typetonamespacesmap.md new file mode 100644 index 0000000000000..4af8c9ddeaff4 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.savedobjectsfindoptions.typetonamespacesmap.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [SavedObjectsFindOptions](./kibana-plugin-core-public.savedobjectsfindoptions.md) > [typeToNamespacesMap](./kibana-plugin-core-public.savedobjectsfindoptions.typetonamespacesmap.md) + +## SavedObjectsFindOptions.typeToNamespacesMap property + +This map defines each type to search for, and the namespace(s) to search for the type in; this is only intended to be used by a saved object client wrapper. If this is defined, it supersedes the `type` and `namespaces` fields when building the Elasticsearch query. Any types that are not included in this map will be excluded entirely. If a type is included but its value is undefined, the operation will search for that type in the Default namespace. + +Signature: + +```typescript +typeToNamespacesMap?: Map; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanarequest.md b/docs/development/core/server/kibana-plugin-core-server.kibanarequest.md index 05e408ab49995..0a49ee6e63d6c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanarequest.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanarequest.md @@ -33,4 +33,5 @@ export declare class KibanaRequestRecursiveReadonly<KibanaRequestRoute<Method>> | matched route details | | [socket](./kibana-plugin-core-server.kibanarequest.socket.md) | | IKibanaSocket | [IKibanaSocket](./kibana-plugin-core-server.ikibanasocket.md) | | [url](./kibana-plugin-core-server.kibanarequest.url.md) | | Url | a WHATWG URL standard object. | +| [uuid](./kibana-plugin-core-server.kibanarequest.uuid.md) | | string | A UUID to identify this request. | diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanarequest.uuid.md b/docs/development/core/server/kibana-plugin-core-server.kibanarequest.uuid.md new file mode 100644 index 0000000000000..8b980b82d0adb --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.kibanarequest.uuid.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [KibanaRequest](./kibana-plugin-core-server.kibanarequest.md) > [uuid](./kibana-plugin-core-server.kibanarequest.uuid.md) + +## KibanaRequest.uuid property + +A UUID to identify this request. + +Signature: + +```typescript +readonly uuid: string; +``` + +## Remarks + +This value is NOT sourced from the incoming request's `X-Opaque-Id` header. it is always a UUID uniquely identifying the request. + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md index 804c83f7c1b48..ce5c20e60ca11 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.md @@ -29,4 +29,5 @@ export interface SavedObjectsFindOptions | [sortField](./kibana-plugin-core-server.savedobjectsfindoptions.sortfield.md) | string | | | [sortOrder](./kibana-plugin-core-server.savedobjectsfindoptions.sortorder.md) | string | | | [type](./kibana-plugin-core-server.savedobjectsfindoptions.type.md) | string | string[] | | +| [typeToNamespacesMap](./kibana-plugin-core-server.savedobjectsfindoptions.typetonamespacesmap.md) | Map<string, string[] | undefined> | This map defines each type to search for, and the namespace(s) to search for the type in; this is only intended to be used by a saved object client wrapper. If this is defined, it supersedes the type and namespaces fields when building the Elasticsearch query. Any types that are not included in this map will be excluded entirely. If a type is included but its value is undefined, the operation will search for that type in the Default namespace. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.typetonamespacesmap.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.typetonamespacesmap.md new file mode 100644 index 0000000000000..8bec759f05580 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindoptions.typetonamespacesmap.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) > [typeToNamespacesMap](./kibana-plugin-core-server.savedobjectsfindoptions.typetonamespacesmap.md) + +## SavedObjectsFindOptions.typeToNamespacesMap property + +This map defines each type to search for, and the namespace(s) to search for the type in; this is only intended to be used by a saved object client wrapper. If this is defined, it supersedes the `type` and `namespaces` fields when building the Elasticsearch query. Any types that are not included in this map will be excluded entirely. If a type is included but its value is undefined, the operation will search for that type in the Default namespace. + +Signature: + +```typescript +typeToNamespacesMap?: Map; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md index 1b562263145da..d3e93e7af2aa0 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.find.md @@ -7,14 +7,14 @@ Signature: ```typescript -find({ search, defaultSearchOperator, searchFields, rootSearchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespaces, type, filter, preference, }: SavedObjectsFindOptions): Promise>; +find(options: SavedObjectsFindOptions): Promise>; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| { search, defaultSearchOperator, searchFields, rootSearchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespaces, type, filter, preference, } | SavedObjectsFindOptions | | +| options | SavedObjectsFindOptions | | Returns: 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 14d3741425987..1d11d5262a9c4 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.md @@ -24,7 +24,7 @@ export declare class SavedObjectsRepository | [delete(type, id, options)](./kibana-plugin-core-server.savedobjectsrepository.delete.md) | | Deletes an object | | [deleteByNamespace(namespace, options)](./kibana-plugin-core-server.savedobjectsrepository.deletebynamespace.md) | | Deletes all objects from the provided namespace. | | [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({ search, defaultSearchOperator, searchFields, rootSearchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespaces, type, filter, preference, })](./kibana-plugin-core-server.savedobjectsrepository.find.md) | | | +| [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. | | [update(type, id, attributes, options)](./kibana-plugin-core-server.savedobjectsrepository.update.md) | | Updates an object | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.createemptyfindresponse.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.createemptyfindresponse.md new file mode 100644 index 0000000000000..40e865cb02ce8 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.createemptyfindresponse.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUtils](./kibana-plugin-core-server.savedobjectsutils.md) > [createEmptyFindResponse](./kibana-plugin-core-server.savedobjectsutils.createemptyfindresponse.md) + +## SavedObjectsUtils.createEmptyFindResponse property + +Creates an empty response for a find operation. This is only intended to be used by saved objects client wrappers. + +Signature: + +```typescript +static createEmptyFindResponse: ({ page, perPage, }: SavedObjectsFindOptions) => SavedObjectsFindResponse; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md index e365dfbcb5142..83831f65bd41a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md @@ -15,6 +15,7 @@ export declare class SavedObjectsUtils | Property | Modifiers | Type | Description | | --- | --- | --- | --- | +| [createEmptyFindResponse](./kibana-plugin-core-server.savedobjectsutils.createemptyfindresponse.md) | static | <T>({ page, perPage, }: SavedObjectsFindOptions) => SavedObjectsFindResponse<T> | Creates an empty response for a find operation. This is only intended to be used by saved objects client wrappers. | | [namespaceIdToString](./kibana-plugin-core-server.savedobjectsutils.namespaceidtostring.md) | static | (namespace?: string | undefined) => string | Converts a given saved object namespace ID to its string representation. All namespace IDs have an identical string representation, with the exception of the undefined namespace ID (which has a namespace string of 'default'). | | [namespaceStringToId](./kibana-plugin-core-server.savedobjectsutils.namespacestringtoid.md) | static | (namespace: string) => string | undefined | Converts a given saved object namespace string to its ID representation. All namespace strings have an identical ID representation, with the exception of the 'default' namespace string (which has a namespace ID of undefined). | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.duplicateindexpatternerror._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.duplicateindexpatternerror._constructor_.md new file mode 100644 index 0000000000000..676f1a2c785f8 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.duplicateindexpatternerror._constructor_.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DuplicateIndexPatternError](./kibana-plugin-plugins-data-public.duplicateindexpatternerror.md) > [(constructor)](./kibana-plugin-plugins-data-public.duplicateindexpatternerror._constructor_.md) + +## DuplicateIndexPatternError.(constructor) + +Constructs a new instance of the `DuplicateIndexPatternError` class + +Signature: + +```typescript +constructor(message: string); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| message | string | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.duplicateindexpatternerror.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.duplicateindexpatternerror.md new file mode 100644 index 0000000000000..7ed8f97976464 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.duplicateindexpatternerror.md @@ -0,0 +1,18 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DuplicateIndexPatternError](./kibana-plugin-plugins-data-public.duplicateindexpatternerror.md) + +## DuplicateIndexPatternError class + +Signature: + +```typescript +export declare class DuplicateIndexPatternError extends Error +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(message)](./kibana-plugin-plugins-data-public.duplicateindexpatternerror._constructor_.md) | | Constructs a new instance of the DuplicateIndexPatternError class | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.fieldformatmap.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.fieldformatmap.md index 2c131c6da9937..60ac95bc21af2 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.fieldformatmap.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.fieldformatmap.md @@ -7,8 +7,5 @@ Signature: ```typescript -fieldFormatMap?: Record; +fieldFormatMap?: Record | undefined>; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.getformatterforfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.getformatterforfield.md new file mode 100644 index 0000000000000..7466e4b9cf658 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.getformatterforfield.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IIndexPattern](./kibana-plugin-plugins-data-public.iindexpattern.md) > [getFormatterForField](./kibana-plugin-plugins-data-public.iindexpattern.getformatterforfield.md) + +## IIndexPattern.getFormatterForField property + +Signature: + +```typescript +getFormatterForField?: (field: IndexPatternField | IndexPatternField['spec'] | IFieldType) => FieldFormat; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md index 1cb89822eb605..ba77e659f0834 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpattern.md @@ -14,8 +14,9 @@ export interface IIndexPattern | Property | Type | Description | | --- | --- | --- | -| [fieldFormatMap](./kibana-plugin-plugins-data-public.iindexpattern.fieldformatmap.md) | Record<string, {
id: string;
params: unknown;
}> | | +| [fieldFormatMap](./kibana-plugin-plugins-data-public.iindexpattern.fieldformatmap.md) | Record<string, SerializedFieldFormat<unknown> | undefined> | | | [fields](./kibana-plugin-plugins-data-public.iindexpattern.fields.md) | IFieldType[] | | +| [getFormatterForField](./kibana-plugin-plugins-data-public.iindexpattern.getformatterforfield.md) | (field: IndexPatternField | IndexPatternField['spec'] | IFieldType) => FieldFormat | | | [id](./kibana-plugin-plugins-data-public.iindexpattern.id.md) | string | | | [timeFieldName](./kibana-plugin-plugins-data-public.iindexpattern.timefieldname.md) | string | | | [title](./kibana-plugin-plugins-data-public.iindexpattern.title.md) | string | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpatternfieldlist.tospec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpatternfieldlist.tospec.md index fd20f2944c5be..0fe62f575a927 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpatternfieldlist.tospec.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iindexpatternfieldlist.tospec.md @@ -9,7 +9,7 @@ ```typescript toSpec(options?: { getFormatterForField?: IndexPattern['getFormatterForField']; - }): FieldSpec[]; + }): IndexPatternFieldMap; ``` ## Parameters @@ -20,5 +20,5 @@ toSpec(options?: { Returns: -`FieldSpec[]` +`IndexPatternFieldMap` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md index a5bb15c963978..4baf98038f89a 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md @@ -9,13 +9,12 @@ Constructs a new instance of the `IndexPattern` class Signature: ```typescript -constructor(id: string | undefined, { savedObjectsClient, apiClient, patternCache, fieldFormats, indexPatternsService, onNotification, onError, shortDotsEnable, metaFields, }: IndexPatternDeps); +constructor({ spec, savedObjectsClient, fieldFormats, shortDotsEnable, metaFields, }: IndexPatternDeps); ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| id | string | undefined | | -| { savedObjectsClient, apiClient, patternCache, fieldFormats, indexPatternsService, onNotification, onError, shortDotsEnable, metaFields, } | IndexPatternDeps | | +| { spec, savedObjectsClient, fieldFormats, shortDotsEnable, metaFields, } | IndexPatternDeps | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._fetchfields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._fetchfields.md deleted file mode 100644 index 8fff8baa71139..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._fetchfields.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [\_fetchFields](./kibana-plugin-plugins-data-public.indexpattern._fetchfields.md) - -## IndexPattern.\_fetchFields() method - -Signature: - -```typescript -_fetchFields(): Promise; -``` -Returns: - -`Promise` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.addscriptedfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.addscriptedfield.md index 4bbbd83c65e10..cc3468531fffa 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.addscriptedfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.addscriptedfield.md @@ -4,10 +4,12 @@ ## IndexPattern.addScriptedField() method +Add scripted field to field list + Signature: ```typescript -addScriptedField(name: string, script: string, fieldType: string | undefined, lang: string): Promise; +addScriptedField(name: string, script: string, fieldType?: string, lang?: string): Promise; ``` ## Parameters @@ -16,7 +18,7 @@ addScriptedField(name: string, script: string, fieldType: string | undefined, la | --- | --- | --- | | name | string | | | script | string | | -| fieldType | string | undefined | | +| fieldType | string | | | lang | string | | Returns: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.create.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.create.md deleted file mode 100644 index 5c122b835f59d..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.create.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [create](./kibana-plugin-plugins-data-public.indexpattern.create.md) - -## IndexPattern.create() method - -Signature: - -```typescript -create(allowOverride?: boolean): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| allowOverride | boolean | | - -Returns: - -`Promise` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fieldformatmap.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fieldformatmap.md index b89b244d9826c..904d52fcd5751 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fieldformatmap.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fieldformatmap.md @@ -7,5 +7,5 @@ Signature: ```typescript -fieldFormatMap: any; +fieldFormatMap: Record; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fields.md index d4dca48c7cd7b..76bc41238526e 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fields.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fields.md @@ -8,6 +8,6 @@ ```typescript fields: IIndexPatternFieldList & { - toSpec: () => FieldSpec[]; + toSpec: () => IndexPatternFieldMap; }; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fieldsfetcher.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fieldsfetcher.md deleted file mode 100644 index 4d44b386a1db1..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.fieldsfetcher.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [fieldsFetcher](./kibana-plugin-plugins-data-public.indexpattern.fieldsfetcher.md) - -## IndexPattern.fieldsFetcher property - -Signature: - -```typescript -fieldsFetcher: any; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.flattenhit.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.flattenhit.md index db28d95197bb3..049c3e5e990f7 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.flattenhit.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.flattenhit.md @@ -7,5 +7,5 @@ Signature: ```typescript -flattenHit: any; +flattenHit: (hit: Record, deep?: boolean) => Record; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.formatfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.formatfield.md index 5a475d6161ac3..aadaddca6cc85 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.formatfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.formatfield.md @@ -7,5 +7,5 @@ Signature: ```typescript -formatField: any; +formatField: FormatFieldFn; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.formathit.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.formathit.md index ac515d374a93f..2be76bf1c1e05 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.formathit.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.formathit.md @@ -7,5 +7,8 @@ Signature: ```typescript -formatHit: any; +formatHit: { + (hit: Record, type?: string): any; + formatField: FormatFieldFn; + }; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.prepbody.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md similarity index 76% rename from docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.prepbody.md rename to docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md index 1d77b2a55860e..2c5f30e4889ea 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.prepbody.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md @@ -1,13 +1,15 @@ -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [prepBody](./kibana-plugin-plugins-data-public.indexpattern.prepbody.md) +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [getAsSavedObjectBody](./kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md) -## IndexPattern.prepBody() method +## IndexPattern.getAsSavedObjectBody() method + +Returns index pattern as saved object body for saving Signature: ```typescript -prepBody(): { +getAsSavedObjectBody(): { title: string; timeFieldName: string | undefined; intervalName: string | undefined; diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getformatterforfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getformatterforfield.md index 7984f7aff1d2d..ba31d60b56892 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getformatterforfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getformatterforfield.md @@ -4,17 +4,19 @@ ## IndexPattern.getFormatterForField() method +Provide a field, get its formatter + Signature: ```typescript -getFormatterForField(field: IndexPatternField | IndexPatternField['spec']): FieldFormat; +getFormatterForField(field: IndexPatternField | IndexPatternField['spec'] | IFieldType): FieldFormat; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| field | IndexPatternField | IndexPatternField['spec'] | | +| field | IndexPatternField | IndexPatternField['spec'] | IFieldType | | Returns: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getoriginalsavedobjectbody.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getoriginalsavedobjectbody.md new file mode 100644 index 0000000000000..349da63c13ca7 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getoriginalsavedobjectbody.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [getOriginalSavedObjectBody](./kibana-plugin-plugins-data-public.indexpattern.getoriginalsavedobjectbody.md) + +## IndexPattern.getOriginalSavedObjectBody property + +Get last saved saved object fields + +Signature: + +```typescript +getOriginalSavedObjectBody: () => { + title?: string | undefined; + timeFieldName?: string | undefined; + intervalName?: string | undefined; + fields?: string | undefined; + sourceFilters?: string | undefined; + fieldFormatMap?: string | undefined; + typeMeta?: string | undefined; + type?: string | undefined; + }; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getsourcefiltering.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getsourcefiltering.md index 121d32c7c40c8..4ce0144b73882 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getsourcefiltering.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getsourcefiltering.md @@ -4,6 +4,8 @@ ## IndexPattern.getSourceFiltering() method +Get the source filtering configuration for that index. + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.init.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.init.md deleted file mode 100644 index 595992dc82b74..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.init.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [init](./kibana-plugin-plugins-data-public.indexpattern.init.md) - -## IndexPattern.init() method - -Signature: - -```typescript -init(): Promise; -``` -Returns: - -`Promise` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.initfromspec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.initfromspec.md deleted file mode 100644 index 764dd11638221..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.initfromspec.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [initFromSpec](./kibana-plugin-plugins-data-public.indexpattern.initfromspec.md) - -## IndexPattern.initFromSpec() method - -Signature: - -```typescript -initFromSpec(spec: IndexPatternSpec): this; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| spec | IndexPatternSpec | | - -Returns: - -`this` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.iswildcard.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.iswildcard.md deleted file mode 100644 index e5ea55ef1dd48..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.iswildcard.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [isWildcard](./kibana-plugin-plugins-data-public.indexpattern.iswildcard.md) - -## IndexPattern.isWildcard() method - -Signature: - -```typescript -isWildcard(): boolean; -``` -Returns: - -`boolean` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index 87ce1e258712a..2ff575bc4fc22 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -14,22 +14,22 @@ export declare class IndexPattern implements IIndexPattern | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(id, { savedObjectsClient, apiClient, patternCache, fieldFormats, indexPatternsService, onNotification, onError, shortDotsEnable, metaFields, })](./kibana-plugin-plugins-data-public.indexpattern._constructor_.md) | | Constructs a new instance of the IndexPattern class | +| [(constructor)({ spec, savedObjectsClient, fieldFormats, shortDotsEnable, metaFields, })](./kibana-plugin-plugins-data-public.indexpattern._constructor_.md) | | Constructs a new instance of the IndexPattern class | ## Properties | Property | Modifiers | Type | Description | | --- | --- | --- | --- | -| [fieldFormatMap](./kibana-plugin-plugins-data-public.indexpattern.fieldformatmap.md) | | any | | -| [fields](./kibana-plugin-plugins-data-public.indexpattern.fields.md) | | IIndexPatternFieldList & {
toSpec: () => FieldSpec[];
} | | -| [fieldsFetcher](./kibana-plugin-plugins-data-public.indexpattern.fieldsfetcher.md) | | any | | -| [flattenHit](./kibana-plugin-plugins-data-public.indexpattern.flattenhit.md) | | any | | -| [formatField](./kibana-plugin-plugins-data-public.indexpattern.formatfield.md) | | any | | -| [formatHit](./kibana-plugin-plugins-data-public.indexpattern.formathit.md) | | any | | +| [fieldFormatMap](./kibana-plugin-plugins-data-public.indexpattern.fieldformatmap.md) | | Record<string, any> | | +| [fields](./kibana-plugin-plugins-data-public.indexpattern.fields.md) | | IIndexPatternFieldList & {
toSpec: () => IndexPatternFieldMap;
} | | +| [flattenHit](./kibana-plugin-plugins-data-public.indexpattern.flattenhit.md) | | (hit: Record<string, any>, deep?: boolean) => Record<string, any> | | +| [formatField](./kibana-plugin-plugins-data-public.indexpattern.formatfield.md) | | FormatFieldFn | | +| [formatHit](./kibana-plugin-plugins-data-public.indexpattern.formathit.md) | | {
(hit: Record<string, any>, type?: string): any;
formatField: FormatFieldFn;
} | | +| [getOriginalSavedObjectBody](./kibana-plugin-plugins-data-public.indexpattern.getoriginalsavedobjectbody.md) | | () => {
title?: string | undefined;
timeFieldName?: string | undefined;
intervalName?: string | undefined;
fields?: string | undefined;
sourceFilters?: string | undefined;
fieldFormatMap?: string | undefined;
typeMeta?: string | undefined;
type?: string | undefined;
} | Get last saved saved object fields | | [id](./kibana-plugin-plugins-data-public.indexpattern.id.md) | | string | | | [intervalName](./kibana-plugin-plugins-data-public.indexpattern.intervalname.md) | | string | undefined | | | [metaFields](./kibana-plugin-plugins-data-public.indexpattern.metafields.md) | | string[] | | -| [originalBody](./kibana-plugin-plugins-data-public.indexpattern.originalbody.md) | | {
[key: string]: any;
} | | +| [resetOriginalSavedObjectBody](./kibana-plugin-plugins-data-public.indexpattern.resetoriginalsavedobjectbody.md) | | () => void | Reset last saved saved object fields. used after saving | | [sourceFilters](./kibana-plugin-plugins-data-public.indexpattern.sourcefilters.md) | | SourceFilter[] | | | [timeFieldName](./kibana-plugin-plugins-data-public.indexpattern.timefieldname.md) | | string | undefined | | | [title](./kibana-plugin-plugins-data-public.indexpattern.title.md) | | string | | @@ -41,26 +41,20 @@ export declare class IndexPattern implements IIndexPattern | Method | Modifiers | Description | | --- | --- | --- | -| [\_fetchFields()](./kibana-plugin-plugins-data-public.indexpattern._fetchfields.md) | | | -| [addScriptedField(name, script, fieldType, lang)](./kibana-plugin-plugins-data-public.indexpattern.addscriptedfield.md) | | | -| [create(allowOverride)](./kibana-plugin-plugins-data-public.indexpattern.create.md) | | | +| [addScriptedField(name, script, fieldType, lang)](./kibana-plugin-plugins-data-public.indexpattern.addscriptedfield.md) | | Add scripted field to field list | | [getAggregationRestrictions()](./kibana-plugin-plugins-data-public.indexpattern.getaggregationrestrictions.md) | | | +| [getAsSavedObjectBody()](./kibana-plugin-plugins-data-public.indexpattern.getassavedobjectbody.md) | | Returns index pattern as saved object body for saving | | [getComputedFields()](./kibana-plugin-plugins-data-public.indexpattern.getcomputedfields.md) | | | | [getFieldByName(name)](./kibana-plugin-plugins-data-public.indexpattern.getfieldbyname.md) | | | -| [getFormatterForField(field)](./kibana-plugin-plugins-data-public.indexpattern.getformatterforfield.md) | | | +| [getFormatterForField(field)](./kibana-plugin-plugins-data-public.indexpattern.getformatterforfield.md) | | Provide a field, get its formatter | | [getNonScriptedFields()](./kibana-plugin-plugins-data-public.indexpattern.getnonscriptedfields.md) | | | | [getScriptedFields()](./kibana-plugin-plugins-data-public.indexpattern.getscriptedfields.md) | | | -| [getSourceFiltering()](./kibana-plugin-plugins-data-public.indexpattern.getsourcefiltering.md) | | | +| [getSourceFiltering()](./kibana-plugin-plugins-data-public.indexpattern.getsourcefiltering.md) | | Get the source filtering configuration for that index. | | [getTimeField()](./kibana-plugin-plugins-data-public.indexpattern.gettimefield.md) | | | -| [init()](./kibana-plugin-plugins-data-public.indexpattern.init.md) | | | -| [initFromSpec(spec)](./kibana-plugin-plugins-data-public.indexpattern.initfromspec.md) | | | | [isTimeBased()](./kibana-plugin-plugins-data-public.indexpattern.istimebased.md) | | | | [isTimeBasedWildcard()](./kibana-plugin-plugins-data-public.indexpattern.istimebasedwildcard.md) | | | | [isTimeNanosBased()](./kibana-plugin-plugins-data-public.indexpattern.istimenanosbased.md) | | | -| [isWildcard()](./kibana-plugin-plugins-data-public.indexpattern.iswildcard.md) | | | | [popularizeField(fieldName, unit)](./kibana-plugin-plugins-data-public.indexpattern.popularizefield.md) | | | -| [prepBody()](./kibana-plugin-plugins-data-public.indexpattern.prepbody.md) | | | -| [refreshFields()](./kibana-plugin-plugins-data-public.indexpattern.refreshfields.md) | | | -| [removeScriptedField(fieldName)](./kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md) | | | +| [removeScriptedField(fieldName)](./kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md) | | Remove scripted field from field list | | [toSpec()](./kibana-plugin-plugins-data-public.indexpattern.tospec.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.originalbody.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.originalbody.md deleted file mode 100644 index 4bc3c76afbae9..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.originalbody.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [originalBody](./kibana-plugin-plugins-data-public.indexpattern.originalbody.md) - -## IndexPattern.originalBody property - -Signature: - -```typescript -originalBody: { - [key: string]: any; - }; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.refreshfields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.refreshfields.md deleted file mode 100644 index 271d0c45a4244..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.refreshfields.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [refreshFields](./kibana-plugin-plugins-data-public.indexpattern.refreshfields.md) - -## IndexPattern.refreshFields() method - -Signature: - -```typescript -refreshFields(): Promise; -``` -Returns: - -`Promise` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md index e902d9c42b082..aaaebdaccca5d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md @@ -4,6 +4,8 @@ ## IndexPattern.removeScriptedField() method +Remove scripted field from field list + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.resetoriginalsavedobjectbody.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.resetoriginalsavedobjectbody.md new file mode 100644 index 0000000000000..6bbc13d8fd410 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.resetoriginalsavedobjectbody.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [resetOriginalSavedObjectBody](./kibana-plugin-plugins-data-public.indexpattern.resetoriginalsavedobjectbody.md) + +## IndexPattern.resetOriginalSavedObjectBody property + +Reset last saved saved object fields. used after saving + +Signature: + +```typescript +resetOriginalSavedObjectBody: () => void; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md index eff2349f053ff..77a8ebb0b2d3f 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternattributes.md @@ -4,12 +4,6 @@ ## IndexPatternAttributes interface -> Warning: This API is now obsolete. -> -> - -Use data plugin interface instead - Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md index 6d62053726197..9b226266f0b5a 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md @@ -4,6 +4,8 @@ ## IndexPatternField.conflictDescriptions property +Description of field type conflicts across different indices in the same index pattern + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.count.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.count.md index 84c0a75fd206d..1b8e13a38c6d9 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.count.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.count.md @@ -4,6 +4,8 @@ ## IndexPatternField.count property +Count is used for field popularity + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.lang.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.lang.md index 0a8446d40e5ec..b81218eb08886 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.lang.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.lang.md @@ -4,6 +4,8 @@ ## IndexPatternField.lang property +Script field language + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md index 215188ffa2607..4f49a9a8fc3ab 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.md @@ -21,15 +21,15 @@ export declare class IndexPatternField implements IFieldType | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [aggregatable](./kibana-plugin-plugins-data-public.indexpatternfield.aggregatable.md) | | boolean | | -| [conflictDescriptions](./kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md) | | Record<string, string[]> | undefined | | -| [count](./kibana-plugin-plugins-data-public.indexpatternfield.count.md) | | number | | +| [conflictDescriptions](./kibana-plugin-plugins-data-public.indexpatternfield.conflictdescriptions.md) | | Record<string, string[]> | undefined | Description of field type conflicts across different indices in the same index pattern | +| [count](./kibana-plugin-plugins-data-public.indexpatternfield.count.md) | | number | Count is used for field popularity | | [displayName](./kibana-plugin-plugins-data-public.indexpatternfield.displayname.md) | | string | | | [esTypes](./kibana-plugin-plugins-data-public.indexpatternfield.estypes.md) | | string[] | undefined | | | [filterable](./kibana-plugin-plugins-data-public.indexpatternfield.filterable.md) | | boolean | | -| [lang](./kibana-plugin-plugins-data-public.indexpatternfield.lang.md) | | string | undefined | | +| [lang](./kibana-plugin-plugins-data-public.indexpatternfield.lang.md) | | string | undefined | Script field language | | [name](./kibana-plugin-plugins-data-public.indexpatternfield.name.md) | | string | | | [readFromDocValues](./kibana-plugin-plugins-data-public.indexpatternfield.readfromdocvalues.md) | | boolean | | -| [script](./kibana-plugin-plugins-data-public.indexpatternfield.script.md) | | string | undefined | | +| [script](./kibana-plugin-plugins-data-public.indexpatternfield.script.md) | | string | undefined | Script field code | | [scripted](./kibana-plugin-plugins-data-public.indexpatternfield.scripted.md) | | boolean | | | [searchable](./kibana-plugin-plugins-data-public.indexpatternfield.searchable.md) | | boolean | | | [sortable](./kibana-plugin-plugins-data-public.indexpatternfield.sortable.md) | | boolean | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.script.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.script.md index 27f9c797c92f2..7501e191d9363 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.script.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.script.md @@ -4,6 +4,8 @@ ## IndexPatternField.script property +Script field code + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tospec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tospec.md index 1d80c90991f55..711d6ad660450 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tospec.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternfield.tospec.md @@ -9,24 +9,7 @@ ```typescript toSpec({ getFormatterForField, }?: { getFormatterForField?: IndexPattern['getFormatterForField']; - }): { - count: number; - script: string | undefined; - lang: string | undefined; - conflictDescriptions: Record | undefined; - name: string; - type: string; - esTypes: string[] | undefined; - scripted: boolean; - searchable: boolean; - aggregatable: boolean; - readFromDocValues: boolean; - subType: import("../types").IFieldSubType | undefined; - format: { - id: any; - params: any; - } | undefined; - }; + }): FieldSpec; ``` ## Parameters @@ -37,22 +20,5 @@ toSpec({ getFormatterForField, }?: { Returns: -`{ - count: number; - script: string | undefined; - lang: string | undefined; - conflictDescriptions: Record | undefined; - name: string; - type: string; - esTypes: string[] | undefined; - scripted: boolean; - searchable: boolean; - aggregatable: boolean; - readFromDocValues: boolean; - subType: import("../types").IFieldSubType | undefined; - format: { - id: any; - params: any; - } | undefined; - }` +`FieldSpec` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.fields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.fields.md new file mode 100644 index 0000000000000..386e080dbe6c2 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.fields.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) > [fields](./kibana-plugin-plugins-data-public.indexpatternspec.fields.md) + +## IndexPatternSpec.fields property + +Signature: + +```typescript +fields?: IndexPatternFieldMap; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.id.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.id.md new file mode 100644 index 0000000000000..55eadbf36c660 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) > [id](./kibana-plugin-plugins-data-public.indexpatternspec.id.md) + +## IndexPatternSpec.id property + +Signature: + +```typescript +id?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.intervalname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.intervalname.md new file mode 100644 index 0000000000000..98748661256da --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.intervalname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) > [intervalName](./kibana-plugin-plugins-data-public.indexpatternspec.intervalname.md) + +## IndexPatternSpec.intervalName property + +Signature: + +```typescript +intervalName?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md new file mode 100644 index 0000000000000..74c4df126e1bf --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) + +## IndexPatternSpec interface + +Signature: + +```typescript +export interface IndexPatternSpec +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [fields](./kibana-plugin-plugins-data-public.indexpatternspec.fields.md) | IndexPatternFieldMap | | +| [id](./kibana-plugin-plugins-data-public.indexpatternspec.id.md) | string | | +| [intervalName](./kibana-plugin-plugins-data-public.indexpatternspec.intervalname.md) | string | | +| [sourceFilters](./kibana-plugin-plugins-data-public.indexpatternspec.sourcefilters.md) | SourceFilter[] | | +| [timeFieldName](./kibana-plugin-plugins-data-public.indexpatternspec.timefieldname.md) | string | | +| [title](./kibana-plugin-plugins-data-public.indexpatternspec.title.md) | string | | +| [type](./kibana-plugin-plugins-data-public.indexpatternspec.type.md) | string | | +| [typeMeta](./kibana-plugin-plugins-data-public.indexpatternspec.typemeta.md) | TypeMeta | | +| [version](./kibana-plugin-plugins-data-public.indexpatternspec.version.md) | string | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.sourcefilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.sourcefilters.md new file mode 100644 index 0000000000000..cda5285730135 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.sourcefilters.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) > [sourceFilters](./kibana-plugin-plugins-data-public.indexpatternspec.sourcefilters.md) + +## IndexPatternSpec.sourceFilters property + +Signature: + +```typescript +sourceFilters?: SourceFilter[]; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.timefieldname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.timefieldname.md new file mode 100644 index 0000000000000..a527e3ac0658b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.timefieldname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) > [timeFieldName](./kibana-plugin-plugins-data-public.indexpatternspec.timefieldname.md) + +## IndexPatternSpec.timeFieldName property + +Signature: + +```typescript +timeFieldName?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.title.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.title.md new file mode 100644 index 0000000000000..4cc6d3c2524a7 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.title.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) > [title](./kibana-plugin-plugins-data-public.indexpatternspec.title.md) + +## IndexPatternSpec.title property + +Signature: + +```typescript +title?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.type.md new file mode 100644 index 0000000000000..d1c49be1b706f --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) > [type](./kibana-plugin-plugins-data-public.indexpatternspec.type.md) + +## IndexPatternSpec.type property + +Signature: + +```typescript +type?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.typemeta.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.typemeta.md new file mode 100644 index 0000000000000..9303047e905d3 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.typemeta.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) > [typeMeta](./kibana-plugin-plugins-data-public.indexpatternspec.typemeta.md) + +## IndexPatternSpec.typeMeta property + +Signature: + +```typescript +typeMeta?: TypeMeta; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.version.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.version.md new file mode 100644 index 0000000000000..43f7cf0226fb0 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternspec.version.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) > [version](./kibana-plugin-plugins-data-public.indexpatternspec.version.md) + +## IndexPatternSpec.version property + +Signature: + +```typescript +version?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice._constructor_.md new file mode 100644 index 0000000000000..ab397efb1fe0e --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice._constructor_.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [(constructor)](./kibana-plugin-plugins-data-public.indexpatternsservice._constructor_.md) + +## IndexPatternsService.(constructor) + +Constructs a new instance of the `IndexPatternsService` class + +Signature: + +```typescript +constructor({ uiSettings, savedObjectsClient, apiClient, fieldFormats, onNotification, onError, onRedirectNoIndexPattern, }: IndexPatternsServiceDeps); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { uiSettings, savedObjectsClient, apiClient, fieldFormats, onNotification, onError, onRedirectNoIndexPattern, } | IndexPatternsServiceDeps | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.clearcache.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.clearcache.md new file mode 100644 index 0000000000000..b371218325086 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.clearcache.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [clearCache](./kibana-plugin-plugins-data-public.indexpatternsservice.clearcache.md) + +## IndexPatternsService.clearCache property + +Clear index pattern list cache + +Signature: + +```typescript +clearCache: (id?: string | undefined) => void; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.create.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.create.md new file mode 100644 index 0000000000000..d7152ba617cc6 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.create.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [create](./kibana-plugin-plugins-data-public.indexpatternsservice.create.md) + +## IndexPatternsService.create() method + +Create a new index pattern instance + +Signature: + +```typescript +create(spec: IndexPatternSpec, skipFetchFields?: boolean): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| spec | IndexPatternSpec | | +| skipFetchFields | boolean | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.createandsave.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.createandsave.md new file mode 100644 index 0000000000000..eebfbb506fb77 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.createandsave.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [createAndSave](./kibana-plugin-plugins-data-public.indexpatternsservice.createandsave.md) + +## IndexPatternsService.createAndSave() method + +Create a new index pattern and save it right away + +Signature: + +```typescript +createAndSave(spec: IndexPatternSpec, override?: boolean, skipFetchFields?: boolean): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| spec | IndexPatternSpec | | +| override | boolean | | +| skipFetchFields | boolean | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.createsavedobject.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.createsavedobject.md new file mode 100644 index 0000000000000..8efb33c423b01 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.createsavedobject.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [createSavedObject](./kibana-plugin-plugins-data-public.indexpatternsservice.createsavedobject.md) + +## IndexPatternsService.createSavedObject() method + +Save a new index pattern + +Signature: + +```typescript +createSavedObject(indexPattern: IndexPattern, override?: boolean): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| indexPattern | IndexPattern | | +| override | boolean | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.delete.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.delete.md new file mode 100644 index 0000000000000..aba31ab2c0d29 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.delete.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [delete](./kibana-plugin-plugins-data-public.indexpatternsservice.delete.md) + +## IndexPatternsService.delete() method + +Deletes an index pattern from .kibana index + +Signature: + +```typescript +delete(indexPatternId: string): Promise<{}>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| indexPatternId | string | | + +Returns: + +`Promise<{}>` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.ensuredefaultindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.ensuredefaultindexpattern.md new file mode 100644 index 0000000000000..3b6a8c7e4a04f --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.ensuredefaultindexpattern.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [ensureDefaultIndexPattern](./kibana-plugin-plugins-data-public.indexpatternsservice.ensuredefaultindexpattern.md) + +## IndexPatternsService.ensureDefaultIndexPattern property + +Signature: + +```typescript +ensureDefaultIndexPattern: EnsureDefaultIndexPattern; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.fieldarraytomap.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.fieldarraytomap.md new file mode 100644 index 0000000000000..ed365fe03f980 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.fieldarraytomap.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [fieldArrayToMap](./kibana-plugin-plugins-data-public.indexpatternsservice.fieldarraytomap.md) + +## IndexPatternsService.fieldArrayToMap property + +Converts field array to map + +Signature: + +```typescript +fieldArrayToMap: (fields: FieldSpec[]) => Record; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.get.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.get.md new file mode 100644 index 0000000000000..4aad6df6b413b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.get.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [get](./kibana-plugin-plugins-data-public.indexpatternsservice.get.md) + +## IndexPatternsService.get property + +Get an index pattern by id. Cache optimized + +Signature: + +```typescript +get: (id: string) => Promise; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md new file mode 100644 index 0000000000000..ad2a167bd8c74 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [getCache](./kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md) + +## IndexPatternsService.getCache property + +Signature: + +```typescript +getCache: () => Promise[] | null | undefined>; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getdefault.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getdefault.md new file mode 100644 index 0000000000000..01d4efeffe921 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getdefault.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [getDefault](./kibana-plugin-plugins-data-public.indexpatternsservice.getdefault.md) + +## IndexPatternsService.getDefault property + +Get default index pattern + +Signature: + +```typescript +getDefault: () => Promise; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md new file mode 100644 index 0000000000000..c06c3c6f68492 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [getFieldsForIndexPattern](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md) + +## IndexPatternsService.getFieldsForIndexPattern property + +Get field list by providing an index patttern (or spec) + +Signature: + +```typescript +getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions) => Promise; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md new file mode 100644 index 0000000000000..aec84866b9e58 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [getFieldsForWildcard](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md) + +## IndexPatternsService.getFieldsForWildcard property + +Get field list by providing { pattern } + +Signature: + +```typescript +getFieldsForWildcard: (options?: GetFieldsOptions) => Promise; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getids.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getids.md new file mode 100644 index 0000000000000..a012e0dc9d9c5 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getids.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [getIds](./kibana-plugin-plugins-data-public.indexpatternsservice.getids.md) + +## IndexPatternsService.getIds property + +Get list of index pattern ids + +Signature: + +```typescript +getIds: (refresh?: boolean) => Promise; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.gettitles.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.gettitles.md new file mode 100644 index 0000000000000..04cc294a79dfc --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.gettitles.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [getTitles](./kibana-plugin-plugins-data-public.indexpatternsservice.gettitles.md) + +## IndexPatternsService.getTitles property + +Get list of index pattern titles + +Signature: + +```typescript +getTitles: (refresh?: boolean) => Promise; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md new file mode 100644 index 0000000000000..0022bff34a8e7 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md @@ -0,0 +1,46 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) + +## IndexPatternsService class + +Signature: + +```typescript +export declare class IndexPatternsService +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)({ uiSettings, savedObjectsClient, apiClient, fieldFormats, onNotification, onError, onRedirectNoIndexPattern, })](./kibana-plugin-plugins-data-public.indexpatternsservice._constructor_.md) | | Constructs a new instance of the IndexPatternsService class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [clearCache](./kibana-plugin-plugins-data-public.indexpatternsservice.clearcache.md) | | (id?: string | undefined) => void | Clear index pattern list cache | +| [ensureDefaultIndexPattern](./kibana-plugin-plugins-data-public.indexpatternsservice.ensuredefaultindexpattern.md) | | EnsureDefaultIndexPattern | | +| [fieldArrayToMap](./kibana-plugin-plugins-data-public.indexpatternsservice.fieldarraytomap.md) | | (fields: FieldSpec[]) => Record<string, FieldSpec> | Converts field array to map | +| [get](./kibana-plugin-plugins-data-public.indexpatternsservice.get.md) | | (id: string) => Promise<IndexPattern> | Get an index pattern by id. Cache optimized | +| [getCache](./kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md) | | () => Promise<SavedObject<IndexPatternSavedObjectAttrs>[] | null | undefined> | | +| [getDefault](./kibana-plugin-plugins-data-public.indexpatternsservice.getdefault.md) | | () => Promise<IndexPattern | null> | Get default index pattern | +| [getFieldsForIndexPattern](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md) | | (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions) => Promise<any> | Get field list by providing an index patttern (or spec) | +| [getFieldsForWildcard](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md) | | (options?: GetFieldsOptions) => Promise<any> | Get field list by providing { pattern } | +| [getIds](./kibana-plugin-plugins-data-public.indexpatternsservice.getids.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern ids | +| [getTitles](./kibana-plugin-plugins-data-public.indexpatternsservice.gettitles.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern titles | +| [refreshFields](./kibana-plugin-plugins-data-public.indexpatternsservice.refreshfields.md) | | (indexPattern: IndexPattern) => Promise<void> | Refresh field list for a given index pattern | +| [savedObjectToSpec](./kibana-plugin-plugins-data-public.indexpatternsservice.savedobjecttospec.md) | | (savedObject: SavedObject<IndexPatternAttributes>) => IndexPatternSpec | Converts index pattern saved object to index pattern spec | +| [setDefault](./kibana-plugin-plugins-data-public.indexpatternsservice.setdefault.md) | | (id: string, force?: boolean) => Promise<void> | Optionally set default index pattern, unless force = true | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [create(spec, skipFetchFields)](./kibana-plugin-plugins-data-public.indexpatternsservice.create.md) | | Create a new index pattern instance | +| [createAndSave(spec, override, skipFetchFields)](./kibana-plugin-plugins-data-public.indexpatternsservice.createandsave.md) | | Create a new index pattern and save it right away | +| [createSavedObject(indexPattern, override)](./kibana-plugin-plugins-data-public.indexpatternsservice.createsavedobject.md) | | Save a new index pattern | +| [delete(indexPatternId)](./kibana-plugin-plugins-data-public.indexpatternsservice.delete.md) | | Deletes an index pattern from .kibana index | +| [updateSavedObject(indexPattern, saveAttempts)](./kibana-plugin-plugins-data-public.indexpatternsservice.updatesavedobject.md) | | Save existing index pattern. Will attempt to merge differences if there are conflicts | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.refreshfields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.refreshfields.md new file mode 100644 index 0000000000000..b7c47efbb445a --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.refreshfields.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [refreshFields](./kibana-plugin-plugins-data-public.indexpatternsservice.refreshfields.md) + +## IndexPatternsService.refreshFields property + +Refresh field list for a given index pattern + +Signature: + +```typescript +refreshFields: (indexPattern: IndexPattern) => Promise; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.savedobjecttospec.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.savedobjecttospec.md new file mode 100644 index 0000000000000..7bd40c9cafd42 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.savedobjecttospec.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [savedObjectToSpec](./kibana-plugin-plugins-data-public.indexpatternsservice.savedobjecttospec.md) + +## IndexPatternsService.savedObjectToSpec property + +Converts index pattern saved object to index pattern spec + +Signature: + +```typescript +savedObjectToSpec: (savedObject: SavedObject) => IndexPatternSpec; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.setdefault.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.setdefault.md new file mode 100644 index 0000000000000..2bf8eaa03d1ae --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.setdefault.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [setDefault](./kibana-plugin-plugins-data-public.indexpatternsservice.setdefault.md) + +## IndexPatternsService.setDefault property + +Optionally set default index pattern, unless force = true + +Signature: + +```typescript +setDefault: (id: string, force?: boolean) => Promise; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.updatesavedobject.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.updatesavedobject.md new file mode 100644 index 0000000000000..3973f5d4c3e7b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.updatesavedobject.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [updateSavedObject](./kibana-plugin-plugins-data-public.indexpatternsservice.updatesavedobject.md) + +## IndexPatternsService.updateSavedObject() method + +Save existing index pattern. Will attempt to merge differences if there are conflicts + +Signature: + +```typescript +updateSavedObject(indexPattern: IndexPattern, saveAttempts?: number): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| indexPattern | IndexPattern | | +| saveAttempts | number | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iscompleteresponse.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iscompleteresponse.md new file mode 100644 index 0000000000000..17acf4e0d1be8 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iscompleteresponse.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [isCompleteResponse](./kibana-plugin-plugins-data-public.iscompleteresponse.md) + +## isCompleteResponse variable + +Signature: + +```typescript +isCompleteResponse: (response?: IEsSearchResponse | undefined) => boolean | undefined +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iserrorresponse.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iserrorresponse.md new file mode 100644 index 0000000000000..3f9b1d593870d --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.iserrorresponse.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [isErrorResponse](./kibana-plugin-plugins-data-public.iserrorresponse.md) + +## isErrorResponse variable + +Signature: + +```typescript +isErrorResponse: (response?: IEsSearchResponse | undefined) => boolean | undefined +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ispartialresponse.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ispartialresponse.md new file mode 100644 index 0000000000000..9f2f1bbf2f9e0 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ispartialresponse.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [isPartialResponse](./kibana-plugin-plugins-data-public.ispartialresponse.md) + +## isPartialResponse variable + +Signature: + +```typescript +isPartialResponse: (response?: IEsSearchResponse | undefined) => boolean | undefined +``` 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 f51549c81fb62..accf46f534e89 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 @@ -11,11 +11,13 @@ | [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) | | | [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) | | | [AggParamType](./kibana-plugin-plugins-data-public.aggparamtype.md) | | +| [DuplicateIndexPatternError](./kibana-plugin-plugins-data-public.duplicateindexpatternerror.md) | | | [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) | | | [FilterManager](./kibana-plugin-plugins-data-public.filtermanager.md) | | | [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) | | | [IndexPatternField](./kibana-plugin-plugins-data-public.indexpatternfield.md) | | | [IndexPatternSelect](./kibana-plugin-plugins-data-public.indexpatternselect.md) | | +| [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) | | | [OptionedParamType](./kibana-plugin-plugins-data-public.optionedparamtype.md) | | | [Plugin](./kibana-plugin-plugins-data-public.plugin.md) | | | [RequestTimeoutError](./kibana-plugin-plugins-data-public.requesttimeouterror.md) | Class used to signify that a request timed out. Useful for applications to conditionally handle this type of error differently than other errors. | @@ -57,7 +59,6 @@ | [EsQueryConfig](./kibana-plugin-plugins-data-public.esqueryconfig.md) | | | [FieldFormatConfig](./kibana-plugin-plugins-data-public.fieldformatconfig.md) | | | [FieldMappingSpec](./kibana-plugin-plugins-data-public.fieldmappingspec.md) | | -| [Filter](./kibana-plugin-plugins-data-public.filter.md) | | | [IDataPluginServices](./kibana-plugin-plugins-data-public.idatapluginservices.md) | | | [IEsSearchRequest](./kibana-plugin-plugins-data-public.iessearchrequest.md) | | | [IEsSearchResponse](./kibana-plugin-plugins-data-public.iessearchresponse.md) | | @@ -67,7 +68,8 @@ | [IIndexPatternFieldList](./kibana-plugin-plugins-data-public.iindexpatternfieldlist.md) | | | [IKibanaSearchRequest](./kibana-plugin-plugins-data-public.ikibanasearchrequest.md) | | | [IKibanaSearchResponse](./kibana-plugin-plugins-data-public.ikibanasearchresponse.md) | | -| [IndexPatternAttributes](./kibana-plugin-plugins-data-public.indexpatternattributes.md) | Use data plugin interface instead | +| [IndexPatternAttributes](./kibana-plugin-plugins-data-public.indexpatternattributes.md) | | +| [IndexPatternSpec](./kibana-plugin-plugins-data-public.indexpatternspec.md) | | | [IndexPatternTypeMeta](./kibana-plugin-plugins-data-public.indexpatterntypemeta.md) | | | [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) | | | [ISearchSetup](./kibana-plugin-plugins-data-public.isearchsetup.md) | The setup contract exposed by the Search plugin exposes the search strategy extension point. | @@ -75,7 +77,6 @@ | [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) | high level search service | | [KueryNode](./kibana-plugin-plugins-data-public.kuerynode.md) | | | [OptionedValueProp](./kibana-plugin-plugins-data-public.optionedvalueprop.md) | | -| [Query](./kibana-plugin-plugins-data-public.query.md) | | | [QueryState](./kibana-plugin-plugins-data-public.querystate.md) | All query state service state | | [QueryStateChange](./kibana-plugin-plugins-data-public.querystatechange.md) | | | [QuerySuggestionBasic](./kibana-plugin-plugins-data-public.querysuggestionbasic.md) | \* | @@ -90,7 +91,6 @@ | [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) | \* | -| [TimeRange](./kibana-plugin-plugins-data-public.timerange.md) | | ## Variables @@ -115,8 +115,11 @@ | [getKbnTypeNames](./kibana-plugin-plugins-data-public.getkbntypenames.md) | Get the esTypes known by all kbnFieldTypes {Array} | | [indexPatterns](./kibana-plugin-plugins-data-public.indexpatterns.md) | | | [injectSearchSourceReferences](./kibana-plugin-plugins-data-public.injectsearchsourcereferences.md) | | +| [isCompleteResponse](./kibana-plugin-plugins-data-public.iscompleteresponse.md) | | +| [isErrorResponse](./kibana-plugin-plugins-data-public.iserrorresponse.md) | | | [isFilter](./kibana-plugin-plugins-data-public.isfilter.md) | | | [isFilters](./kibana-plugin-plugins-data-public.isfilters.md) | | +| [isPartialResponse](./kibana-plugin-plugins-data-public.ispartialresponse.md) | | | [isQuery](./kibana-plugin-plugins-data-public.isquery.md) | | | [isTimeRange](./kibana-plugin-plugins-data-public.istimerange.md) | | | [parseSearchSourceJSON](./kibana-plugin-plugins-data-public.parsesearchsourcejson.md) | | @@ -145,6 +148,7 @@ | [FieldFormatsContentType](./kibana-plugin-plugins-data-public.fieldformatscontenttype.md) | \* | | [FieldFormatsGetConfigFn](./kibana-plugin-plugins-data-public.fieldformatsgetconfigfn.md) | | | [FieldFormatsStart](./kibana-plugin-plugins-data-public.fieldformatsstart.md) | | +| [Filter](./kibana-plugin-plugins-data-public.filter.md) | | | [IAggConfig](./kibana-plugin-plugins-data-public.iaggconfig.md) | AggConfig This class represents an aggregation, which is displayed in the left-hand nav of the Visualize app. | | [IAggType](./kibana-plugin-plugins-data-public.iaggtype.md) | | | [IFieldFormat](./kibana-plugin-plugins-data-public.ifieldformat.md) | | @@ -162,6 +166,7 @@ | [ParsedInterval](./kibana-plugin-plugins-data-public.parsedinterval.md) | | | [PhraseFilter](./kibana-plugin-plugins-data-public.phrasefilter.md) | | | [PhrasesFilter](./kibana-plugin-plugins-data-public.phrasesfilter.md) | | +| [Query](./kibana-plugin-plugins-data-public.query.md) | | | [QueryStart](./kibana-plugin-plugins-data-public.querystart.md) | | | [QuerySuggestion](./kibana-plugin-plugins-data-public.querysuggestion.md) | \* | | [QuerySuggestionGetFn](./kibana-plugin-plugins-data-public.querysuggestiongetfn.md) | | @@ -173,4 +178,5 @@ | [TabbedAggRow](./kibana-plugin-plugins-data-public.tabbedaggrow.md) | \* | | [TimefilterContract](./kibana-plugin-plugins-data-public.timefiltercontract.md) | | | [TimeHistoryContract](./kibana-plugin-plugins-data-public.timehistorycontract.md) | | +| [TimeRange](./kibana-plugin-plugins-data-public.timerange.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.aggregatable.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.aggregatable.md new file mode 100644 index 0000000000000..2889ee34ad77b --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.aggregatable.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [FieldDescriptor](./kibana-plugin-plugins-data-server.fielddescriptor.md) > [aggregatable](./kibana-plugin-plugins-data-server.fielddescriptor.aggregatable.md) + +## FieldDescriptor.aggregatable property + +Signature: + +```typescript +aggregatable: boolean; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.estypes.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.estypes.md new file mode 100644 index 0000000000000..9caa374d8da48 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.estypes.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [FieldDescriptor](./kibana-plugin-plugins-data-server.fielddescriptor.md) > [esTypes](./kibana-plugin-plugins-data-server.fielddescriptor.estypes.md) + +## FieldDescriptor.esTypes property + +Signature: + +```typescript +esTypes: string[]; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.md new file mode 100644 index 0000000000000..693de675da940 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [FieldDescriptor](./kibana-plugin-plugins-data-server.fielddescriptor.md) + +## FieldDescriptor interface + +Signature: + +```typescript +export interface FieldDescriptor +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [aggregatable](./kibana-plugin-plugins-data-server.fielddescriptor.aggregatable.md) | boolean | | +| [esTypes](./kibana-plugin-plugins-data-server.fielddescriptor.estypes.md) | string[] | | +| [name](./kibana-plugin-plugins-data-server.fielddescriptor.name.md) | string | | +| [readFromDocValues](./kibana-plugin-plugins-data-server.fielddescriptor.readfromdocvalues.md) | boolean | | +| [searchable](./kibana-plugin-plugins-data-server.fielddescriptor.searchable.md) | boolean | | +| [subType](./kibana-plugin-plugins-data-server.fielddescriptor.subtype.md) | FieldSubType | | +| [type](./kibana-plugin-plugins-data-server.fielddescriptor.type.md) | string | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.name.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.name.md new file mode 100644 index 0000000000000..178880a34cd4d --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.name.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [FieldDescriptor](./kibana-plugin-plugins-data-server.fielddescriptor.md) > [name](./kibana-plugin-plugins-data-server.fielddescriptor.name.md) + +## FieldDescriptor.name property + +Signature: + +```typescript +name: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.readfromdocvalues.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.readfromdocvalues.md new file mode 100644 index 0000000000000..b60dc5d0dfed0 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.readfromdocvalues.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [FieldDescriptor](./kibana-plugin-plugins-data-server.fielddescriptor.md) > [readFromDocValues](./kibana-plugin-plugins-data-server.fielddescriptor.readfromdocvalues.md) + +## FieldDescriptor.readFromDocValues property + +Signature: + +```typescript +readFromDocValues: boolean; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.searchable.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.searchable.md new file mode 100644 index 0000000000000..efc7b4219a355 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.searchable.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [FieldDescriptor](./kibana-plugin-plugins-data-server.fielddescriptor.md) > [searchable](./kibana-plugin-plugins-data-server.fielddescriptor.searchable.md) + +## FieldDescriptor.searchable property + +Signature: + +```typescript +searchable: boolean; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.subtype.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.subtype.md new file mode 100644 index 0000000000000..b08179f12f250 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.subtype.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [FieldDescriptor](./kibana-plugin-plugins-data-server.fielddescriptor.md) > [subType](./kibana-plugin-plugins-data-server.fielddescriptor.subtype.md) + +## FieldDescriptor.subType property + +Signature: + +```typescript +subType?: FieldSubType; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.type.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.type.md new file mode 100644 index 0000000000000..7b0513a60c90e --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fielddescriptor.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [FieldDescriptor](./kibana-plugin-plugins-data-server.fielddescriptor.md) > [type](./kibana-plugin-plugins-data-server.fielddescriptor.type.md) + +## FieldDescriptor.type property + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.fieldformatmap.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.fieldformatmap.md deleted file mode 100644 index ab9e3171d7d7b..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.fieldformatmap.md +++ /dev/null @@ -1,14 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IIndexPattern](./kibana-plugin-plugins-data-server.iindexpattern.md) > [fieldFormatMap](./kibana-plugin-plugins-data-server.iindexpattern.fieldformatmap.md) - -## IIndexPattern.fieldFormatMap property - -Signature: - -```typescript -fieldFormatMap?: Record; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.fields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.fields.md deleted file mode 100644 index fb6d046ff2174..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.fields.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IIndexPattern](./kibana-plugin-plugins-data-server.iindexpattern.md) > [fields](./kibana-plugin-plugins-data-server.iindexpattern.fields.md) - -## IIndexPattern.fields property - -Signature: - -```typescript -fields: IFieldType[]; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.gettimefield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.gettimefield.md deleted file mode 100644 index a4d6abcf86a94..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.gettimefield.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IIndexPattern](./kibana-plugin-plugins-data-server.iindexpattern.md) > [getTimeField](./kibana-plugin-plugins-data-server.iindexpattern.gettimefield.md) - -## IIndexPattern.getTimeField() method - -Signature: - -```typescript -getTimeField?(): IFieldType | undefined; -``` -Returns: - -`IFieldType | undefined` - diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.id.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.id.md deleted file mode 100644 index cac263df0f9aa..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.id.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IIndexPattern](./kibana-plugin-plugins-data-server.iindexpattern.md) > [id](./kibana-plugin-plugins-data-server.iindexpattern.id.md) - -## IIndexPattern.id property - -Signature: - -```typescript -id?: string; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.md deleted file mode 100644 index a79244a24acf5..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.md +++ /dev/null @@ -1,29 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IIndexPattern](./kibana-plugin-plugins-data-server.iindexpattern.md) - -## IIndexPattern interface - -Signature: - -```typescript -export interface IIndexPattern -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [fieldFormatMap](./kibana-plugin-plugins-data-server.iindexpattern.fieldformatmap.md) | Record<string, {
id: string;
params: unknown;
}> | | -| [fields](./kibana-plugin-plugins-data-server.iindexpattern.fields.md) | IFieldType[] | | -| [id](./kibana-plugin-plugins-data-server.iindexpattern.id.md) | string | | -| [timeFieldName](./kibana-plugin-plugins-data-server.iindexpattern.timefieldname.md) | string | | -| [title](./kibana-plugin-plugins-data-server.iindexpattern.title.md) | string | | -| [type](./kibana-plugin-plugins-data-server.iindexpattern.type.md) | string | | - -## Methods - -| Method | Description | -| --- | --- | -| [getTimeField()](./kibana-plugin-plugins-data-server.iindexpattern.gettimefield.md) | | - diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.timefieldname.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.timefieldname.md deleted file mode 100644 index 14cf514477da4..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.timefieldname.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IIndexPattern](./kibana-plugin-plugins-data-server.iindexpattern.md) > [timeFieldName](./kibana-plugin-plugins-data-server.iindexpattern.timefieldname.md) - -## IIndexPattern.timeFieldName property - -Signature: - -```typescript -timeFieldName?: string; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.title.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.title.md deleted file mode 100644 index 119963d7ff95d..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.title.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IIndexPattern](./kibana-plugin-plugins-data-server.iindexpattern.md) > [title](./kibana-plugin-plugins-data-server.iindexpattern.title.md) - -## IIndexPattern.title property - -Signature: - -```typescript -title: string; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.type.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.type.md deleted file mode 100644 index 6b89b71664b23..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.iindexpattern.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IIndexPattern](./kibana-plugin-plugins-data-server.iindexpattern.md) > [type](./kibana-plugin-plugins-data-server.iindexpattern.type.md) - -## IIndexPattern.type property - -Signature: - -```typescript -type?: string; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern._constructor_.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern._constructor_.md new file mode 100644 index 0000000000000..f7f8e51c4b632 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern._constructor_.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [(constructor)](./kibana-plugin-plugins-data-server.indexpattern._constructor_.md) + +## IndexPattern.(constructor) + +Constructs a new instance of the `IndexPattern` class + +Signature: + +```typescript +constructor({ spec, savedObjectsClient, fieldFormats, shortDotsEnable, metaFields, }: IndexPatternDeps); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { spec, savedObjectsClient, fieldFormats, shortDotsEnable, metaFields, } | IndexPatternDeps | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md new file mode 100644 index 0000000000000..6d206e88b5b56 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [addScriptedField](./kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md) + +## IndexPattern.addScriptedField() method + +Add scripted field to field list + +Signature: + +```typescript +addScriptedField(name: string, script: string, fieldType?: string, lang?: string): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| name | string | | +| script | string | | +| fieldType | string | | +| lang | string | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldformatmap.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldformatmap.md new file mode 100644 index 0000000000000..2f686bd313d58 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fieldformatmap.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [fieldFormatMap](./kibana-plugin-plugins-data-server.indexpattern.fieldformatmap.md) + +## IndexPattern.fieldFormatMap property + +Signature: + +```typescript +fieldFormatMap: Record; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fields.md new file mode 100644 index 0000000000000..5b22014486c02 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.fields.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [fields](./kibana-plugin-plugins-data-server.indexpattern.fields.md) + +## IndexPattern.fields property + +Signature: + +```typescript +fields: IIndexPatternFieldList & { + toSpec: () => IndexPatternFieldMap; + }; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.flattenhit.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.flattenhit.md new file mode 100644 index 0000000000000..33c6dedc6dcd8 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.flattenhit.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [flattenHit](./kibana-plugin-plugins-data-server.indexpattern.flattenhit.md) + +## IndexPattern.flattenHit property + +Signature: + +```typescript +flattenHit: (hit: Record, deep?: boolean) => Record; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.formatfield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.formatfield.md new file mode 100644 index 0000000000000..07db8a0805b07 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.formatfield.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [formatField](./kibana-plugin-plugins-data-server.indexpattern.formatfield.md) + +## IndexPattern.formatField property + +Signature: + +```typescript +formatField: FormatFieldFn; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.formathit.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.formathit.md new file mode 100644 index 0000000000000..75f282a8991fc --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.formathit.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [formatHit](./kibana-plugin-plugins-data-server.indexpattern.formathit.md) + +## IndexPattern.formatHit property + +Signature: + +```typescript +formatHit: { + (hit: Record, type?: string): any; + formatField: FormatFieldFn; + }; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getaggregationrestrictions.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getaggregationrestrictions.md new file mode 100644 index 0000000000000..b655e779e4fa4 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getaggregationrestrictions.md @@ -0,0 +1,29 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [getAggregationRestrictions](./kibana-plugin-plugins-data-server.indexpattern.getaggregationrestrictions.md) + +## IndexPattern.getAggregationRestrictions() method + +Signature: + +```typescript +getAggregationRestrictions(): Record> | undefined; +``` +Returns: + +`Record> | undefined` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md new file mode 100644 index 0000000000000..f1bdb2f729414 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md @@ -0,0 +1,35 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [getAsSavedObjectBody](./kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md) + +## IndexPattern.getAsSavedObjectBody() method + +Returns index pattern as saved object body for saving + +Signature: + +```typescript +getAsSavedObjectBody(): { + title: string; + timeFieldName: string | undefined; + intervalName: string | undefined; + sourceFilters: string | undefined; + fields: string | undefined; + fieldFormatMap: string | undefined; + type: string | undefined; + typeMeta: string | undefined; + }; +``` +Returns: + +`{ + title: string; + timeFieldName: string | undefined; + intervalName: string | undefined; + sourceFilters: string | undefined; + fields: string | undefined; + fieldFormatMap: string | undefined; + type: string | undefined; + typeMeta: string | undefined; + }` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getcomputedfields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getcomputedfields.md new file mode 100644 index 0000000000000..eab6ae9bf9033 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getcomputedfields.md @@ -0,0 +1,29 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [getComputedFields](./kibana-plugin-plugins-data-server.indexpattern.getcomputedfields.md) + +## IndexPattern.getComputedFields() method + +Signature: + +```typescript +getComputedFields(): { + storedFields: string[]; + scriptFields: any; + docvalueFields: { + field: any; + format: string; + }[]; + }; +``` +Returns: + +`{ + storedFields: string[]; + scriptFields: any; + docvalueFields: { + field: any; + format: string; + }[]; + }` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getfieldbyname.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getfieldbyname.md new file mode 100644 index 0000000000000..712be3b72828a --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getfieldbyname.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [getFieldByName](./kibana-plugin-plugins-data-server.indexpattern.getfieldbyname.md) + +## IndexPattern.getFieldByName() method + +Signature: + +```typescript +getFieldByName(name: string): IndexPatternField | undefined; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| name | string | | + +Returns: + +`IndexPatternField | undefined` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getformatterforfield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getformatterforfield.md new file mode 100644 index 0000000000000..7dc2756009f4e --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getformatterforfield.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [getFormatterForField](./kibana-plugin-plugins-data-server.indexpattern.getformatterforfield.md) + +## IndexPattern.getFormatterForField() method + +Provide a field, get its formatter + +Signature: + +```typescript +getFormatterForField(field: IndexPatternField | IndexPatternField['spec'] | IFieldType): FieldFormat; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| field | IndexPatternField | IndexPatternField['spec'] | IFieldType | | + +Returns: + +`FieldFormat` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getnonscriptedfields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getnonscriptedfields.md new file mode 100644 index 0000000000000..89d79d9b750fa --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getnonscriptedfields.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [getNonScriptedFields](./kibana-plugin-plugins-data-server.indexpattern.getnonscriptedfields.md) + +## IndexPattern.getNonScriptedFields() method + +Signature: + +```typescript +getNonScriptedFields(): IndexPatternField[]; +``` +Returns: + +`IndexPatternField[]` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getoriginalsavedobjectbody.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getoriginalsavedobjectbody.md new file mode 100644 index 0000000000000..324f9d0152ab5 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getoriginalsavedobjectbody.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [getOriginalSavedObjectBody](./kibana-plugin-plugins-data-server.indexpattern.getoriginalsavedobjectbody.md) + +## IndexPattern.getOriginalSavedObjectBody property + +Get last saved saved object fields + +Signature: + +```typescript +getOriginalSavedObjectBody: () => { + title?: string | undefined; + timeFieldName?: string | undefined; + intervalName?: string | undefined; + fields?: string | undefined; + sourceFilters?: string | undefined; + fieldFormatMap?: string | undefined; + typeMeta?: string | undefined; + type?: string | undefined; + }; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getscriptedfields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getscriptedfields.md new file mode 100644 index 0000000000000..edfff8ec5efac --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getscriptedfields.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [getScriptedFields](./kibana-plugin-plugins-data-server.indexpattern.getscriptedfields.md) + +## IndexPattern.getScriptedFields() method + +Signature: + +```typescript +getScriptedFields(): IndexPatternField[]; +``` +Returns: + +`IndexPatternField[]` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getsourcefiltering.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getsourcefiltering.md new file mode 100644 index 0000000000000..240f9b4fb0aa2 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getsourcefiltering.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [getSourceFiltering](./kibana-plugin-plugins-data-server.indexpattern.getsourcefiltering.md) + +## IndexPattern.getSourceFiltering() method + +Get the source filtering configuration for that index. + +Signature: + +```typescript +getSourceFiltering(): { + excludes: any[]; + }; +``` +Returns: + +`{ + excludes: any[]; + }` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.gettimefield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.gettimefield.md new file mode 100644 index 0000000000000..b5806f883fb9f --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.gettimefield.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [getTimeField](./kibana-plugin-plugins-data-server.indexpattern.gettimefield.md) + +## IndexPattern.getTimeField() method + +Signature: + +```typescript +getTimeField(): IndexPatternField | undefined; +``` +Returns: + +`IndexPatternField | undefined` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.id.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.id.md new file mode 100644 index 0000000000000..8fad82bd06705 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [id](./kibana-plugin-plugins-data-server.indexpattern.id.md) + +## IndexPattern.id property + +Signature: + +```typescript +id?: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.intervalname.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.intervalname.md new file mode 100644 index 0000000000000..caaa6929235f8 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.intervalname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [intervalName](./kibana-plugin-plugins-data-server.indexpattern.intervalname.md) + +## IndexPattern.intervalName property + +Signature: + +```typescript +intervalName: string | undefined; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.istimebased.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.istimebased.md new file mode 100644 index 0000000000000..790744979942d --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.istimebased.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [isTimeBased](./kibana-plugin-plugins-data-server.indexpattern.istimebased.md) + +## IndexPattern.isTimeBased() method + +Signature: + +```typescript +isTimeBased(): boolean; +``` +Returns: + +`boolean` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.istimebasedwildcard.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.istimebasedwildcard.md new file mode 100644 index 0000000000000..7ef5e8318040a --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.istimebasedwildcard.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [isTimeBasedWildcard](./kibana-plugin-plugins-data-server.indexpattern.istimebasedwildcard.md) + +## IndexPattern.isTimeBasedWildcard() method + +Signature: + +```typescript +isTimeBasedWildcard(): boolean; +``` +Returns: + +`boolean` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.istimenanosbased.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.istimenanosbased.md new file mode 100644 index 0000000000000..22fb60eba4f6e --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.istimenanosbased.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [isTimeNanosBased](./kibana-plugin-plugins-data-server.indexpattern.istimenanosbased.md) + +## IndexPattern.isTimeNanosBased() method + +Signature: + +```typescript +isTimeNanosBased(): boolean; +``` +Returns: + +`boolean` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md new file mode 100644 index 0000000000000..d877854444a09 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.md @@ -0,0 +1,60 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) + +## IndexPattern class + +Signature: + +```typescript +export declare class IndexPattern implements IIndexPattern +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)({ spec, savedObjectsClient, fieldFormats, shortDotsEnable, metaFields, })](./kibana-plugin-plugins-data-server.indexpattern._constructor_.md) | | Constructs a new instance of the IndexPattern class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [fieldFormatMap](./kibana-plugin-plugins-data-server.indexpattern.fieldformatmap.md) | | Record<string, any> | | +| [fields](./kibana-plugin-plugins-data-server.indexpattern.fields.md) | | IIndexPatternFieldList & {
toSpec: () => IndexPatternFieldMap;
} | | +| [flattenHit](./kibana-plugin-plugins-data-server.indexpattern.flattenhit.md) | | (hit: Record<string, any>, deep?: boolean) => Record<string, any> | | +| [formatField](./kibana-plugin-plugins-data-server.indexpattern.formatfield.md) | | FormatFieldFn | | +| [formatHit](./kibana-plugin-plugins-data-server.indexpattern.formathit.md) | | {
(hit: Record<string, any>, type?: string): any;
formatField: FormatFieldFn;
} | | +| [getOriginalSavedObjectBody](./kibana-plugin-plugins-data-server.indexpattern.getoriginalsavedobjectbody.md) | | () => {
title?: string | undefined;
timeFieldName?: string | undefined;
intervalName?: string | undefined;
fields?: string | undefined;
sourceFilters?: string | undefined;
fieldFormatMap?: string | undefined;
typeMeta?: string | undefined;
type?: string | undefined;
} | Get last saved saved object fields | +| [id](./kibana-plugin-plugins-data-server.indexpattern.id.md) | | string | | +| [intervalName](./kibana-plugin-plugins-data-server.indexpattern.intervalname.md) | | string | undefined | | +| [metaFields](./kibana-plugin-plugins-data-server.indexpattern.metafields.md) | | string[] | | +| [resetOriginalSavedObjectBody](./kibana-plugin-plugins-data-server.indexpattern.resetoriginalsavedobjectbody.md) | | () => void | Reset last saved saved object fields. used after saving | +| [sourceFilters](./kibana-plugin-plugins-data-server.indexpattern.sourcefilters.md) | | SourceFilter[] | | +| [timeFieldName](./kibana-plugin-plugins-data-server.indexpattern.timefieldname.md) | | string | undefined | | +| [title](./kibana-plugin-plugins-data-server.indexpattern.title.md) | | string | | +| [type](./kibana-plugin-plugins-data-server.indexpattern.type.md) | | string | undefined | | +| [typeMeta](./kibana-plugin-plugins-data-server.indexpattern.typemeta.md) | | TypeMeta | | +| [version](./kibana-plugin-plugins-data-server.indexpattern.version.md) | | string | undefined | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [addScriptedField(name, script, fieldType, lang)](./kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md) | | Add scripted field to field list | +| [getAggregationRestrictions()](./kibana-plugin-plugins-data-server.indexpattern.getaggregationrestrictions.md) | | | +| [getAsSavedObjectBody()](./kibana-plugin-plugins-data-server.indexpattern.getassavedobjectbody.md) | | Returns index pattern as saved object body for saving | +| [getComputedFields()](./kibana-plugin-plugins-data-server.indexpattern.getcomputedfields.md) | | | +| [getFieldByName(name)](./kibana-plugin-plugins-data-server.indexpattern.getfieldbyname.md) | | | +| [getFormatterForField(field)](./kibana-plugin-plugins-data-server.indexpattern.getformatterforfield.md) | | Provide a field, get its formatter | +| [getNonScriptedFields()](./kibana-plugin-plugins-data-server.indexpattern.getnonscriptedfields.md) | | | +| [getScriptedFields()](./kibana-plugin-plugins-data-server.indexpattern.getscriptedfields.md) | | | +| [getSourceFiltering()](./kibana-plugin-plugins-data-server.indexpattern.getsourcefiltering.md) | | Get the source filtering configuration for that index. | +| [getTimeField()](./kibana-plugin-plugins-data-server.indexpattern.gettimefield.md) | | | +| [isTimeBased()](./kibana-plugin-plugins-data-server.indexpattern.istimebased.md) | | | +| [isTimeBasedWildcard()](./kibana-plugin-plugins-data-server.indexpattern.istimebasedwildcard.md) | | | +| [isTimeNanosBased()](./kibana-plugin-plugins-data-server.indexpattern.istimenanosbased.md) | | | +| [popularizeField(fieldName, unit)](./kibana-plugin-plugins-data-server.indexpattern.popularizefield.md) | | | +| [removeScriptedField(fieldName)](./kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md) | | Remove scripted field from field list | +| [toSpec()](./kibana-plugin-plugins-data-server.indexpattern.tospec.md) | | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.metafields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.metafields.md new file mode 100644 index 0000000000000..a2c7c806d6057 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.metafields.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [metaFields](./kibana-plugin-plugins-data-server.indexpattern.metafields.md) + +## IndexPattern.metaFields property + +Signature: + +```typescript +metaFields: string[]; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.popularizefield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.popularizefield.md new file mode 100644 index 0000000000000..8b2c9242a6256 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.popularizefield.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [popularizeField](./kibana-plugin-plugins-data-server.indexpattern.popularizefield.md) + +## IndexPattern.popularizeField() method + +Signature: + +```typescript +popularizeField(fieldName: string, unit?: number): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| fieldName | string | | +| unit | number | | + +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md new file mode 100644 index 0000000000000..3162a7f42dd12 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [removeScriptedField](./kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md) + +## IndexPattern.removeScriptedField() method + +Remove scripted field from field list + +Signature: + +```typescript +removeScriptedField(fieldName: string): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| fieldName | string | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.resetoriginalsavedobjectbody.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.resetoriginalsavedobjectbody.md new file mode 100644 index 0000000000000..18ec7070bd577 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.resetoriginalsavedobjectbody.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [resetOriginalSavedObjectBody](./kibana-plugin-plugins-data-server.indexpattern.resetoriginalsavedobjectbody.md) + +## IndexPattern.resetOriginalSavedObjectBody property + +Reset last saved saved object fields. used after saving + +Signature: + +```typescript +resetOriginalSavedObjectBody: () => void; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.sourcefilters.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.sourcefilters.md new file mode 100644 index 0000000000000..d359bef2f30a9 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.sourcefilters.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [sourceFilters](./kibana-plugin-plugins-data-server.indexpattern.sourcefilters.md) + +## IndexPattern.sourceFilters property + +Signature: + +```typescript +sourceFilters?: SourceFilter[]; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.timefieldname.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.timefieldname.md new file mode 100644 index 0000000000000..35740afa4e3dc --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.timefieldname.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [timeFieldName](./kibana-plugin-plugins-data-server.indexpattern.timefieldname.md) + +## IndexPattern.timeFieldName property + +Signature: + +```typescript +timeFieldName: string | undefined; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.title.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.title.md new file mode 100644 index 0000000000000..4cebde989aebd --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.title.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [title](./kibana-plugin-plugins-data-server.indexpattern.title.md) + +## IndexPattern.title property + +Signature: + +```typescript +title: string; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.tospec.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.tospec.md new file mode 100644 index 0000000000000..5d76b8f00853b --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.tospec.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [toSpec](./kibana-plugin-plugins-data-server.indexpattern.tospec.md) + +## IndexPattern.toSpec() method + +Signature: + +```typescript +toSpec(): IndexPatternSpec; +``` +Returns: + +`IndexPatternSpec` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.type.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.type.md new file mode 100644 index 0000000000000..01154ab5444d1 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.type.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [type](./kibana-plugin-plugins-data-server.indexpattern.type.md) + +## IndexPattern.type property + +Signature: + +```typescript +type: string | undefined; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.typemeta.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.typemeta.md new file mode 100644 index 0000000000000..b16bcec404d97 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.typemeta.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [typeMeta](./kibana-plugin-plugins-data-server.indexpattern.typemeta.md) + +## IndexPattern.typeMeta property + +Signature: + +```typescript +typeMeta?: TypeMeta; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.version.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.version.md new file mode 100644 index 0000000000000..e4297d8389111 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.version.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) > [version](./kibana-plugin-plugins-data-server.indexpattern.version.md) + +## IndexPattern.version property + +Signature: + +```typescript +version: string | undefined; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md index 4a5b61f5c179b..40b029da00469 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternattributes.md @@ -4,12 +4,6 @@ ## IndexPatternAttributes interface -> Warning: This API is now obsolete. -> -> - -Use data plugin interface instead - Signature: ```typescript diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.aggregatable.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.aggregatable.md deleted file mode 100644 index 92994b851ec85..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.aggregatable.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternFieldDescriptor](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.md) > [aggregatable](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.aggregatable.md) - -## IndexPatternFieldDescriptor.aggregatable property - -Signature: - -```typescript -aggregatable: boolean; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.estypes.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.estypes.md deleted file mode 100644 index f24ba9a48d85e..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.estypes.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternFieldDescriptor](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.md) > [esTypes](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.estypes.md) - -## IndexPatternFieldDescriptor.esTypes property - -Signature: - -```typescript -esTypes: string[]; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.md deleted file mode 100644 index d84d0cba06ac6..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternFieldDescriptor](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.md) - -## IndexPatternFieldDescriptor interface - -Signature: - -```typescript -export interface FieldDescriptor -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [aggregatable](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.aggregatable.md) | boolean | | -| [esTypes](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.estypes.md) | string[] | | -| [name](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.name.md) | string | | -| [readFromDocValues](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.readfromdocvalues.md) | boolean | | -| [searchable](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.searchable.md) | boolean | | -| [subType](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.subtype.md) | FieldSubType | | -| [type](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.type.md) | string | | - diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.name.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.name.md deleted file mode 100644 index 16ea60c5b8ae2..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.name.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternFieldDescriptor](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.md) > [name](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.name.md) - -## IndexPatternFieldDescriptor.name property - -Signature: - -```typescript -name: string; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.readfromdocvalues.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.readfromdocvalues.md deleted file mode 100644 index fc8667196c879..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.readfromdocvalues.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternFieldDescriptor](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.md) > [readFromDocValues](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.readfromdocvalues.md) - -## IndexPatternFieldDescriptor.readFromDocValues property - -Signature: - -```typescript -readFromDocValues: boolean; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.searchable.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.searchable.md deleted file mode 100644 index 7d159c65b40bd..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.searchable.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternFieldDescriptor](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.md) > [searchable](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.searchable.md) - -## IndexPatternFieldDescriptor.searchable property - -Signature: - -```typescript -searchable: boolean; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.subtype.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.subtype.md deleted file mode 100644 index 7053eaf08138c..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.subtype.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternFieldDescriptor](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.md) > [subType](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.subtype.md) - -## IndexPatternFieldDescriptor.subType property - -Signature: - -```typescript -subType?: FieldSubType; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.type.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.type.md deleted file mode 100644 index bb571d1bee14a..0000000000000 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternfielddescriptor.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternFieldDescriptor](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.md) > [type](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.type.md) - -## IndexPatternFieldDescriptor.type property - -Signature: - -```typescript -type: string; -``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md index 6bd3bbf2433cd..52382372d6d96 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md @@ -12,6 +12,9 @@ Get a list of field objects for an index pattern that may contain wildcards getFieldsForWildcard(options: { pattern: string | string[]; metaFields?: string[]; + fieldCapsOptions?: { + allowNoIndices: boolean; + }; }): Promise; ``` @@ -19,7 +22,7 @@ getFieldsForWildcard(options: { | Parameter | Type | Description | | --- | --- | --- | -| options | {
pattern: string | string[];
metaFields?: string[];
} | | +| options | {
pattern: string | string[];
metaFields?: string[];
fieldCapsOptions?: {
allowNoIndices: boolean;
};
} | | Returns: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md new file mode 100644 index 0000000000000..aa78c055f4f5c --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) + +## IndexPatternsService class + +Signature: + +```typescript +export declare class IndexPatternsService implements Plugin +``` + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [setup(core)](./kibana-plugin-plugins-data-server.indexpatternsservice.setup.md) | | | +| [start(core, { fieldFormats, logger })](./kibana-plugin-plugins-data-server.indexpatternsservice.start.md) | | | + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setup.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setup.md new file mode 100644 index 0000000000000..a354fbc2a477b --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setup.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [setup](./kibana-plugin-plugins-data-server.indexpatternsservice.setup.md) + +## IndexPatternsService.setup() method + +Signature: + +```typescript +setup(core: CoreSetup): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreSetup | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md new file mode 100644 index 0000000000000..d35dc3aa11000 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.start.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [start](./kibana-plugin-plugins-data-server.indexpatternsservice.start.md) + +## IndexPatternsService.start() method + +Signature: + +```typescript +start(core: CoreStart, { fieldFormats, logger }: IndexPatternsServiceStartDeps): { + indexPatternsServiceFactory: (kibanaRequest: KibanaRequest) => Promise; + }; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| core | CoreStart | | +| { fieldFormats, logger } | IndexPatternsServiceStartDeps | | + +Returns: + +`{ + indexPatternsServiceFactory: (kibanaRequest: KibanaRequest) => Promise; + }` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index 3c477e17503f4..7113ac935907f 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -9,7 +9,9 @@ | Class | Description | | --- | --- | | [AggParamType](./kibana-plugin-plugins-data-server.aggparamtype.md) | | +| [IndexPattern](./kibana-plugin-plugins-data-server.indexpattern.md) | | | [IndexPatternsFetcher](./kibana-plugin-plugins-data-server.indexpatternsfetcher.md) | | +| [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) | | | [OptionedParamType](./kibana-plugin-plugins-data-server.optionedparamtype.md) | | | [Plugin](./kibana-plugin-plugins-data-server.plugin.md) | | @@ -41,14 +43,13 @@ | --- | --- | | [AggParamOption](./kibana-plugin-plugins-data-server.aggparamoption.md) | | | [EsQueryConfig](./kibana-plugin-plugins-data-server.esqueryconfig.md) | | +| [FieldDescriptor](./kibana-plugin-plugins-data-server.fielddescriptor.md) | | | [FieldFormatConfig](./kibana-plugin-plugins-data-server.fieldformatconfig.md) | | | [IEsSearchRequest](./kibana-plugin-plugins-data-server.iessearchrequest.md) | | | [IEsSearchResponse](./kibana-plugin-plugins-data-server.iessearchresponse.md) | | | [IFieldSubType](./kibana-plugin-plugins-data-server.ifieldsubtype.md) | | | [IFieldType](./kibana-plugin-plugins-data-server.ifieldtype.md) | | -| [IIndexPattern](./kibana-plugin-plugins-data-server.iindexpattern.md) | | -| [IndexPatternAttributes](./kibana-plugin-plugins-data-server.indexpatternattributes.md) | Use data plugin interface instead | -| [IndexPatternFieldDescriptor](./kibana-plugin-plugins-data-server.indexpatternfielddescriptor.md) | | +| [IndexPatternAttributes](./kibana-plugin-plugins-data-server.indexpatternattributes.md) | | | [ISearchOptions](./kibana-plugin-plugins-data-server.isearchoptions.md) | | | [ISearchSetup](./kibana-plugin-plugins-data-server.isearchsetup.md) | | | [ISearchStart](./kibana-plugin-plugins-data-server.isearchstart.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md index 455c5ecdd8195..84aeb4cf80cce 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md @@ -13,7 +13,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; + indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; }; }; ``` @@ -32,7 +32,7 @@ start(core: CoreStart): { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; indexPatterns: { - indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; + indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; }; }` diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index ed20166c87f29..1bae04cc2e58b 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -1,9 +1,10 @@ [[advanced-options]] == Advanced Settings -The *Advanced Settings* UI enables you to edit settings that control the behavior of Kibana. -For example, you can change the format used to display dates, specify the default index pattern, and set the precision -for displayed decimal values. +The *Advanced Settings* UI enables you to edit settings that control the +behavior of Kibana. For example, you can change the format used to display dates, +specify the default index pattern, and set the precision for displayed decimal +values. . Open the menu, then go to *Stack Management > {kib} > Advanced Settings*. . Scroll or search for the setting you want to modify. @@ -15,8 +16,9 @@ for displayed decimal values. [[settings-read-only-access]] === [xpack]#Read only access# When you have insufficient privileges to edit advanced settings, the following -indicator in Kibana will be displayed. The buttons to edit settings won't be visible. -For more information on granting access to Kibana see <>. +indicator in Kibana will be displayed. The buttons to edit settings won't be +visible. For more information on granting access to Kibana, see +<>. [role="screenshot"] image::images/settings-read-only-badge.png[Example of Advanced Settings Management's read only access indicator in Kibana's header] @@ -25,12 +27,11 @@ image::images/settings-read-only-badge.png[Example of Advanced Settings Manageme [[kibana-settings-reference]] === Kibana settings reference -WARNING: Modifying a setting can affect {kib} -performance and cause problems that are -difficult to diagnose. Setting a property value to a blank field reverts -to the default behavior, which might not be -compatible with other configuration settings. Deleting a custom setting -removes it from {kib} permanently. +WARNING: Modifying a setting can affect {kib} performance and cause problems +that are difficult to diagnose. Setting a property value to a blank field +reverts to the default behavior, which might not be compatible with other +configuration settings. Deleting a custom setting removes it from {kib} +permanently. [float] @@ -38,72 +39,159 @@ removes it from {kib} permanently. ==== General [horizontal] -`csv:quoteValues`:: Set this property to `true` to quote exported values. -`csv:separator`:: A string that serves as the separator for exported values. -`dateFormat`:: The format to use for displaying https://momentjs.com/docs/#/displaying/format/[pretty formatted dates]. -`dateFormat:dow`:: The day that a week should start on. -`dateFormat:scaled`:: The values that define the format to use to render ordered time-based data. Formatted timestamps must -adapt to the interval between measurements. Keys are http://en.wikipedia.org/wiki/ISO_8601#Time_intervals[ISO8601 intervals]. -`dateFormat:tz`:: The timezone that Kibana uses. The default value of `Browser` uses the timezone detected by the browser. -`dateNanosFormat`:: The format to use for displaying https://momentjs.com/docs/#/displaying/format/[pretty formatted dates] of {ref}/date_nanos.html[Elasticsearch date_nanos type]. -`defaultIndex`:: The index to access if no index is set. The default is `null`. -`defaultRoute`:: The default route when opening Kibana. Use this setting to route users to a specific dashboard, application, or saved object as they enter each space. -`fields:popularLimit`:: The top N most popular fields to show. -`filterEditor:suggestValues`:: Set this property to `false` to prevent the filter editor from suggesting values for fields. -`filters:pinnedByDefault`:: Set this property to `true` to make filters have a global state (be pinned) by default. -`format:bytes:defaultPattern`:: The default <> format for the "bytes" format. -`format:currency:defaultPattern`:: The default <> format for the "currency" format. -`format:defaultTypeMap`:: A map of the default format name for each field type. Field types that are not explicitly -mentioned use "\_default_". -`format:number:defaultLocale`:: The <> locale. -`format:number:defaultPattern`:: The <> for the "number" format. -`format:percent:defaultPattern`:: The <> for the "percent" format. -`histogram:barTarget`:: When date histograms use the `auto` interval, Kibana attempts to generate this number of bars. -`histogram:maxBars`:: Date histograms are not generated with more bars than the value of this property, scaling values -when necessary. -`history:limit`:: In fields that have history, such as query inputs, show this many recent values. -`indexPattern:placeholder`:: The default placeholder value to use in Management > Index Patterns > Create Index Pattern. -`metaFields`:: Fields that exist outside of `_source`. Kibana merges these fields -into the document when displaying it. -`metrics:max_buckets`:: The maximum numbers of buckets that a single -data source can return. This might arise when the user selects a -short interval (for example, 1s) for a long time period (1 year). -`pageNavigation`:: The style of navigation menu for Kibana. -Choices are Legacy, the legacy style where every plugin is represented in the nav, -and Modern, a new format that bundles related plugins together in flyaway nested navigation. -`query:allowLeadingWildcards`:: Allows a wildcard (*) as the first character -in a query clause. Only applies when experimental query features are -enabled in the query bar. To disallow leading wildcards in Lucene queries, -use `query:queryString:options`. -`query:queryString:options`:: Options for the Lucene query string parser. Only -used when "Query language" is set to Lucene. -`savedObjects:listingLimit`:: The number of objects to fetch for lists of saved objects. -The default value is 1000. Do not set above 10000. -`savedObjects:perPage`:: The number of objects to show on each page of the -list of saved objects. The default is 5. -`search:queryLanguage`:: The query language to use in the query bar. -Choices are <>, a language built specifically for {kib}, and the <>. -`shortDots:enable`:: Set this property to `true` to shorten long -field names in visualizations. For example, show `f.b.baz` instead of `foo.bar.baz`. -`sort:options`:: Options for the Elasticsearch {ref}/search-request-body.html#request-body-search-sort[sort] parameter. -`state:storeInSessionStorage`:: [experimental] Kibana tracks UI state in the -URL, which can lead to problems when there is a lot of state information, -and the URL gets very long. -Enabling this setting stores part of the URL in your browser session to keep the -URL short. -`theme:darkMode`:: Set to `true` to enable a dark mode for the {kib} UI. You must -refresh the page to apply the setting. -`timepicker:quickRanges`:: The list of ranges to show in the Quick section of -the time filter. This should be an array of objects, with each object containing -`from`, `to` (see {ref}/common-options.html#date-math[accepted formats]), -and `display` (the title to be displayed). -`timepicker:refreshIntervalDefaults`:: The default refresh interval for the time filter. Example: `{ "display": "15 seconds", "pause": true, "value": 15000 }`. -`timepicker:timeDefaults`:: The default selection in the time filter. -`truncate:maxHeight`:: The maximum height that a cell occupies in a table. Set to 0 to disable +[[csv-quotevalues]]`csv:quoteValues`:: +Set this property to `true` to quote exported values. + +[[csv-separator]]`csv:separator`:: +A string that serves as the separator for exported values. + +[[dateformat]]`dateFormat`:: +The format to use for displaying +https://momentjs.com/docs/#/displaying/format/[pretty formatted dates]. + +[[dateformat-dow]]`dateFormat:dow`:: +The day that a week should start on. + +[[dateformat-scaled]]`dateFormat:scaled`:: +The values that define the format to use to render ordered time-based data. +Formatted timestamps must adapt to the interval between measurements. Keys are +http://en.wikipedia.org/wiki/ISO_8601#Time_intervals[ISO8601 intervals]. + +[[dateformat-tz]]`dateFormat:tz`:: +The timezone that Kibana uses. The default value of `Browser` uses the timezone +detected by the browser. + +[[datenanosformat]]`dateNanosFormat`:: +The format to use for displaying +https://momentjs.com/docs/#/displaying/format/[pretty formatted dates] of +{ref}/date_nanos.html[Elasticsearch date_nanos type]. + +[[defaultindex]]`defaultIndex`:: +The index to access if no index is set. The default is `null`. + +[[defaultroute]]`defaultRoute`:: +The default route when opening Kibana. Use this setting to route users to a +specific dashboard, application, or saved object as they enter each space. + +[[fields-popularlimit]]`fields:popularLimit`:: +The top N most popular fields to show. + +[[filtereditor-suggestvalues]]`filterEditor:suggestValues`:: +Set this property to `false` to prevent the filter editor from suggesting values +for fields. + +[[filters-pinnedbydefault]]`filters:pinnedByDefault`:: +Set this property to `true` to make filters have a global state (be pinned) by +default. + +[[format-bytes-defaultpattern]]`format:bytes:defaultPattern`:: +The default <> format for the "bytes" format. + +[[format-currency-defaultpattern]]`format:currency:defaultPattern`:: +The default <> format for the "currency" format. + +[[format-defaulttypemap]]`format:defaultTypeMap`:: +A map of the default format name for each field type. Field types that are not +explicitly mentioned use "\_default_". + +[[format-number-defaultlocale]]`format:number:defaultLocale`:: +The <> locale. + +[[format-number-defaultpattern]]`format:number:defaultPattern`:: +The <> for the "number" format. + +[[format-percent-defaultpattern]]`format:percent:defaultPattern`:: +The <> for the "percent" format. + +[[histogram-bartarget]]`histogram:barTarget`:: +When date histograms use the `auto` interval, Kibana attempts to generate this +number of bars. + +[[histogram-maxbars]]`histogram:maxBars`:: +Date histograms are not generated with more bars than the value of this property, +scaling values when necessary. + +[[history-limit]]`history:limit`:: +In fields that have history, such as query inputs, show this many recent values. + +[[indexpattern-placeholder]]`indexPattern:placeholder`:: +The default placeholder value to use in +*Management > Index Patterns > Create Index Pattern*. + +[[metafields]]`metaFields`:: +Fields that exist outside of `_source`. Kibana merges these fields into the +document when displaying it. + +[[metrics-maxbuckets]]`metrics:max_buckets`:: +The maximum numbers of buckets that a single data source can return. This might +arise when the user selects a short interval (for example, 1s) for a long time +period (1 year). + +[[pagenavigation]]`pageNavigation`:: +The style of navigation menu for Kibana. Choices are Legacy, the legacy style +where every plugin is represented in the nav, and Modern, a new format that +bundles related plugins together in flyaway nested navigation. + +[[query-allowleadingwildcards]]`query:allowLeadingWildcards`:: +Allows a wildcard (*) as the first character in a query clause. Only applies +when experimental query features are enabled in the query bar. To disallow +leading wildcards in Lucene queries, use `query:queryString:options`. + +[[query-querystring-options]]`query:queryString:options`:: +Options for the Lucene query string parser. Only used when "Query language" is +set to Lucene. + +[[savedobjects-listinglimit]]`savedObjects:listingLimit`:: +The number of objects to fetch for lists of saved objects. The default value +is 1000. Do not set above 10000. + +[[savedobjects-perpage]]`savedObjects:perPage`:: +The number of objects to show on each page of the list of saved objects. The +default is 5. + +[[search-querylanguage]]`search:queryLanguage`:: +The query language to use in the query bar. Choices are <>, a +language built specifically for {kib}, and the +<>. + +[[shortdots-enable]]`shortDots:enable`:: +Set this property to `true` to shorten long field names in visualizations. For +example, show `f.b.baz` instead of `foo.bar.baz`. + +[[sort-options]]`sort:options`:: Options for the Elasticsearch +{ref}/search-request-body.html#request-body-search-sort[sort] parameter. + +[[state-storeinsessionstorage]]`state:storeInSessionStorage`:: +experimental:[] +Kibana tracks UI state in the URL, which can lead to problems +when there is a lot of state information, and the URL gets very long. Enabling +this setting stores part of the URL in your browser session to keep the URL +short. + +[[theme-darkmode]]`theme:darkMode`:: +Set to `true` to enable a dark mode for the {kib} UI. You must refresh the page +to apply the setting. + +[[timepicker-quickranges]]`timepicker:quickRanges`:: +The list of ranges to show in the Quick section of the time filter. This should +be an array of objects, with each object containing `from`, `to` (see +{ref}/common-options.html#date-math[accepted formats]), and `display` (the title +to be displayed). + +[[timepicker-refreshintervaldefaults]]`timepicker:refreshIntervalDefaults`:: +The default refresh interval for the time filter. Example: +`{ "display": "15 seconds", "pause": true, "value": 15000 }`. + +[[timepicker-timedefaults]]`timepicker:timeDefaults`:: +The default selection in the time filter. + +[[truncate-maxheight]]`truncate:maxHeight`:: +The maximum height that a cell occupies in a table. Set to 0 to disable truncation. -`xPack:defaultAdminEmail`:: Email address for X-Pack admin operations, such as -cluster alert notifications from Monitoring. + +[[xpack-defaultadminemail]]`xPack:defaultAdminEmail`:: +Email address for {xpack} admin operations, such as cluster alert notifications +from *{stack-monitor-app}*. [float] @@ -111,15 +199,17 @@ cluster alert notifications from Monitoring. ==== Accessibility [horizontal] -`accessibility:disableAnimations`:: Turns off all unnecessary animations in the -{kib} UI. Refresh the page to apply the changes. +[[accessibility-disableanimations]]`accessibility:disableAnimations`:: +Turns off all unnecessary animations in the {kib} UI. Refresh the page to apply +the changes. [float] [[kibana-dashboard-settings]] ==== Dashboard [horizontal] -`xpackDashboardMode:roles`:: **Deprecated. Use <> instead.** +[[xpackdashboardmode-roles]]`xpackDashboardMode:roles`:: +**Deprecated. Use <> instead.** The roles that belong to <>. [float] @@ -127,38 +217,63 @@ The roles that belong to <>. ==== Discover [horizontal] -`context:defaultSize`:: The number of surrounding entries to display in the context view. The default value is 5. -`context:step`:: The number by which to increment or decrement the context size. The default value is 5. -`context:tieBreakerFields`:: A comma-separated list of fields to use -for breaking a tie between documents that have the same timestamp value. The first -field that is present and sortable in the current index pattern is used. -`defaultColumns`:: The columns that appear by default on the Discover page. -The default is `_source`. -`discover:aggs:terms:size`:: The number terms that are visualized when clicking -the Visualize button in the field drop down. The default is `20`. -`discover:sampleSize`:: The number of rows to show in the Discover table. -`discover:sort:defaultOrder`:: The default sort direction for time-based index patterns. -`discover:searchOnPageLoad`:: Controls whether a search is executed when Discover first loads. -This setting does not have an effect when loading a saved search. -`doc_table:hideTimeColumn`:: Hides the "Time" column in Discover and in all saved searches on dashboards. -`doc_table:highlight`:: Highlights results in Discover and saved searches on dashboards. -Highlighting slows requests when -working on big documents. +[[context-defaultsize]]`context:defaultSize`:: +The number of surrounding entries to display in the context view. The default +value is 5. + +[[context-step]]`context:step`:: +The number by which to increment or decrement the context size. The default +value is 5. + +[[context-tiebreakerfields]]`context:tieBreakerFields`:: +A comma-separated list of fields to use for breaking a tie between documents +that have the same timestamp value. The first field that is present and sortable +in the current index pattern is used. + +[[defaultcolumns]]`defaultColumns`:: +The columns that appear by default on the *Discover* page. The default is +`_source`. + +[[discover-aggs-terms-size]]`discover:aggs:terms:size`:: +The number terms that are visualized when clicking the *Visualize* button in the +field drop down. The default is `20`. + +[[discover-samplesize]]`discover:sampleSize`:: +The number of rows to show in the *Discover* table. + +[[discover-sort-defaultorder]]`discover:sort:defaultOrder`:: +The default sort direction for time-based index patterns. + +[[discover-searchonpageload]]`discover:searchOnPageLoad`:: +Controls whether a search is executed when *Discover* first loads. This setting +does not have an effect when loading a saved search. + +[[doctable-hidetimecolumn]]`doc_table:hideTimeColumn`:: +Hides the "Time" column in *Discover* and in all saved searches on dashboards. + +[[doctable-highlight]]`doc_table:highlight`:: +Highlights results in *Discover* and saved searches on dashboards. Highlighting +slows requests when working on big documents. [float] [[kibana-ml-settings]] ==== Machine learning [horizontal] -`ml:anomalyDetection:results:enableTimeDefaults`:: Use the default time filter -in the *Single Metric Viewer* and *Anomaly Explorer*. If this setting is -disabled, the results for the full time range are shown. -`ml:anomalyDetection:results:timeDefaults`:: Sets the default time filter for -viewing {anomaly-job} results. This setting must contain `from` and `to` values (see {ref}/common-options.html#date-math[accepted formats]). It is ignored -unless `ml:anomalyDetection:results:enableTimeDefaults` is enabled. -`ml:fileDataVisualizerMaxFileSize`:: Sets the file size limit when importing -data in the {data-viz}. The default value is `100MB`. The highest supported -value for this setting is `1GB`. +[[ml-anomalydetection-results-enabletimedefaults]]`ml:anomalyDetection:results:enableTimeDefaults`:: +Use the default time filter in the *Single Metric Viewer* and +*Anomaly Explorer*. If this setting is disabled, the results for the full time +range are shown. + +[[ml-anomalydetection-results-timedefaults]]`ml:anomalyDetection:results:timeDefaults`:: +Sets the default time filter for viewing {anomaly-job} results. This setting +must contain `from` and `to` values (see +{ref}/common-options.html#date-math[accepted formats]). It is ignored unless +`ml:anomalyDetection:results:enableTimeDefaults` is enabled. + +[[ml-filedatavisualizermaxfilesize]]`ml:fileDataVisualizerMaxFileSize`:: +Sets the file size limit when importing data in the {data-viz}. The default +value is `100MB`. The highest supported value for this setting is `1GB`. [float] @@ -166,18 +281,26 @@ value for this setting is `1GB`. ==== Notifications [horizontal] -`notifications:banner`:: A custom banner intended for temporary notices to all users. -Supports https://help.github.com/en/articles/basic-writing-and-formatting-syntax[Markdown]. -`notifications:lifetime:banner`:: The duration, in milliseconds, for banner -notification displays. The default value is 3000000. Set this field to `Infinity` -to disable banner notifications. -`notifications:lifetime:error`:: The duration, in milliseconds, for error -notification displays. The default value is 300000. Set this field to `Infinity` to disable error notifications. -`notifications:lifetime:info`:: The duration, in milliseconds, for information notification displays. -The default value is 5000. Set this field to `Infinity` to disable information notifications. -`notifications:lifetime:warning`:: The duration, in milliseconds, for warning notification -displays. The default value is 10000. Set this field to `Infinity` to disable warning notifications. +[[notifications-banner]]`notifications:banner`:: +A custom banner intended for temporary notices to all users. Supports +https://help.github.com/en/articles/basic-writing-and-formatting-syntax[Markdown]. +[[notifications-lifetime-banner]]`notifications:lifetime:banner`:: +The duration, in milliseconds, for banner notification displays. The default +value is 3000000. Set this field to `Infinity` to disable banner notifications. + +[[notificatios-lifetime-error]]`notifications:lifetime:error`:: +The duration, in milliseconds, for error notification displays. The default +value is 300000. Set this field to `Infinity` to disable error notifications. + +[[notifications-lifetime-info]]`notifications:lifetime:info`:: +The duration, in milliseconds, for information notification displays. The +default value is 5000. Set this field to `Infinity` to disable information +notifications. + +[[notifications-lifetime-warning]]`notifications:lifetime:warning`:: +The duration, in milliseconds, for warning notification displays. The default +value is 10000. Set this field to `Infinity` to disable warning notifications. [float] @@ -185,7 +308,8 @@ displays. The default value is 10000. Set this field to `Infinity` to disable wa ==== Reporting [horizontal] -`xpackReporting:customPdfLogo`:: A custom image to use in the footer of the PDF. +[[xpackreporting-custompdflogo]]`xpackReporting:customPdfLogo`:: +A custom image to use in the footer of the PDF. [float] @@ -193,9 +317,10 @@ displays. The default value is 10000. Set this field to `Infinity` to disable wa ==== Rollup [horizontal] -`rollups:enableIndexPatterns`:: Enables the creation of index patterns that -capture rollup indices, which in turn enables visualizations based on rollup data. -Refresh the page to apply the changes. +[[rollups-enableindexpatterns]]`rollups:enableIndexPatterns`:: +Enables the creation of index patterns that capture rollup indices, which in +turn enables visualizations based on rollup data. Refresh the page to apply the +changes. [float] @@ -203,67 +328,117 @@ Refresh the page to apply the changes. ==== Search [horizontal] -`courier:batchSearches`:: **Deprecated in 7.6. Starting in 8.0, this setting will be optimized internally.** -When disabled, dashboard panels will load individually, and search requests will terminate when -users navigate away or update the query. When enabled, dashboard panels will load together when all of the data is loaded, -and searches will not terminate. -`courier:customRequestPreference`:: {ref}/search-request-body.html#request-body-search-preference[Request preference] +[[courier-batchsearches]]`courier:batchSearches`:: +**Deprecated in 7.6. Starting in 8.0, this setting will be optimized internally.** +When disabled, dashboard panels will load individually, and search requests will +terminate when users navigate away or update the query. When enabled, dashboard +panels will load together when all of the data is loaded, and searches will not +terminate. + +[[courier-customrequestpreference]]`courier:customRequestPreference`:: +{ref}/search-request-body.html#request-body-search-preference[Request preference] to use when `courier:setRequestPreference` is set to "custom". -`courier:ignoreFilterIfFieldNotInIndex`:: Skips filters that apply to fields that don't exist in the index for a visualization. -Useful when dashboards consist of visualizations from multiple index patterns. -`courier:maxConcurrentShardRequests`:: Controls the {ref}/search-multi-search.html[max_concurrent_shard_requests] + +[[courier-ignorefilteriffieldnotinindex]]`courier:ignoreFilterIfFieldNotInIndex`:: +Skips filters that apply to fields that don't exist in the index for a +visualization. Useful when dashboards consist of visualizations from multiple +index patterns. + +[[courier-maxconcurrentshardrequests]]`courier:maxConcurrentShardRequests`:: +Controls the {ref}/search-multi-search.html[max_concurrent_shard_requests] setting used for `_msearch` requests sent by {kib}. Set to 0 to disable this config and use the {es} default. -`courier:setRequestPreference`:: Enables you to set which shards handle your search requests. -* *Session ID:* Restricts operations to execute all search requests on the same shards. -This has the benefit of reusing shard caches across requests. -* *Custom:* Allows you to define your own preference. Use `courier:customRequestPreference` -to customize your preference value. + +[[courier-setrequestpreference]]`courier:setRequestPreference`:: +Enables you to set which shards handle your search requests. +* *Session ID:* Restricts operations to execute all search requests on the same +shards. This has the benefit of reusing shard caches across requests. +* *Custom:* Allows you to define your own preference. Use +`courier:customRequestPreference` to customize your preference value. * *None:* Do not set a preference. This might provide better performance because requests can be spread across all shard copies. However, results might be inconsistent because different shards might be in different refresh states. -`search:includeFrozen`:: Includes {ref}/frozen-indices.html[frozen indices] in results. -Searching through frozen indices -might increase the search time. This setting is off by default. Users must opt-in to include frozen indices. -`search:timeout`:: Change the maximum timeout for a search session or set to 0 to disable the timeout and allow queries to run to completion. + +[[search-includefrozen]]`search:includeFrozen`:: +Includes {ref}/frozen-indices.html[frozen indices] in results. Searching through +frozen indices might increase the search time. This setting is off by default. +Users must opt-in to include frozen indices. + +[[search-timeout]]`search:timeout`:: Change the maximum timeout for a search +session or set to 0 to disable the timeout and allow queries to run to +completion. [float] [[kibana-siem-settings]] -==== Security Solution +==== Security solution [horizontal] -`securitySolution:defaultAnomalyScore`:: The threshold above which Machine Learning job anomalies are displayed in the Security app. -`securitySolution:defaultIndex`:: A comma-delimited list of Elasticsearch indices from which the Security app collects events. -`securitySolution:ipReputationLinks`:: A JSON array containing links for verifying the reputation of an IP address. The links are displayed on -{security-guide}/network-page-overview.html[IP detail] pages. -`securitySolution:enableNewsFeed`:: Enables the security news feed on the Security *Overview* -page. -`securitySolution:newsFeedUrl`:: The URL from which the security news feed content is -retrieved. -`securitySolution:refreshIntervalDefaults`:: The default refresh interval for the Security time filter, in milliseconds. -`securitySolution:timeDefaults`:: The default period of time in the Security time filter. +[[securitysolution-defaultanomalyscore]]`securitySolution:defaultAnomalyScore`:: +The threshold above which {ml} job anomalies are displayed in the {security-app}. + +[[securitysolution-defaultindex]]`securitySolution:defaultIndex`:: +A comma-delimited list of {es} indices from which the {security-app} collects +events. + +[[securitysolution-ipreputationlinks]]`securitySolution:ipReputationLinks`:: +A JSON array containing links for verifying the reputation of an IP address. The +links are displayed on {security-guide}/network-page-overview.html[IP detail] +pages. + +[[securitysolution-enablenewsfeed]]`securitySolution:enableNewsFeed`:: Enables +the security news feed on the Security *Overview* page. + +[[securitysolution-newsfeedurl]]`securitySolution:newsFeedUrl`:: +The URL from which the security news feed content is retrieved. + +[[securitysolution-refreshintervaldefaults]]`securitySolution:refreshIntervalDefaults`:: +The default refresh interval for the Security time filter, in milliseconds. + +[[securitysolution-timedefaults]]`securitySolution:timeDefaults`:: +The default period of time in the Security time filter. [float] [[kibana-timelion-settings]] ==== Timelion [horizontal] -`timelion:default_columns`:: The default number of columns to use on a Timelion sheet. -`timelion:default_rows`:: The default number of rows to use on a Timelion sheet. -`timelion:es.default_index`:: The default index when using the `.es()` query. -`timelion:es.timefield`:: The default field containing a timestamp when using the `.es()` query. -`timelion:graphite.url`:: [experimental] Used with graphite queries, this is the URL of your graphite host -in the form https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite. This URL can be -selected from an allow-list configured in the `kibana.yml` under `timelion.graphiteUrls`. -`timelion:max_buckets`:: The maximum number of buckets a single data source can return. -This value is used for calculating automatic intervals in visualizations. -`timelion:min_interval`:: The smallest interval to calculate when using "auto". -`timelion:quandl.key`:: [experimental] Used with quandl queries, this is your API key from https://www.quandl.com/[www.quandl.com]. -`timelion:showTutorial`:: Shows the Timelion tutorial -to users when they first open the Timelion app. -`timelion:target_buckets`:: Used for calculating automatic intervals in visualizations, -this is the number of buckets to try to represent. +[[timelion-defaultcolumns]]`timelion:default_columns`:: +The default number of columns to use on a Timelion sheet. + +[[timelion-defaultrows]]`timelion:default_rows`:: +The default number of rows to use on a Timelion sheet. + +[[timelion-esdefaultindex]]`timelion:es.default_index`:: +The default index when using the `.es()` query. + +[[timelion-estimefield]]`timelion:es.timefield`:: +The default field containing a timestamp when using the `.es()` query. + +[[timelion-graphite-url]]`timelion:graphite.url`:: +experimental:[] +Used with graphite queries, this is the URL of your graphite host +in the form https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite. This URL can +be selected from an allow-list configured in the `kibana.yml` under +`timelion.graphiteUrls`. + +[[timelion-maxbuckets]]`timelion:max_buckets`:: +The maximum number of buckets a single data source can return. This value is +used for calculating automatic intervals in visualizations. +[[timelion-mininterval]]`timelion:min_interval`:: +The smallest interval to calculate when using "auto". + +[[timelion-quandlkey]]`timelion:quandl.key`:: +experimental:[] +Used with quandl queries, this is your API key from +https://www.quandl.com/[www.quandl.com]. + +[[timelion-showtutorial]]`timelion:showTutorial`:: +Shows the Timelion tutorial to users when they first open the Timelion app. + +[[timelion-targetbuckets]]`timelion:target_buckets`:: +Used for calculating automatic intervals in visualizations, this is the number +of buckets to try to represent. [float] @@ -271,20 +446,32 @@ this is the number of buckets to try to represent. ==== Visualization [horizontal] -`visualization:colorMapping`:: Maps values to specified colors in visualizations. -`visualization:dimmingOpacity`:: The opacity of the chart items that are dimmed -when highlighting another element of the chart. The lower this number, the more -the highlighted element stands out. This must be a number between 0 and 1. -`visualization:loadingDelay`:: The time to wait before dimming visualizations -during a query. -`visualization:regionmap:showWarnings`:: Shows -a warning in a region map when terms cannot be joined to a shape. -`visualization:tileMap:WMSdefaults`:: The default properties for the WMS map server support in the coordinate map. -`visualization:tileMap:maxPrecision`:: The maximum geoHash precision displayed on tile maps: 7 is high, 10 is very high, +[[visualization-colormapping]]`visualization:colorMapping`:: +Maps values to specified colors in visualizations. + +[[visualization-dimmingopacity]]`visualization:dimmingOpacity`:: +The opacity of the chart items that are dimmed when highlighting another element +of the chart. The lower this number, the more the highlighted element stands out. +This must be a number between 0 and 1. + +[[visualization-loadingdelay]]`visualization:loadingDelay`:: +The time to wait before dimming visualizations during a query. + +[[visualization-regionmap-showwarnings]]`visualization:regionmap:showWarnings`:: +Shows a warning in a region map when terms cannot be joined to a shape. + +[[visualization-tilemap-wmsdefaults]]`visualization:tileMap:WMSdefaults`:: +The default properties for the WMS map server support in the coordinate map. + +[[visualization-tilemap-maxprecision]]`visualization:tileMap:maxPrecision`:: +The maximum geoHash precision displayed on tile maps: 7 is high, 10 is very high, and 12 is the maximum. See this {ref}/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator[explanation of cell dimensions]. -`visualize:enableLabs`:: Enables users to create, view, and edit experimental visualizations. -If disabled, only visualizations that are considered production-ready are available to the user. + +[[visualize-enablelabs]]`visualize:enableLabs`:: +Enables users to create, view, and edit experimental visualizations. If disabled, +only visualizations that are considered production-ready are available to the +user. [float] diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index 6c8632efa9cc0..917821ad09e2f 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -33,7 +33,7 @@ For more information, see |=== | `monitoring.enabled` | Set to `true` (default) to enable the {monitor-features} in {kib}. Unlike the - `monitoring.ui.enabled` setting, when this setting is `false`, the + <> setting, when this setting is `false`, the monitoring back-end does not run and {kib} stats are not sent to the monitoring cluster. @@ -44,7 +44,7 @@ a|`monitoring.cluster_alerts.` | `monitoring.ui.elasticsearch.hosts` | Specifies the location of the {es} cluster where your monitoring data is stored. - By default, this is the same as `elasticsearch.hosts`. This setting enables + By default, this is the same as <>. This setting enables you to use a single {kib} instance to search and visualize data in your production cluster as well as monitor data sent to a dedicated monitoring cluster. @@ -58,7 +58,7 @@ a|`monitoring.cluster_alerts.` cluster uses the authenticated user's credentials, which must be the same on both the {es} monitoring cluster and the {es} production cluster. + + - If not set, {kib} uses the value of the `elasticsearch.username` setting. + If not set, {kib} uses the value of the <> setting. | `monitoring.ui.elasticsearch.password` | Specifies the password used by {kib} monitoring to establish a persistent connection @@ -69,11 +69,11 @@ a|`monitoring.cluster_alerts.` cluster uses the authenticated user's credentials, which must be the same on both the {es} monitoring cluster and the {es} production cluster. + + - If not set, {kib} uses the value of the `elasticsearch.password` setting. + If not set, {kib} uses the value of the <> setting. | `monitoring.ui.elasticsearch.pingTimeout` | Specifies the time in milliseconds to wait for {es} to respond to internal - health checks. By default, it matches the `elasticsearch.pingTimeout` setting, + health checks. By default, it matches the <> setting, which has a default value of `30000`. |=== @@ -112,7 +112,7 @@ about configuring {kib}, see | Specifies the number of log entries to display in *{stack-monitor-app}*. Defaults to `10`. The maximum value is `50`. -| `monitoring.ui.enabled` +|[[monitoring-ui-enabled]] `monitoring.ui.enabled` | Set to `false` to hide *{stack-monitor-app}*. The monitoring back-end continues to run as an agent for sending {kib} stats to the monitoring cluster. Defaults to `true`. diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index 3489dcd018293..adfc3964d4204 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -20,7 +20,7 @@ You can configure `xpack.reporting` settings in your `kibana.yml` to: | [[xpack-enable-reporting]]`xpack.reporting.enabled` {ess-icon} | Set to `false` to disable the {report-features}. -| `xpack.reporting.encryptionKey` {ess-icon} +|[[xpack-reporting-encryptionKey]] `xpack.reporting.encryptionKey` {ess-icon} | Set to an alphanumeric, at least 32 characters long text string. By default, {kib} will generate a random key when it starts, which will cause pending reports to fail after restart. Configure this setting to preserve the same key across multiple restarts and multiple instances of {kib}. @@ -53,20 +53,20 @@ proxy host requires that the {kib} server has network access to the proxy. [cols="2*<"] |=== | `xpack.reporting.kibanaServer.port` - | The port for accessing {kib}, if different from the `server.port` value. + | The port for accessing {kib}, if different from the <> value. | `xpack.reporting.kibanaServer.protocol` | The protocol for accessing {kib}, typically `http` or `https`. -| `xpack.reporting.kibanaServer.hostname` - | The hostname for accessing {kib}, if different from the `server.host` value. +|[[xpack-kibanaServer-hostname]] `xpack.reporting.kibanaServer.hostname` + | The hostname for accessing {kib}, if different from the <> value. |=== [NOTE] ============ Reporting authenticates requests on the Kibana page only when the hostname matches the -`xpack.reporting.kibanaServer.hostname` setting. Therefore Reporting would fail if the +<> setting. Therefore Reporting would fail if the set value redirects to another server. For that reason, `"0"` is an invalid setting because, in the Reporting browser, it becomes an automatic redirect to `"0.0.0.0"`. ============ @@ -97,8 +97,8 @@ reports, you might need to change the following settings. [NOTE] ============ Running multiple instances of {kib} in a cluster for load balancing of -reporting requires identical values for `xpack.reporting.encryptionKey` and, if -security is enabled, `xpack.security.encryptionKey`. +reporting requires identical values for <> and, if +security is enabled, <>. ============ [cols="2*<"] @@ -177,7 +177,7 @@ available, but there will likely be errors in the visualizations in the report. [[reporting-chromium-settings]] ==== Chromium settings -When `xpack.reporting.capture.browser.type` is set to `chromium` (default) you can also specify the following settings. +When <> is set to `chromium` (default) you can also specify the following settings. [cols="2*<"] |=== @@ -229,10 +229,10 @@ a| `xpack.reporting.capture.browser` See OWASP: https://www.owasp.org/index.php/CSV_Injection Defaults to `true`. -| `xpack.reporting.csv.enablePanelActionDownload` - | Enables CSV export from a saved search on a dashboard. This action is available in the dashboard - panel menu for the saved search. - Defaults to `true`. +| `xpack.reporting.csv` `.enablePanelActionDownload` + | Enables CSV export from a saved search on a dashboard. This action is available in the dashboard panel menu for the saved search. + *Note:* This setting exists for backwards compatibility, but is unused and hardcoded to `true`. CSV export from a saved search on a dashboard + is enabled when Reporting is enabled. |=== @@ -246,7 +246,7 @@ a| `xpack.reporting.capture.browser` | Reporting uses a weekly index in {es} to store the reporting job and the report content. The index is automatically created if it does not already exist. Configure this to a unique value, beginning with `.reporting-`, for every - {kib} instance that has a unique `kibana.index` setting. Defaults to `.reporting`. + {kib} instance that has a unique <> setting. Defaults to `.reporting`. | `xpack.reporting.roles.allow` | Specifies the roles in addition to superusers that can use reporting. diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index b6eecc6ea9f04..00e5f973f7d87 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -190,26 +190,26 @@ You can configure the following settings in the `kibana.yml` file. | `xpack.security.cookieName` | Sets the name of the cookie used for the session. The default value is `"sid"`. -| `xpack.security.encryptionKey` +|[[xpack-security-encryptionKey]] `xpack.security.encryptionKey` | An arbitrary string of 32 characters or more that is used to encrypt session information. Do **not** expose this key to users of {kib}. By default, a value is automatically generated in memory. If you use that default behavior, all sessions are invalidated when {kib} restarts. In addition, high-availability deployments of {kib} will behave unexpectedly if this setting isn't the same for all instances of {kib}. -| `xpack.security.secureCookies` +|[[xpack-security-secureCookies]] `xpack.security.secureCookies` | Sets the `secure` flag of the session cookie. The default value is `false`. It - is automatically set to `true` if `server.ssl.enabled` is set to `true`. Set + is automatically set to `true` if <> is set to `true`. Set this to `true` if SSL is configured outside of {kib} (for example, you are routing requests through a load balancer or proxy). | `xpack.security.sameSiteCookies` {ess-icon} | Sets the `SameSite` attribute of the session cookie. This allows you to declare whether your cookie should be restricted to a first-party or same-site context. Valid values are `Strict`, `Lax`, `None`. - This is *not set* by default, which modern browsers will treat as `Lax`. If you use Kibana embedded in an iframe in modern browsers, you might need to set it to `None`. Setting this value to `None` requires cookies to be sent over a secure connection by setting `xpack.security.secureCookies: true`. + This is *not set* by default, which modern browsers will treat as `Lax`. If you use Kibana embedded in an iframe in modern browsers, you might need to set it to `None`. Setting this value to `None` requires cookies to be sent over a secure connection by setting <>: `true`. -| `xpack.security.session.idleTimeout` {ess-icon} - | Ensures that user sessions will expire after a period of inactivity. This and `xpack.security.session.lifespan` are both +|[[xpack-session-idleTimeout]] `xpack.security.session.idleTimeout` {ess-icon} + | Ensures that user sessions will expire after a period of inactivity. This and <> are both highly recommended. By default, this setting is not set. 2+a| @@ -218,9 +218,9 @@ highly recommended. By default, this setting is not set. The format is a string of `[ms\|s\|m\|h\|d\|w\|M\|Y]` (e.g. '20m', '24h', '7d', '1w'). ============ -| `xpack.security.session.lifespan` {ess-icon} +|[[xpack-session-lifespan]] `xpack.security.session.lifespan` {ess-icon} | Ensures that user sessions will expire after the defined time period. This behavior also known as an "absolute timeout". If -this is _not_ set, user sessions could stay active indefinitely. This and `xpack.security.session.idleTimeout` are both highly +this is _not_ set, user sessions could stay active indefinitely. This and <> are both highly recommended. By default, this setting is not set. 2+a| diff --git a/docs/settings/telemetry-settings.asciidoc b/docs/settings/telemetry-settings.asciidoc index 89c018a86eca6..0329e2f010e80 100644 --- a/docs/settings/telemetry-settings.asciidoc +++ b/docs/settings/telemetry-settings.asciidoc @@ -19,7 +19,7 @@ See our https://www.elastic.co/legal/privacy-statement[Privacy Statement] to lea [cols="2*<"] |=== -| `telemetry.enabled` +|[[telemetry-enabled]] `telemetry.enabled` | Set to `true` to send cluster statistics to Elastic. Reporting your cluster statistics helps us improve your user experience. Your data is never shared with anyone. Set to `false` to disable statistics reporting from any @@ -31,16 +31,16 @@ See our https://www.elastic.co/legal/privacy-statement[Privacy Statement] to lea it is behind a firewall and falls back to `'browser'` to send it from users' browsers when they are navigating through {kib}. Defaults to `'server'`. -| `telemetry.optIn` +|[[telemetry-optIn]] `telemetry.optIn` | Set to `true` to automatically opt into reporting cluster statistics. You can also opt out through *Advanced Settings* in {kib}. Defaults to `true`. | `telemetry.allowChangingOptInStatus` - | Set to `true` to allow overwriting the `telemetry.optIn` setting via the {kib} UI. Defaults to `true`. + + | Set to `true` to allow overwriting the <> setting via the {kib} UI. Defaults to `true`. + |=== [NOTE] ============ -When `false`, `telemetry.optIn` must be `true`. To disable telemetry and not allow users to change that parameter, use `telemetry.enabled`. +When `false`, <> must be `true`. To disable telemetry and not allow users to change that parameter, use <>. ============ diff --git a/docs/setup/secure-settings.asciidoc b/docs/setup/secure-settings.asciidoc index 10380eb5d8fa4..840a18ac03bab 100644 --- a/docs/setup/secure-settings.asciidoc +++ b/docs/setup/secure-settings.asciidoc @@ -19,7 +19,7 @@ bin/kibana-keystore create ---------------------------------------------------------------- The file `kibana.keystore` will be created in the directory defined by the -`path.data` configuration setting. +<> configuration setting. [float] [[list-settings]] diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index f03022e9e9f00..7f48f21db7197 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -20,11 +20,11 @@ which may cause a delay before pages start being served. Set to `false` to disable Console. *Default: `true`* | `cpu.cgroup.path.override:` - | *deprecated* This setting has been renamed to `ops.cGroupOverrides.cpuPath` + | *deprecated* This setting has been renamed to <> and the old name will no longer be supported as of 8.0. | `cpuacct.cgroup.path.override:` - | *deprecated* This setting has been renamed to `ops.cGroupOverrides.cpuAcctPath` + | *deprecated* This setting has been renamed to <> and the old name will no longer be supported as of 8.0. | `csp.rules:` @@ -33,7 +33,7 @@ that disables certain unnecessary and potentially insecure capabilities in the browser. It is strongly recommended that you keep the default CSP rules that ship with {kib}. -| `csp.strict:` +|[[csp-strict]] `csp.strict:` | Blocks {kib} access to any browser that does not enforce even rudimentary CSP rules. In practice, this disables support for older, less safe browsers like Internet Explorer. @@ -43,15 +43,15 @@ For more information, refer to <>. | `csp.warnLegacyBrowsers:` | Shows a warning message after loading {kib} to any browser that does not enforce even rudimentary CSP rules, though {kib} is still accessible. This -configuration is effectively ignored when `csp.strict` is enabled. +configuration is effectively ignored when <> is enabled. *Default: `true`* | `elasticsearch.customHeaders:` | Header names and values to send to {es}. Any custom headers cannot be overwritten by client-side headers, regardless of the -`elasticsearch.requestHeadersWhitelist` configuration. *Default: `{}`* +<> configuration. *Default: `{}`* -| `elasticsearch.hosts:` +|[[elasticsearch-hosts]] `elasticsearch.hosts:` | The URLs of the {es} instances to use for all your queries. All nodes listed here must be on the same cluster. *Default: `[ "http://localhost:9200" ]`* + @@ -59,28 +59,28 @@ To enable SSL/TLS for outbound connections to {es}, use the `https` protocol in this setting. | `elasticsearch.logQueries:` - | Log queries sent to {es}. Requires `logging.verbose` set to `true`. + | Log queries sent to {es}. Requires <> set to `true`. This is useful for seeing the query DSL generated by applications that currently do not have an inspector, for example Timelion and Monitoring. *Default: `false`* -| `elasticsearch.pingTimeout:` +|[[elasticsearch-pingTimeout]] `elasticsearch.pingTimeout:` | Time in milliseconds to wait for {es} to respond to pings. -*Default: the value of the `elasticsearch.requestTimeout` setting* +*Default: the value of the <> setting* | `elasticsearch.preserveHost:` | When the value is `true`, {kib} uses the hostname specified in the -`server.host` setting. When the value is `false`, {kib} uses +<> setting. When the value is `false`, {kib} uses the hostname of the host that connects to this {kib} instance. *Default: `true`* -| `elasticsearch.requestHeadersWhitelist:` +|[[elasticsearch-requestHeadersWhitelist]] `elasticsearch.requestHeadersWhitelist:` | List of {kib} client-side headers to send to {es}. To send *no* client-side headers, set this value to [] (an empty list). Removing the `authorization` header from being whitelisted means that you cannot use <> in {kib}. *Default: `[ 'authorization' ]`* -| `elasticsearch.requestTimeout:` +|[[elasticsearch-requestTimeout]] `elasticsearch.requestTimeout:` | Time in milliseconds to wait for responses from the back end or {es}. This value must be a positive integer. *Default: `30000`* @@ -99,7 +99,7 @@ nodes. *Default: `false`* | Update the list of {es} nodes immediately following a connection fault. *Default: `false`* -| `elasticsearch.ssl.alwaysPresentCertificate:` +|[[elasticsearch-ssl-alwaysPresentCertificate]] `elasticsearch.ssl.alwaysPresentCertificate:` | Controls {kib} behavior in regard to presenting a client certificate when requested by {es}. This setting applies to all outbound SSL/TLS connections to {es}, including requests that are proxied for end users. *Default: `false`* @@ -109,7 +109,7 @@ to {es}, including requests that are proxied for end users. *Default: `false`* [WARNING] ============ When {es} uses certificates to authenticate end users with a PKI realm -and `elasticsearch.ssl.alwaysPresentCertificate` is `true`, +and <> is `true`, proxied requests may be executed as the identity that is tied to the {kib} server. ============ @@ -117,7 +117,7 @@ server. [cols="2*<"] |=== -| `elasticsearch.ssl.certificate:` and `elasticsearch.ssl.key:` +|[[elasticsearch-ssl-cert-key]] `elasticsearch.ssl.certificate:` and `elasticsearch.ssl.key:` | Paths to a PEM-encoded X.509 client certificate and its corresponding private key. These are used by {kib} to authenticate itself when making outbound SSL/TLS connections to {es}. For this setting to take effect, the @@ -129,27 +129,29 @@ be set to `"required"` or `"optional"` to request a client certificate from [NOTE] ============ -These settings cannot be used in conjunction with `elasticsearch.ssl.keystore.path`. +These settings cannot be used in conjunction with +<>. ============ [cols="2*<"] |=== -| `elasticsearch.ssl.certificateAuthorities:` +|[[elasticsearch-ssl-certificateAuthorities]] `elasticsearch.ssl.certificateAuthorities:` | Paths to one or more PEM-encoded X.509 certificate authority (CA) certificates, which make up a trusted certificate chain for {es}. This chain is used by {kib} to establish trust when making outbound SSL/TLS connections to {es}. + In addition to this setting, trusted certificates may be specified via -`elasticsearch.ssl.keystore.path` and/or `elasticsearch.ssl.truststore.path`. +<> and/or +<>. | `elasticsearch.ssl.keyPassphrase:` | The password that decrypts the private key that is specified -via `elasticsearch.ssl.key`. This value is optional, as the key may not be +via <>. This value is optional, as the key may not be encrypted. -| `elasticsearch.ssl.keystore.path:` +|[[elasticsearch-ssl-keystore-path]] `elasticsearch.ssl.keystore.path:` | Path to a PKCS#12 keystore that contains an X.509 client certificate and it's corresponding private key. These are used by {kib} to authenticate itself when making outbound SSL/TLS connections to {es}. For this setting, you must also set @@ -160,15 +162,15 @@ If the keystore contains any additional certificates, they are used as a trusted certificate chain for {es}. This chain is used by {kib} to establish trust when making outbound SSL/TLS connections to {es}. In addition to this setting, trusted certificates may be specified via -`elasticsearch.ssl.certificateAuthorities` and/or -`elasticsearch.ssl.truststore.path`. +<> and/or +<>. |=== [NOTE] ============ This setting cannot be used in conjunction with -`elasticsearch.ssl.certificate` or `elasticsearch.ssl.key`. +<> or <>. ============ [cols="2*<"] @@ -176,24 +178,24 @@ This setting cannot be used in conjunction with | `elasticsearch.ssl.keystore.password:` | The password that decrypts the keystore specified via -`elasticsearch.ssl.keystore.path`. If the keystore has no password, leave this +<>. If the keystore has no password, leave this as blank. If the keystore has an empty password, set this to `""`. -| `elasticsearch.ssl.truststore.path:`:: +|[[elasticsearch-ssl-truststore-path]] `elasticsearch.ssl.truststore.path:` | Path to a PKCS#12 trust store that contains one or more X.509 certificate authority (CA) certificates, which make up a trusted certificate chain for {es}. This chain is used by {kib} to establish trust when making outbound SSL/TLS connections to {es}. + In addition to this setting, trusted certificates may be specified via -`elasticsearch.ssl.certificateAuthorities` and/or -`elasticsearch.ssl.keystore.path`. +<> and/or +<>. |`elasticsearch.ssl.truststore.password:` | The password that decrypts the trust store specified via -`elasticsearch.ssl.truststore.path`. If the trust store has no password, -leave this as blank. If the trust store has an empty password, set this to `""`. +<>. If the trust store +has no password, leave this as blank. If the trust store has an empty password, set this to `""`. | `elasticsearch.ssl.verificationMode:` | Controls the verification of the server certificate that {kib} receives when @@ -206,7 +208,7 @@ verification entirely. *Default: `"full"`* | Time in milliseconds to wait for {es} at {kib} startup before retrying. *Default: `5000`* -| `elasticsearch.username:` and `elasticsearch.password:` +|[[elasticsearch-user-passwd]] `elasticsearch.username:` and `elasticsearch.password:` | If your {es} is protected with basic authentication, these settings provide the username and password that the {kib} server uses to perform maintenance on the {kib} index at startup. {kib} users still need to authenticate with @@ -220,7 +222,7 @@ on the {kib} index at startup. {kib} users still need to authenticate with Please use the `defaultRoute` advanced setting instead. The default application to load. *Default: `"home"`* -| `kibana.index:` +|[[kibana-index]] `kibana.index:` | {kib} uses an index in {es} to store saved searches, visualizations, and dashboards. {kib} creates a new index if the index doesn’t already exist. If you configure a custom index, the name must be lowercase, and conform to the @@ -236,7 +238,7 @@ This value must be a whole number greater than zero. *Default: `"1000"`* suggestions. This value must be a whole number greater than zero. *Default: `"100000"`* -| `logging.dest:` +|[[logging-dest]] `logging.dest:` | Enables you to specify a file where {kib} stores log output. *Default: `stdout`* @@ -244,7 +246,7 @@ suggestions. This value must be a whole number greater than zero. | Logs output as JSON. When set to `true`, the logs are formatted as JSON strings that include timestamp, log level, context, message text, and any other metadata that may be associated with the log message. -When `logging.dest.stdout` is set, and there is no interactive terminal ("TTY"), +When <> is set, and there is no interactive terminal ("TTY"), this setting defaults to `true`. *Default: `false`* | `logging.quiet:` @@ -271,7 +273,7 @@ The following example shows a valid logging rotate configuration: | `logging.rotate.enabled:` | experimental[] Set the value of this setting to `true` to -enable log rotation. If you do not have a `logging.dest` set that is different from `stdout` +enable log rotation. If you do not have a <> set that is different from `stdout` that feature would not take any effect. *Default: `false`* | `logging.rotate.everyBytes:` @@ -286,9 +288,9 @@ option has to be in the range of 2 to 1024 files. *Default: `7`* | `logging.rotate.pollingInterval:` | experimental[] The number of milliseconds for the polling strategy in case -the `logging.rotate.usePolling` is enabled. `logging.rotate.usePolling` must be in the 5000 to 3600000 millisecond range. *Default: `10000`* +the <> is enabled. `logging.rotate.usePolling` must be in the 5000 to 3600000 millisecond range. *Default: `10000`* -| `logging.rotate.usePolling:` +|[[logging-rotate-usePolling]] `logging.rotate.usePolling:` | experimental[] By default we try to understand the best way to monitoring the log file and warning about it. Please be aware there are some systems where watch api is not accurate. In those cases, in order to get the feature working, the `polling` method could be used enabling that option. *Default: `false`* @@ -308,8 +310,8 @@ requests. *Default: `false`* | `map.includeElasticMapsService:` {ess-icon} | Set to `false` to disable connections to Elastic Maps Service. -When `includeElasticMapsService` is turned off, only the vector layers configured by `map.regionmap` -and the tile layer configured by `map.tilemap.url` are available in <>. *Default: `true`* +When `includeElasticMapsService` is turned off, only the vector layers configured by <> +and the tile layer configured by <> are available in <>. *Default: `true`* | `map.proxyElasticMapsServiceInMaps:` | Set to `true` to proxy all <> Elastic Maps Service @@ -427,7 +429,7 @@ override this parameter to use their own Tile Map Service. For example: system for the {kib} UI notification center. Set to `false` to disable the newsfeed system. *Default: `true`* -| `path.data:` +|[[path-data]] `path.data:` | The path where {kib} stores persistent data not saved in {es}. *Default: `data`* @@ -438,17 +440,17 @@ not saved in {es}. *Default: `data`* | Set the interval in milliseconds to sample system and process performance metrics. The minimum value is 100. *Default: `5000`* -| `ops.cGroupOverrides.cpuPath:` +|[[ops-cGroupOverrides-cpuPath]] `ops.cGroupOverrides.cpuPath:` | Override for cgroup cpu path when mounted in a manner that is inconsistent with `/proc/self/cgroup`. -| `ops.cGroupOverrides.cpuAcctPath:` +|[[ops-cGroupOverrides-cpuAcctPath]] `ops.cGroupOverrides.cpuAcctPath:` | Override for cgroup cpuacct path when mounted in a manner that is inconsistent with `/proc/self/cgroup`. -| `server.basePath:` +|[[server-basePath]] `server.basePath:` | Enables you to specify a path to mount {kib} at if you are -running behind a proxy. Use the `server.rewriteBasePath` setting to tell {kib} +running behind a proxy. Use the <> setting to tell {kib} if it should remove the basePath from requests it receives, and to prevent a deprecation warning at startup. This setting cannot end in a slash (`/`). @@ -458,19 +460,19 @@ deprecation warning at startup. This setting cannot end in a slash (`/`). | `server.compression.referrerWhitelist:` | Specifies an array of trusted hostnames, such as the {kib} host, or a reverse proxy sitting in front of it. This determines whether HTTP compression may be used for responses, based on the request `Referer` header. -This setting may not be used when `server.compression.enabled` is set to `false`. *Default: `none`* +This setting may not be used when <> is set to `false`. *Default: `none`* | `server.customResponseHeaders:` {ess-icon} | Header names and values to send on all responses to the client from the {kib} server. *Default: `{}`* -| `server.host:` +|[[server-host]] `server.host:` | This setting specifies the host of the back end server. To allow remote users to connect, set the value to the IP address or DNS name of the {kib} server. *Default: `"localhost"`* | `server.keepaliveTimeout:` | The number of milliseconds to wait for additional data before restarting -the `server.socketTimeout` counter. *Default: `"120000"`* +the <> counter. *Default: `"120000"`* | `server.maxPayloadBytes:` | The maximum payload size in bytes @@ -480,28 +482,28 @@ for incoming server requests. *Default: `1048576`* | A human-readable display name that identifies this {kib} instance. *Default: `"your-hostname"`* -| `server.port:` +|[[server-port]] `server.port:` | {kib} is served by a back end server. This setting specifies the port to use. *Default: `5601`* -| `server.requestId.allowFromAnyIp:` +|[[server-requestId-allowFromAnyIp]] `server.requestId.allowFromAnyIp:` | Sets whether or not the X-Opaque-Id header should be trusted from any IP address for identifying requests in logs and forwarded to Elasticsearch. | `server.requestId.ipAllowlist:` - | A list of IPv4 and IPv6 address which the `X-Opaque-Id` header should be trusted from. Normally this would be set to the IP addresses of the load balancers or reverse-proxy that end users use to access Kibana. If any are set, `server.requestId.allowFromAnyIp` must also be set to `false.` + | A list of IPv4 and IPv6 address which the `X-Opaque-Id` header should be trusted from. Normally this would be set to the IP addresses of the load balancers or reverse-proxy that end users use to access Kibana. If any are set, <> must also be set to `false.` -| `server.rewriteBasePath:` +|[[server-rewriteBasePath]] `server.rewriteBasePath:` | Specifies whether {kib} should -rewrite requests that are prefixed with `server.basePath` or require that they +rewrite requests that are prefixed with <> or require that they are rewritten by your reverse proxy. In {kib} 6.3 and earlier, the default is `false`. In {kib} 7.x, the setting is deprecated. In {kib} 8.0 and later, the default is `true`. *Default: `deprecated`* -| `server.socketTimeout:` +|[[server-socketTimeout]] `server.socketTimeout:` | The number of milliseconds to wait before closing an inactive socket. *Default: `"120000"`* -| `server.ssl.certificate:` and `server.ssl.key:` +|[[server-ssl-cert-key]] `server.ssl.certificate:` and `server.ssl.key:` | Paths to a PEM-encoded X.509 server certificate and its corresponding private key. These are used by {kib} to establish trust when receiving inbound SSL/TLS connections from users. @@ -509,18 +511,18 @@ are used by {kib} to establish trust when receiving inbound SSL/TLS connections [NOTE] ============ -These settings cannot be used in conjunction with `server.ssl.keystore.path`. +These settings cannot be used in conjunction with <>. ============ [cols="2*<"] |=== -| `server.ssl.certificateAuthorities:` +|[[server-ssl-certificateAuthorities]] `server.ssl.certificateAuthorities:` | Paths to one or more PEM-encoded X.509 certificate authority (CA) certificates which make up a trusted certificate chain for {kib}. This chain is used by {kib} to establish trust when receiving inbound SSL/TLS connections from end users. If PKI authentication is enabled, this chain is also used by {kib} to verify client certificates from end users. + -In addition to this setting, trusted certificates may be specified via `server.ssl.keystore.path` and/or `server.ssl.truststore.path`. +In addition to this setting, trusted certificates may be specified via <> and/or <>. | `server.ssl.cipherSuites:` | Details on the format, and the valid options, are available via the @@ -533,53 +535,53 @@ connections. Valid values are `"required"`, `"optional"`, and `"none"`. Using `" client presents a certificate, using `"optional"` will allow a client to present a certificate if it has one, and using `"none"` will prevent a client from presenting a certificate. *Default: `"none"`* -| `server.ssl.enabled:` +|[[server-ssl-enabled]] `server.ssl.enabled:` | Enables SSL/TLS for inbound connections to {kib}. When set to `true`, a certificate and its -corresponding private key must be provided. These can be specified via `server.ssl.keystore.path` or the combination of -`server.ssl.certificate` and `server.ssl.key`. *Default: `false`* +corresponding private key must be provided. These can be specified via <> or the combination of +<> and <>. *Default: `false`* | `server.ssl.keyPassphrase:` - | The password that decrypts the private key that is specified via `server.ssl.key`. This value + | The password that decrypts the private key that is specified via <>. This value is optional, as the key may not be encrypted. -| `server.ssl.keystore.path:` +|[[server-ssl-keystore-path]] `server.ssl.keystore.path:` | Path to a PKCS#12 keystore that contains an X.509 server certificate and its corresponding private key. If the keystore contains any additional certificates, those will be used as a trusted certificate chain for {kib}. All of these are used by {kib} to establish trust when receiving inbound SSL/TLS connections from end users. The certificate chain is also used by {kib} to verify client certificates from end users when PKI authentication is enabled. + -In addition to this setting, trusted certificates may be specified via `server.ssl.certificateAuthorities` and/or -`server.ssl.truststore.path`. +In addition to this setting, trusted certificates may be specified via <> and/or +<>. |=== [NOTE] ============ -This setting cannot be used in conjunction with `server.ssl.certificate` or `server.ssl.key` +This setting cannot be used in conjunction with <> or <> ============ [cols="2*<"] |=== | `server.ssl.keystore.password:` - | The password that will be used to decrypt the keystore specified via `server.ssl.keystore.path`. If the + | The password that will be used to decrypt the keystore specified via <>. If the keystore has no password, leave this unset. If the keystore has an empty password, set this to `""`. -| `server.ssl.truststore.path:` +|[[server-ssl-truststore-path]] `server.ssl.truststore.path:` | Path to a PKCS#12 trust store that contains one or more X.509 certificate authority (CA) certificates which make up a trusted certificate chain for {kib}. This chain is used by {kib} to establish trust when receiving inbound SSL/TLS connections from end users. If PKI authentication is enabled, this chain is also used by {kib} to verify client certificates from end users. + -In addition to this setting, trusted certificates may be specified via `server.ssl.certificateAuthorities` and/or -`server.ssl.keystore.path`. +In addition to this setting, trusted certificates may be specified via <> and/or +<>. | `server.ssl.truststore.password:` - | The password that will be used to decrypt the trust store specified via `server.ssl.truststore.path`. If + | The password that will be used to decrypt the trust store specified via <>. If the trust store has no password, leave this unset. If the trust store has an empty password, set this to `""`. | `server.ssl.redirectHttpFromPort:` | {kib} binds to this port and redirects -all http requests to https over the port configured as `server.port`. +all http requests to https over the port configured as <>. | `server.ssl.supportedProtocols:` | An array of supported protocols with versions. @@ -588,7 +590,7 @@ Valid protocols: `TLSv1`, `TLSv1.1`, `TLSv1.2`. *Default: TLSv1.1, TLSv1.2* | [[settings-xsrf-whitelist]] `server.xsrf.whitelist:` | It is not recommended to disable protections for arbitrary API endpoints. Instead, supply the `kbn-xsrf` header. -The `server.xsrf.whitelist` setting requires the following format: +The <> setting requires the following format: |=== @@ -608,18 +610,18 @@ The `server.xsrf.whitelist` setting requires the following format: setting this to `true` enables unauthenticated users to access the {kib} server status API and status page. *Default: `false`* -| `telemetry.allowChangingOptInStatus` +|[[telemetry-allowChangingOptInStatus]] `telemetry.allowChangingOptInStatus` | When `true`, users are able to change the telemetry setting at a later time in <>. When `false`, -{kib} looks at the value of `telemetry.optIn` to determine whether to send -telemetry data or not. `telemetry.allowChangingOptInStatus` and `telemetry.optIn` +{kib} looks at the value of <> to determine whether to send +telemetry data or not. <> and <> cannot be `false` at the same time. *Default: `true`*. -| `telemetry.optIn` +|[[settings-telemetry-optIn]] `telemetry.optIn` | When `true`, telemetry data is sent to Elastic. When `false`, collection of telemetry data is disabled. To enable telemetry and prevent users from disabling it, -set `telemetry.allowChangingOptInStatus` to `false` and `telemetry.optIn` to `true`. +set <> to `false` and <> to `true`. *Default: `true`* | `telemetry.enabled` diff --git a/docs/user/dashboard/url-drilldown.asciidoc b/docs/user/dashboard/url-drilldown.asciidoc index e6daf89d72718..ee879256a1fae 100644 --- a/docs/user/dashboard/url-drilldown.asciidoc +++ b/docs/user/dashboard/url-drilldown.asciidoc @@ -238,3 +238,14 @@ Tip: Consider using <> helper for date formatting. | Aggregation field behind the selected range, if available. |=== + +[float] +[[disable]] +==== Disable URL drilldown + +You can disable URL drilldown feature on your {kib} instance by disabling the plugin: + +["source","yml"] +----------- +url_drilldown.enabled: false +----------- diff --git a/docs/user/monitoring/viewing-metrics.asciidoc b/docs/user/monitoring/viewing-metrics.asciidoc index f35caea025cdd..0c48e3b7d011d 100644 --- a/docs/user/monitoring/viewing-metrics.asciidoc +++ b/docs/user/monitoring/viewing-metrics.asciidoc @@ -13,13 +13,19 @@ At a minimum, you must have monitoring data for the {es} production cluster. Once that data exists, {kib} can display monitoring data for other products in the cluster. +TIP: If you use a separate monitoring cluster to store the monitoring data, it +is strongly recommended that you use a separate {kib} instance to view it. If +you log in to {kib} using SAML, Kerberos, PKI, OpenID Connect, or token +authentication providers, a dedicated {kib} instance is *required*. The security +tokens that are used in these contexts are cluster-specific, therefore you +cannot use a single {kib} instance to connect to both production and monitoring +clusters. For more information about the recommended configuration, see +{ref}/monitoring-overview.html[Monitoring overview]. + . Identify where to retrieve monitoring data from. + -- -The cluster that contains the monitoring data is referred to -as the _monitoring cluster_. - -TIP: If the monitoring data is stored on a *dedicated* monitoring cluster, it is +If the monitoring data is stored on a dedicated monitoring cluster, it is accessible even when the cluster you're monitoring is not. If you have at least a gold license, you can send data from multiple clusters to the same monitoring cluster and view them all through the same instance of {kib}. diff --git a/examples/search_examples/public/components/app.tsx b/examples/search_examples/public/components/app.tsx index 704d31d42e640..ab0ce185f0602 100644 --- a/examples/search_examples/public/components/app.tsx +++ b/examples/search_examples/public/components/app.tsx @@ -56,6 +56,8 @@ import { IndexPatternSelect, IndexPattern, IndexPatternField, + isCompleteResponse, + isErrorResponse, } from '../../../../src/plugins/data/public'; interface SearchExamplesAppDeps { @@ -144,7 +146,7 @@ export const SearchExamplesApp = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { setTimeTook(response.rawResponse.took); const avgResult: number | undefined = response.rawResponse.aggregations ? response.rawResponse.aggregations[1].value @@ -162,7 +164,7 @@ export const SearchExamplesApp = ({ text: mountReactNode(message), }); searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { // TODO: Make response error status clearer notifications.toasts.addWarning('An error has occurred'); searchSubscription$.unsubscribe(); diff --git a/kibana.d.ts b/kibana.d.ts index b707405ffbeaf..50f8b8690d944 100644 --- a/kibana.d.ts +++ b/kibana.d.ts @@ -28,7 +28,6 @@ export { Public, Server }; /** * All exports from TS ambient definitions (where types are added for JS source in a .d.ts file). */ -import * as LegacyKibanaPluginSpec from './src/legacy/plugin_discovery/plugin_spec/plugin_spec_options'; import * as LegacyKibanaServer from './src/legacy/server/kbn_server'; /** @@ -39,8 +38,4 @@ export namespace Legacy { export type Request = LegacyKibanaServer.Request; export type ResponseToolkit = LegacyKibanaServer.ResponseToolkit; export type Server = LegacyKibanaServer.Server; - - export type InitPluginFunction = LegacyKibanaPluginSpec.InitPluginFunction; - export type UiExports = LegacyKibanaPluginSpec.UiExports; - export type PluginSpecOptions = LegacyKibanaPluginSpec.PluginSpecOptions; } diff --git a/package.json b/package.json index 1f2749ea44a90..57f5ac16059c9 100644 --- a/package.json +++ b/package.json @@ -350,7 +350,7 @@ "babel-eslint": "^10.0.3", "babel-jest": "^25.5.1", "babel-plugin-istanbul": "^6.0.0", - "backport": "5.5.1", + "backport": "5.6.0", "brace": "0.11.1", "chai": "3.5.0", "chance": "1.0.18", diff --git a/packages/kbn-es-archiver/src/cli.ts b/packages/kbn-es-archiver/src/cli.ts index 41abe83c148cd..87df07fe865bd 100644 --- a/packages/kbn-es-archiver/src/cli.ts +++ b/packages/kbn-es-archiver/src/cli.ts @@ -144,7 +144,7 @@ export function runCli() { const query = flags.query; let parsedQuery; - if (typeof query === 'string') { + if (typeof query === 'string' && query.length > 0) { try { parsedQuery = JSON.parse(query); } catch (err) { diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index d8bd39b9dcdf4..a1715cf3dba2c 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -48,11 +48,6 @@ const CAN_CLUSTER = canRequire(CLUSTER_MANAGER_PATH); const REPL_PATH = resolve(__dirname, '../repl'); const CAN_REPL = canRequire(REPL_PATH); -// xpack is installed in both dev and the distributable, it's optional if -// install is a link to the source, not an actual install -const XPACK_DIR = resolve(__dirname, '../../../x-pack'); -const XPACK_INSTALLED = canRequire(XPACK_DIR); - const pathCollector = function () { const paths = []; return function (path) { @@ -137,16 +132,7 @@ function applyConfigOverrides(rawConfig, opts, extraCliOptions) { if (opts.logFile) set('logging.dest', opts.logFile); set('plugins.scanDirs', _.compact([].concat(get('plugins.scanDirs'), opts.pluginDir))); - set( - 'plugins.paths', - _.compact( - [].concat( - get('plugins.paths'), - opts.pluginPath, - XPACK_INSTALLED && !opts.oss ? [XPACK_DIR] : [] - ) - ) - ); + set('plugins.paths', _.compact([].concat(get('plugins.paths'), opts.pluginPath))); merge(extraCliOptions); merge(readKeystore()); diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index 5b51bc823d166..bd8c9e91f15a2 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -58,19 +58,6 @@ export interface InjectedMetadataParams { uiPlugins: InjectedPluginMetadata[]; anonymousStatusPage: boolean; legacyMetadata: { - app: { - id: string; - title: string; - }; - bundleId: string; - version: string; - branch: string; - buildNum: number; - buildSha: string; - basePath: string; - serverName: string; - devMode: boolean; - category?: AppCategory; uiSettings: { defaults: Record; user?: Record; @@ -167,18 +154,6 @@ export interface InjectedMetadataSetup { getPlugins: () => InjectedPluginMetadata[]; getAnonymousStatusPage: () => boolean; getLegacyMetadata: () => { - app: { - id: string; - title: string; - }; - bundleId: string; - version: string; - branch: string; - buildNum: number; - buildSha: string; - basePath: string; - serverName: string; - devMode: boolean; uiSettings: { defaults: Record; user?: Record | undefined; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 1c17be50454c5..7179c6cf8b133 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -1079,6 +1079,7 @@ export interface SavedObjectsFindOptions { sortOrder?: string; // (undocumented) type: string | string[]; + typeToNamespacesMap?: Map; } // @public diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index 5a8949ca2f55f..6a10eb44d9ca4 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -34,7 +34,7 @@ import { HttpFetchOptions, HttpSetup } from '../http'; type SavedObjectsFindOptions = Omit< SavedObjectFindOptionsServer, - 'namespace' | 'sortOrder' | 'rootSearchFields' + 'sortOrder' | 'rootSearchFields' | 'typeToNamespacesMap' >; type PromiseType> = T extends Promise ? U : never; diff --git a/src/core/server/elasticsearch/client/cluster_client.test.ts b/src/core/server/elasticsearch/client/cluster_client.test.ts index 121ef3aa42d51..e35d9962e9e7e 100644 --- a/src/core/server/elasticsearch/client/cluster_client.test.ts +++ b/src/core/server/elasticsearch/client/cluster_client.test.ts @@ -206,7 +206,7 @@ describe('ClusterClient', () => { const clusterClient = new ClusterClient(config, logger, getAuthHeaders); const request = httpServerMock.createKibanaRequest({ - kibanaRequestState: { requestId: 'my-fake-id' }, + kibanaRequestState: { requestId: 'my-fake-id', requestUuid: 'ignore-this-id' }, }); clusterClient.asScoped(request); @@ -284,7 +284,7 @@ describe('ClusterClient', () => { const clusterClient = new ClusterClient(config, logger, getAuthHeaders); const request = httpServerMock.createKibanaRequest({ headers: { foo: 'request' }, - kibanaRequestState: { requestId: 'from request' }, + kibanaRequestState: { requestId: 'from request', requestUuid: 'ignore-this-id' }, }); clusterClient.asScoped(request); diff --git a/src/core/server/elasticsearch/legacy/cluster_client.test.ts b/src/core/server/elasticsearch/legacy/cluster_client.test.ts index 73d941053e84b..745ef4304d0b1 100644 --- a/src/core/server/elasticsearch/legacy/cluster_client.test.ts +++ b/src/core/server/elasticsearch/legacy/cluster_client.test.ts @@ -351,7 +351,9 @@ describe('#asScoped', () => { test('passes x-opaque-id header with request id', () => { clusterClient.asScoped( - httpServerMock.createKibanaRequest({ kibanaRequestState: { requestId: 'alpha' } }) + httpServerMock.createKibanaRequest({ + kibanaRequestState: { requestId: 'alpha', requestUuid: 'ignore-this-id' }, + }) ); expect(MockScopedClusterClient).toHaveBeenCalledTimes(1); diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts index 6d096b76263b5..9deaa73d8aacf 100644 --- a/src/core/server/http/http_server.mocks.ts +++ b/src/core/server/http/http_server.mocks.ts @@ -68,7 +68,7 @@ function createKibanaRequestMock

({ routeAuthRequired, validation = {}, kibanaRouteOptions = { xsrfRequired: true }, - kibanaRequestState = { requestId: '123' }, + kibanaRequestState = { requestId: '123', requestUuid: '123e4567-e89b-12d3-a456-426614174000' }, auth = { isAuthenticated: true }, }: RequestFixtureOptions = {}) { const queryString = stringify(query, { sort: false }); diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts index 7609f23fe0c51..2440f2b1da0bd 100644 --- a/src/core/server/http/http_server.ts +++ b/src/core/server/http/http_server.ts @@ -19,6 +19,7 @@ import { Server } from 'hapi'; import HapiStaticFiles from 'inert'; import url from 'url'; +import uuid from 'uuid'; import { Logger, LoggerFactory } from '../logging'; import { HttpConfig } from './http_config'; @@ -315,6 +316,7 @@ export class HttpServer { request.app = { ...(request.app ?? {}), requestId: getRequestId(request, config.requestId), + requestUuid: uuid.v4(), } as KibanaRequestState; return responseToolkit.continue; }); diff --git a/src/core/server/http/integration_tests/request.test.ts b/src/core/server/http/integration_tests/request.test.ts index 0727ff848c189..0170e94867c06 100644 --- a/src/core/server/http/integration_tests/request.test.ts +++ b/src/core/server/http/integration_tests/request.test.ts @@ -16,6 +16,11 @@ * specific language governing permissions and limitations * under the License. */ + +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'), +})); + import supertest from 'supertest'; import { HttpService } from '../http_service'; @@ -308,4 +313,20 @@ describe('KibanaRequest', () => { expect(resp3.body).toEqual({ requestId: 'gamma' }); }); }); + + describe('request uuid', () => { + it('generates a UUID', async () => { + const { server: innerServer, createRouter } = await server.setup(setupDeps); + const router = createRouter('/'); + router.get({ path: '/', validate: false }, async (context, req, res) => { + return res.ok({ body: { requestUuid: req.uuid } }); + }); + await server.start(); + + const st = supertest(innerServer.listener); + + const resp1 = await st.get('/').expect(200); + expect(resp1.body.requestUuid).toBe('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'); + }); + }); }); diff --git a/src/core/server/http/router/request.test.ts b/src/core/server/http/router/request.test.ts index e741121f3d70c..0bf81a7aca852 100644 --- a/src/core/server/http/router/request.test.ts +++ b/src/core/server/http/router/request.test.ts @@ -55,6 +55,34 @@ describe('KibanaRequest', () => { }); }); + describe('uuid property', () => { + it('uses the request.app.requestUuid property if present', () => { + const request = httpServerMock.createRawRequest({ + app: { requestUuid: '123e4567-e89b-12d3-a456-426614174000' }, + }); + const kibanaRequest = KibanaRequest.from(request); + expect(kibanaRequest.uuid).toEqual('123e4567-e89b-12d3-a456-426614174000'); + }); + + it('generates a new UUID if request.app property is not present', () => { + // Undefined app property + const request = httpServerMock.createRawRequest({ + app: undefined, + }); + const kibanaRequest = KibanaRequest.from(request); + expect(kibanaRequest.uuid).toEqual('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'); + }); + + it('generates a new UUID if request.app.requestUuid property is not present', () => { + // Undefined app.requestUuid property + const request = httpServerMock.createRawRequest({ + app: {}, + }); + const kibanaRequest = KibanaRequest.from(request); + expect(kibanaRequest.uuid).toEqual('xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'); + }); + }); + describe('get all headers', () => { it('returns all headers', () => { const request = httpServerMock.createRawRequest({ diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index e04f8585981b5..903eb75022df3 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -44,6 +44,7 @@ export interface KibanaRouteOptions extends RouteOptionsApp { */ export interface KibanaRequestState extends ApplicationState { requestId: string; + requestUuid: string; } /** @@ -152,6 +153,14 @@ export class KibanaRequest< * per request. */ public readonly id: string; + /** + * A UUID to identify this request. + * + * @remarks + * This value is NOT sourced from the incoming request's `X-Opaque-Id` header. it + * is always a UUID uniquely identifying the request. + */ + public readonly uuid: string; /** a WHATWG URL standard object. */ public readonly url: Url; /** matched route details */ @@ -189,10 +198,11 @@ export class KibanaRequest< // until that time we have to expose all the headers private readonly withoutSecretHeaders: boolean ) { - // The `requestId` property will not be populated for requests that are 'faked' by internal systems that leverage + // The `requestId` and `requestUuid` properties will not be populated for requests that are 'faked' by internal systems that leverage // KibanaRequest in conjunction with scoped Elaticcsearch and SavedObjectsClient in order to pass credentials. - // In these cases, the id defaults to a newly generated UUID. + // In these cases, the ids default to a newly generated UUID. this.id = (request.app as KibanaRequestState | undefined)?.requestId ?? uuid.v4(); + this.uuid = (request.app as KibanaRequestState | undefined)?.requestUuid ?? uuid.v4(); this.url = request.url; this.headers = deepFreeze({ ...request.headers }); diff --git a/src/core/server/index.ts b/src/core/server/index.ts index e136c699f7246..70ef93963c69f 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -339,14 +339,7 @@ export { SavedObjectsMigrationVersion, } from './types'; -export { - LegacyServiceSetupDeps, - LegacyServiceStartDeps, - LegacyServiceDiscoverPlugins, - LegacyConfig, - LegacyUiExports, - LegacyInternals, -} from './legacy'; +export { LegacyServiceSetupDeps, LegacyServiceStartDeps, LegacyConfig } from './legacy'; export { CoreStatus, diff --git a/src/core/server/legacy/config/ensure_valid_configuration.test.ts b/src/core/server/legacy/config/ensure_valid_configuration.test.ts index 702840b8a0a6a..700fe69954655 100644 --- a/src/core/server/legacy/config/ensure_valid_configuration.test.ts +++ b/src/core/server/legacy/config/ensure_valid_configuration.test.ts @@ -39,17 +39,12 @@ describe('ensureValidConfiguration', () => { configService as any, { settings: 'settings', - pluginSpecs: 'pluginSpecs', - disabledPluginSpecs: 'disabledPluginSpecs', - pluginExtendedConfig: 'pluginExtendedConfig', - uiExports: 'uiExports', + legacyConfig: 'pluginExtendedConfig', } as any ); expect(getUnusedConfigKeys).toHaveBeenCalledTimes(1); expect(getUnusedConfigKeys).toHaveBeenCalledWith({ coreHandledConfigPaths: ['core', 'elastic'], - pluginSpecs: 'pluginSpecs', - disabledPluginSpecs: 'disabledPluginSpecs', settings: 'settings', legacyConfig: 'pluginExtendedConfig', }); diff --git a/src/core/server/legacy/config/ensure_valid_configuration.ts b/src/core/server/legacy/config/ensure_valid_configuration.ts index 5cd1603ea65fb..34f98b9b3a795 100644 --- a/src/core/server/legacy/config/ensure_valid_configuration.ts +++ b/src/core/server/legacy/config/ensure_valid_configuration.ts @@ -19,19 +19,17 @@ import { getUnusedConfigKeys } from './get_unused_config_keys'; import { ConfigService } from '../../config'; -import { LegacyServiceDiscoverPlugins } from '../types'; import { CriticalError } from '../../errors'; +import { LegacyServiceSetupConfig } from '../types'; export async function ensureValidConfiguration( configService: ConfigService, - { pluginSpecs, disabledPluginSpecs, pluginExtendedConfig, settings }: LegacyServiceDiscoverPlugins + { legacyConfig, settings }: LegacyServiceSetupConfig ) { const unusedConfigKeys = await getUnusedConfigKeys({ coreHandledConfigPaths: await configService.getUsedPaths(), - pluginSpecs, - disabledPluginSpecs, settings, - legacyConfig: pluginExtendedConfig, + legacyConfig, }); if (unusedConfigKeys.length > 0) { diff --git a/src/core/server/legacy/config/get_unused_config_keys.test.ts b/src/core/server/legacy/config/get_unused_config_keys.test.ts index f8506b5744030..6ce69fca0270a 100644 --- a/src/core/server/legacy/config/get_unused_config_keys.test.ts +++ b/src/core/server/legacy/config/get_unused_config_keys.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { LegacyPluginSpec, LegacyConfig, LegacyVars } from '../types'; +import { LegacyConfig, LegacyVars } from '../types'; import { getUnusedConfigKeys } from './get_unused_config_keys'; describe('getUnusedConfigKeys', () => { @@ -35,8 +35,6 @@ describe('getUnusedConfigKeys', () => { expect( await getUnusedConfigKeys({ coreHandledConfigPaths: [], - pluginSpecs: [], - disabledPluginSpecs: [], settings: {}, legacyConfig: getConfig(), }) @@ -47,8 +45,6 @@ describe('getUnusedConfigKeys', () => { expect( await getUnusedConfigKeys({ coreHandledConfigPaths: [], - pluginSpecs: [], - disabledPluginSpecs: [], settings: { presentInBoth: true, alsoInBoth: 'someValue', @@ -65,8 +61,6 @@ describe('getUnusedConfigKeys', () => { expect( await getUnusedConfigKeys({ coreHandledConfigPaths: [], - pluginSpecs: [], - disabledPluginSpecs: [], settings: { presentInBoth: true, }, @@ -82,8 +76,6 @@ describe('getUnusedConfigKeys', () => { expect( await getUnusedConfigKeys({ coreHandledConfigPaths: [], - pluginSpecs: [], - disabledPluginSpecs: [], settings: { presentInBoth: true, onlyInSetting: 'value', @@ -99,8 +91,6 @@ describe('getUnusedConfigKeys', () => { expect( await getUnusedConfigKeys({ coreHandledConfigPaths: [], - pluginSpecs: [], - disabledPluginSpecs: [], settings: { elasticsearch: { username: 'foo', @@ -121,8 +111,6 @@ describe('getUnusedConfigKeys', () => { expect( await getUnusedConfigKeys({ coreHandledConfigPaths: [], - pluginSpecs: [], - disabledPluginSpecs: [], settings: { env: 'development', }, @@ -139,8 +127,6 @@ describe('getUnusedConfigKeys', () => { expect( await getUnusedConfigKeys({ coreHandledConfigPaths: [], - pluginSpecs: [], - disabledPluginSpecs: [], settings: { prop: ['a', 'b', 'c'], }, @@ -152,40 +138,10 @@ describe('getUnusedConfigKeys', () => { }); }); - it('ignores config for plugins that are disabled', async () => { - expect( - await getUnusedConfigKeys({ - coreHandledConfigPaths: [], - pluginSpecs: [], - disabledPluginSpecs: [ - ({ - id: 'foo', - getConfigPrefix: () => 'foo.bar', - } as unknown) as LegacyPluginSpec, - ], - settings: { - foo: { - bar: { - unused: true, - }, - }, - plugin: { - missingProp: false, - }, - }, - legacyConfig: getConfig({ - prop: 'a', - }), - }) - ).toEqual(['plugin.missingProp']); - }); - it('ignores properties managed by the new platform', async () => { expect( await getUnusedConfigKeys({ coreHandledConfigPaths: ['core', 'foo.bar'], - pluginSpecs: [], - disabledPluginSpecs: [], settings: { core: { prop: 'value', @@ -204,8 +160,6 @@ describe('getUnusedConfigKeys', () => { expect( await getUnusedConfigKeys({ coreHandledConfigPaths: ['core', 'array'], - pluginSpecs: [], - disabledPluginSpecs: [], settings: { core: { prop: 'value', diff --git a/src/core/server/legacy/config/get_unused_config_keys.ts b/src/core/server/legacy/config/get_unused_config_keys.ts index c15c3b270df05..5bbe169033e39 100644 --- a/src/core/server/legacy/config/get_unused_config_keys.ts +++ b/src/core/server/legacy/config/get_unused_config_keys.ts @@ -19,30 +19,20 @@ import { difference } from 'lodash'; import { getFlattenedObject } from '@kbn/std'; -import { unset } from '../../../../legacy/utils'; import { hasConfigPathIntersection } from '../../config'; -import { LegacyPluginSpec, LegacyConfig, LegacyVars } from '../types'; +import { LegacyConfig, LegacyVars } from '../types'; const getFlattenedKeys = (object: object) => Object.keys(getFlattenedObject(object)); export async function getUnusedConfigKeys({ coreHandledConfigPaths, - pluginSpecs, - disabledPluginSpecs, settings, legacyConfig, }: { coreHandledConfigPaths: string[]; - pluginSpecs: LegacyPluginSpec[]; - disabledPluginSpecs: LegacyPluginSpec[]; settings: LegacyVars; legacyConfig: LegacyConfig; }) { - // remove config values from disabled plugins - for (const spec of disabledPluginSpecs) { - unset(settings, spec.getConfigPrefix()); - } - const inputKeys = getFlattenedKeys(settings); const appliedKeys = getFlattenedKeys(legacyConfig.get()); diff --git a/src/core/server/legacy/index.ts b/src/core/server/legacy/index.ts index 6b0963e3129c6..1a0bc8955be0f 100644 --- a/src/core/server/legacy/index.ts +++ b/src/core/server/legacy/index.ts @@ -20,8 +20,6 @@ /** @internal */ export { ensureValidConfiguration } from './config'; /** @internal */ -export { LegacyInternals } from './legacy_internals'; -/** @internal */ export { LegacyService, ILegacyService } from './legacy_service'; /** @internal */ export * from './types'; diff --git a/src/core/server/legacy/legacy_internals.test.ts b/src/core/server/legacy/legacy_internals.test.ts deleted file mode 100644 index 935e36a989a0c..0000000000000 --- a/src/core/server/legacy/legacy_internals.test.ts +++ /dev/null @@ -1,211 +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 { Server } from 'hapi'; - -import { configMock } from '../config/mocks'; -import { httpServiceMock } from '../http/http_service.mock'; -import { httpServerMock } from '../http/http_server.mocks'; -import { findLegacyPluginSpecsMock } from './legacy_service.test.mocks'; -import { LegacyInternals } from './legacy_internals'; -import { ILegacyInternals, LegacyConfig, LegacyVars, LegacyUiExports } from './types'; - -function varsProvider(vars: LegacyVars, configValue?: any) { - return { - fn: jest.fn().mockReturnValue(vars), - pluginSpec: { - readConfigValue: jest.fn().mockReturnValue(configValue), - }, - }; -} - -describe('LegacyInternals', () => { - describe('getInjectedUiAppVars()', () => { - let uiExports: LegacyUiExports; - let config: LegacyConfig; - let server: Server; - let legacyInternals: ILegacyInternals; - - beforeEach(async () => { - uiExports = findLegacyPluginSpecsMock().uiExports; - config = configMock.create() as any; - server = httpServiceMock.createInternalSetupContract().server; - legacyInternals = new LegacyInternals(uiExports, config, server); - }); - - it('gets with no injectors', async () => { - await expect(legacyInternals.getInjectedUiAppVars('core')).resolves.toMatchInlineSnapshot( - `Object {}` - ); - }); - - it('gets with no matching injectors', async () => { - const injector = jest.fn().mockResolvedValue({ not: 'core' }); - legacyInternals.injectUiAppVars('not-core', injector); - - await expect(legacyInternals.getInjectedUiAppVars('core')).resolves.toMatchInlineSnapshot( - `Object {}` - ); - expect(injector).not.toHaveBeenCalled(); - }); - - it('gets with single matching injector', async () => { - const injector = jest.fn().mockResolvedValue({ is: 'core' }); - legacyInternals.injectUiAppVars('core', injector); - - await expect(legacyInternals.getInjectedUiAppVars('core')).resolves.toMatchInlineSnapshot(` - Object { - "is": "core", - } - `); - expect(injector).toHaveBeenCalled(); - }); - - it('gets with multiple matching injectors', async () => { - const injectors = [ - jest.fn().mockResolvedValue({ is: 'core' }), - jest.fn().mockReturnValue({ sync: 'injector' }), - jest.fn().mockResolvedValue({ is: 'merged-core' }), - ]; - - injectors.forEach((injector) => legacyInternals.injectUiAppVars('core', injector)); - - await expect(legacyInternals.getInjectedUiAppVars('core')).resolves.toMatchInlineSnapshot(` - Object { - "is": "merged-core", - "sync": "injector", - } - `); - expect(injectors[0]).toHaveBeenCalled(); - expect(injectors[1]).toHaveBeenCalled(); - expect(injectors[2]).toHaveBeenCalled(); - }); - }); - - describe('getVars()', () => { - let uiExports: LegacyUiExports; - let config: LegacyConfig; - let server: Server; - let legacyInternals: LegacyInternals; - - beforeEach(async () => { - uiExports = findLegacyPluginSpecsMock().uiExports; - config = configMock.create() as any; - server = httpServiceMock.createInternalSetupContract().server; - legacyInternals = new LegacyInternals(uiExports, config, server); - }); - - it('gets: no default injectors, no injected vars replacers, no ui app injectors, no inject arg', async () => { - const vars = await legacyInternals.getVars('core', httpServerMock.createRawRequest()); - - expect(vars).toMatchInlineSnapshot(`Object {}`); - }); - - it('gets: with default injectors, no injected vars replacers, no ui app injectors, no inject arg', async () => { - uiExports.defaultInjectedVarProviders = [ - varsProvider({ alpha: 'alpha' }), - varsProvider({ gamma: 'gamma' }), - varsProvider({ alpha: 'beta' }), - ]; - - const vars = await legacyInternals.getVars('core', httpServerMock.createRawRequest()); - - expect(vars).toMatchInlineSnapshot(` - Object { - "alpha": "beta", - "gamma": "gamma", - } - `); - }); - - it('gets: no default injectors, with injected vars replacers, with ui app injectors, no inject arg', async () => { - uiExports.injectedVarsReplacers = [ - jest.fn(async (vars) => ({ ...vars, added: 'key' })), - jest.fn((vars) => vars), - jest.fn((vars) => ({ replaced: 'all' })), - jest.fn(async (vars) => ({ ...vars, added: 'last-key' })), - ]; - - const request = httpServerMock.createRawRequest(); - const vars = await legacyInternals.getVars('core', request); - - expect(vars).toMatchInlineSnapshot(` - Object { - "added": "last-key", - "replaced": "all", - } - `); - }); - - it('gets: no default injectors, no injected vars replacers, with ui app injectors, no inject arg', async () => { - legacyInternals.injectUiAppVars('core', async () => ({ is: 'core' })); - legacyInternals.injectUiAppVars('core', () => ({ sync: 'injector' })); - legacyInternals.injectUiAppVars('core', async () => ({ is: 'merged-core' })); - - const vars = await legacyInternals.getVars('core', httpServerMock.createRawRequest()); - - expect(vars).toMatchInlineSnapshot(` - Object { - "is": "merged-core", - "sync": "injector", - } - `); - }); - - it('gets: no default injectors, no injected vars replacers, no ui app injectors, with inject arg', async () => { - const vars = await legacyInternals.getVars('core', httpServerMock.createRawRequest(), { - injected: 'arg', - }); - - expect(vars).toMatchInlineSnapshot(` - Object { - "injected": "arg", - } - `); - }); - - it('gets: with default injectors, with injected vars replacers, with ui app injectors, with inject arg', async () => { - uiExports.defaultInjectedVarProviders = [ - varsProvider({ alpha: 'alpha' }), - varsProvider({ gamma: 'gamma' }), - varsProvider({ alpha: 'beta' }), - ]; - uiExports.injectedVarsReplacers = [jest.fn(async (vars) => ({ ...vars, gamma: 'delta' }))]; - - legacyInternals.injectUiAppVars('core', async () => ({ is: 'core' })); - legacyInternals.injectUiAppVars('core', () => ({ sync: 'injector' })); - legacyInternals.injectUiAppVars('core', async () => ({ is: 'merged-core' })); - - const vars = await legacyInternals.getVars('core', httpServerMock.createRawRequest(), { - injected: 'arg', - sync: 'arg', - }); - - expect(vars).toMatchInlineSnapshot(` - Object { - "alpha": "beta", - "gamma": "delta", - "injected": "arg", - "is": "merged-core", - "sync": "arg", - } - `); - }); - }); -}); diff --git a/src/core/server/legacy/legacy_internals.ts b/src/core/server/legacy/legacy_internals.ts deleted file mode 100644 index 628ca4ed12f6b..0000000000000 --- a/src/core/server/legacy/legacy_internals.ts +++ /dev/null @@ -1,93 +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 { Server } from 'hapi'; - -import { KibanaRequest, LegacyRequest } from '../http'; -import { ensureRawRequest } from '../http/router'; -import { mergeVars } from './merge_vars'; -import { ILegacyInternals, LegacyVars, VarsInjector, LegacyConfig, LegacyUiExports } from './types'; - -/** - * @internal - * @deprecated - */ -export class LegacyInternals implements ILegacyInternals { - private readonly injectors = new Map>(); - private cachedDefaultVars?: LegacyVars; - - constructor( - private readonly uiExports: LegacyUiExports, - private readonly config: LegacyConfig, - private readonly server: Server - ) {} - - private get defaultVars(): LegacyVars { - if (this.cachedDefaultVars) { - return this.cachedDefaultVars; - } - - const { defaultInjectedVarProviders = [] } = this.uiExports; - - return (this.cachedDefaultVars = defaultInjectedVarProviders.reduce( - (vars, { fn, pluginSpec }) => - mergeVars(vars, fn(this.server, pluginSpec.readConfigValue(this.config, []))), - {} - )); - } - - private replaceVars(vars: LegacyVars, request: KibanaRequest | LegacyRequest) { - const { injectedVarsReplacers = [] } = this.uiExports; - - return injectedVarsReplacers.reduce( - async (injected, replacer) => - replacer(await injected, ensureRawRequest(request), this.server), - Promise.resolve(vars) - ); - } - - public injectUiAppVars(id: string, injector: VarsInjector) { - if (!this.injectors.has(id)) { - this.injectors.set(id, new Set()); - } - - this.injectors.get(id)!.add(injector); - } - - public getInjectedUiAppVars(id: string) { - return [...(this.injectors.get(id) || [])].reduce( - async (promise, injector) => ({ - ...(await promise), - ...(await injector()), - }), - Promise.resolve({}) - ); - } - - public async getVars( - id: string, - request: KibanaRequest | LegacyRequest, - injected: LegacyVars = {} - ) { - return this.replaceVars( - mergeVars(this.defaultVars, await this.getInjectedUiAppVars(id), injected), - request - ); - } -} diff --git a/src/core/server/legacy/legacy_service.mock.ts b/src/core/server/legacy/legacy_service.mock.ts index ab501bd6bb53b..781874f702cf8 100644 --- a/src/core/server/legacy/legacy_service.mock.ts +++ b/src/core/server/legacy/legacy_service.mock.ts @@ -18,26 +18,13 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; import { LegacyService } from './legacy_service'; -import { LegacyConfig, LegacyServiceDiscoverPlugins, LegacyServiceSetupDeps } from './types'; +import { LegacyConfig, LegacyServiceSetupDeps } from './types'; type LegacyServiceMock = jest.Mocked & { legacyId: symbol }>; -const createDiscoverPluginsMock = (): LegacyServiceDiscoverPlugins => ({ - pluginSpecs: [], - uiExports: {}, - navLinks: [], - pluginExtendedConfig: { - get: jest.fn(), - has: jest.fn(), - set: jest.fn(), - }, - disabledPluginSpecs: [], - settings: {}, -}); - const createLegacyServiceMock = (): LegacyServiceMock => ({ legacyId: Symbol(), - discoverPlugins: jest.fn().mockResolvedValue(createDiscoverPluginsMock()), + setupLegacyConfig: jest.fn(), setup: jest.fn(), start: jest.fn(), stop: jest.fn(), @@ -52,6 +39,5 @@ const createLegacyConfigMock = (): jest.Mocked => ({ export const legacyServiceMock = { create: createLegacyServiceMock, createSetupContract: (deps: LegacyServiceSetupDeps) => createLegacyServiceMock().setup(deps), - createDiscoverPlugins: createDiscoverPluginsMock, createLegacyConfig: createLegacyConfigMock, }; diff --git a/src/core/server/legacy/legacy_service.test.mocks.ts b/src/core/server/legacy/legacy_service.test.mocks.ts deleted file mode 100644 index 9ad554d63add0..0000000000000 --- a/src/core/server/legacy/legacy_service.test.mocks.ts +++ /dev/null @@ -1,40 +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 { LegacyVars } from './types'; - -export const findLegacyPluginSpecsMock = jest.fn().mockImplementation((settings: LegacyVars) => ({ - pluginSpecs: [], - pluginExtendedConfig: { - has: jest.fn(), - get: jest.fn().mockReturnValue(settings), - set: jest.fn(), - }, - disabledPluginSpecs: [], - uiExports: {}, - navLinks: [], -})); -jest.doMock('./plugins/find_legacy_plugin_specs', () => ({ - findLegacyPluginSpecs: findLegacyPluginSpecsMock, -})); - -export const logLegacyThirdPartyPluginDeprecationWarningMock = jest.fn(); -jest.doMock('./plugins/log_legacy_plugins_warning', () => ({ - logLegacyThirdPartyPluginDeprecationWarning: logLegacyThirdPartyPluginDeprecationWarningMock, -})); diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index a6fe95deb3979..57009f0d35c16 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -19,10 +19,6 @@ jest.mock('../../../legacy/server/kbn_server'); jest.mock('./cluster_manager'); -import { - findLegacyPluginSpecsMock, - logLegacyThirdPartyPluginDeprecationWarningMock, -} from './legacy_service.test.mocks'; import { BehaviorSubject, throwError } from 'rxjs'; import { REPO_ROOT } from '@kbn/dev-utils'; @@ -44,8 +40,7 @@ import { capabilitiesServiceMock } from '../capabilities/capabilities_service.mo import { httpResourcesMock } from '../http_resources/http_resources_service.mock'; import { setupMock as renderingServiceMock } from '../rendering/__mocks__/rendering_service'; import { environmentServiceMock } from '../environment/environment_service.mock'; -import { findLegacyPluginSpecs } from './plugins'; -import { LegacyVars, LegacyServiceSetupDeps, LegacyServiceStartDeps } from './types'; +import { LegacyServiceSetupDeps, LegacyServiceStartDeps } from './types'; import { LegacyService } from './legacy_service'; import { coreMock } from '../mocks'; import { statusServiceMock } from '../status/status_service.mock'; @@ -73,7 +68,6 @@ beforeEach(() => { configService = configServiceMock.create(); environmentSetup = environmentServiceMock.createSetupContract(); - findLegacyPluginSpecsMock.mockClear(); MockKbnServer.prototype.ready = jest.fn().mockReturnValue(Promise.resolve()); MockKbnServer.prototype.listen = jest.fn(); @@ -149,10 +143,10 @@ describe('once LegacyService is set up with connection info', () => { coreId, env, logger, - configService: configService as any, + configService, }); - await legacyService.discoverPlugins(); + await legacyService.setupLegacyConfig(); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -160,13 +154,14 @@ describe('once LegacyService is set up with connection info', () => { expect(MockKbnServer).toHaveBeenCalledWith( { path: { autoListen: true }, server: { autoListen: true } }, // Because of the mock, path also gets the value expect.objectContaining({ get: expect.any(Function) }), - expect.any(Object), - { disabledPluginSpecs: [], pluginSpecs: [], uiExports: {}, navLinks: [] } + expect.any(Object) + ); + expect(MockKbnServer.mock.calls[0][1].get()).toEqual( + expect.objectContaining({ + path: expect.objectContaining({ autoListen: true }), + server: expect.objectContaining({ autoListen: true }), + }) ); - expect(MockKbnServer.mock.calls[0][1].get()).toEqual({ - path: { autoListen: true }, - server: { autoListen: true }, - }); const [mockKbnServer] = MockKbnServer.mock.instances; expect(mockKbnServer.listen).toHaveBeenCalledTimes(1); @@ -182,7 +177,7 @@ describe('once LegacyService is set up with connection info', () => { logger, configService: configService as any, }); - await legacyService.discoverPlugins(); + await legacyService.setupLegacyConfig(); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -190,13 +185,12 @@ describe('once LegacyService is set up with connection info', () => { expect(MockKbnServer).toHaveBeenCalledWith( { path: { autoListen: false }, server: { autoListen: true } }, expect.objectContaining({ get: expect.any(Function) }), - expect.any(Object), - { disabledPluginSpecs: [], pluginSpecs: [], uiExports: {}, navLinks: [] } + expect.any(Object) ); - expect(MockKbnServer.mock.calls[0][1].get()).toEqual({ - path: { autoListen: false }, - server: { autoListen: true }, - }); + + const legacyConfig = MockKbnServer.mock.calls[0][1].get(); + expect(legacyConfig.path.autoListen).toBe(false); + expect(legacyConfig.server.autoListen).toBe(true); const [mockKbnServer] = MockKbnServer.mock.instances; expect(mockKbnServer.ready).toHaveBeenCalledTimes(1); @@ -214,7 +208,7 @@ describe('once LegacyService is set up with connection info', () => { configService: configService as any, }); - await legacyService.discoverPlugins(); + await legacyService.setupLegacyConfig(); await legacyService.setup(setupDeps); await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot( `"something failed"` @@ -234,11 +228,11 @@ describe('once LegacyService is set up with connection info', () => { configService: configService as any, }); - await expect(legacyService.discoverPlugins()).rejects.toThrowErrorMatchingInlineSnapshot( + await expect(legacyService.setupLegacyConfig()).rejects.toThrowErrorMatchingInlineSnapshot( `"something failed"` ); await expect(legacyService.setup(setupDeps)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Legacy service has not discovered legacy plugins yet. Ensure LegacyService.discoverPlugins() is called before LegacyService.setup()"` + `"Legacy config not initialized yet. Ensure LegacyService.setupLegacyConfig() is called before LegacyService.setup()"` ); await expect(legacyService.start(startDeps)).rejects.toThrowErrorMatchingInlineSnapshot( `"Legacy service is not setup yet."` @@ -255,7 +249,7 @@ describe('once LegacyService is set up with connection info', () => { logger, configService: configService as any, }); - await legacyService.discoverPlugins(); + await legacyService.setupLegacyConfig(); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -276,7 +270,7 @@ describe('once LegacyService is set up with connection info', () => { logger, configService: configService as any, }); - await legacyService.discoverPlugins(); + await legacyService.setupLegacyConfig(); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -301,7 +295,7 @@ describe('once LegacyService is set up with connection info', () => { logger, configService: configService as any, }); - await legacyService.discoverPlugins(); + await legacyService.setupLegacyConfig(); await legacyService.setup(setupDeps); await legacyService.start(startDeps); @@ -321,7 +315,7 @@ describe('once LegacyService is set up without connection info', () => { let legacyService: LegacyService; beforeEach(async () => { legacyService = new LegacyService({ coreId, env, logger, configService: configService as any }); - await legacyService.discoverPlugins(); + await legacyService.setupLegacyConfig(); await legacyService.setup(setupDeps); await legacyService.start(startDeps); }); @@ -331,13 +325,13 @@ describe('once LegacyService is set up without connection info', () => { expect(MockKbnServer).toHaveBeenCalledWith( { path: {}, server: { autoListen: true } }, expect.objectContaining({ get: expect.any(Function) }), - expect.any(Object), - { disabledPluginSpecs: [], pluginSpecs: [], uiExports: {}, navLinks: [] } + expect.any(Object) + ); + expect(MockKbnServer.mock.calls[0][1].get()).toEqual( + expect.objectContaining({ + server: expect.objectContaining({ autoListen: true }), + }) ); - expect(MockKbnServer.mock.calls[0][1].get()).toEqual({ - path: {}, - server: { autoListen: true }, - }); }); test('reconfigures logging configuration if new config is received.', async () => { @@ -375,7 +369,7 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => { configService: configService as any, }); - await devClusterLegacyService.discoverPlugins(); + await devClusterLegacyService.setupLegacyConfig(); await devClusterLegacyService.setup(setupDeps); await devClusterLegacyService.start(startDeps); @@ -404,7 +398,7 @@ describe('once LegacyService is set up in `devClusterMaster` mode', () => { configService: configService as any, }); - await devClusterLegacyService.discoverPlugins(); + await devClusterLegacyService.setupLegacyConfig(); await devClusterLegacyService.setup(setupDeps); await devClusterLegacyService.start(startDeps); @@ -434,50 +428,6 @@ describe('start', () => { }); }); -describe('#discoverPlugins()', () => { - it('calls findLegacyPluginSpecs with correct parameters', async () => { - const legacyService = new LegacyService({ - coreId, - env, - logger, - configService: configService as any, - }); - - await legacyService.discoverPlugins(); - expect(findLegacyPluginSpecs).toHaveBeenCalledTimes(1); - expect(findLegacyPluginSpecs).toHaveBeenCalledWith(expect.any(Object), logger, env.packageInfo); - }); - - it(`logs deprecations for legacy third party plugins`, async () => { - const pluginSpecs = [{ getId: () => 'pluginA' }, { getId: () => 'pluginB' }]; - findLegacyPluginSpecsMock.mockImplementation( - (settings) => - Promise.resolve({ - pluginSpecs, - pluginExtendedConfig: settings, - disabledPluginSpecs: [], - uiExports: {}, - navLinks: [], - }) as any - ); - - const legacyService = new LegacyService({ - coreId, - env, - logger, - configService: configService as any, - }); - - await legacyService.discoverPlugins(); - - expect(logLegacyThirdPartyPluginDeprecationWarningMock).toHaveBeenCalledTimes(1); - expect(logLegacyThirdPartyPluginDeprecationWarningMock).toHaveBeenCalledWith({ - specs: pluginSpecs, - log: expect.any(Object), - }); - }); -}); - test('Sets the server.uuid property on the legacy configuration', async () => { configService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); const legacyService = new LegacyService({ @@ -489,23 +439,8 @@ test('Sets the server.uuid property on the legacy configuration', async () => { environmentSetup.instanceUuid = 'UUID_FROM_SERVICE'; - const configSetMock = jest.fn(); - - findLegacyPluginSpecsMock.mockImplementation((settings: LegacyVars) => ({ - pluginSpecs: [], - pluginExtendedConfig: { - has: jest.fn(), - get: jest.fn().mockReturnValue(settings), - set: configSetMock, - }, - disabledPluginSpecs: [], - uiExports: {}, - navLinks: [], - })); - - await legacyService.discoverPlugins(); + const { legacyConfig } = await legacyService.setupLegacyConfig(); await legacyService.setup(setupDeps); - expect(configSetMock).toHaveBeenCalledTimes(1); - expect(configSetMock).toHaveBeenCalledWith('server.uuid', 'UUID_FROM_SERVICE'); + expect(legacyConfig.get('server.uuid')).toBe('UUID_FROM_SERVICE'); }); diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 4dc22be2a9971..086e20c98c1a3 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -16,11 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -import type { PublicMethodsOf } from '@kbn/utility-types'; + import { combineLatest, ConnectableObservable, EMPTY, Observable, Subscription } from 'rxjs'; import { first, map, publishReplay, tap } from 'rxjs/operators'; - +import type { PublicMethodsOf } from '@kbn/utility-types'; import { PathConfigType } from '@kbn/utils'; + +// @ts-expect-error legacy config class +import { Config as LegacyConfigClass } from '../../../legacy/server/config'; import { CoreService } from '../../types'; import { Config } from '../config'; import { CoreContext } from '../core_context'; @@ -28,17 +31,7 @@ import { CspConfigType, config as cspConfig } from '../csp'; import { DevConfig, DevConfigType, config as devConfig } from '../dev'; import { BasePathProxyServer, HttpConfig, HttpConfigType, config as httpConfig } from '../http'; import { Logger } from '../logging'; -import { findLegacyPluginSpecs, logLegacyThirdPartyPluginDeprecationWarning } from './plugins'; -import { - ILegacyInternals, - LegacyServiceSetupDeps, - LegacyServiceStartDeps, - LegacyPlugins, - LegacyServiceDiscoverPlugins, - LegacyConfig, - LegacyVars, -} from './types'; -import { LegacyInternals } from './legacy_internals'; +import { LegacyServiceSetupDeps, LegacyServiceStartDeps, LegacyConfig, LegacyVars } from './types'; import { CoreSetup, CoreStart } from '..'; interface LegacyKbnServer { @@ -80,9 +73,7 @@ export class LegacyService implements CoreService { private setupDeps?: LegacyServiceSetupDeps; private update$?: ConnectableObservable<[Config, PathConfigType]>; private legacyRawConfig?: LegacyConfig; - private legacyPlugins?: LegacyPlugins; private settings?: LegacyVars; - public legacyInternals?: ILegacyInternals; constructor(private readonly coreContext: CoreContext) { const { logger, configService } = coreContext; @@ -97,11 +88,11 @@ export class LegacyService implements CoreService { ).pipe(map(([http, csp]) => new HttpConfig(http, csp))); } - public async discoverPlugins(): Promise { - this.update$ = combineLatest( + public async setupLegacyConfig() { + this.update$ = combineLatest([ this.coreContext.configService.getConfig$(), - this.coreContext.configService.atPath('path') - ).pipe( + this.coreContext.configService.atPath('path'), + ]).pipe( tap(([config, pathConfig]) => { if (this.kbnServer !== undefined) { this.kbnServer.applyLoggingConfiguration(getLegacyRawConfig(config, pathConfig)); @@ -120,74 +111,33 @@ export class LegacyService implements CoreService { ) .toPromise(); - const { - pluginSpecs, - pluginExtendedConfig, - disabledPluginSpecs, - uiExports, - navLinks, - } = await findLegacyPluginSpecs( - this.settings, - this.coreContext.logger, - this.coreContext.env.packageInfo - ); - - logLegacyThirdPartyPluginDeprecationWarning({ - specs: pluginSpecs, - log: this.log, - }); - - this.legacyPlugins = { - pluginSpecs, - disabledPluginSpecs, - uiExports, - navLinks, - }; - - this.legacyRawConfig = pluginExtendedConfig; - - // check for unknown uiExport types - if (uiExports.unknown && uiExports.unknown.length > 0) { - throw new Error( - `Unknown uiExport types: ${uiExports.unknown - .map(({ pluginSpec, type }) => `${type} from ${pluginSpec.getId()}`) - .join(', ')}` - ); - } + this.legacyRawConfig = LegacyConfigClass.withDefaultSchema(this.settings); return { - pluginSpecs, - disabledPluginSpecs, - uiExports, - navLinks, - pluginExtendedConfig, settings: this.settings, + legacyConfig: this.legacyRawConfig!, }; } public async setup(setupDeps: LegacyServiceSetupDeps) { this.log.debug('setting up legacy service'); - if (!this.legacyPlugins) { + if (!this.legacyRawConfig) { throw new Error( - 'Legacy service has not discovered legacy plugins yet. Ensure LegacyService.discoverPlugins() is called before LegacyService.setup()' + 'Legacy config not initialized yet. Ensure LegacyService.setupLegacyConfig() is called before LegacyService.setup()' ); } // propagate the instance uuid to the legacy config, as it was the legacy way to access it. this.legacyRawConfig!.set('server.uuid', setupDeps.core.environment.instanceUuid); + this.setupDeps = setupDeps; - this.legacyInternals = new LegacyInternals( - this.legacyPlugins.uiExports, - this.legacyRawConfig!, - setupDeps.core.http.server - ); } public async start(startDeps: LegacyServiceStartDeps) { const { setupDeps } = this; - if (!setupDeps || !this.legacyPlugins) { + if (!setupDeps || !this.legacyRawConfig) { throw new Error('Legacy service is not setup yet.'); } @@ -201,8 +151,7 @@ export class LegacyService implements CoreService { this.settings!, this.legacyRawConfig!, setupDeps, - startDeps, - this.legacyPlugins! + startDeps ); } } @@ -245,8 +194,7 @@ export class LegacyService implements CoreService { settings: LegacyVars, config: LegacyConfig, setupDeps: LegacyServiceSetupDeps, - startDeps: LegacyServiceStartDeps, - legacyPlugins: LegacyPlugins + startDeps: LegacyServiceStartDeps ) { const coreStart: CoreStart = { capabilities: startDeps.core.capabilities, @@ -337,36 +285,26 @@ export class LegacyService implements CoreService { // eslint-disable-next-line @typescript-eslint/no-var-requires const KbnServer = require('../../../legacy/server/kbn_server'); - const kbnServer: LegacyKbnServer = new KbnServer( - settings, - config, - { - env: { - mode: this.coreContext.env.mode, - packageInfo: this.coreContext.env.packageInfo, - }, - setupDeps: { - core: coreSetup, - plugins: setupDeps.plugins, - }, - startDeps: { - core: coreStart, - plugins: startDeps.plugins, - }, - __internals: { - http: { - registerStaticDir: setupDeps.core.http.registerStaticDir, - }, - hapiServer: setupDeps.core.http.server, - uiPlugins: setupDeps.uiPlugins, - elasticsearch: setupDeps.core.elasticsearch, - rendering: setupDeps.core.rendering, - legacy: this.legacyInternals, - }, - logger: this.coreContext.logger, + const kbnServer: LegacyKbnServer = new KbnServer(settings, config, { + env: { + mode: this.coreContext.env.mode, + packageInfo: this.coreContext.env.packageInfo, }, - legacyPlugins - ); + setupDeps: { + core: coreSetup, + plugins: setupDeps.plugins, + }, + startDeps: { + core: coreStart, + plugins: startDeps.plugins, + }, + __internals: { + hapiServer: setupDeps.core.http.server, + uiPlugins: setupDeps.uiPlugins, + rendering: setupDeps.core.rendering, + }, + logger: this.coreContext.logger, + }); // The kbnWorkerType check is necessary to prevent the repl // from being started multiple times in different processes. diff --git a/src/core/server/legacy/plugins/collect_ui_exports.js b/src/core/server/legacy/plugins/collect_ui_exports.js deleted file mode 100644 index 842ab554d79d1..0000000000000 --- a/src/core/server/legacy/plugins/collect_ui_exports.js +++ /dev/null @@ -1,21 +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. - */ - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -export { collectUiExports } from '../../../../legacy/ui/ui_exports/collect_ui_exports'; diff --git a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts b/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts deleted file mode 100644 index cb4277b130a88..0000000000000 --- a/src/core/server/legacy/plugins/find_legacy_plugin_specs.ts +++ /dev/null @@ -1,135 +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 { Observable, merge, forkJoin } from 'rxjs'; -import { toArray, tap, distinct, map } from 'rxjs/operators'; - -import { - findPluginSpecs, - defaultConfig, - // @ts-expect-error -} from '../../../../legacy/plugin_discovery/find_plugin_specs.js'; -// @ts-expect-error -import { collectUiExports as collectLegacyUiExports } from './collect_ui_exports'; - -import { LoggerFactory } from '../../logging'; -import { PackageInfo } from '../../config'; -import { LegacyUiExports, LegacyPluginSpec, LegacyPluginPack, LegacyConfig } from '../types'; - -export async function findLegacyPluginSpecs( - settings: unknown, - loggerFactory: LoggerFactory, - packageInfo: PackageInfo -) { - const configToMutate: LegacyConfig = defaultConfig(settings); - const { - pack$, - invalidDirectoryError$, - invalidPackError$, - otherError$, - deprecation$, - invalidVersionSpec$, - spec$, - disabledSpec$, - }: { - pack$: Observable; - invalidDirectoryError$: Observable<{ path: string }>; - invalidPackError$: Observable<{ path: string }>; - otherError$: Observable; - deprecation$: Observable<{ spec: LegacyPluginSpec; message: string }>; - invalidVersionSpec$: Observable; - spec$: Observable; - disabledSpec$: Observable; - } = findPluginSpecs(settings, configToMutate) as any; - - const logger = loggerFactory.get('legacy-plugins'); - - const log$ = merge( - pack$.pipe( - tap((definition) => { - const path = definition.getPath(); - logger.debug(`Found plugin at ${path}`, { path }); - }) - ), - - invalidDirectoryError$.pipe( - tap((error) => { - logger.warn(`Unable to scan directory for plugins "${error.path}"`, { - err: error, - dir: error.path, - }); - }) - ), - - invalidPackError$.pipe( - tap((error) => { - logger.warn(`Skipping non-plugin directory at ${error.path}`, { - path: error.path, - }); - }) - ), - - otherError$.pipe( - tap((error) => { - // rethrow unhandled errors, which will fail the server - throw error; - }) - ), - - invalidVersionSpec$.pipe( - map((spec) => { - const name = spec.getId(); - const pluginVersion = spec.getExpectedKibanaVersion(); - const kibanaVersion = packageInfo.version; - return `Plugin "${name}" was disabled because it expected Kibana version "${pluginVersion}", and found "${kibanaVersion}".`; - }), - distinct(), - tap((message) => { - logger.warn(message); - }) - ), - - deprecation$.pipe( - tap(({ spec, message }) => { - const deprecationLogger = loggerFactory.get( - 'plugins', - spec.getConfigPrefix(), - 'config', - 'deprecation' - ); - deprecationLogger.warn(message); - }) - ) - ); - - const [disabledPluginSpecs, pluginSpecs] = await forkJoin( - disabledSpec$.pipe(toArray()), - spec$.pipe(toArray()), - log$.pipe(toArray()) - ).toPromise(); - const uiExports: LegacyUiExports = collectLegacyUiExports(pluginSpecs); - - return { - disabledPluginSpecs, - pluginSpecs, - pluginExtendedConfig: configToMutate, - uiExports, - navLinks: [], - }; -} diff --git a/src/core/server/legacy/plugins/index.ts b/src/core/server/legacy/plugins/index.ts deleted file mode 100644 index 7ec5dbc1983ab..0000000000000 --- a/src/core/server/legacy/plugins/index.ts +++ /dev/null @@ -1,21 +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. - */ - -export { findLegacyPluginSpecs } from './find_legacy_plugin_specs'; -export { logLegacyThirdPartyPluginDeprecationWarning } from './log_legacy_plugins_warning'; diff --git a/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts b/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts deleted file mode 100644 index 2317f1036ce42..0000000000000 --- a/src/core/server/legacy/plugins/log_legacy_plugins_warning.test.ts +++ /dev/null @@ -1,89 +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 { loggerMock } from '../../logging/logger.mock'; -import { logLegacyThirdPartyPluginDeprecationWarning } from './log_legacy_plugins_warning'; -import { LegacyPluginSpec } from '../types'; - -const createPluginSpec = ({ id, path }: { id: string; path: string }): LegacyPluginSpec => { - return { - getId: () => id, - getExpectedKibanaVersion: () => 'kibana', - getConfigPrefix: () => 'plugin.config', - getPack: () => ({ - getPath: () => path, - }), - }; -}; - -describe('logLegacyThirdPartyPluginDeprecationWarning', () => { - let log: ReturnType; - - beforeEach(() => { - log = loggerMock.create(); - }); - - it('logs warning for third party plugins', () => { - logLegacyThirdPartyPluginDeprecationWarning({ - specs: [createPluginSpec({ id: 'plugin', path: '/some-external-path' })], - log, - }); - expect(log.warn).toHaveBeenCalledTimes(1); - expect(log.warn.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "Some installed third party plugin(s) [plugin] are using the legacy plugin format and will no longer work in a future Kibana release. Please refer to https://ela.st/kibana-breaking-changes-8-0 for a list of breaking changes and https://ela.st/kibana-platform-migration for documentation on how to migrate legacy plugins.", - ] - `); - }); - - it('lists all the deprecated plugins and only log once', () => { - logLegacyThirdPartyPluginDeprecationWarning({ - specs: [ - createPluginSpec({ id: 'pluginA', path: '/abs/path/to/pluginA' }), - createPluginSpec({ id: 'pluginB', path: '/abs/path/to/pluginB' }), - createPluginSpec({ id: 'pluginC', path: '/abs/path/to/pluginC' }), - ], - log, - }); - expect(log.warn).toHaveBeenCalledTimes(1); - expect(log.warn.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "Some installed third party plugin(s) [pluginA, pluginB, pluginC] are using the legacy plugin format and will no longer work in a future Kibana release. Please refer to https://ela.st/kibana-breaking-changes-8-0 for a list of breaking changes and https://ela.st/kibana-platform-migration for documentation on how to migrate legacy plugins.", - ] - `); - }); - - it('does not log warning for internal legacy plugins', () => { - logLegacyThirdPartyPluginDeprecationWarning({ - specs: [ - createPluginSpec({ - id: 'plugin', - path: '/absolute/path/to/kibana/src/legacy/core_plugins', - }), - createPluginSpec({ - id: 'plugin', - path: '/absolute/path/to/kibana/x-pack', - }), - ], - log, - }); - - expect(log.warn).not.toHaveBeenCalled(); - }); -}); diff --git a/src/core/server/legacy/plugins/log_legacy_plugins_warning.ts b/src/core/server/legacy/plugins/log_legacy_plugins_warning.ts deleted file mode 100644 index 4a4a1b1b0e60b..0000000000000 --- a/src/core/server/legacy/plugins/log_legacy_plugins_warning.ts +++ /dev/null @@ -1,53 +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 { Logger } from '../../logging'; -import { LegacyPluginSpec } from '../types'; - -const internalPaths = ['/src/legacy/core_plugins', '/x-pack']; - -// Use shortened URLs so destinations can be updated if/when documentation moves -// All platform team members have access to edit these -const breakingChangesUrl = 'https://ela.st/kibana-breaking-changes-8-0'; -const migrationGuideUrl = 'https://ela.st/kibana-platform-migration'; - -export const logLegacyThirdPartyPluginDeprecationWarning = ({ - specs, - log, -}: { - specs: LegacyPluginSpec[]; - log: Logger; -}) => { - const thirdPartySpecs = specs.filter(isThirdPartyPluginSpec); - if (thirdPartySpecs.length > 0) { - const pluginIds = thirdPartySpecs.map((spec) => spec.getId()); - log.warn( - `Some installed third party plugin(s) [${pluginIds.join( - ', ' - )}] are using the legacy plugin format and will no longer work in a future Kibana release. ` + - `Please refer to ${breakingChangesUrl} for a list of breaking changes ` + - `and ${migrationGuideUrl} for documentation on how to migrate legacy plugins.` - ); - } -}; - -const isThirdPartyPluginSpec = (spec: LegacyPluginSpec): boolean => { - const pluginPath = spec.getPack().getPath(); - return !internalPaths.some((internalPath) => pluginPath.indexOf(internalPath) > -1); -}; diff --git a/src/core/server/legacy/types.ts b/src/core/server/legacy/types.ts index 1105308fd44cf..12bfddfff1961 100644 --- a/src/core/server/legacy/types.ts +++ b/src/core/server/legacy/types.ts @@ -17,10 +17,6 @@ * under the License. */ -import { Server } from 'hapi'; - -import { ChromeNavLink } from '../../public'; -import { KibanaRequest, LegacyRequest } from '../http'; import { InternalCoreSetup, InternalCoreStart } from '../internal_types'; import { PluginsServiceSetup, PluginsServiceStart, UiPlugins } from '../plugins'; import { InternalRenderingServiceSetup } from '../rendering'; @@ -50,91 +46,6 @@ export interface LegacyConfig { set(config: LegacyVars): void; } -/** - * @internal - * @deprecated - */ -export interface LegacyPluginPack { - getPath(): string; -} - -/** - * @internal - * @deprecated - */ -export interface LegacyPluginSpec { - getId: () => unknown; - getExpectedKibanaVersion: () => string; - getConfigPrefix: () => string; - getPack: () => LegacyPluginPack; -} - -/** - * @internal - * @deprecated - */ -export interface VarsProvider { - fn: (server: Server, configValue: any) => LegacyVars; - pluginSpec: { - readConfigValue(config: any, key: string | string[]): any; - }; -} - -/** - * @internal - * @deprecated - */ -export type VarsInjector = () => LegacyVars; - -/** - * @internal - * @deprecated - */ -export type VarsReplacer = ( - vars: LegacyVars, - request: LegacyRequest, - server: Server -) => LegacyVars | Promise; - -/** - * @internal - * @deprecated - */ -export type LegacyNavLinkSpec = Partial & { - id: string; - title: string; - url: string; -}; - -/** - * @internal - * @deprecated - */ -export type LegacyAppSpec = Partial & { - pluginId?: string; - listed?: boolean; -}; - -/** - * @internal - * @deprecated - */ -export type LegacyNavLink = Omit & { - order: number; -}; - -/** - * @internal - * @deprecated - */ -export interface LegacyUiExports { - defaultInjectedVarProviders?: VarsProvider[]; - injectedVarsReplacers?: VarsReplacer[]; - navLinkSpecs?: LegacyNavLinkSpec[] | null; - uiAppSpecs?: Array; - unknown?: [{ pluginSpec: LegacyPluginSpec; type: unknown }]; -} - /** * @public * @deprecated @@ -158,43 +69,7 @@ export interface LegacyServiceStartDeps { * @internal * @deprecated */ -export interface ILegacyInternals { - /** - * Inject UI app vars for a particular plugin - */ - injectUiAppVars(id: string, injector: VarsInjector): void; - - /** - * Get all the merged injected UI app vars for a particular plugin - */ - getInjectedUiAppVars(id: string): Promise; - - /** - * Get the metadata vars for a particular plugin - */ - getVars( - id: string, - request: KibanaRequest | LegacyRequest, - injected?: LegacyVars - ): Promise; -} - -/** - * @internal - * @deprecated - */ -export interface LegacyPlugins { - disabledPluginSpecs: LegacyPluginSpec[]; - pluginSpecs: LegacyPluginSpec[]; - uiExports: LegacyUiExports; - navLinks: LegacyNavLink[]; -} - -/** - * @internal - * @deprecated - */ -export interface LegacyServiceDiscoverPlugins extends LegacyPlugins { - pluginExtendedConfig: LegacyConfig; +export interface LegacyServiceSetupConfig { + legacyConfig: LegacyConfig; settings: LegacyVars; } diff --git a/src/core/server/rendering/__mocks__/params.ts b/src/core/server/rendering/__mocks__/params.ts index 0901cec768cd2..ae3830f703a53 100644 --- a/src/core/server/rendering/__mocks__/params.ts +++ b/src/core/server/rendering/__mocks__/params.ts @@ -20,19 +20,16 @@ import { mockCoreContext } from '../../core_context.mock'; import { httpServiceMock } from '../../http/http_service.mock'; import { pluginServiceMock } from '../../plugins/plugins_service.mock'; -import { legacyServiceMock } from '../../legacy/legacy_service.mock'; import { statusServiceMock } from '../../status/status_service.mock'; const context = mockCoreContext.create(); const http = httpServiceMock.createInternalSetupContract(); const uiPlugins = pluginServiceMock.createUiPlugins(); -const legacyPlugins = legacyServiceMock.createDiscoverPlugins(); const status = statusServiceMock.createInternalSetupContract(); export const mockRenderingServiceParams = context; export const mockRenderingSetupDeps = { http, - legacyPlugins, uiPlugins, status, }; diff --git a/src/core/server/rendering/__mocks__/rendering_service.ts b/src/core/server/rendering/__mocks__/rendering_service.ts index 179a09b8619b0..01d084f9ae53c 100644 --- a/src/core/server/rendering/__mocks__/rendering_service.ts +++ b/src/core/server/rendering/__mocks__/rendering_service.ts @@ -27,11 +27,9 @@ export const setupMock: jest.Mocked = { render: jest.fn(), }; export const mockSetup = jest.fn().mockResolvedValue(setupMock); -export const mockStart = jest.fn(); export const mockStop = jest.fn(); export const mockRenderingService: jest.Mocked = { setup: mockSetup, - start: mockStart, stop: mockStop, }; export const RenderingService = jest.fn( diff --git a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap index ab828a1780425..07ca59a48c6b0 100644 --- a/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap +++ b/src/core/server/rendering/__snapshots__/rendering_service.test.ts.snap @@ -27,15 +27,6 @@ Object { "translationsUrl": "/mock-server-basepath/translations/en.json", }, "legacyMetadata": Object { - "app": Object {}, - "basePath": "/mock-server-basepath", - "branch": Any, - "buildNum": Any, - "buildSha": Any, - "bundleId": "app:core", - "devMode": true, - "nav": Array [], - "serverName": "http-server-test", "uiSettings": Object { "defaults": Object { "registered": Object { @@ -44,7 +35,6 @@ Object { }, "user": Object {}, }, - "version": Any, }, "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], @@ -80,15 +70,6 @@ Object { "translationsUrl": "/mock-server-basepath/translations/en.json", }, "legacyMetadata": Object { - "app": Object {}, - "basePath": "/mock-server-basepath", - "branch": Any, - "buildNum": Any, - "buildSha": Any, - "bundleId": "app:core", - "devMode": true, - "nav": Array [], - "serverName": "http-server-test", "uiSettings": Object { "defaults": Object { "registered": Object { @@ -97,7 +78,6 @@ Object { }, "user": Object {}, }, - "version": Any, }, "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], @@ -133,15 +113,6 @@ Object { "translationsUrl": "/mock-server-basepath/translations/en.json", }, "legacyMetadata": Object { - "app": Object {}, - "basePath": "/mock-server-basepath", - "branch": Any, - "buildNum": Any, - "buildSha": Any, - "bundleId": "app:core", - "devMode": true, - "nav": Array [], - "serverName": "http-server-test", "uiSettings": Object { "defaults": Object { "registered": Object { @@ -154,7 +125,6 @@ Object { }, }, }, - "version": Any, }, "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], @@ -190,15 +160,6 @@ Object { "translationsUrl": "/translations/en.json", }, "legacyMetadata": Object { - "app": Object {}, - "basePath": "", - "branch": Any, - "buildNum": Any, - "buildSha": Any, - "bundleId": "app:core", - "devMode": true, - "nav": Array [], - "serverName": "http-server-test", "uiSettings": Object { "defaults": Object { "registered": Object { @@ -207,7 +168,6 @@ Object { }, "user": Object {}, }, - "version": Any, }, "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], @@ -243,15 +203,6 @@ Object { "translationsUrl": "/mock-server-basepath/translations/en.json", }, "legacyMetadata": Object { - "app": Object {}, - "basePath": "/mock-server-basepath", - "branch": Any, - "buildNum": Any, - "buildSha": Any, - "bundleId": "app:core", - "devMode": true, - "nav": Array [], - "serverName": "http-server-test", "uiSettings": Object { "defaults": Object { "registered": Object { @@ -260,7 +211,6 @@ Object { }, "user": Object {}, }, - "version": Any, }, "serverBasePath": "/mock-server-basepath", "uiPlugins": Array [], diff --git a/src/core/server/rendering/rendering_service.test.ts b/src/core/server/rendering/rendering_service.test.ts index 254bafed5b194..08978cd1df64d 100644 --- a/src/core/server/rendering/rendering_service.test.ts +++ b/src/core/server/rendering/rendering_service.test.ts @@ -43,12 +43,6 @@ const INJECTED_METADATA = { version: expect.any(String), }, }, - legacyMetadata: { - branch: expect.any(String), - buildNum: expect.any(Number), - buildSha: expect.any(String), - version: expect.any(String), - }, }; const { createKibanaRequest, createRawRequest } = httpServerMock; @@ -72,13 +66,6 @@ describe('RenderingService', () => { registered: { name: 'title' }, }); render = (await service.setup(mockRenderingSetupDeps)).render; - await service.start({ - legacy: { - legacyInternals: { - getVars: () => ({}), - }, - }, - } as any); }); it('renders "core" page', async () => { diff --git a/src/core/server/rendering/rendering_service.tsx b/src/core/server/rendering/rendering_service.tsx index 7761c89044f6f..738787f940905 100644 --- a/src/core/server/rendering/rendering_service.tsx +++ b/src/core/server/rendering/rendering_service.tsx @@ -20,14 +20,11 @@ import React from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { take } from 'rxjs/operators'; - import { i18n } from '@kbn/i18n'; import { UiPlugins } from '../plugins'; -import { CoreService } from '../../types'; import { CoreContext } from '../core_context'; import { Template } from './views'; -import { LegacyService } from '../legacy'; import { IRenderOptions, RenderingSetupDeps, @@ -36,25 +33,20 @@ import { } from './types'; /** @internal */ -export class RenderingService implements CoreService { - private legacyInternals?: LegacyService['legacyInternals']; +export class RenderingService { constructor(private readonly coreContext: CoreContext) {} public async setup({ http, status, - legacyPlugins, uiPlugins, }: RenderingSetupDeps): Promise { return { render: async ( request, uiSettings, - { app = { getId: () => 'core' }, includeUserSettings = true, vars }: IRenderOptions = {} + { includeUserSettings = true, vars }: IRenderOptions = {} ) => { - if (!this.legacyInternals) { - throw new Error('Cannot render before "start"'); - } const env = { mode: this.coreContext.env.mode, packageInfo: this.coreContext.env.packageInfo, @@ -65,7 +57,6 @@ export class RenderingService implements CoreService ({ id, @@ -96,16 +87,6 @@ export class RenderingService implements CoreService; }>; legacyMetadata: { - app: { getId(): string }; - bundleId: string; - nav: LegacyNavLink[]; - version: string; - branch: string; - buildNum: number; - buildSha: string; - serverName: string; - devMode: boolean; - basePath: string; uiSettings: { defaults: Record; user: Record>; @@ -78,7 +67,6 @@ export interface RenderingMetadata { /** @internal */ export interface RenderingSetupDeps { http: InternalHttpServiceSetup; - legacyPlugins: LegacyServiceDiscoverPlugins; status: InternalStatusServiceSetup; uiPlugins: UiPlugins; } @@ -91,14 +79,6 @@ export interface IRenderOptions { */ includeUserSettings?: boolean; - /** - * Render the bootstrapped HTML content for an optional legacy application. - * Defaults to `core`. - * @deprecated for legacy use only, remove with ui_render_mixin - * @internal - */ - app?: { getId(): string }; - /** * Inject custom vars into the page metadata. * @deprecated for legacy use only, remove with ui_render_mixin diff --git a/src/core/server/saved_objects/routes/integration_tests/import.test.ts b/src/core/server/saved_objects/routes/integration_tests/import.test.ts index 0bc03fbcf8038..67be2b56b4447 100644 --- a/src/core/server/saved_objects/routes/integration_tests/import.test.ts +++ b/src/core/server/saved_objects/routes/integration_tests/import.test.ts @@ -471,7 +471,11 @@ describe(`POST ${URL}`, () => { describe('createNewCopies enabled', () => { it('imports objects, regenerating all IDs/reference IDs present, and resetting all origin IDs', async () => { - mockUuidv4.mockReturnValueOnce('new-id-1').mockReturnValueOnce('new-id-2'); + mockUuidv4 + .mockReturnValueOnce('foo') // a uuid.v4() is generated for the request.id + .mockReturnValueOnce('foo') // another uuid.v4() is used for the request.uuid + .mockReturnValueOnce('new-id-1') + .mockReturnValueOnce('new-id-2'); savedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [mockIndexPattern] }); const obj1 = { type: 'visualization', @@ -490,7 +494,6 @@ describe(`POST ${URL}`, () => { const result = await supertest(httpSetup.server.listener) .post(`${URL}?createNewCopies=true`) .set('content-Type', 'multipart/form-data; boundary=EXAMPLE') - .set('x-opaque-id', uuidv4()) // prevents src/core/server/http/http_tools.ts from using our mocked uuidv4 to generate a unique ID for this request .send( [ '--EXAMPLE', 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 352ce4c1c16eb..0e72ad2fec06c 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -2477,6 +2477,33 @@ describe('SavedObjectsRepository', () => { expect(client.search).not.toHaveBeenCalled(); }); + it(`throws when namespaces is an empty array`, async () => { + await expect( + savedObjectsRepository.find({ type: 'foo', namespaces: [] }) + ).rejects.toThrowError('options.namespaces cannot be an empty array'); + expect(client.search).not.toHaveBeenCalled(); + }); + + it(`throws when type is not falsy and typeToNamespacesMap is defined`, async () => { + await expect( + savedObjectsRepository.find({ type: 'foo', typeToNamespacesMap: new Map() }) + ).rejects.toThrowError( + 'options.type must be an empty string when options.typeToNamespacesMap is used' + ); + expect(client.search).not.toHaveBeenCalled(); + }); + + it(`throws when type is not an empty array and typeToNamespacesMap is defined`, async () => { + const test = async (args) => { + await expect(savedObjectsRepository.find(args)).rejects.toThrowError( + 'options.namespaces must be an empty array when options.typeToNamespacesMap is used' + ); + expect(client.search).not.toHaveBeenCalled(); + }; + await test({ type: '', typeToNamespacesMap: new Map() }); + await test({ type: '', namespaces: ['some-ns'], typeToNamespacesMap: new Map() }); + }); + it(`throws when searchFields is defined but not an array`, async () => { await expect( savedObjectsRepository.find({ type, searchFields: 'string' }) @@ -2493,7 +2520,7 @@ describe('SavedObjectsRepository', () => { it(`throws when KQL filter syntax is invalid`, async () => { const findOpts = { - namespace, + namespaces: [namespace], search: 'foo*', searchFields: ['foo'], type: ['dashboard'], @@ -2577,38 +2604,70 @@ describe('SavedObjectsRepository', () => { const test = async (types) => { const result = await savedObjectsRepository.find({ type: types }); expect(result).toEqual(expect.objectContaining({ saved_objects: [] })); + expect(client.search).not.toHaveBeenCalled(); }; await test('unknownType'); await test(HIDDEN_TYPE); await test(['unknownType', HIDDEN_TYPE]); }); + + it(`should return empty results when attempting to find only invalid or hidden types using typeToNamespacesMap`, async () => { + const test = async (types) => { + const result = await savedObjectsRepository.find({ + typeToNamespacesMap: new Map(types.map((x) => [x, undefined])), + type: '', + namespaces: [], + }); + expect(result).toEqual(expect.objectContaining({ saved_objects: [] })); + expect(client.search).not.toHaveBeenCalled(); + }; + + await test(['unknownType']); + await test([HIDDEN_TYPE]); + await test(['unknownType', HIDDEN_TYPE]); + }); }); describe('search dsl', () => { - it(`passes mappings, registry, search, defaultSearchOperator, searchFields, type, sortField, sortOrder and hasReference to getSearchDsl`, async () => { + const commonOptions = { + type: [type], // cannot be used when `typeToNamespacesMap` is present + namespaces: [namespace], // cannot be used when `typeToNamespacesMap` is present + search: 'foo*', + searchFields: ['foo'], + sortField: 'name', + sortOrder: 'desc', + defaultSearchOperator: 'AND', + hasReference: { + type: 'foo', + id: '1', + }, + kueryNode: undefined, + }; + + it(`passes mappings, registry, and search options to getSearchDsl`, async () => { + await findSuccess(commonOptions, namespace); + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, commonOptions); + }); + + it(`accepts typeToNamespacesMap`, async () => { const relevantOpts = { - namespaces: [namespace], - search: 'foo*', - searchFields: ['foo'], - type: [type], - sortField: 'name', - sortOrder: 'desc', - defaultSearchOperator: 'AND', - hasReference: { - type: 'foo', - id: '1', - }, - kueryNode: undefined, + ...commonOptions, + type: '', + namespaces: [], + typeToNamespacesMap: new Map([[type, [namespace]]]), // can only be used when `type` is falsy and `namespaces` is an empty array }; await findSuccess(relevantOpts, namespace); - expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, relevantOpts); + expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, { + ...relevantOpts, + type: [type], + }); }); it(`accepts KQL expression filter and passes KueryNode to getSearchDsl`, async () => { const findOpts = { - namespace, + namespaces: [namespace], search: 'foo*', searchFields: ['foo'], type: ['dashboard'], @@ -2649,7 +2708,7 @@ describe('SavedObjectsRepository', () => { it(`accepts KQL KueryNode filter and passes KueryNode to getSearchDsl`, async () => { const findOpts = { - namespace, + namespaces: [namespace], search: 'foo*', searchFields: ['foo'], type: ['dashboard'], diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 125f97e7feb11..a83c86e585628 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -67,7 +67,7 @@ import { } from '../../types'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { validateConvertFilterToKueryNode } from './filter_utils'; -import { SavedObjectsUtils } from './utils'; +import { FIND_DEFAULT_PAGE, FIND_DEFAULT_PER_PAGE, SavedObjectsUtils } from './utils'; // BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository // so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. @@ -693,37 +693,51 @@ export class SavedObjectsRepository { * @property {string} [options.preference] * @returns {promise} - { saved_objects: [{ id, type, version, attributes }], total, per_page, page } */ - async find({ - search, - defaultSearchOperator = 'OR', - searchFields, - rootSearchFields, - hasReference, - page = 1, - perPage = 20, - sortField, - sortOrder, - fields, - namespaces, - type, - filter, - preference, - }: SavedObjectsFindOptions): Promise> { - if (!type) { + async find(options: SavedObjectsFindOptions): Promise> { + const { + search, + defaultSearchOperator = 'OR', + searchFields, + rootSearchFields, + hasReference, + page = FIND_DEFAULT_PAGE, + perPage = FIND_DEFAULT_PER_PAGE, + sortField, + sortOrder, + fields, + namespaces, + type, + typeToNamespacesMap, + filter, + preference, + } = options; + + if (!type && !typeToNamespacesMap) { throw SavedObjectsErrorHelpers.createBadRequestError( 'options.type must be a string or an array of strings' ); + } else if (namespaces?.length === 0 && !typeToNamespacesMap) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'options.namespaces cannot be an empty array' + ); + } else if (type && typeToNamespacesMap) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'options.type must be an empty string when options.typeToNamespacesMap is used' + ); + } else if ((!namespaces || namespaces?.length) && typeToNamespacesMap) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'options.namespaces must be an empty array when options.typeToNamespacesMap is used' + ); } - const types = Array.isArray(type) ? type : [type]; + const types = type + ? Array.isArray(type) + ? type + : [type] + : Array.from(typeToNamespacesMap!.keys()); const allowedTypes = types.filter((t) => this._allowedTypes.includes(t)); if (allowedTypes.length === 0) { - return { - page, - per_page: perPage, - total: 0, - saved_objects: [], - }; + return SavedObjectsUtils.createEmptyFindResponse(options); } if (searchFields && !Array.isArray(searchFields)) { @@ -766,6 +780,7 @@ export class SavedObjectsRepository { sortField, sortOrder, namespaces, + typeToNamespacesMap, hasReference, kueryNode, }), diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts index 4adc92df31805..e13c67a720400 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.test.ts @@ -50,6 +50,40 @@ const ALL_TYPE_SUBSETS = ALL_TYPES.reduce( .filter((x) => x.length) // exclude empty set .map((x) => (x.length === 1 ? x[0] : x)); // if a subset is a single string, destructure it +const createTypeClause = (type: string, namespaces?: string[]) => { + if (registry.isMultiNamespace(type)) { + return { + bool: { + must: expect.arrayContaining([{ terms: { namespaces: namespaces ?? ['default'] } }]), + must_not: [{ exists: { field: 'namespace' } }], + }, + }; + } else if (registry.isSingleNamespace(type)) { + const nonDefaultNamespaces = namespaces?.filter((n) => n !== 'default') ?? []; + const should: any = []; + if (nonDefaultNamespaces.length > 0) { + should.push({ terms: { namespace: nonDefaultNamespaces } }); + } + if (namespaces?.includes('default')) { + should.push({ bool: { must_not: [{ exists: { field: 'namespace' } }] } }); + } + return { + bool: { + must: [{ term: { type } }], + should: expect.arrayContaining(should), + minimum_should_match: 1, + must_not: [{ exists: { field: 'namespaces' } }], + }, + }; + } + // isNamespaceAgnostic + return { + bool: expect.objectContaining({ + must_not: [{ exists: { field: 'namespace' } }, { exists: { field: 'namespaces' } }], + }), + }; +}; + /** * Note: these tests cases are defined in the order they appear in the source code, for readability's sake */ @@ -198,40 +232,6 @@ describe('#getQueryParams', () => { }); describe('`namespaces` parameter', () => { - const createTypeClause = (type: string, namespaces?: string[]) => { - if (registry.isMultiNamespace(type)) { - return { - bool: { - must: expect.arrayContaining([{ terms: { namespaces: namespaces ?? ['default'] } }]), - must_not: [{ exists: { field: 'namespace' } }], - }, - }; - } else if (registry.isSingleNamespace(type)) { - const nonDefaultNamespaces = namespaces?.filter((n) => n !== 'default') ?? []; - const should: any = []; - if (nonDefaultNamespaces.length > 0) { - should.push({ terms: { namespace: nonDefaultNamespaces } }); - } - if (namespaces?.includes('default')) { - should.push({ bool: { must_not: [{ exists: { field: 'namespace' } }] } }); - } - return { - bool: { - must: [{ term: { type } }], - should: expect.arrayContaining(should), - minimum_should_match: 1, - must_not: [{ exists: { field: 'namespaces' } }], - }, - }; - } - // isNamespaceAgnostic - return { - bool: expect.objectContaining({ - must_not: [{ exists: { field: 'namespace' } }, { exists: { field: 'namespaces' } }], - }), - }; - }; - const expectResult = (result: Result, ...typeClauses: any) => { expect(result.query.bool.filter).toEqual( expect.arrayContaining([ @@ -281,6 +281,37 @@ describe('#getQueryParams', () => { test(['default']); }); }); + + describe('`typeToNamespacesMap` parameter', () => { + const expectResult = (result: Result, ...typeClauses: any) => { + expect(result.query.bool.filter).toEqual( + expect.arrayContaining([ + { bool: expect.objectContaining({ should: typeClauses, minimum_should_match: 1 }) }, + ]) + ); + }; + + it('supersedes `type` and `namespaces` parameters', () => { + const result = getQueryParams({ + mappings, + registry, + type: ['pending', 'saved', 'shared', 'global'], + namespaces: ['foo', 'bar', 'default'], + typeToNamespacesMap: new Map([ + ['pending', ['foo']], // 'pending' is only authorized in the 'foo' namespace + // 'saved' is not authorized in any namespaces + ['shared', ['bar', 'default']], // 'shared' is only authorized in the 'bar' and 'default' namespaces + ['global', ['foo', 'bar', 'default']], // 'global' is authorized in all namespaces (which are ignored anyway) + ]), + }); + expectResult( + result, + createTypeClause('pending', ['foo']), + createTypeClause('shared', ['bar', 'default']), + createTypeClause('global') + ); + }); + }); }); describe('search clause (query.bool.must.simple_query_string)', () => { diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts index 642d51c70766e..eaddc05fa921c 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts @@ -129,6 +129,7 @@ interface QueryParams { registry: ISavedObjectTypeRegistry; namespaces?: string[]; type?: string | string[]; + typeToNamespacesMap?: Map; search?: string; searchFields?: string[]; rootSearchFields?: string[]; @@ -145,6 +146,7 @@ export function getQueryParams({ registry, namespaces, type, + typeToNamespacesMap, search, searchFields, rootSearchFields, @@ -152,7 +154,10 @@ export function getQueryParams({ hasReference, kueryNode, }: QueryParams) { - const types = getTypes(mappings, type); + const types = getTypes( + mappings, + typeToNamespacesMap ? Array.from(typeToNamespacesMap.keys()) : type + ); // A de-duplicated set of namespaces makes for a more effecient query. // @@ -163,9 +168,12 @@ export function getQueryParams({ // since that is consistent with how a single-namespace search behaves in the OSS distribution. Leaving the wildcard in place // would result in no results being returned, as the wildcard is treated as a literal, and not _actually_ as a wildcard. // We had a good discussion around the tradeoffs here: https://github.com/elastic/kibana/pull/67644#discussion_r441055716 - const normalizedNamespaces = namespaces - ? Array.from(new Set(namespaces.map((x) => (x === '*' ? DEFAULT_NAMESPACE_STRING : x)))) - : undefined; + const normalizeNamespaces = (namespacesToNormalize?: string[]) => + namespacesToNormalize + ? Array.from( + new Set(namespacesToNormalize.map((x) => (x === '*' ? DEFAULT_NAMESPACE_STRING : x))) + ) + : undefined; const bool: any = { filter: [ @@ -197,9 +205,12 @@ export function getQueryParams({ }, ] : undefined, - should: types.map((shouldType) => - getClauseForType(registry, normalizedNamespaces, shouldType) - ), + should: types.map((shouldType) => { + const normalizedNamespaces = normalizeNamespaces( + typeToNamespacesMap ? typeToNamespacesMap.get(shouldType) : namespaces + ); + return getClauseForType(registry, normalizedNamespaces, shouldType); + }), minimum_should_match: 1, }, }, diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts index 62e629ad33cc8..7276e505bce7d 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.test.ts @@ -57,10 +57,11 @@ describe('getSearchDsl', () => { }); describe('passes control', () => { - it('passes (mappings, schema, namespaces, type, search, searchFields, rootSearchFields, hasReference) to getQueryParams', () => { + it('passes (mappings, schema, namespaces, type, typeToNamespacesMap, search, searchFields, rootSearchFields, hasReference) to getQueryParams', () => { const opts = { namespaces: ['foo-namespace'], type: 'foo', + typeToNamespacesMap: new Map(), search: 'bar', searchFields: ['baz'], rootSearchFields: ['qux'], @@ -78,6 +79,7 @@ describe('getSearchDsl', () => { registry, namespaces: opts.namespaces, type: opts.type, + typeToNamespacesMap: opts.typeToNamespacesMap, search: opts.search, searchFields: opts.searchFields, rootSearchFields: opts.rootSearchFields, diff --git a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts index aa79a10b2a9be..858770579fb9e 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/search_dsl.ts @@ -35,6 +35,7 @@ interface GetSearchDslOptions { sortField?: string; sortOrder?: string; namespaces?: string[]; + typeToNamespacesMap?: Map; hasReference?: { type: string; id: string; @@ -56,6 +57,7 @@ export function getSearchDsl( sortField, sortOrder, namespaces, + typeToNamespacesMap, hasReference, kueryNode, } = options; @@ -74,6 +76,7 @@ export function getSearchDsl( registry, namespaces, type, + typeToNamespacesMap, search, searchFields, rootSearchFields, diff --git a/src/core/server/saved_objects/service/lib/utils.test.ts b/src/core/server/saved_objects/service/lib/utils.test.ts index ea4fa68242bea..ac06ca9275783 100644 --- a/src/core/server/saved_objects/service/lib/utils.test.ts +++ b/src/core/server/saved_objects/service/lib/utils.test.ts @@ -17,10 +17,11 @@ * under the License. */ +import { SavedObjectsFindOptions } from '../../types'; import { SavedObjectsUtils } from './utils'; describe('SavedObjectsUtils', () => { - const { namespaceIdToString, namespaceStringToId } = SavedObjectsUtils; + const { namespaceIdToString, namespaceStringToId, createEmptyFindResponse } = SavedObjectsUtils; describe('#namespaceIdToString', () => { it('converts `undefined` to default namespace string', () => { @@ -54,4 +55,26 @@ describe('SavedObjectsUtils', () => { test(''); }); }); + + describe('#createEmptyFindResponse', () => { + it('returns expected result', () => { + const options = {} as SavedObjectsFindOptions; + expect(createEmptyFindResponse(options)).toEqual({ + page: 1, + per_page: 20, + total: 0, + saved_objects: [], + }); + }); + + it('handles `page` field', () => { + const options = { page: 42 } as SavedObjectsFindOptions; + expect(createEmptyFindResponse(options).page).toEqual(42); + }); + + it('handles `perPage` field', () => { + const options = { perPage: 42 } as SavedObjectsFindOptions; + expect(createEmptyFindResponse(options).per_page).toEqual(42); + }); + }); }); diff --git a/src/core/server/saved_objects/service/lib/utils.ts b/src/core/server/saved_objects/service/lib/utils.ts index 6101ad57cc401..3efe8614da1d7 100644 --- a/src/core/server/saved_objects/service/lib/utils.ts +++ b/src/core/server/saved_objects/service/lib/utils.ts @@ -17,7 +17,12 @@ * under the License. */ +import { SavedObjectsFindOptions } from '../../types'; +import { SavedObjectsFindResponse } from '..'; + export const DEFAULT_NAMESPACE_STRING = 'default'; +export const FIND_DEFAULT_PAGE = 1; +export const FIND_DEFAULT_PER_PAGE = 20; /** * @public @@ -50,4 +55,17 @@ export class SavedObjectsUtils { return namespace !== DEFAULT_NAMESPACE_STRING ? namespace : undefined; }; + + /** + * Creates an empty response for a find operation. This is only intended to be used by saved objects client wrappers. + */ + public static createEmptyFindResponse = ({ + page = FIND_DEFAULT_PAGE, + perPage = FIND_DEFAULT_PER_PAGE, + }: SavedObjectsFindOptions): SavedObjectsFindResponse => ({ + page, + per_page: perPage, + total: 0, + saved_objects: [], + }); } diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 1885f5ec50139..01128e4f8cf51 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -89,6 +89,14 @@ export interface SavedObjectsFindOptions { defaultSearchOperator?: 'AND' | 'OR'; filter?: string | KueryNode; namespaces?: string[]; + /** + * This map defines each type to search for, and the namespace(s) to search for the type in; this is only intended to be used by a saved + * object client wrapper. + * If this is defined, it supersedes the `type` and `namespaces` fields when building the Elasticsearch query. + * Any types that are not included in this map will be excluded entirely. + * If a type is included but its value is undefined, the operation will search for that type in the Default namespace. + */ + typeToNamespacesMap?: Map; /** An optional ES preference value to be used for the query **/ preference?: string; } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 11a14457784fd..cc51d27589ce7 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -864,10 +864,6 @@ export interface IndexSettingsDeprecationInfo { // @public (undocumented) export interface IRenderOptions { - // @internal @deprecated - app?: { - getId(): string; - }; includeUserSettings?: boolean; // @internal @deprecated vars?: Record; @@ -943,6 +939,7 @@ export class KibanaRequest>; - // (undocumented) - getVars(id: string, request: KibanaRequest | LegacyRequest, injected?: LegacyVars): Promise>; - // Warning: (ae-forgotten-export) The symbol "VarsInjector" needs to be exported by the entry point index.d.ts - // - // (undocumented) - injectUiAppVars(id: string, injector: VarsInjector): void; - } - // @public @deprecated (undocumented) export interface LegacyRequest extends Request { } @@ -1311,16 +1293,6 @@ export class LegacyScopedClusterClient implements ILegacyScopedClusterClient { callAsInternalUser(endpoint: string, clientParams?: Record, options?: LegacyCallAPIOptions): Promise; } -// Warning: (ae-forgotten-export) The symbol "LegacyPlugins" needs to be exported by the entry point index.d.ts -// -// @internal @deprecated (undocumented) -export interface LegacyServiceDiscoverPlugins extends LegacyPlugins { - // (undocumented) - pluginExtendedConfig: LegacyConfig; - // (undocumented) - settings: LegacyVars; -} - // @public @deprecated (undocumented) export interface LegacyServiceSetupDeps { // Warning: (ae-forgotten-export) The symbol "LegacyCoreSetup" needs to be exported by the entry point index.d.ts @@ -1345,31 +1317,6 @@ export interface LegacyServiceStartDeps { plugins: Record; } -// @internal @deprecated (undocumented) -export interface LegacyUiExports { - // Warning: (ae-forgotten-export) The symbol "VarsProvider" needs to be exported by the entry point index.d.ts - // - // (undocumented) - defaultInjectedVarProviders?: VarsProvider[]; - // Warning: (ae-forgotten-export) The symbol "VarsReplacer" needs to be exported by the entry point index.d.ts - // - // (undocumented) - injectedVarsReplacers?: VarsReplacer[]; - // Warning: (ae-forgotten-export) The symbol "LegacyNavLinkSpec" needs to be exported by the entry point index.d.ts - // - // (undocumented) - navLinkSpecs?: LegacyNavLinkSpec[] | null; - // Warning: (ae-forgotten-export) The symbol "LegacyAppSpec" needs to be exported by the entry point index.d.ts - // - // (undocumented) - uiAppSpecs?: Array; - // (undocumented) - unknown?: [{ - pluginSpec: LegacyPluginSpec; - type: unknown; - }]; -} - // Warning: (ae-forgotten-export) The symbol "lifecycleResponseFactory" needs to be exported by the entry point index.d.ts // // @public @@ -2176,6 +2123,7 @@ export interface SavedObjectsFindOptions { sortOrder?: string; // (undocumented) type: string | string[]; + typeToNamespacesMap?: Map; } // @public @@ -2387,7 +2335,7 @@ export class SavedObjectsRepository { deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOptions): Promise; deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise; // (undocumented) - find({ search, defaultSearchOperator, searchFields, rootSearchFields, hasReference, page, perPage, sortField, sortOrder, fields, namespaces, type, filter, preference, }: SavedObjectsFindOptions): Promise>; + find(options: SavedObjectsFindOptions): Promise>; get(type: string, id: string, options?: SavedObjectsBaseOptions): Promise>; incrementCounter(type: string, id: string, counterFieldName: string, options?: SavedObjectsIncrementCounterOptions): Promise; update(type: string, id: string, attributes: Partial, options?: SavedObjectsUpdateOptions): Promise>; @@ -2495,6 +2443,7 @@ export interface SavedObjectsUpdateResponse extends Omit({ page, perPage, }: SavedObjectsFindOptions) => SavedObjectsFindResponse; static namespaceIdToString: (namespace?: string | undefined) => string; static namespaceStringToId: (namespace: string) => string | undefined; } @@ -2731,7 +2680,6 @@ export const validBodyOutput: readonly ["data", "stream"]; // Warnings were encountered during analysis: // // src/core/server/http/router/response.ts:316:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts -// src/core/server/legacy/types.ts:135:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:274:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:274:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:277:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 8502f563cb0c2..5935636d54f9d 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -113,11 +113,12 @@ export class Server { const { pluginTree, uiPlugins } = await this.plugins.discover({ environment: environmentSetup, }); - const legacyPlugins = await this.legacy.discoverPlugins(); + const legacyConfigSetup = await this.legacy.setupLegacyConfig(); // Immediately terminate in case of invalid configuration + // This needs to be done after plugin discovery await this.configService.validate(); - await ensureValidConfiguration(this.configService, legacyPlugins); + await ensureValidConfiguration(this.configService, legacyConfigSetup); const contextServiceSetup = this.context.setup({ // We inject a fake "legacy plugin" with dependencies on every plugin so that legacy plugins: @@ -166,7 +167,6 @@ export class Server { const renderingSetup = await this.rendering.setup({ http: httpSetup, status: statusSetup, - legacyPlugins, uiPlugins, }); @@ -248,10 +248,6 @@ export class Server { await this.http.start(); - await this.rendering.start({ - legacy: this.legacy, - }); - return this.coreStart; } diff --git a/src/core/tsconfig.json b/src/core/tsconfig.json index b8780321e11dd..a3531057767d4 100644 --- a/src/core/tsconfig.json +++ b/src/core/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "composite": true, - "outDir": "./target", + "outDir": "./target/types", "emitDeclarationOnly": true, "declaration": true, "declarationMap": true diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index b02b7cc16ec4a..884e7e38494cc 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -235,6 +235,7 @@ kibana_vars=( xpack.security.cookieName xpack.security.enabled xpack.security.encryptionKey + xpack.security.sameSiteCookies xpack.security.secureCookies xpack.security.sessionTimeout xpack.security.session.idleTimeout diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 5d31db63773fa..3c556a4f1ba3c 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -55,7 +55,6 @@ export default { '@elastic/eui$': '/node_modules/@elastic/eui/test-env', '@elastic/eui/lib/(.*)?': '/node_modules/@elastic/eui/test-env/$1', '^src/plugins/(.*)': '/src/plugins/$1', - '^uiExports/(.*)': '/src/dev/jest/mocks/file_mock.js', '^test_utils/(.*)': '/src/test_utils/public/$1', '^fixtures/(.*)': '/src/fixtures/$1', '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': diff --git a/src/fixtures/stubbed_saved_object_index_pattern.ts b/src/fixtures/stubbed_saved_object_index_pattern.ts index 44b391f14cf9c..261e451db5452 100644 --- a/src/fixtures/stubbed_saved_object_index_pattern.ts +++ b/src/fixtures/stubbed_saved_object_index_pattern.ts @@ -28,10 +28,10 @@ export function stubbedSavedObjectIndexPattern(id: string | null = null) { type: 'index-pattern', attributes: { timeFieldName: 'timestamp', - customFormats: '{}', + customFormats: {}, fields: mockLogstashFields, title: 'title', }, - version: 2, + version: '2', }; } diff --git a/src/legacy/plugin_discovery/README.md b/src/legacy/plugin_discovery/README.md deleted file mode 100644 index 83e7c10d16fff..0000000000000 --- a/src/legacy/plugin_discovery/README.md +++ /dev/null @@ -1,148 +0,0 @@ -# Plugin Discovery - -The plugin discovery module defines the core plugin loading logic used by the Kibana server. It exports functions for - - -## `findPluginSpecs(settings, [config])` - -Finds [`PluginSpec`][PluginSpec] objects - -### params - - `settings`: the same settings object accepted by [`KbnServer`][KbnServer] - - `[config]`: Optional - a [`Config`][Config] service. Using this param causes `findPluginSpecs()` to modify `config`'s schema to support the configuration for each discovered [`PluginSpec`][PluginSpec]. If you can, please use the [`Config`][Config] service produced by `extendedConfig$` rather than passing in an existing service so that `findPluginSpecs()` is side-effect free. - -### return value - -`findPluginSpecs()` returns an object of Observables which produce values at different parts of the process. Since the Observables are all aware of their own dependencies you can subscribe to any combination (within the same tick) and only the necessary plugin logic will be executed. - -If you *never* subscribe to any of the Observables then plugin discovery won't actually run. - - - `pack$`: emits every [`PluginPack`][PluginPack] found - - `invalidDirectoryError$: Observable`: emits [`InvalidDirectoryError`][Errors]s caused by `settings.plugins.scanDirs` values that don't point to actual directories. `findPluginSpecs()` will not abort when this error is encountered. - - `invalidPackError$: Observable`: emits [`InvalidPackError`][Errors]s caused by children of `settings.plugins.scanDirs` or `settings.plugins.paths` values which don't meet the requirements of a [`PluginPack`][PluginPack] (probably missing a `package.json`). `findPluginSpecs()` will not abort when this error is encountered. - - `deprecation$: Observable`: emits deprecation warnings that are produces when reading each [`PluginPack`][PluginPack]'s configuration - - `extendedConfig$: Observable`: emits the [`Config`][Config] service that was passed to `findPluginSpecs()` (or created internally if none was passed) after it has been extended with the configuration from each plugin - - `spec$: Observable`: emits every *enabled* [`PluginSpec`][PluginSpec] defined by the discovered [`PluginPack`][PluginPack]s - - `disabledSpec$: Observable`: emits every *disabled* [`PluginSpec`][PluginSpec] defined by the discovered [`PluginPack`][PluginPack]s - - `invalidVersionSpec$: Observable`: emits every [`PluginSpec`][PluginSpec] who's required kibana version does not match the version exposed by `config.get('pkg.version')` - -### example - -Just get the plugin specs, only fail if there is an uncaught error of some sort: -```js -const { pack$ } = findPluginSpecs(settings); -const packs = await pack$.pipe(toArray()).toPromise() -``` - -Just log the deprecation messages: -```js -const { deprecation$ } = findPluginSpecs(settings); -for (const warning of await deprecation$.pipe(toArray()).toPromise()) { - console.log('DEPRECATION:', warning) -} -``` - -Get the packs but fail if any packs are invalid: -```js -const { pack$, invalidDirectoryError$ } = findPluginSpecs(settings); -const packs = await Rx.merge( - pack$.pipe(toArray()), - - // if we ever get an InvalidDirectoryError, throw it - // into the stream so that all streams are unsubscribed, - // the discovery process is aborted, and the promise rejects - invalidDirectoryError$.pipe( - map(error => { throw error }) - ), -).toPromise() -``` - -Handle everything -```js -const { - pack$, - invalidDirectoryError$, - invalidPackError$, - deprecation$, - extendedConfig$, - spec$, - disabledSpecs$, - invalidVersionSpec$, -} = findPluginSpecs(settings); - -Rx.merge( - pack$.pipe( - tap(pluginPack => console.log('Found plugin pack', pluginPack)) - ), - - invalidDirectoryError$.pipe( - tap(error => console.log('Invalid directory error', error)) - ), - - invalidPackError$.pipe( - tap(error => console.log('Invalid plugin pack error', error)) - ), - - deprecation$.pipe( - tap(msg => console.log('DEPRECATION:', msg)) - ), - - extendedConfig$.pipe( - tap(config => console.log('config service extended by plugins', config)) - ), - - spec$.pipe( - tap(pluginSpec => console.log('enabled plugin spec found', spec)) - ), - - disabledSpec$.pipe( - tap(pluginSpec => console.log('disabled plugin spec found', spec)) - ), - - invalidVersionSpec$.pipe( - tap(pluginSpec => console.log('plugin spec with invalid version found', spec)) - ), -) -.toPromise() -.then(() => { - console.log('plugin discovery complete') -}) -.catch((error) => { - console.log('plugin discovery failed', error) -}) - -``` - -## `reduceExportSpecs(pluginSpecs, reducers, [defaults={}])` - -Reduces every value exported by the [`PluginSpec`][PluginSpec]s to produce a single value. If an exported value is an array each item in the array will be reduced individually. If the exported value is `undefined` it will be ignored. The reducer is called with the signature: - -```js -reducer( - // the result of the previous reducer call, or `defaults` - acc: any, - // the exported value, found at `uiExports[type]` or `uiExports[type][i]` - // in the PluginSpec config. - spec: any, - // the key in `uiExports` where this export was found - type: string, - // the PluginSpec which exported this spec - pluginSpec: PluginSpec -) -``` - -## `new PluginPack(options)` class - -Only exported so that `PluginPack` instances can be created in tests and used in place of on-disk plugin fixtures. Use `findPluginSpecs()`, or the cached result of a call to `findPluginSpecs()` (like `kbnServer.pluginSpecs`) any time you might need access to `PluginPack` objects in distributed code. - -### params - - - `options.path`: absolute path to where this plugin pack was found, this is normally a direct child of `./src/legacy/core_plugins` or `./plugins` - - `options.pkg`: the parsed `package.json` for this pack, used for defaults in `PluginSpec` objects defined by this pack - - `options.provider`: the default export of the pack, a function which is called with the `PluginSpec` class which should return one or more `PluginSpec` objects. - -[PluginPack]: ./plugin_pack/plugin_pack.js "PluginPath class definition" -[PluginSpec]: ./plugin_spec/plugin_spec.js "PluginSpec class definition" -[Errors]: ./errors.js "PluginDiscover specific error types" -[KbnServer]: ../server/kbn_server.js "KbnServer class definition" -[Config]: ../server/config/config.js "KbnServer/Config class definition" diff --git a/src/legacy/plugin_discovery/__tests__/find_plugin_specs.js b/src/legacy/plugin_discovery/__tests__/find_plugin_specs.js deleted file mode 100644 index e6af23d69c549..0000000000000 --- a/src/legacy/plugin_discovery/__tests__/find_plugin_specs.js +++ /dev/null @@ -1,219 +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 { resolve } from 'path'; -import { toArray } from 'rxjs/operators'; - -import expect from '@kbn/expect'; -import { isEqual } from 'lodash'; -import { findPluginSpecs } from '../find_plugin_specs'; -import { PluginSpec } from '../plugin_spec'; - -const PLUGIN_FIXTURES = resolve(__dirname, 'fixtures/plugins'); -const CONFLICT_FIXTURES = resolve(__dirname, 'fixtures/conflicts'); - -describe('plugin discovery', () => { - describe('findPluginSpecs()', function () { - this.timeout(10000); - - describe('spec$', () => { - it('finds specs for specified plugin paths', async () => { - const { spec$ } = findPluginSpecs({ - plugins: { - paths: [ - resolve(PLUGIN_FIXTURES, 'foo'), - resolve(PLUGIN_FIXTURES, 'bar'), - resolve(PLUGIN_FIXTURES, 'broken'), - ], - }, - }); - - const specs = await spec$.pipe(toArray()).toPromise(); - expect(specs).to.have.length(3); - specs.forEach((spec) => { - expect(spec).to.be.a(PluginSpec); - }); - expect(specs.map((s) => s.getId()).sort()).to.eql(['bar:one', 'bar:two', 'foo']); - }); - - it('finds all specs in scanDirs', async () => { - const { spec$ } = findPluginSpecs({ - // used to ensure the dev_mode plugin is enabled - env: 'development', - - plugins: { - scanDirs: [PLUGIN_FIXTURES], - }, - }); - - const specs = await spec$.pipe(toArray()).toPromise(); - expect(specs).to.have.length(3); - specs.forEach((spec) => { - expect(spec).to.be.a(PluginSpec); - }); - expect(specs.map((s) => s.getId()).sort()).to.eql(['bar:one', 'bar:two', 'foo']); - }); - - it('does not find disabled plugins', async () => { - const { spec$ } = findPluginSpecs({ - 'bar:one': { - enabled: false, - }, - - plugins: { - paths: [ - resolve(PLUGIN_FIXTURES, 'foo'), - resolve(PLUGIN_FIXTURES, 'bar'), - resolve(PLUGIN_FIXTURES, 'broken'), - ], - }, - }); - - const specs = await spec$.pipe(toArray()).toPromise(); - expect(specs).to.have.length(2); - specs.forEach((spec) => { - expect(spec).to.be.a(PluginSpec); - }); - expect(specs.map((s) => s.getId()).sort()).to.eql(['bar:two', 'foo']); - }); - - it('dedupes duplicate packs', async () => { - const { spec$ } = findPluginSpecs({ - plugins: { - scanDirs: [PLUGIN_FIXTURES], - paths: [ - resolve(PLUGIN_FIXTURES, 'foo'), - resolve(PLUGIN_FIXTURES, 'foo'), - resolve(PLUGIN_FIXTURES, 'bar'), - resolve(PLUGIN_FIXTURES, 'bar'), - resolve(PLUGIN_FIXTURES, 'broken'), - resolve(PLUGIN_FIXTURES, 'broken'), - ], - }, - }); - - const specs = await spec$.pipe(toArray()).toPromise(); - expect(specs).to.have.length(3); - specs.forEach((spec) => { - expect(spec).to.be.a(PluginSpec); - }); - expect(specs.map((s) => s.getId()).sort()).to.eql(['bar:one', 'bar:two', 'foo']); - }); - - describe('conflicting plugin spec ids', () => { - it('fails with informative message', async () => { - const { spec$ } = findPluginSpecs({ - plugins: { - scanDirs: [], - paths: [resolve(CONFLICT_FIXTURES, 'foo')], - }, - }); - - try { - await spec$.pipe(toArray()).toPromise(); - throw new Error('expected spec$ to throw an error'); - } catch (error) { - expect(error.message).to.contain('Multiple plugins found with the id "foo"'); - expect(error.message).to.contain(CONFLICT_FIXTURES); - } - }); - }); - }); - - describe('packageJson$', () => { - const checkPackageJsons = (packageJsons) => { - expect(packageJsons).to.have.length(2); - const package1 = packageJsons.find((packageJson) => - isEqual( - { - directoryPath: resolve(PLUGIN_FIXTURES, 'foo'), - contents: { - name: 'foo', - version: 'kibana', - }, - }, - packageJson - ) - ); - expect(package1).to.be.an(Object); - const package2 = packageJsons.find((packageJson) => - isEqual( - { - directoryPath: resolve(PLUGIN_FIXTURES, 'bar'), - contents: { - name: 'foo', - version: 'kibana', - }, - }, - packageJson - ) - ); - expect(package2).to.be.an(Object); - }; - - it('finds packageJson for specified plugin paths', async () => { - const { packageJson$ } = findPluginSpecs({ - plugins: { - paths: [ - resolve(PLUGIN_FIXTURES, 'foo'), - resolve(PLUGIN_FIXTURES, 'bar'), - resolve(PLUGIN_FIXTURES, 'broken'), - ], - }, - }); - - const packageJsons = await packageJson$.pipe(toArray()).toPromise(); - checkPackageJsons(packageJsons); - }); - - it('finds all packageJsons in scanDirs', async () => { - const { packageJson$ } = findPluginSpecs({ - // used to ensure the dev_mode plugin is enabled - env: 'development', - - plugins: { - scanDirs: [PLUGIN_FIXTURES], - }, - }); - - const packageJsons = await packageJson$.pipe(toArray()).toPromise(); - checkPackageJsons(packageJsons); - }); - - it('dedupes duplicate packageJson', async () => { - const { packageJson$ } = findPluginSpecs({ - plugins: { - scanDirs: [PLUGIN_FIXTURES], - paths: [ - resolve(PLUGIN_FIXTURES, 'foo'), - resolve(PLUGIN_FIXTURES, 'foo'), - resolve(PLUGIN_FIXTURES, 'bar'), - resolve(PLUGIN_FIXTURES, 'bar'), - resolve(PLUGIN_FIXTURES, 'broken'), - resolve(PLUGIN_FIXTURES, 'broken'), - ], - }, - }); - - const packageJsons = await packageJson$.pipe(toArray()).toPromise(); - checkPackageJsons(packageJsons); - }); - }); - }); -}); diff --git a/src/legacy/plugin_discovery/__tests__/fixtures/conflicts/foo/index.js b/src/legacy/plugin_discovery/__tests__/fixtures/conflicts/foo/index.js deleted file mode 100644 index fcbe3487463b7..0000000000000 --- a/src/legacy/plugin_discovery/__tests__/fixtures/conflicts/foo/index.js +++ /dev/null @@ -1,27 +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. - */ - -export default function (kibana) { - return [ - // two plugins exported without ids will both inherit - // the id of the pack and conflict - new kibana.Plugin({}), - new kibana.Plugin({}), - ]; -} diff --git a/src/legacy/plugin_discovery/__tests__/fixtures/conflicts/foo/package.json b/src/legacy/plugin_discovery/__tests__/fixtures/conflicts/foo/package.json deleted file mode 100644 index e43c2f0bc984c..0000000000000 --- a/src/legacy/plugin_discovery/__tests__/fixtures/conflicts/foo/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "foo", - "version": "kibana" -} diff --git a/src/legacy/plugin_discovery/__tests__/fixtures/plugins/bar/package.json b/src/legacy/plugin_discovery/__tests__/fixtures/plugins/bar/package.json deleted file mode 100644 index e43c2f0bc984c..0000000000000 --- a/src/legacy/plugin_discovery/__tests__/fixtures/plugins/bar/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "foo", - "version": "kibana" -} diff --git a/src/legacy/plugin_discovery/__tests__/fixtures/plugins/broken/index.js b/src/legacy/plugin_discovery/__tests__/fixtures/plugins/broken/index.js deleted file mode 100644 index 59f4a2649f019..0000000000000 --- a/src/legacy/plugin_discovery/__tests__/fixtures/plugins/broken/index.js +++ /dev/null @@ -1,22 +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. - */ - -export default { - foo: 'bar', -}; diff --git a/src/legacy/plugin_discovery/__tests__/fixtures/plugins/broken/package1.json b/src/legacy/plugin_discovery/__tests__/fixtures/plugins/broken/package1.json deleted file mode 100644 index 81ddb6221d515..0000000000000 --- a/src/legacy/plugin_discovery/__tests__/fixtures/plugins/broken/package1.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "baz", - "version": "kibana" -} diff --git a/src/legacy/plugin_discovery/__tests__/fixtures/plugins/foo/index.js b/src/legacy/plugin_discovery/__tests__/fixtures/plugins/foo/index.js deleted file mode 100644 index e43a1dcedb372..0000000000000 --- a/src/legacy/plugin_discovery/__tests__/fixtures/plugins/foo/index.js +++ /dev/null @@ -1,24 +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. - */ - -module.exports = function (kibana) { - return new kibana.Plugin({ - id: 'foo', - }); -}; diff --git a/src/legacy/plugin_discovery/__tests__/fixtures/plugins/foo/package.json b/src/legacy/plugin_discovery/__tests__/fixtures/plugins/foo/package.json deleted file mode 100644 index e43c2f0bc984c..0000000000000 --- a/src/legacy/plugin_discovery/__tests__/fixtures/plugins/foo/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "foo", - "version": "kibana" -} diff --git a/src/legacy/plugin_discovery/errors.js b/src/legacy/plugin_discovery/errors.js deleted file mode 100644 index 02d81b32d1fd1..0000000000000 --- a/src/legacy/plugin_discovery/errors.js +++ /dev/null @@ -1,84 +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. - */ - -const errorCodeProperty = Symbol('pluginDiscovery/errorCode'); - -/** - * Thrown when reading a plugin directory fails, wraps failure - * @type {String} - */ -const ERROR_INVALID_DIRECTORY = 'ERROR_INVALID_DIRECTORY'; -export function createInvalidDirectoryError(sourceError, path) { - sourceError[errorCodeProperty] = ERROR_INVALID_DIRECTORY; - sourceError.path = path; - return sourceError; -} -export function isInvalidDirectoryError(error) { - return error && error[errorCodeProperty] === ERROR_INVALID_DIRECTORY; -} - -/** - * Thrown when trying to create a PluginPack for a path that - * is not a valid plugin definition - * @type {String} - */ -const ERROR_INVALID_PACK = 'ERROR_INVALID_PACK'; -export function createInvalidPackError(path, reason) { - const error = new Error(`PluginPack${path ? ` at "${path}"` : ''} ${reason}`); - error[errorCodeProperty] = ERROR_INVALID_PACK; - error.path = path; - return error; -} -export function isInvalidPackError(error) { - return error && error[errorCodeProperty] === ERROR_INVALID_PACK; -} - -/** - * Thrown when trying to load a PluginSpec that is invalid for some reason - * @type {String} - */ -const ERROR_INVALID_PLUGIN = 'ERROR_INVALID_PLUGIN'; -export function createInvalidPluginError(spec, reason) { - const error = new Error( - `Plugin from ${spec.getId()} at ${spec.getPack().getPath()} is invalid because ${reason}` - ); - error[errorCodeProperty] = ERROR_INVALID_PLUGIN; - error.spec = spec; - return error; -} -export function isInvalidPluginError(error) { - return error && error[errorCodeProperty] === ERROR_INVALID_PLUGIN; -} - -/** - * Thrown when trying to load a PluginSpec whose version is incompatible - * @type {String} - */ -const ERROR_INCOMPATIBLE_PLUGIN_VERSION = 'ERROR_INCOMPATIBLE_PLUGIN_VERSION'; -export function createIncompatiblePluginVersionError(spec) { - const error = new Error( - `Plugin ${spec.getId()} is only compatible with Kibana version ${spec.getExpectedKibanaVersion()}` - ); - error[errorCodeProperty] = ERROR_INCOMPATIBLE_PLUGIN_VERSION; - error.spec = spec; - return error; -} -export function isIncompatiblePluginVersionError(error) { - return error && error[errorCodeProperty] === ERROR_INCOMPATIBLE_PLUGIN_VERSION; -} diff --git a/src/legacy/plugin_discovery/find_plugin_specs.js b/src/legacy/plugin_discovery/find_plugin_specs.js deleted file mode 100644 index b97476bb456a5..0000000000000 --- a/src/legacy/plugin_discovery/find_plugin_specs.js +++ /dev/null @@ -1,234 +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 * as Rx from 'rxjs'; -import { - distinct, - toArray, - mergeMap, - share, - shareReplay, - filter, - last, - map, - tap, -} from 'rxjs/operators'; -import { realpathSync } from 'fs'; - -import { Config } from '../server/config'; - -import { extendConfigService, disableConfigExtension } from './plugin_config'; - -import { - createPack$, - createPackageJsonAtPath$, - createPackageJsonsInDirectory$, -} from './plugin_pack'; - -import { isInvalidDirectoryError, isInvalidPackError } from './errors'; - -export function defaultConfig(settings) { - return Config.withDefaultSchema(settings); -} - -function bufferAllResults(observable) { - return observable.pipe( - // buffer all results into a single array - toArray(), - // merge the array back into the stream when complete - mergeMap((array) => array) - ); -} - -/** - * Determine a distinct value for each result from find$ - * so they can be deduplicated - * @param {{error?,pack?}} result - * @return {Any} - */ -function getDistinctKeyForFindResult(result) { - // errors are distinct by their message - if (result.error) { - return result.error.message; - } - - // packs are distinct by their absolute and real path - if (result.packageJson) { - return realpathSync(result.packageJson.directoryPath); - } - - // non error/pack results shouldn't exist, but if they do they are all unique - return result; -} - -function groupSpecsById(specs) { - const specsById = new Map(); - for (const spec of specs) { - const id = spec.getId(); - if (specsById.has(id)) { - specsById.get(id).push(spec); - } else { - specsById.set(id, [spec]); - } - } - return specsById; -} - -/** - * Creates a collection of observables for discovering pluginSpecs - * using Kibana's defaults, settings, and config service - * - * @param {Object} settings - * @param {ConfigService} [configToMutate] when supplied **it is mutated** to - * include the config from discovered plugin specs - * @return {Object} - */ -export function findPluginSpecs(settings, configToMutate) { - const config$ = Rx.defer(async () => { - if (configToMutate) { - return configToMutate; - } - - return defaultConfig(settings); - }).pipe(shareReplay()); - - // find plugin packs in configured paths/dirs - const packageJson$ = config$.pipe( - mergeMap((config) => - Rx.merge( - ...config.get('plugins.paths').map(createPackageJsonAtPath$), - ...config.get('plugins.scanDirs').map(createPackageJsonsInDirectory$) - ) - ), - distinct(getDistinctKeyForFindResult), - share() - ); - - const pack$ = createPack$(packageJson$).pipe(share()); - - const extendConfig$ = config$.pipe( - mergeMap((config) => - pack$.pipe( - // get the specs for each found plugin pack - mergeMap(({ pack }) => (pack ? pack.getPluginSpecs() : [])), - // make sure that none of the plugin specs have conflicting ids, fail - // early if conflicts detected or merge the specs back into the stream - toArray(), - mergeMap((allSpecs) => { - for (const [id, specs] of groupSpecsById(allSpecs)) { - if (specs.length > 1) { - throw new Error( - `Multiple plugins found with the id "${id}":\n${specs - .map((spec) => ` - ${id} at ${spec.getPath()}`) - .join('\n')}` - ); - } - } - - return allSpecs; - }), - mergeMap(async (spec) => { - // extend the config service with this plugin spec and - // collect its deprecations messages if some of its - // settings are outdated - const deprecations = []; - await extendConfigService(spec, config, settings, (message) => { - deprecations.push({ spec, message }); - }); - - return { - spec, - deprecations, - }; - }), - // extend the config with all plugins before determining enabled status - bufferAllResults, - map(({ spec, deprecations }) => { - const isRightVersion = spec.isVersionCompatible(config.get('pkg.version')); - const enabled = isRightVersion && spec.isEnabled(config); - return { - config, - spec, - deprecations, - enabledSpecs: enabled ? [spec] : [], - disabledSpecs: enabled ? [] : [spec], - invalidVersionSpecs: isRightVersion ? [] : [spec], - }; - }), - // determine which plugins are disabled before actually removing things from the config - bufferAllResults, - tap((result) => { - for (const spec of result.disabledSpecs) { - disableConfigExtension(spec, config); - } - }) - ) - ), - share() - ); - - return { - // package JSONs found when searching configure paths - packageJson$: packageJson$.pipe( - mergeMap((result) => (result.packageJson ? [result.packageJson] : [])) - ), - - // plugin packs found when searching configured paths - pack$: pack$.pipe(mergeMap((result) => (result.pack ? [result.pack] : []))), - - // errors caused by invalid directories of plugin directories - invalidDirectoryError$: pack$.pipe( - mergeMap((result) => (isInvalidDirectoryError(result.error) ? [result.error] : [])) - ), - - // errors caused by directories that we expected to be plugin but were invalid - invalidPackError$: pack$.pipe( - mergeMap((result) => (isInvalidPackError(result.error) ? [result.error] : [])) - ), - - otherError$: pack$.pipe( - mergeMap((result) => (isUnhandledError(result.error) ? [result.error] : [])) - ), - - // { spec, message } objects produced when transforming deprecated - // settings for a plugin spec - deprecation$: extendConfig$.pipe(mergeMap((result) => result.deprecations)), - - // the config service we extended with all of the plugin specs, - // only emitted once it is fully extended by all - extendedConfig$: extendConfig$.pipe( - mergeMap((result) => result.config), - filter(Boolean), - last() - ), - - // all enabled PluginSpec objects - spec$: extendConfig$.pipe(mergeMap((result) => result.enabledSpecs)), - - // all disabled PluginSpec objects - disabledSpec$: extendConfig$.pipe(mergeMap((result) => result.disabledSpecs)), - - // all PluginSpec objects that were disabled because their version was incompatible - invalidVersionSpec$: extendConfig$.pipe(mergeMap((result) => result.invalidVersionSpecs)), - }; -} - -function isUnhandledError(error) { - return error != null && !isInvalidDirectoryError(error) && !isInvalidPackError(error); -} diff --git a/src/legacy/plugin_discovery/index.js b/src/legacy/plugin_discovery/index.js deleted file mode 100644 index b60806f6cbc23..0000000000000 --- a/src/legacy/plugin_discovery/index.js +++ /dev/null @@ -1,22 +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. - */ - -export { findPluginSpecs } from './find_plugin_specs'; -export { reduceExportSpecs } from './plugin_exports'; -export { PluginPack } from './plugin_pack'; diff --git a/src/legacy/plugin_discovery/plugin_config/__tests__/extend_config_service.js b/src/legacy/plugin_discovery/plugin_config/__tests__/extend_config_service.js deleted file mode 100644 index 40f84f6f54b3b..0000000000000 --- a/src/legacy/plugin_discovery/plugin_config/__tests__/extend_config_service.js +++ /dev/null @@ -1,162 +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 sinon from 'sinon'; -import expect from '@kbn/expect'; - -import { Config } from '../../../server/config'; -import { PluginPack } from '../../plugin_pack'; -import { extendConfigService, disableConfigExtension } from '../extend_config_service'; -import * as SchemaNS from '../schema'; -import * as SettingsNS from '../settings'; - -describe('plugin discovery/extend config service', () => { - const sandbox = sinon.createSandbox(); - afterEach(() => sandbox.restore()); - - const pluginSpec = new PluginPack({ - path: '/dev/null', - pkg: { - name: 'test', - version: 'kibana', - }, - provider: ({ Plugin }) => - new Plugin({ - configPrefix: 'foo.bar.baz', - - config: (Joi) => - Joi.object({ - enabled: Joi.boolean().default(true), - test: Joi.string().default('bonk'), - }).default(), - }), - }) - .getPluginSpecs() - .pop(); - - describe('extendConfigService()', () => { - it('calls getSettings, getSchema, and Config.extendSchema() correctly', async () => { - const rootSettings = { - foo: { - bar: { - enabled: false, - }, - }, - }; - const schema = { - validate: () => {}, - }; - const configPrefix = 'foo.bar'; - const config = { - extendSchema: sandbox.stub(), - }; - const pluginSpec = { - getConfigPrefix: sandbox.stub().returns(configPrefix), - }; - - const getSettings = sandbox.stub(SettingsNS, 'getSettings').returns(rootSettings.foo.bar); - - const getSchema = sandbox.stub(SchemaNS, 'getSchema').returns(schema); - - await extendConfigService(pluginSpec, config, rootSettings); - - sinon.assert.calledOnce(getSettings); - sinon.assert.calledWithExactly(getSettings, pluginSpec, rootSettings); - - sinon.assert.calledOnce(getSchema); - sinon.assert.calledWithExactly(getSchema, pluginSpec); - - sinon.assert.calledOnce(config.extendSchema); - sinon.assert.calledWithExactly( - config.extendSchema, - schema, - rootSettings.foo.bar, - configPrefix - ); - }); - - it('adds the schema for a plugin spec to its config prefix', async () => { - const config = Config.withDefaultSchema(); - expect(config.has('foo.bar.baz')).to.be(false); - await extendConfigService(pluginSpec, config); - expect(config.has('foo.bar.baz')).to.be(true); - }); - - it('initializes it with the default settings', async () => { - const config = Config.withDefaultSchema(); - await extendConfigService(pluginSpec, config); - expect(config.get('foo.bar.baz.enabled')).to.be(true); - expect(config.get('foo.bar.baz.test')).to.be('bonk'); - }); - - it('initializes it with values from root settings if defined', async () => { - const config = Config.withDefaultSchema(); - await extendConfigService(pluginSpec, config, { - foo: { - bar: { - baz: { - test: 'hello world', - }, - }, - }, - }); - - expect(config.get('foo.bar.baz.test')).to.be('hello world'); - }); - - it('throws if root settings are invalid', async () => { - const config = Config.withDefaultSchema(); - try { - await extendConfigService(pluginSpec, config, { - foo: { - bar: { - baz: { - test: { - 'not a string': true, - }, - }, - }, - }, - }); - throw new Error('Expected extendConfigService() to throw because of bad settings'); - } catch (error) { - expect(error.message).to.contain('"test" must be a string'); - } - }); - }); - - describe('disableConfigExtension()', () => { - it('removes added config', async () => { - const config = Config.withDefaultSchema(); - await extendConfigService(pluginSpec, config); - expect(config.has('foo.bar.baz.test')).to.be(true); - await disableConfigExtension(pluginSpec, config); - expect(config.has('foo.bar.baz.test')).to.be(false); - }); - - it('leaves {configPrefix}.enabled config', async () => { - const config = Config.withDefaultSchema(); - expect(config.has('foo.bar.baz.enabled')).to.be(false); - await extendConfigService(pluginSpec, config); - expect(config.get('foo.bar.baz.enabled')).to.be(true); - await disableConfigExtension(pluginSpec, config); - expect(config.get('foo.bar.baz.enabled')).to.be(false); - }); - }); -}); diff --git a/src/legacy/plugin_discovery/plugin_config/__tests__/schema.js b/src/legacy/plugin_discovery/plugin_config/__tests__/schema.js deleted file mode 100644 index 78adb1e680e20..0000000000000 --- a/src/legacy/plugin_discovery/plugin_config/__tests__/schema.js +++ /dev/null @@ -1,92 +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 expect from '@kbn/expect'; - -import { PluginPack } from '../../plugin_pack'; -import { getSchema, getStubSchema } from '../schema'; - -describe('plugin discovery/schema', () => { - function createPluginSpec(configProvider) { - return new PluginPack({ - path: '/dev/null', - pkg: { - name: 'test', - version: 'kibana', - }, - provider: ({ Plugin }) => - new Plugin({ - configPrefix: 'foo.bar.baz', - config: configProvider, - }), - }) - .getPluginSpecs() - .pop(); - } - - describe('getSchema()', () => { - it('calls the config provider and returns its return value', async () => { - const pluginSpec = createPluginSpec(() => 'foo'); - expect(await getSchema(pluginSpec)).to.be('foo'); - }); - - it('supports config provider that returns a promise', async () => { - const pluginSpec = createPluginSpec(() => Promise.resolve('foo')); - expect(await getSchema(pluginSpec)).to.be('foo'); - }); - - it('uses default schema when no config provider', async () => { - const schema = await getSchema(createPluginSpec()); - expect(schema).to.be.an('object'); - expect(schema).to.have.property('validate').a('function'); - expect(schema.validate({}).value).to.eql({ - enabled: true, - }); - }); - - it('uses default schema when config returns falsy value', async () => { - const schema = await getSchema(createPluginSpec(() => null)); - expect(schema).to.be.an('object'); - expect(schema).to.have.property('validate').a('function'); - expect(schema.validate({}).value).to.eql({ - enabled: true, - }); - }); - - it('uses default schema when config promise resolves to falsy value', async () => { - const schema = await getSchema(createPluginSpec(() => Promise.resolve(null))); - expect(schema).to.be.an('object'); - expect(schema).to.have.property('validate').a('function'); - expect(schema.validate({}).value).to.eql({ - enabled: true, - }); - }); - }); - - describe('getStubSchema()', () => { - it('returns schema with enabled: false', async () => { - const schema = await getStubSchema(); - expect(schema).to.be.an('object'); - expect(schema).to.have.property('validate').a('function'); - expect(schema.validate({}).value).to.eql({ - enabled: false, - }); - }); - }); -}); diff --git a/src/legacy/plugin_discovery/plugin_config/__tests__/settings.js b/src/legacy/plugin_discovery/plugin_config/__tests__/settings.js deleted file mode 100644 index 750c5ee6c6f50..0000000000000 --- a/src/legacy/plugin_discovery/plugin_config/__tests__/settings.js +++ /dev/null @@ -1,61 +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 expect from '@kbn/expect'; - -import { PluginPack } from '../../plugin_pack'; -import { getSettings } from '../settings'; - -describe('plugin_discovery/settings', () => { - const pluginSpec = new PluginPack({ - path: '/dev/null', - pkg: { - name: 'test', - version: 'kibana', - }, - provider: ({ Plugin }) => - new Plugin({ - configPrefix: 'a.b.c', - }), - }) - .getPluginSpecs() - .pop(); - - describe('getSettings()', () => { - it('reads settings from config prefix', async () => { - const rootSettings = { - a: { - b: { - c: { - enabled: false, - }, - }, - }, - }; - - expect(await getSettings(pluginSpec, rootSettings)).to.eql({ - enabled: false, - }); - }); - - it('allows rootSettings to be undefined', async () => { - expect(await getSettings(pluginSpec)).to.eql(undefined); - }); - }); -}); diff --git a/src/legacy/plugin_discovery/plugin_config/extend_config_service.js b/src/legacy/plugin_discovery/plugin_config/extend_config_service.js deleted file mode 100644 index a6d5d4ae5f990..0000000000000 --- a/src/legacy/plugin_discovery/plugin_config/extend_config_service.js +++ /dev/null @@ -1,50 +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 { getSettings } from './settings'; -import { getSchema, getStubSchema } from './schema'; - -/** - * Extend a config service with the schema and settings for a - * plugin spec and optionally call logDeprecation with warning - * messages about deprecated settings that are used - * @param {PluginSpec} spec - * @param {Server.Config} config - * @param {Object} rootSettings - * @param {Function} [logDeprecation] - * @return {Promise} - */ -export async function extendConfigService(spec, config, rootSettings) { - const settings = await getSettings(spec, rootSettings); - const schema = await getSchema(spec); - config.extendSchema(schema, settings, spec.getConfigPrefix()); -} - -/** - * Disable the schema and settings applied to a config service for - * a plugin spec - * @param {PluginSpec} spec - * @param {Server.Config} config - * @return {undefined} - */ -export function disableConfigExtension(spec, config) { - const prefix = spec.getConfigPrefix(); - config.removeSchema(prefix); - config.extendSchema(getStubSchema(), { enabled: false }, prefix); -} diff --git a/src/legacy/plugin_discovery/plugin_config/index.js b/src/legacy/plugin_discovery/plugin_config/index.js deleted file mode 100644 index a27463bc9c7f5..0000000000000 --- a/src/legacy/plugin_discovery/plugin_config/index.js +++ /dev/null @@ -1,20 +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. - */ - -export { extendConfigService, disableConfigExtension } from './extend_config_service'; diff --git a/src/legacy/plugin_discovery/plugin_config/schema.js b/src/legacy/plugin_discovery/plugin_config/schema.js deleted file mode 100644 index 14d10aa5568da..0000000000000 --- a/src/legacy/plugin_discovery/plugin_config/schema.js +++ /dev/null @@ -1,46 +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 Joi from 'joi'; - -const STUB_CONFIG_SCHEMA = Joi.object() - .keys({ - enabled: Joi.valid(false).default(false), - }) - .default(); - -const DEFAULT_CONFIG_SCHEMA = Joi.object() - .keys({ - enabled: Joi.boolean().default(true), - }) - .default(); - -/** - * Get the config schema for a plugin spec - * @param {PluginSpec} spec - * @return {Promise} - */ -export async function getSchema(spec) { - const provider = spec.getConfigSchemaProvider(); - return (provider && (await provider(Joi))) || DEFAULT_CONFIG_SCHEMA; -} - -export function getStubSchema() { - return STUB_CONFIG_SCHEMA; -} diff --git a/src/legacy/plugin_discovery/plugin_config/settings.js b/src/legacy/plugin_discovery/plugin_config/settings.js deleted file mode 100644 index e6a4741d76eca..0000000000000 --- a/src/legacy/plugin_discovery/plugin_config/settings.js +++ /dev/null @@ -1,34 +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 { get } from 'lodash'; - -/** - * Get the settings for a pluginSpec from the raw root settings while - * optionally calling logDeprecation() with warnings about deprecated - * settings that were used - * @param {PluginSpec} spec - * @param {Object} rootSettings - * @return {Promise} - */ -export async function getSettings(spec, rootSettings) { - const prefix = spec.getConfigPrefix(); - const rawSettings = get(rootSettings, prefix); - return rawSettings; -} diff --git a/src/legacy/plugin_discovery/plugin_exports/__tests__/reduce_export_specs.js b/src/legacy/plugin_discovery/plugin_exports/__tests__/reduce_export_specs.js deleted file mode 100644 index 3beaacc1a8293..0000000000000 --- a/src/legacy/plugin_discovery/plugin_exports/__tests__/reduce_export_specs.js +++ /dev/null @@ -1,75 +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 expect from '@kbn/expect'; - -import { PluginPack } from '../../plugin_pack'; -import { reduceExportSpecs } from '../reduce_export_specs'; - -const PLUGIN = new PluginPack({ - path: __dirname, - pkg: { - name: 'foo', - version: 'kibana', - }, - provider: ({ Plugin }) => - new Plugin({ - uiExports: { - concatNames: { - name: 'export1', - }, - - concat: ['export2', 'export3'], - }, - }), -}); - -const REDUCERS = { - concatNames(acc, spec, type, pluginSpec) { - return { - names: [].concat(acc.names || [], `${pluginSpec.getId()}:${spec.name}`), - }; - }, - concat(acc, spec, type, pluginSpec) { - return { - names: [].concat(acc.names || [], `${pluginSpec.getId()}:${spec}`), - }; - }, -}; - -const PLUGIN_SPECS = PLUGIN.getPluginSpecs(); - -describe('reduceExportSpecs', () => { - it('combines ui exports from a list of plugin definitions', () => { - const exports = reduceExportSpecs(PLUGIN_SPECS, REDUCERS); - expect(exports).to.eql({ - names: ['foo:export1', 'foo:export2', 'foo:export3'], - }); - }); - - it('starts with the defaults', () => { - const exports = reduceExportSpecs(PLUGIN_SPECS, REDUCERS, { - names: ['default'], - }); - - expect(exports).to.eql({ - names: ['default', 'foo:export1', 'foo:export2', 'foo:export3'], - }); - }); -}); diff --git a/src/legacy/plugin_discovery/plugin_exports/index.js b/src/legacy/plugin_discovery/plugin_exports/index.js deleted file mode 100644 index 0e3511ea85dd4..0000000000000 --- a/src/legacy/plugin_discovery/plugin_exports/index.js +++ /dev/null @@ -1,20 +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. - */ - -export { reduceExportSpecs } from './reduce_export_specs'; diff --git a/src/legacy/plugin_discovery/plugin_exports/reduce_export_specs.js b/src/legacy/plugin_discovery/plugin_exports/reduce_export_specs.js deleted file mode 100644 index a3adc3091085d..0000000000000 --- a/src/legacy/plugin_discovery/plugin_exports/reduce_export_specs.js +++ /dev/null @@ -1,47 +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. - */ - -/** - * Combine the exportSpecs from a list of pluginSpecs - * by calling the reducers for each export type - * @param {Array} pluginSpecs - * @param {Object} reducers - * @param {Object} [defaults={}] - * @return {Object} - */ -export function reduceExportSpecs(pluginSpecs, reducers, defaults = {}) { - return pluginSpecs.reduce((acc, pluginSpec) => { - const specsByType = pluginSpec.getExportSpecs() || {}; - const types = Object.keys(specsByType); - - return types.reduce((acc, type) => { - const reducer = reducers[type] || reducers.unknown; - - if (!reducer) { - throw new Error(`Unknown export type ${type}`); - } - - // convert specs to an array if not already one or - // ignore the spec if it is undefined - const specs = [].concat(specsByType[type] === undefined ? [] : specsByType[type]); - - return specs.reduce((acc, spec) => reducer(acc, spec, type, pluginSpec), acc); - }, acc); - }, defaults); -} diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/create_pack.js b/src/legacy/plugin_discovery/plugin_pack/__tests__/create_pack.js deleted file mode 100644 index b17bd69479ffa..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/create_pack.js +++ /dev/null @@ -1,85 +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 { resolve } from 'path'; -import * as Rx from 'rxjs'; -import { toArray } from 'rxjs/operators'; -import expect from '@kbn/expect'; - -import { createPack$ } from '../create_pack'; -import { PluginPack } from '../plugin_pack'; - -import { PLUGINS_DIR, assertInvalidPackError } from './utils'; - -describe('plugin discovery/create pack', () => { - it('creates PluginPack', async () => { - const packageJson$ = Rx.from([ - { - packageJson: { - directoryPath: resolve(PLUGINS_DIR, 'prebuilt'), - contents: { - name: 'prebuilt', - }, - }, - }, - ]); - const results = await createPack$(packageJson$).pipe(toArray()).toPromise(); - expect(results).to.have.length(1); - expect(results[0]).to.only.have.keys(['pack']); - const { pack } = results[0]; - expect(pack).to.be.a(PluginPack); - }); - - describe('errors thrown', () => { - async function checkError(path, check) { - const packageJson$ = Rx.from([ - { - packageJson: { - directoryPath: path, - }, - }, - ]); - - const results = await createPack$(packageJson$).pipe(toArray()).toPromise(); - expect(results).to.have.length(1); - expect(results[0]).to.only.have.keys(['error']); - const { error } = results[0]; - await check(error); - } - it('default export is an object', () => - checkError(resolve(PLUGINS_DIR, 'exports_object'), (error) => { - assertInvalidPackError(error); - expect(error.message).to.contain('must export a function'); - })); - it('default export is an number', () => - checkError(resolve(PLUGINS_DIR, 'exports_number'), (error) => { - assertInvalidPackError(error); - expect(error.message).to.contain('must export a function'); - })); - it('default export is an string', () => - checkError(resolve(PLUGINS_DIR, 'exports_string'), (error) => { - assertInvalidPackError(error); - expect(error.message).to.contain('must export a function'); - })); - it('directory with code that fails when required', () => - checkError(resolve(PLUGINS_DIR, 'broken_code'), (error) => { - expect(error.message).to.contain("Cannot find module 'does-not-exist'"); - })); - }); -}); diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken/package.json b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken/package.json deleted file mode 100644 index f830e8b60c02d..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": -} diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken_code/index.js b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken_code/index.js deleted file mode 100644 index bdb26504d6b6e..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken_code/index.js +++ /dev/null @@ -1,7 +0,0 @@ -const brokenRequire = require('does-not-exist'); // eslint-disable-line - -module.exports = function (kibana) { - return new kibana.Plugin({ - id: 'foo', - }); -}; diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken_code/package.json b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken_code/package.json deleted file mode 100644 index e43c2f0bc984c..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/broken_code/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "foo", - "version": "kibana" -} diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_number/index.js b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_number/index.js deleted file mode 100644 index f24fc54e38d9a..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_number/index.js +++ /dev/null @@ -1,20 +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. - */ - -export default 1; diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_number/package.json b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_number/package.json deleted file mode 100644 index e43c2f0bc984c..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_number/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "foo", - "version": "kibana" -} diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_object/package.json b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_object/package.json deleted file mode 100644 index e43c2f0bc984c..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_object/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "foo", - "version": "kibana" -} diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_string/package.json b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_string/package.json deleted file mode 100644 index e43c2f0bc984c..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_string/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "foo", - "version": "kibana" -} diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/foo/index.js b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/foo/index.js deleted file mode 100644 index e43a1dcedb372..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/foo/index.js +++ /dev/null @@ -1,24 +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. - */ - -module.exports = function (kibana) { - return new kibana.Plugin({ - id: 'foo', - }); -}; diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/foo/package.json b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/foo/package.json deleted file mode 100644 index e43c2f0bc984c..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/foo/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "foo", - "version": "kibana" -} diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/index.js b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/index.js deleted file mode 100644 index edb1dd15673da..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/index.js +++ /dev/null @@ -1,20 +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. - */ - -console.log('hello world'); diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/lib/index.js b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/lib/index.js deleted file mode 100644 index 050ffdfbde9ea..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/lib/index.js +++ /dev/null @@ -1,20 +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. - */ - -export { myLib } from './my_lib'; diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/lib/my_lib.js b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/lib/my_lib.js deleted file mode 100644 index 94e511632d9a6..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/lib/my_lib.js +++ /dev/null @@ -1,22 +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. - */ - -export function myLib() { - console.log('lib'); -} diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/prebuilt/index.js b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/prebuilt/index.js deleted file mode 100644 index 89744b2dd3fd9..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/prebuilt/index.js +++ /dev/null @@ -1,14 +0,0 @@ -/* eslint-disable */ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -exports.default = function (_ref) { - var Plugin = _ref.Plugin; - - return new Plugin({ - id: 'foo' - }); -}; diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/prebuilt/package.json b/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/prebuilt/package.json deleted file mode 100644 index b1b74e0e76b12..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/prebuilt/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name": "prebuilt" -} diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/package_json_at_path.js b/src/legacy/plugin_discovery/plugin_pack/__tests__/package_json_at_path.js deleted file mode 100644 index fa1033180954e..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/package_json_at_path.js +++ /dev/null @@ -1,88 +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 { resolve } from 'path'; -import { toArray } from 'rxjs/operators'; - -import expect from '@kbn/expect'; - -import { createPackageJsonAtPath$ } from '../package_json_at_path'; -import { PLUGINS_DIR, assertInvalidPackError, assertInvalidDirectoryError } from './utils'; - -describe('plugin discovery/plugin_pack', () => { - describe('createPackageJsonAtPath$()', () => { - it('returns an observable', () => { - expect(createPackageJsonAtPath$()).to.have.property('subscribe').a('function'); - }); - it('gets the default provider from prebuilt babel modules', async () => { - const results = await createPackageJsonAtPath$(resolve(PLUGINS_DIR, 'prebuilt')) - .pipe(toArray()) - .toPromise(); - expect(results).to.have.length(1); - expect(results[0]).to.only.have.keys(['packageJson']); - expect(results[0].packageJson).to.be.an(Object); - expect(results[0].packageJson.directoryPath).to.be(resolve(PLUGINS_DIR, 'prebuilt')); - expect(results[0].packageJson.contents).to.eql({ name: 'prebuilt' }); - }); - describe('errors emitted as { error } results', () => { - async function checkError(path, check) { - const results = await createPackageJsonAtPath$(path).pipe(toArray()).toPromise(); - expect(results).to.have.length(1); - expect(results[0]).to.only.have.keys(['error']); - const { error } = results[0]; - await check(error); - } - it('undefined path', () => - checkError(undefined, (error) => { - assertInvalidDirectoryError(error); - expect(error.message).to.contain('path must be a string'); - })); - it('relative path', () => - checkError('plugins/foo', (error) => { - assertInvalidDirectoryError(error); - expect(error.message).to.contain('path must be absolute'); - })); - it('./relative path', () => - checkError('./plugins/foo', (error) => { - assertInvalidDirectoryError(error); - expect(error.message).to.contain('path must be absolute'); - })); - it('non-existent path', () => - checkError(resolve(PLUGINS_DIR, 'baz'), (error) => { - assertInvalidPackError(error); - expect(error.message).to.contain('must be a directory'); - })); - it('path to a file', () => - checkError(resolve(PLUGINS_DIR, 'index.js'), (error) => { - assertInvalidPackError(error); - expect(error.message).to.contain('must be a directory'); - })); - it('directory without a package.json', () => - checkError(resolve(PLUGINS_DIR, 'lib'), (error) => { - assertInvalidPackError(error); - expect(error.message).to.contain('must have a package.json file'); - })); - it('directory with an invalid package.json', () => - checkError(resolve(PLUGINS_DIR, 'broken'), (error) => { - assertInvalidPackError(error); - expect(error.message).to.contain('must have a valid package.json file'); - })); - }); - }); -}); diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/package_jsons_in_directory.js b/src/legacy/plugin_discovery/plugin_pack/__tests__/package_jsons_in_directory.js deleted file mode 100644 index 37cb4cc064da7..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/package_jsons_in_directory.js +++ /dev/null @@ -1,83 +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 { resolve } from 'path'; - -import { toArray } from 'rxjs/operators'; -import expect from '@kbn/expect'; - -import { createPackageJsonsInDirectory$ } from '../package_jsons_in_directory'; - -import { PLUGINS_DIR, assertInvalidDirectoryError } from './utils'; - -describe('plugin discovery/packs in directory', () => { - describe('createPackageJsonsInDirectory$()', () => { - describe('errors emitted as { error } results', () => { - async function checkError(path, check) { - const results = await createPackageJsonsInDirectory$(path).pipe(toArray()).toPromise(); - expect(results).to.have.length(1); - expect(results[0]).to.only.have.keys('error'); - const { error } = results[0]; - await check(error); - } - - it('undefined path', () => - checkError(undefined, (error) => { - assertInvalidDirectoryError(error); - expect(error.message).to.contain('path must be a string'); - })); - it('relative path', () => - checkError('my/plugins', (error) => { - assertInvalidDirectoryError(error); - expect(error.message).to.contain('path must be absolute'); - })); - it('./relative path', () => - checkError('./my/pluginsd', (error) => { - assertInvalidDirectoryError(error); - expect(error.message).to.contain('path must be absolute'); - })); - it('non-existent path', () => - checkError(resolve(PLUGINS_DIR, 'notreal'), (error) => { - assertInvalidDirectoryError(error); - expect(error.message).to.contain('no such file or directory'); - })); - it('path to a file', () => - checkError(resolve(PLUGINS_DIR, 'index.js'), (error) => { - assertInvalidDirectoryError(error); - expect(error.message).to.contain('not a directory'); - })); - }); - - it('includes child errors for invalid packageJsons within a valid directory', async () => { - const results = await createPackageJsonsInDirectory$(PLUGINS_DIR).pipe(toArray()).toPromise(); - - const errors = results.map((result) => result.error).filter(Boolean); - - const packageJsons = results.map((result) => result.packageJson).filter(Boolean); - - packageJsons.forEach((pack) => expect(pack).to.be.an(Object)); - // there should be one result for each item in PLUGINS_DIR - expect(results).to.have.length(8); - // three of the fixtures are errors of some sort - expect(errors).to.have.length(2); - // six of them are valid - expect(packageJsons).to.have.length(6); - }); - }); -}); diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/plugin_pack.js b/src/legacy/plugin_discovery/plugin_pack/__tests__/plugin_pack.js deleted file mode 100644 index 769fcd74ce6fb..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/plugin_pack.js +++ /dev/null @@ -1,126 +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 expect from '@kbn/expect'; -import sinon from 'sinon'; - -import { PluginPack } from '../plugin_pack'; -import { PluginSpec } from '../../plugin_spec'; - -describe('plugin discovery/plugin pack', () => { - describe('constructor', () => { - it('requires an object', () => { - expect(() => { - new PluginPack(); - }).to.throwError(); - }); - }); - describe('#getPkg()', () => { - it('returns the `pkg` constructor argument', () => { - const pkg = {}; - const pack = new PluginPack({ pkg }); - expect(pack.getPkg()).to.be(pkg); - }); - }); - describe('#getPath()', () => { - it('returns the `path` constructor argument', () => { - const path = {}; - const pack = new PluginPack({ path }); - expect(pack.getPath()).to.be(path); - }); - }); - describe('#getPluginSpecs()', () => { - it('calls the `provider` constructor argument with an api including a single sub class of PluginSpec', () => { - const provider = sinon.stub(); - const pack = new PluginPack({ provider }); - sinon.assert.notCalled(provider); - pack.getPluginSpecs(); - sinon.assert.calledOnce(provider); - sinon.assert.calledWithExactly(provider, { - Plugin: sinon.match((Class) => { - return Class.prototype instanceof PluginSpec; - }, 'Subclass of PluginSpec'), - }); - }); - - it('casts undefined return value to array', () => { - const pack = new PluginPack({ provider: () => undefined }); - expect(pack.getPluginSpecs()).to.eql([]); - }); - - it('casts single PluginSpec to an array', () => { - const pack = new PluginPack({ - path: '/dev/null', - pkg: { name: 'foo', version: 'kibana' }, - provider: ({ Plugin }) => new Plugin({}), - }); - - const specs = pack.getPluginSpecs(); - expect(specs).to.be.an('array'); - expect(specs).to.have.length(1); - expect(specs[0]).to.be.a(PluginSpec); - }); - - it('returns an array of PluginSpec', () => { - const pack = new PluginPack({ - path: '/dev/null', - pkg: { name: 'foo', version: 'kibana' }, - provider: ({ Plugin }) => [new Plugin({}), new Plugin({})], - }); - - const specs = pack.getPluginSpecs(); - expect(specs).to.be.an('array'); - expect(specs).to.have.length(2); - expect(specs[0]).to.be.a(PluginSpec); - expect(specs[1]).to.be.a(PluginSpec); - }); - - it('throws if non-undefined return value is not an instance of api.Plugin', () => { - let OtherPluginSpecClass; - const otherPack = new PluginPack({ - path: '/dev/null', - pkg: { name: 'foo', version: 'kibana' }, - provider: (api) => { - OtherPluginSpecClass = api.Plugin; - }, - }); - - // call getPluginSpecs() on other pack to get it's api.Plugin class - otherPack.getPluginSpecs(); - - const badPacks = [ - new PluginPack({ provider: () => false }), - new PluginPack({ provider: () => null }), - new PluginPack({ provider: () => 1 }), - new PluginPack({ provider: () => 'true' }), - new PluginPack({ provider: () => true }), - new PluginPack({ provider: () => new Date() }), - new PluginPack({ provider: () => /foo.*bar/ }), - new PluginPack({ provider: () => function () {} }), - new PluginPack({ provider: () => new OtherPluginSpecClass({}) }), - ]; - - for (const pack of badPacks) { - expect(() => pack.getPluginSpecs()).to.throwError((error) => { - expect(error.message).to.contain('unexpected plugin export'); - }); - } - }); - }); -}); diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/utils.js b/src/legacy/plugin_discovery/plugin_pack/__tests__/utils.js deleted file mode 100644 index adcf60d809ff7..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/utils.js +++ /dev/null @@ -1,37 +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 { resolve } from 'path'; -import { inspect } from 'util'; - -import { isInvalidPackError, isInvalidDirectoryError } from '../../errors'; - -export const PLUGINS_DIR = resolve(__dirname, 'fixtures/plugins'); - -export function assertInvalidDirectoryError(error) { - if (!isInvalidDirectoryError(error)) { - throw new Error(`Expected ${inspect(error)} to be an 'InvalidDirectoryError'`); - } -} - -export function assertInvalidPackError(error) { - if (!isInvalidPackError(error)) { - throw new Error(`Expected ${inspect(error)} to be an 'InvalidPackError'`); - } -} diff --git a/src/legacy/plugin_discovery/plugin_pack/create_pack.js b/src/legacy/plugin_discovery/plugin_pack/create_pack.js deleted file mode 100644 index 189c2ea324103..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/create_pack.js +++ /dev/null @@ -1,54 +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 { PluginPack } from './plugin_pack'; -import { map, catchError } from 'rxjs/operators'; -import { createInvalidPackError } from '../errors'; - -function createPack(packageJson) { - let provider = require(packageJson.directoryPath); // eslint-disable-line import/no-dynamic-require - if (provider.__esModule) { - provider = provider.default; - } - if (typeof provider !== 'function') { - throw createInvalidPackError(packageJson.directoryPath, 'must export a function'); - } - - return new PluginPack({ path: packageJson.directoryPath, pkg: packageJson.contents, provider }); -} - -export const createPack$ = (packageJson$) => - packageJson$.pipe( - map(({ error, packageJson }) => { - if (error) { - return { error }; - } - - if (!packageJson) { - throw new Error('packageJson is required to create the pack'); - } - - return { - pack: createPack(packageJson), - }; - }), - // createPack can throw errors, and we want them to be represented - // like the errors we consume from createPackageJsonAtPath/Directory - catchError((error) => [{ error }]) - ); diff --git a/src/legacy/plugin_discovery/plugin_pack/index.js b/src/legacy/plugin_discovery/plugin_pack/index.js deleted file mode 100644 index 69e55baee660b..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/index.js +++ /dev/null @@ -1,23 +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. - */ - -export { createPack$ } from './create_pack'; -export { createPackageJsonAtPath$ } from './package_json_at_path'; -export { createPackageJsonsInDirectory$ } from './package_jsons_in_directory'; -export { PluginPack } from './plugin_pack'; diff --git a/src/legacy/plugin_discovery/plugin_pack/lib/fs.js b/src/legacy/plugin_discovery/plugin_pack/lib/fs.js deleted file mode 100644 index 2b531e314df52..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/lib/fs.js +++ /dev/null @@ -1,85 +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 { stat, readdir } from 'fs'; -import { resolve, isAbsolute } from 'path'; - -import { fromNode as fcb } from 'bluebird'; -import * as Rx from 'rxjs'; -import { catchError, mergeAll, filter, map, mergeMap } from 'rxjs/operators'; - -import { createInvalidDirectoryError } from '../../errors'; - -function assertAbsolutePath(path) { - if (typeof path !== 'string') { - throw createInvalidDirectoryError(new TypeError('path must be a string'), path); - } - - if (!isAbsolute(path)) { - throw createInvalidDirectoryError(new TypeError('path must be absolute'), path); - } -} - -async function statTest(path, test) { - try { - const stats = await fcb((cb) => stat(path, cb)); - return Boolean(test(stats)); - } catch (error) { - if (error.code !== 'ENOENT') { - throw error; - } - } - return false; -} - -/** - * Determine if a path currently points to a directory - * @param {String} path - * @return {Promise} - */ -export async function isDirectory(path) { - assertAbsolutePath(path); - return await statTest(path, (stat) => stat.isDirectory()); -} - -/** - * Get absolute paths for child directories within a path - * @param {string} path - * @return {Promise>} - */ -export const createChildDirectory$ = (path) => - Rx.defer(() => { - assertAbsolutePath(path); - return fcb((cb) => readdir(path, cb)); - }).pipe( - catchError((error) => { - throw createInvalidDirectoryError(error, path); - }), - mergeAll(), - filter((name) => !name.startsWith('.')), - map((name) => resolve(path, name)), - mergeMap(async (absolute) => { - if (await isDirectory(absolute)) { - return [absolute]; - } else { - return []; - } - }), - mergeAll() - ); diff --git a/src/legacy/plugin_discovery/plugin_pack/lib/index.js b/src/legacy/plugin_discovery/plugin_pack/lib/index.js deleted file mode 100644 index 491deeda27516..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/lib/index.js +++ /dev/null @@ -1,20 +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. - */ - -export { isDirectory, createChildDirectory$ } from './fs'; diff --git a/src/legacy/plugin_discovery/plugin_pack/package_json_at_path.js b/src/legacy/plugin_discovery/plugin_pack/package_json_at_path.js deleted file mode 100644 index 18629ef3ea802..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/package_json_at_path.js +++ /dev/null @@ -1,62 +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 { readFileSync } from 'fs'; -import * as Rx from 'rxjs'; -import { map, mergeMap, catchError } from 'rxjs/operators'; -import { resolve } from 'path'; -import { createInvalidPackError } from '../errors'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { isNewPlatformPlugin } from '../../../core/server/plugins'; - -import { isDirectory } from './lib'; - -async function createPackageJsonAtPath(path) { - if (!(await isDirectory(path))) { - throw createInvalidPackError(path, 'must be a directory'); - } - - let str; - try { - str = readFileSync(resolve(path, 'package.json')); - } catch (err) { - throw createInvalidPackError(path, 'must have a package.json file'); - } - - let pkg; - try { - pkg = JSON.parse(str); - } catch (err) { - throw createInvalidPackError(path, 'must have a valid package.json file'); - } - - return { - directoryPath: path, - contents: pkg, - }; -} - -export const createPackageJsonAtPath$ = (path) => - // If plugin directory contains manifest file, we should skip it since it - // should have been handled by the core plugin system already. - Rx.defer(() => isNewPlatformPlugin(path)).pipe( - mergeMap((isNewPlatformPlugin) => (isNewPlatformPlugin ? [] : createPackageJsonAtPath(path))), - map((packageJson) => ({ packageJson })), - catchError((error) => [{ error }]) - ); diff --git a/src/legacy/plugin_discovery/plugin_pack/package_jsons_in_directory.js b/src/legacy/plugin_discovery/plugin_pack/package_jsons_in_directory.js deleted file mode 100644 index 5f0977f4829b8..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/package_jsons_in_directory.js +++ /dev/null @@ -1,52 +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 { mergeMap, catchError } from 'rxjs/operators'; -import { isInvalidDirectoryError } from '../errors'; - -import { createChildDirectory$ } from './lib'; -import { createPackageJsonAtPath$ } from './package_json_at_path'; - -/** - * Finds the plugins within a directory. Results are - * an array of objects with either `pack` or `error` - * keys. - * - * - `{ error }` results are provided when the path is not - * a directory, or one of the child directories is not a - * valid plugin pack. - * - `{ pack }` results are for discovered plugins defs - * - * @param {String} path - * @return {Array<{pack}|{error}>} - */ -export const createPackageJsonsInDirectory$ = (path) => - createChildDirectory$(path).pipe( - mergeMap(createPackageJsonAtPath$), - catchError((error) => { - // this error is produced by createChildDirectory$() when the path - // is invalid, we return them as an error result similar to how - // createPackAtPath$ works when it finds invalid packs in a directory - if (isInvalidDirectoryError(error)) { - return [{ error }]; - } - - throw error; - }) - ); diff --git a/src/legacy/plugin_discovery/plugin_pack/plugin_pack.js b/src/legacy/plugin_discovery/plugin_pack/plugin_pack.js deleted file mode 100644 index 1baf3d104ca84..0000000000000 --- a/src/legacy/plugin_discovery/plugin_pack/plugin_pack.js +++ /dev/null @@ -1,74 +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 { inspect } from 'util'; - -import { PluginSpec } from '../plugin_spec'; - -export class PluginPack { - constructor({ path, pkg, provider }) { - this._path = path; - this._pkg = pkg; - this._provider = provider; - } - - /** - * Get the contents of this plugin pack's package.json file - * @return {Object} - */ - getPkg() { - return this._pkg; - } - - /** - * Get the absolute path to this plugin pack on disk - * @return {String} - */ - getPath() { - return this._path; - } - - /** - * Invoke the plugin pack's provider to get the list - * of specs defined in this plugin. - * @return {Array} - */ - getPluginSpecs() { - const pack = this; - const api = { - Plugin: class ScopedPluginSpec extends PluginSpec { - constructor(options) { - super(pack, options); - } - }, - }; - - const result = this._provider(api); - const specs = [].concat(result === undefined ? [] : result); - - // verify that all specs are instances of passed "Plugin" class - specs.forEach((spec) => { - if (!(spec instanceof api.Plugin)) { - throw new TypeError('unexpected plugin export ' + inspect(spec)); - } - }); - - return specs; - } -} diff --git a/src/legacy/plugin_discovery/plugin_spec/__tests__/is_version_compatible.js b/src/legacy/plugin_discovery/plugin_spec/__tests__/is_version_compatible.js deleted file mode 100644 index 897184496af37..0000000000000 --- a/src/legacy/plugin_discovery/plugin_spec/__tests__/is_version_compatible.js +++ /dev/null @@ -1,48 +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 expect from '@kbn/expect'; - -import { isVersionCompatible } from '../is_version_compatible'; - -describe('plugin discovery/plugin spec', () => { - describe('isVersionCompatible()', () => { - const tests = [ - ['kibana', '6.0.0', true], - ['kibana', '6.0.0-rc1', true], - ['6.0.0-rc1', '6.0.0', true], - ['6.0.0', '6.0.0-rc1', true], - ['6.0.0-rc2', '6.0.0-rc1', true], - ['6.0.0-rc2', '6.0.0-rc3', true], - ['foo', 'bar', false], - ['6.0.0', '5.1.4', false], - ['5.1.4', '6.0.0', false], - ['5.1.4-SNAPSHOT', '6.0.0-rc2-SNAPSHOT', false], - ['5.1.4', '6.0.0-rc2-SNAPSHOT', false], - ['5.1.4-SNAPSHOT', '6.0.0', false], - ['5.1.4-SNAPSHOT', '6.0.0-rc2', false], - ]; - - for (const [plugin, kibana, shouldPass] of tests) { - it(`${shouldPass ? 'should' : `shouldn't`} allow plugin: ${plugin} kibana: ${kibana}`, () => { - expect(isVersionCompatible(plugin, kibana)).to.be(shouldPass); - }); - } - }); -}); diff --git a/src/legacy/plugin_discovery/plugin_spec/__tests__/plugin_spec.js b/src/legacy/plugin_discovery/plugin_spec/__tests__/plugin_spec.js deleted file mode 100644 index 02675f0bd60f8..0000000000000 --- a/src/legacy/plugin_discovery/plugin_spec/__tests__/plugin_spec.js +++ /dev/null @@ -1,496 +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 { resolve } from 'path'; - -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -import { PluginPack } from '../../plugin_pack'; -import { PluginSpec } from '../plugin_spec'; -import * as IsVersionCompatibleNS from '../is_version_compatible'; - -const fooPack = new PluginPack({ - path: '/dev/null', - pkg: { name: 'foo', version: 'kibana' }, -}); - -describe('plugin discovery/plugin spec', () => { - describe('PluginSpec', () => { - describe('validation', () => { - it('throws if missing spec.id AND Pack has no name', () => { - const pack = new PluginPack({ pkg: {} }); - expect(() => new PluginSpec(pack, {})).to.throwError((error) => { - expect(error.message).to.contain('Unable to determine plugin id'); - }); - }); - - it('throws if missing spec.kibanaVersion AND Pack has no version', () => { - const pack = new PluginPack({ pkg: { name: 'foo' } }); - expect(() => new PluginSpec(pack, {})).to.throwError((error) => { - expect(error.message).to.contain('Unable to determine plugin version'); - }); - }); - - it('throws if spec.require is defined, but not an array', () => { - function assert(require) { - expect(() => new PluginSpec(fooPack, { require })).to.throwError((error) => { - expect(error.message).to.contain('"plugin.require" must be an array of plugin ids'); - }); - } - - assert(null); - assert(''); - assert('kibana'); - assert(1); - assert(0); - assert(/a.*b/); - }); - - it('throws if spec.publicDir is truthy and not a string', () => { - function assert(publicDir) { - expect(() => new PluginSpec(fooPack, { publicDir })).to.throwError((error) => { - expect(error.message).to.contain( - `The "path" argument must be of type string. Received type ${typeof publicDir}` - ); - }); - } - - assert(1); - assert(function () {}); - assert([]); - assert(/a.*b/); - }); - - it('throws if spec.publicDir is not an absolute path', () => { - function assert(publicDir) { - expect(() => new PluginSpec(fooPack, { publicDir })).to.throwError((error) => { - expect(error.message).to.contain('plugin.publicDir must be an absolute path'); - }); - } - - assert('relative/path'); - assert('./relative/path'); - }); - - it('throws if spec.publicDir basename is not `public`', () => { - function assert(publicDir) { - expect(() => new PluginSpec(fooPack, { publicDir })).to.throwError((error) => { - expect(error.message).to.contain('must end with a "public" directory'); - }); - } - - assert('/www'); - assert('/www/'); - assert('/www/public/my_plugin'); - assert('/www/public/my_plugin/'); - }); - }); - - describe('#getPack()', () => { - it('returns the pack', () => { - const spec = new PluginSpec(fooPack, {}); - expect(spec.getPack()).to.be(fooPack); - }); - }); - - describe('#getPkg()', () => { - it('returns the pkg from the pack', () => { - const spec = new PluginSpec(fooPack, {}); - expect(spec.getPkg()).to.be(fooPack.getPkg()); - }); - }); - - describe('#getPath()', () => { - it('returns the path from the pack', () => { - const spec = new PluginSpec(fooPack, {}); - expect(spec.getPath()).to.be(fooPack.getPath()); - }); - }); - - describe('#getId()', () => { - it('uses spec.id', () => { - const spec = new PluginSpec(fooPack, { - id: 'bar', - }); - - expect(spec.getId()).to.be('bar'); - }); - - it('defaults to pack.pkg.name', () => { - const spec = new PluginSpec(fooPack, {}); - - expect(spec.getId()).to.be('foo'); - }); - }); - - describe('#getVersion()', () => { - it('uses spec.version', () => { - const spec = new PluginSpec(fooPack, { - version: 'bar', - }); - - expect(spec.getVersion()).to.be('bar'); - }); - - it('defaults to pack.pkg.version', () => { - const spec = new PluginSpec(fooPack, {}); - - expect(spec.getVersion()).to.be('kibana'); - }); - }); - - describe('#isEnabled()', () => { - describe('spec.isEnabled is not defined', () => { - function setup(configPrefix, configGetImpl) { - const spec = new PluginSpec(fooPack, { configPrefix }); - const config = { - get: sinon.spy(configGetImpl), - has: sinon.stub(), - }; - - return { spec, config }; - } - - it('throws if not passed a config service', () => { - const { spec } = setup('a.b.c', () => true); - - expect(() => spec.isEnabled()).to.throwError((error) => { - expect(error.message).to.contain('must be called with a config service'); - }); - expect(() => spec.isEnabled(null)).to.throwError((error) => { - expect(error.message).to.contain('must be called with a config service'); - }); - expect(() => spec.isEnabled({ get: () => {} })).to.throwError((error) => { - expect(error.message).to.contain('must be called with a config service'); - }); - }); - - it('returns true when config.get([...configPrefix, "enabled"]) returns true', () => { - const { spec, config } = setup('d.e.f', () => true); - - expect(spec.isEnabled(config)).to.be(true); - sinon.assert.calledOnce(config.get); - sinon.assert.calledWithExactly(config.get, ['d', 'e', 'f', 'enabled']); - }); - - it('returns false when config.get([...configPrefix, "enabled"]) returns false', () => { - const { spec, config } = setup('g.h.i', () => false); - - expect(spec.isEnabled(config)).to.be(false); - sinon.assert.calledOnce(config.get); - sinon.assert.calledWithExactly(config.get, ['g', 'h', 'i', 'enabled']); - }); - }); - - describe('spec.isEnabled is defined', () => { - function setup(isEnabledImpl) { - const isEnabled = sinon.spy(isEnabledImpl); - const spec = new PluginSpec(fooPack, { isEnabled }); - const config = { - get: sinon.stub(), - has: sinon.stub(), - }; - - return { isEnabled, spec, config }; - } - - it('throws if not passed a config service', () => { - const { spec } = setup(() => true); - - expect(() => spec.isEnabled()).to.throwError((error) => { - expect(error.message).to.contain('must be called with a config service'); - }); - expect(() => spec.isEnabled(null)).to.throwError((error) => { - expect(error.message).to.contain('must be called with a config service'); - }); - expect(() => spec.isEnabled({ get: () => {} })).to.throwError((error) => { - expect(error.message).to.contain('must be called with a config service'); - }); - }); - - it('does not check config if spec.isEnabled returns true', () => { - const { spec, isEnabled, config } = setup(() => true); - - expect(spec.isEnabled(config)).to.be(true); - sinon.assert.calledOnce(isEnabled); - sinon.assert.notCalled(config.get); - }); - - it('does not check config if spec.isEnabled returns false', () => { - const { spec, isEnabled, config } = setup(() => false); - - expect(spec.isEnabled(config)).to.be(false); - sinon.assert.calledOnce(isEnabled); - sinon.assert.notCalled(config.get); - }); - }); - }); - - describe('#getExpectedKibanaVersion()', () => { - describe('has: spec.kibanaVersion,pkg.kibana.version,spec.version,pkg.version', () => { - it('uses spec.kibanaVersion', () => { - const pack = new PluginPack({ - path: '/dev/null', - pkg: { - name: 'expkv', - version: '1.0.0', - kibana: { - version: '6.0.0', - }, - }, - }); - - const spec = new PluginSpec(pack, { - version: '2.0.0', - kibanaVersion: '5.0.0', - }); - - expect(spec.getExpectedKibanaVersion()).to.be('5.0.0'); - }); - }); - describe('missing: spec.kibanaVersion, has: pkg.kibana.version,spec.version,pkg.version', () => { - it('uses pkg.kibana.version', () => { - const pack = new PluginPack({ - path: '/dev/null', - pkg: { - name: 'expkv', - version: '1.0.0', - kibana: { - version: '6.0.0', - }, - }, - }); - - const spec = new PluginSpec(pack, { - version: '2.0.0', - }); - - expect(spec.getExpectedKibanaVersion()).to.be('6.0.0'); - }); - }); - describe('missing: spec.kibanaVersion,pkg.kibana.version, has: spec.version,pkg.version', () => { - it('uses spec.version', () => { - const pack = new PluginPack({ - path: '/dev/null', - pkg: { - name: 'expkv', - version: '1.0.0', - }, - }); - - const spec = new PluginSpec(pack, { - version: '2.0.0', - }); - - expect(spec.getExpectedKibanaVersion()).to.be('2.0.0'); - }); - }); - describe('missing: spec.kibanaVersion,pkg.kibana.version,spec.version, has: pkg.version', () => { - it('uses pkg.version', () => { - const pack = new PluginPack({ - path: '/dev/null', - pkg: { - name: 'expkv', - version: '1.0.0', - }, - }); - - const spec = new PluginSpec(pack, {}); - - expect(spec.getExpectedKibanaVersion()).to.be('1.0.0'); - }); - }); - }); - - describe('#isVersionCompatible()', () => { - it('passes this.getExpectedKibanaVersion() and arg to isVersionCompatible(), returns its result', () => { - const spec = new PluginSpec(fooPack, { version: '1.0.0' }); - sinon.stub(spec, 'getExpectedKibanaVersion').returns('foo'); - const isVersionCompatible = sinon - .stub(IsVersionCompatibleNS, 'isVersionCompatible') - .returns('bar'); - expect(spec.isVersionCompatible('baz')).to.be('bar'); - - sinon.assert.calledOnce(spec.getExpectedKibanaVersion); - sinon.assert.calledWithExactly(spec.getExpectedKibanaVersion); - - sinon.assert.calledOnce(isVersionCompatible); - sinon.assert.calledWithExactly(isVersionCompatible, 'foo', 'baz'); - }); - }); - - describe('#getRequiredPluginIds()', () => { - it('returns spec.require', () => { - const spec = new PluginSpec(fooPack, { require: [1, 2, 3] }); - expect(spec.getRequiredPluginIds()).to.eql([1, 2, 3]); - }); - }); - - describe('#getPublicDir()', () => { - describe('spec.publicDir === false', () => { - it('returns null', () => { - const spec = new PluginSpec(fooPack, { publicDir: false }); - expect(spec.getPublicDir()).to.be(null); - }); - }); - - describe('spec.publicDir is falsy', () => { - it('returns public child of pack path', () => { - function assert(publicDir) { - const spec = new PluginSpec(fooPack, { publicDir }); - expect(spec.getPublicDir()).to.be(resolve('/dev/null/public')); - } - - assert(0); - assert(''); - assert(null); - assert(undefined); - assert(NaN); - }); - }); - - describe('spec.publicDir is an absolute path', () => { - it('returns the path', () => { - const spec = new PluginSpec(fooPack, { - publicDir: '/var/www/public', - }); - - expect(spec.getPublicDir()).to.be('/var/www/public'); - }); - }); - - // NOTE: see constructor tests for other truthy-tests that throw in constructor - }); - - describe('#getExportSpecs()', () => { - it('returns spec.uiExports', () => { - const spec = new PluginSpec(fooPack, { - uiExports: 'foo', - }); - - expect(spec.getExportSpecs()).to.be('foo'); - }); - }); - - describe('#getPreInitHandler()', () => { - it('returns spec.preInit', () => { - const spec = new PluginSpec(fooPack, { - preInit: 'foo', - }); - - expect(spec.getPreInitHandler()).to.be('foo'); - }); - }); - - describe('#getInitHandler()', () => { - it('returns spec.init', () => { - const spec = new PluginSpec(fooPack, { - init: 'foo', - }); - - expect(spec.getInitHandler()).to.be('foo'); - }); - }); - - describe('#getConfigPrefix()', () => { - describe('spec.configPrefix is truthy', () => { - it('returns spec.configPrefix', () => { - const spec = new PluginSpec(fooPack, { - configPrefix: 'foo.bar.baz', - }); - - expect(spec.getConfigPrefix()).to.be('foo.bar.baz'); - }); - }); - describe('spec.configPrefix is falsy', () => { - it('returns spec.getId()', () => { - function assert(configPrefix) { - const spec = new PluginSpec(fooPack, { configPrefix }); - sinon.stub(spec, 'getId').returns('foo'); - expect(spec.getConfigPrefix()).to.be('foo'); - sinon.assert.calledOnce(spec.getId); - } - - assert(false); - assert(null); - assert(undefined); - assert(''); - assert(0); - }); - }); - }); - - describe('#getConfigSchemaProvider()', () => { - it('returns spec.config', () => { - const spec = new PluginSpec(fooPack, { - config: 'foo', - }); - - expect(spec.getConfigSchemaProvider()).to.be('foo'); - }); - }); - - describe('#readConfigValue()', () => { - const spec = new PluginSpec(fooPack, { - configPrefix: 'foo.bar', - }); - - const config = { - get: sinon.stub(), - }; - - afterEach(() => config.get.resetHistory()); - - describe('key = "foo"', () => { - it('passes key as own array item', () => { - spec.readConfigValue(config, 'foo'); - sinon.assert.calledOnce(config.get); - sinon.assert.calledWithExactly(config.get, ['foo', 'bar', 'foo']); - }); - }); - - describe('key = "foo.bar"', () => { - it('passes key as two array items', () => { - spec.readConfigValue(config, 'foo.bar'); - sinon.assert.calledOnce(config.get); - sinon.assert.calledWithExactly(config.get, ['foo', 'bar', 'foo', 'bar']); - }); - }); - - describe('key = ["foo", "bar"]', () => { - it('merged keys into array', () => { - spec.readConfigValue(config, ['foo', 'bar']); - sinon.assert.calledOnce(config.get); - sinon.assert.calledWithExactly(config.get, ['foo', 'bar', 'foo', 'bar']); - }); - }); - }); - - describe('#getDeprecationsProvider()', () => { - it('returns spec.deprecations', () => { - const spec = new PluginSpec(fooPack, { - deprecations: 'foo', - }); - - expect(spec.getDeprecationsProvider()).to.be('foo'); - }); - }); - }); -}); diff --git a/src/legacy/plugin_discovery/plugin_spec/index.js b/src/legacy/plugin_discovery/plugin_spec/index.js deleted file mode 100644 index 671d311b152e2..0000000000000 --- a/src/legacy/plugin_discovery/plugin_spec/index.js +++ /dev/null @@ -1,20 +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. - */ - -export { PluginSpec } from './plugin_spec'; diff --git a/src/legacy/plugin_discovery/plugin_spec/is_version_compatible.js b/src/legacy/plugin_discovery/plugin_spec/is_version_compatible.js deleted file mode 100644 index 6822c168f368d..0000000000000 --- a/src/legacy/plugin_discovery/plugin_spec/is_version_compatible.js +++ /dev/null @@ -1,30 +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 { cleanVersion, versionSatisfies } from '../../utils/version'; - -export function isVersionCompatible(version, compatibleWith) { - // the special "kibana" version can be used to always be compatible, - // but is intentionally not supported by the plugin installer - if (version === 'kibana') { - return true; - } - - return versionSatisfies(cleanVersion(version), cleanVersion(compatibleWith)); -} diff --git a/src/legacy/plugin_discovery/plugin_spec/plugin_spec.js b/src/legacy/plugin_discovery/plugin_spec/plugin_spec.js deleted file mode 100644 index db1ec425f2ce5..0000000000000 --- a/src/legacy/plugin_discovery/plugin_spec/plugin_spec.js +++ /dev/null @@ -1,210 +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 { resolve, basename, isAbsolute as isAbsolutePath } from 'path'; - -import { get, toPath } from 'lodash'; - -import { createInvalidPluginError } from '../errors'; -import { isVersionCompatible } from './is_version_compatible'; - -export class PluginSpec { - /** - * @param {PluginPack} pack The plugin pack that produced this spec - * @param {Object} opts the options for this plugin - * @param {String} [opts.id=pkg.name] the id for this plugin. - * @param {Object} [opts.uiExports] a mapping of UiExport types to - * UI modules or metadata about the UI module - * @param {Array} [opts.require] the other plugins that this plugin - * requires. These plugins must exist and be enabled for this plugin - * to function. The require'd plugins will also be initialized first, - * in order to make sure that dependencies provided by these plugins - * are available - * @param {String} [opts.version=pkg.version] the version of this plugin - * @param {Function} [opts.init] A function that will be called to initialize - * this plugin at the appropriate time. - * @param {Function} [opts.configPrefix=this.id] The prefix to use for - * configuration values in the main configuration service - * @param {Function} [opts.config] A function that produces a configuration - * schema using Joi, which is passed as its first argument. - * @param {String|False} [opts.publicDir=path + '/public'] the public - * directory for this plugin. The final directory must have the name "public", - * though it can be located somewhere besides the root of the plugin. Set - * this to false to disable exposure of a public directory - */ - constructor(pack, options) { - const { - id, - require, - version, - kibanaVersion, - uiExports, - uiCapabilities, - publicDir, - configPrefix, - config, - deprecations, - preInit, - init, - postInit, - isEnabled, - } = options; - - this._id = id; - this._pack = pack; - this._version = version; - this._kibanaVersion = kibanaVersion; - this._require = require; - - this._publicDir = publicDir; - this._uiExports = uiExports; - this._uiCapabilities = uiCapabilities; - - this._configPrefix = configPrefix; - this._configSchemaProvider = config; - this._configDeprecationsProvider = deprecations; - - this._isEnabled = isEnabled; - this._preInit = preInit; - this._init = init; - this._postInit = postInit; - - if (!this.getId()) { - throw createInvalidPluginError(this, 'Unable to determine plugin id'); - } - - if (!this.getVersion()) { - throw createInvalidPluginError(this, 'Unable to determine plugin version'); - } - - if (this.getRequiredPluginIds() !== undefined && !Array.isArray(this.getRequiredPluginIds())) { - throw createInvalidPluginError(this, '"plugin.require" must be an array of plugin ids'); - } - - if (this._publicDir) { - if (!isAbsolutePath(this._publicDir)) { - throw createInvalidPluginError(this, 'plugin.publicDir must be an absolute path'); - } - if (basename(this._publicDir) !== 'public') { - throw createInvalidPluginError( - this, - `publicDir for plugin ${this.getId()} must end with a "public" directory.` - ); - } - } - } - - getPack() { - return this._pack; - } - - getPkg() { - return this._pack.getPkg(); - } - - getPath() { - return this._pack.getPath(); - } - - getId() { - return this._id || this.getPkg().name; - } - - getVersion() { - return this._version || this.getPkg().version; - } - - isEnabled(config) { - if (!config || typeof config.get !== 'function' || typeof config.has !== 'function') { - throw new TypeError('PluginSpec#isEnabled() must be called with a config service'); - } - - if (this._isEnabled) { - return this._isEnabled(config); - } - - return Boolean(this.readConfigValue(config, 'enabled')); - } - - getExpectedKibanaVersion() { - // Plugins must specify their version, and by default that version should match - // the version of kibana down to the patch level. If these two versions need - // to diverge, they can specify a kibana.version in the package to indicate the - // version of kibana the plugin is intended to work with. - return ( - this._kibanaVersion || get(this.getPack().getPkg(), 'kibana.version') || this.getVersion() - ); - } - - isVersionCompatible(actualKibanaVersion) { - return isVersionCompatible(this.getExpectedKibanaVersion(), actualKibanaVersion); - } - - getRequiredPluginIds() { - return this._require; - } - - getPublicDir() { - if (this._publicDir === false) { - return null; - } - - if (!this._publicDir) { - return resolve(this.getPack().getPath(), 'public'); - } - - return this._publicDir; - } - - getExportSpecs() { - return this._uiExports; - } - - getUiCapabilitiesProvider() { - return this._uiCapabilities; - } - - getPreInitHandler() { - return this._preInit; - } - - getInitHandler() { - return this._init; - } - - getPostInitHandler() { - return this._postInit; - } - - getConfigPrefix() { - return this._configPrefix || this.getId(); - } - - getConfigSchemaProvider() { - return this._configSchemaProvider; - } - - readConfigValue(config, key) { - return config.get([...toPath(this.getConfigPrefix()), ...toPath(key)]); - } - - getDeprecationsProvider() { - return this._configDeprecationsProvider; - } -} diff --git a/src/legacy/plugin_discovery/plugin_spec/plugin_spec_options.d.ts b/src/legacy/plugin_discovery/plugin_spec/plugin_spec_options.d.ts deleted file mode 100644 index e1ed2f57375a4..0000000000000 --- a/src/legacy/plugin_discovery/plugin_spec/plugin_spec_options.d.ts +++ /dev/null @@ -1,35 +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 { Server } from '../../server/kbn_server'; -import { Capabilities } from '../../../core/server'; - -export type InitPluginFunction = (server: Server) => void; -export interface UiExports { - injectDefaultVars?: (server: Server) => { [key: string]: any }; -} - -export interface PluginSpecOptions { - id: string; - require?: string[]; - publicDir?: string; - uiExports?: UiExports; - uiCapabilities?: Capabilities; - init?: InitPluginFunction; - config?: any; -} diff --git a/src/legacy/plugin_discovery/types.ts b/src/legacy/plugin_discovery/types.ts deleted file mode 100644 index 700ca6fa68c95..0000000000000 --- a/src/legacy/plugin_discovery/types.ts +++ /dev/null @@ -1,107 +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 { Server } from '../server/kbn_server'; -import { Capabilities } from '../../core/server'; -import { AppCategory } from '../../core/types'; - -/** - * Usage - * - * ``` - * const apmOss: LegacyPlugin = (kibana) => { - * return new kibana.Plugin({ - * id: 'apm_oss', - * // ... - * }); - * }; - * ``` - */ -export type LegacyPluginInitializer = (kibana: LegacyPluginApi) => ArrayOrItem; - -export type ArrayOrItem = T | T[]; - -export interface LegacyPluginApi { - Plugin: new (options: Partial) => LegacyPluginSpec; -} - -export interface LegacyPluginOptions { - id: string; - require: string[]; - version: string; - kibanaVersion: 'kibana'; - uiExports: Partial<{ - app: Partial<{ - title: string; - category?: AppCategory; - description: string; - main: string; - icon: string; - euiIconType: string; - order: number; - listed: boolean; - }>; - apps: any; - hacks: string[]; - visualize: string[]; - devTools: string[]; - injectDefaultVars: (server: Server) => Record; - home: string[]; - mappings: any; - migrations: any; - visTypes: string[]; - embeddableActions?: string[]; - embeddableFactories?: string[]; - uiSettingDefaults?: Record; - interpreter: string | string[]; - }>; - uiCapabilities?: Capabilities; - publicDir: any; - configPrefix: any; - config: any; - deprecations: any; - preInit: any; - init: InitPluginFunction; - postInit: any; - isEnabled: boolean; -} - -export type InitPluginFunction = (server: Server) => void; - -export interface LegacyPluginSpec { - getPack(): any; - getPkg(): any; - getPath(): string; - getId(): string; - getVersion(): string; - isEnabled(config: any): boolean; - getExpectedKibanaVersion(): string; - isVersionCompatible(actualKibanaVersion: any): boolean; - getRequiredPluginIds(): string[]; - getPublicDir(): string | null; - getExportSpecs(): any; - getUiCapabilitiesProvider(): any; - getPreInitHandler(): any; - getInitHandler(): any; - getPostInitHandler(): any; - getConfigPrefix(): string; - getConfigSchemaProvider(): any; - readConfigValue(config: any, key: string): any; - getDeprecationsProvider(): any; -} diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index f8736fb30f90e..a94766ef06926 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -131,6 +131,7 @@ export default () => }), }).default(), + // still used by the legacy i18n mixin plugins: Joi.object({ paths: Joi.array().items(Joi.string()).default([]), scanDirs: Joi.array().items(Joi.string()).default([]), @@ -146,71 +147,8 @@ export default () => status: Joi.object({ allowAnonymous: Joi.boolean().default(false), }).default(), - map: Joi.object({ - includeElasticMapsService: Joi.boolean().default(true), - proxyElasticMapsServiceInMaps: Joi.boolean().default(false), - tilemap: Joi.object({ - url: Joi.string(), - options: Joi.object({ - attribution: Joi.string(), - minZoom: Joi.number().min(0, 'Must be 0 or higher').default(0), - maxZoom: Joi.number().default(10), - tileSize: Joi.number(), - subdomains: Joi.array().items(Joi.string()).single(), - errorTileUrl: Joi.string().uri(), - tms: Joi.boolean(), - reuseTiles: Joi.boolean(), - bounds: Joi.array().items(Joi.array().items(Joi.number()).min(2).required()).min(2), - default: Joi.boolean(), - }).default({ - default: true, - }), - }).default(), - regionmap: Joi.object({ - includeElasticMapsService: Joi.boolean().default(true), - layers: Joi.array() - .items( - Joi.object({ - url: Joi.string(), - format: Joi.object({ - type: Joi.string().default('geojson'), - }).default({ - type: 'geojson', - }), - meta: Joi.object({ - feature_collection_path: Joi.string().default('data'), - }).default({ - feature_collection_path: 'data', - }), - attribution: Joi.string(), - name: Joi.string(), - fields: Joi.array().items( - Joi.object({ - name: Joi.string(), - description: Joi.string(), - }) - ), - }) - ) - .default([]), - }).default(), - manifestServiceUrl: Joi.string().default('').allow(''), - emsFileApiUrl: Joi.string().default('https://vector.maps.elastic.co'), - emsTileApiUrl: Joi.string().default('https://tiles.maps.elastic.co'), - emsLandingPageUrl: Joi.string().default('https://maps.elastic.co/v7.9'), - emsFontLibraryUrl: Joi.string().default( - 'https://tiles.maps.elastic.co/fonts/{fontstack}/{range}.pbf' - ), - emsTileLayerId: Joi.object({ - bright: Joi.string().default('road_map'), - desaturated: Joi.string().default('road_map_desaturated'), - dark: Joi.string().default('dark_map'), - }).default({ - bright: 'road_map', - desaturated: 'road_map_desaturated', - dark: 'dark_map', - }), - }).default(), + + map: HANDLED_IN_NEW_PLATFORM, i18n: Joi.object({ locale: Joi.string().default('en'), diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 3cfda0e0696bb..1718a9a8f55da 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -26,11 +26,10 @@ import { LoggerFactory, PackageInfo, LegacyServiceSetupDeps, - LegacyServiceDiscoverPlugins, } from '../../core/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LegacyConfig, ILegacyInternals } from '../../core/server/legacy'; +import { LegacyConfig } from '../../core/server/legacy'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { UiPlugins } from '../../core/server/plugins'; @@ -58,9 +57,7 @@ export interface PluginsSetup { export interface KibanaCore { __internals: { - elasticsearch: LegacyServiceSetupDeps['core']['elasticsearch']; hapiServer: LegacyServiceSetupDeps['core']['http']['server']; - legacy: ILegacyInternals; rendering: LegacyServiceSetupDeps['core']['rendering']; uiPlugins: UiPlugins; }; @@ -90,31 +87,18 @@ export interface NewPlatform { stop: null; } -export type LegacyPlugins = Pick< - LegacyServiceDiscoverPlugins, - 'pluginSpecs' | 'disabledPluginSpecs' | 'uiExports' ->; - // eslint-disable-next-line import/no-default-export export default class KbnServer { public readonly newPlatform: NewPlatform; public server: Server; public inject: Server['inject']; - public pluginSpecs: any[]; - public uiBundles: any; - constructor( - settings: Record, - config: KibanaConfig, - core: KibanaCore, - legacyPlugins: LegacyPlugins - ); + constructor(settings: Record, config: KibanaConfig, core: KibanaCore); public ready(): Promise; public mixin(...fns: KbnMixinFunc[]): Promise; public listen(): Promise; public close(): Promise; - public afterPluginsInit(callback: () => void): void; public applyLoggingConfiguration(settings: any): void; public config: KibanaConfig; } diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index 107e5f6387833..e29563a7c6266 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -30,7 +30,6 @@ import { loggingMixin } from './logging'; import warningsMixin from './warnings'; import configCompleteMixin from './config/complete'; import { optimizeMixin } from '../../optimize'; -import * as Plugins from './plugins'; import { uiMixin } from '../ui'; import { i18nMixin } from './i18n'; @@ -47,9 +46,8 @@ export default class KbnServer { * @param {Record} settings * @param {KibanaConfig} config * @param {KibanaCore} core - * @param {LegacyPlugins} legacyPlugins */ - constructor(settings, config, core, legacyPlugins) { + constructor(settings, config, core) { this.name = pkg.name; this.version = pkg.version; this.build = pkg.build || false; @@ -74,14 +72,8 @@ export default class KbnServer { stop: null, }; - this.uiExports = legacyPlugins.uiExports; - this.pluginSpecs = legacyPlugins.pluginSpecs; - this.disabledPluginSpecs = legacyPlugins.disabledPluginSpecs; - this.ready = constant( this.mixin( - Plugins.waitForInitSetupMixin, - // Sets global HTTP behaviors httpMixin, @@ -93,22 +85,13 @@ export default class KbnServer { // scan translations dirs, register locale files and initialize i18n engine. i18nMixin, - // find plugins and set this.plugins and this.pluginSpecs - Plugins.scanMixin, - // tell the config we are done loading plugins configCompleteMixin, uiMixin, // setup routes that serve the @kbn/optimizer output - optimizeMixin, - - // initialize the plugins - Plugins.initializeMixin, - - // notify any deferred setup logic that plugins have initialized - Plugins.waitForInitResolveMixin + optimizeMixin ) ); diff --git a/src/legacy/server/plugins/index.js b/src/legacy/server/plugins/index.js deleted file mode 100644 index 1511b63b519ae..0000000000000 --- a/src/legacy/server/plugins/index.js +++ /dev/null @@ -1,22 +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. - */ - -export { scanMixin } from './scan_mixin'; -export { initializeMixin } from './initialize_mixin'; -export { waitForInitSetupMixin, waitForInitResolveMixin } from './wait_for_plugins_init'; diff --git a/src/legacy/server/plugins/initialize_mixin.js b/src/legacy/server/plugins/initialize_mixin.js deleted file mode 100644 index ccf4cd1c1a404..0000000000000 --- a/src/legacy/server/plugins/initialize_mixin.js +++ /dev/null @@ -1,47 +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 { callPluginHook } from './lib'; - -/** - * KbnServer mixin that initializes all plugins found in ./scan mixin - * @param {KbnServer} kbnServer - * @param {Hapi.Server} server - * @param {Config} config - * @return {Promise} - */ -export async function initializeMixin(kbnServer, server, config) { - if (!config.get('plugins.initialize')) { - server.log(['info'], 'Plugin initialization disabled.'); - return; - } - - async function callHookOnPlugins(hookName) { - const { plugins } = kbnServer; - const ids = plugins.map((p) => p.id); - - for (const id of ids) { - await callPluginHook(hookName, plugins, id, []); - } - } - - await callHookOnPlugins('preInit'); - await callHookOnPlugins('init'); - await callHookOnPlugins('postInit'); -} diff --git a/src/legacy/server/plugins/lib/call_plugin_hook.js b/src/legacy/server/plugins/lib/call_plugin_hook.js deleted file mode 100644 index b665869f5d25f..0000000000000 --- a/src/legacy/server/plugins/lib/call_plugin_hook.js +++ /dev/null @@ -1,50 +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 { last } from 'lodash'; - -export async function callPluginHook(hookName, plugins, id, history) { - const plugin = plugins.find((plugin) => plugin.id === id); - - // make sure this is a valid plugin id - if (!plugin) { - if (history.length) { - throw new Error(`Unmet requirement "${id}" for plugin "${last(history)}"`); - } else { - throw new Error(`Unknown plugin "${id}"`); - } - } - - const circleStart = history.indexOf(id); - const path = [...history, id]; - - // make sure we are not trying to load a dependency within itself - if (circleStart > -1) { - const circle = path.slice(circleStart); - throw new Error(`circular dependency found: "${circle.join(' -> ')}"`); - } - - // call hook on all dependencies - for (const req of plugin.requiredIds) { - await callPluginHook(hookName, plugins, req, path); - } - - // call hook on this plugin - await plugin[hookName](); -} diff --git a/src/legacy/server/plugins/lib/call_plugin_hook.test.js b/src/legacy/server/plugins/lib/call_plugin_hook.test.js deleted file mode 100644 index 30dc2d91a9ab2..0000000000000 --- a/src/legacy/server/plugins/lib/call_plugin_hook.test.js +++ /dev/null @@ -1,101 +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 sinon from 'sinon'; -import { callPluginHook } from './call_plugin_hook'; - -describe('server/plugins/callPluginHook', () => { - it('should call in correct order based on requirements', async () => { - const plugins = [ - { - id: 'foo', - init: sinon.spy(), - preInit: sinon.spy(), - requiredIds: ['bar', 'baz'], - }, - { - id: 'bar', - init: sinon.spy(), - preInit: sinon.spy(), - requiredIds: [], - }, - { - id: 'baz', - init: sinon.spy(), - preInit: sinon.spy(), - requiredIds: ['bar'], - }, - ]; - - await callPluginHook('init', plugins, 'foo', []); - const [foo, bar, baz] = plugins; - sinon.assert.calledOnce(foo.init); - sinon.assert.calledTwice(bar.init); - sinon.assert.calledOnce(baz.init); - sinon.assert.callOrder(bar.init, baz.init, foo.init); - }); - - it('throws meaningful error when required plugin is missing', async () => { - const plugins = [ - { - id: 'foo', - init: sinon.spy(), - preInit: sinon.spy(), - requiredIds: ['bar'], - }, - ]; - - try { - await callPluginHook('init', plugins, 'foo', []); - throw new Error('expected callPluginHook to throw'); - } catch (error) { - expect(error.message).toContain('"bar" for plugin "foo"'); - } - }); - - it('throws meaningful error when dependencies are circular', async () => { - const plugins = [ - { - id: 'foo', - init: sinon.spy(), - preInit: sinon.spy(), - requiredIds: ['bar'], - }, - { - id: 'bar', - init: sinon.spy(), - preInit: sinon.spy(), - requiredIds: ['baz'], - }, - { - id: 'baz', - init: sinon.spy(), - preInit: sinon.spy(), - requiredIds: ['foo'], - }, - ]; - - try { - await callPluginHook('init', plugins, 'foo', []); - throw new Error('expected callPluginHook to throw'); - } catch (error) { - expect(error.message).toContain('foo -> bar -> baz -> foo'); - } - }); -}); diff --git a/src/legacy/server/plugins/lib/index.js b/src/legacy/server/plugins/lib/index.js deleted file mode 100644 index 2329d24498b6b..0000000000000 --- a/src/legacy/server/plugins/lib/index.js +++ /dev/null @@ -1,21 +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. - */ - -export { callPluginHook } from './call_plugin_hook'; -export { Plugin } from './plugin'; diff --git a/src/legacy/server/plugins/lib/plugin.js b/src/legacy/server/plugins/lib/plugin.js deleted file mode 100644 index 48389061199ff..0000000000000 --- a/src/legacy/server/plugins/lib/plugin.js +++ /dev/null @@ -1,114 +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 { once } from 'lodash'; - -/** - * The server plugin class, used to extend the server - * and add custom behavior. A "scoped" plugin class is - * created by the PluginApi class and provided to plugin - * providers that automatically binds all but the `opts` - * arguments. - * - * @class Plugin - * @param {KbnServer} kbnServer - the KbnServer this plugin - * belongs to. - * @param {PluginDefinition} def - * @param {PluginSpec} spec - */ -export class Plugin { - constructor(kbnServer, spec) { - this.kbnServer = kbnServer; - this.spec = spec; - this.pkg = spec.getPkg(); - this.path = spec.getPath(); - this.id = spec.getId(); - this.version = spec.getVersion(); - this.requiredIds = spec.getRequiredPluginIds() || []; - this.externalPreInit = spec.getPreInitHandler(); - this.externalInit = spec.getInitHandler(); - this.externalPostInit = spec.getPostInitHandler(); - this.enabled = spec.isEnabled(kbnServer.config); - this.configPrefix = spec.getConfigPrefix(); - this.publicDir = spec.getPublicDir(); - - this.preInit = once(this.preInit); - this.init = once(this.init); - this.postInit = once(this.postInit); - } - - async preInit() { - if (this.externalPreInit) { - return await this.externalPreInit(this.kbnServer.server); - } - } - - async init() { - const { id, version, kbnServer, configPrefix } = this; - const { config } = kbnServer; - - // setup the hapi register function and get on with it - const register = async (server, options) => { - this._server = server; - this._options = options; - - server.logWithMetadata(['plugins', 'debug'], `Initializing plugin ${this.toString()}`, { - plugin: this, - }); - - if (this.publicDir) { - server.newPlatform.__internals.http.registerStaticDir( - `/plugins/${id}/{path*}`, - this.publicDir - ); - } - - if (this.externalInit) { - await this.externalInit(server, options); - } - }; - - await kbnServer.server.register({ - plugin: { register, name: id, version }, - options: config.has(configPrefix) ? config.get(configPrefix) : null, - }); - } - - async postInit() { - if (this.externalPostInit) { - return await this.externalPostInit(this.kbnServer.server); - } - } - - getServer() { - return this._server; - } - - getOptions() { - return this._options; - } - - toJSON() { - return this.pkg; - } - - toString() { - return `${this.id}@${this.version}`; - } -} diff --git a/src/legacy/server/plugins/scan_mixin.js b/src/legacy/server/plugins/scan_mixin.js deleted file mode 100644 index 89ebaf920d9d1..0000000000000 --- a/src/legacy/server/plugins/scan_mixin.js +++ /dev/null @@ -1,23 +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 { Plugin } from './lib'; - -export async function scanMixin(kbnServer) { - kbnServer.plugins = kbnServer.pluginSpecs.map((spec) => new Plugin(kbnServer, spec)); -} diff --git a/src/legacy/server/plugins/wait_for_plugins_init.js b/src/legacy/server/plugins/wait_for_plugins_init.js deleted file mode 100644 index 144eb5ef803cc..0000000000000 --- a/src/legacy/server/plugins/wait_for_plugins_init.js +++ /dev/null @@ -1,53 +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. - */ - -/** - * Tracks the individual queue for each kbnServer, rather than attaching - * it to the kbnServer object via a property or something - * @type {WeakMap} - */ -const queues = new WeakMap(); - -export function waitForInitSetupMixin(kbnServer) { - queues.set(kbnServer, []); - - kbnServer.afterPluginsInit = function (callback) { - const queue = queues.get(kbnServer); - - if (!queue) { - throw new Error( - 'Plugins have already initialized. Only use this method for setup logic that must wait for plugins to initialize.' - ); - } - - queue.push(callback); - }; -} - -export async function waitForInitResolveMixin(kbnServer, server, config) { - const queue = queues.get(kbnServer); - queues.set(kbnServer, null); - - // only actually call the callbacks if we are really initializing - if (config.get('plugins.initialize')) { - for (const cb of queue) { - await cb(); - } - } -} diff --git a/src/legacy/types.ts b/src/legacy/types.ts deleted file mode 100644 index 43c9ac79538b1..0000000000000 --- a/src/legacy/types.ts +++ /dev/null @@ -1,20 +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. - */ - -export * from './plugin_discovery/types'; diff --git a/src/legacy/ui/__tests__/fixtures/plugin_async_foo/index.js b/src/legacy/ui/__tests__/fixtures/plugin_async_foo/index.js deleted file mode 100644 index afe618c6d3d9c..0000000000000 --- a/src/legacy/ui/__tests__/fixtures/plugin_async_foo/index.js +++ /dev/null @@ -1,40 +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 Bluebird from 'bluebird'; - -export default (kibana) => - new kibana.Plugin({ - config(Joi) { - return Joi.object() - .keys({ - enabled: Joi.boolean().default(true), - delay: Joi.number().required(), - shared: Joi.string(), - }) - .default(); - }, - - uiExports: { - async injectDefaultVars(server, options) { - await Bluebird.delay(options.delay); - return { shared: options.shared }; - }, - }, - }); diff --git a/src/legacy/ui/__tests__/fixtures/plugin_async_foo/package.json b/src/legacy/ui/__tests__/fixtures/plugin_async_foo/package.json deleted file mode 100644 index fc1c8d8088f1b..0000000000000 --- a/src/legacy/ui/__tests__/fixtures/plugin_async_foo/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "plugin_async_foo", - "version": "kibana" -} diff --git a/src/legacy/ui/__tests__/fixtures/plugin_bar/index.js b/src/legacy/ui/__tests__/fixtures/plugin_bar/index.js deleted file mode 100644 index 975a1dc7c92e7..0000000000000 --- a/src/legacy/ui/__tests__/fixtures/plugin_bar/index.js +++ /dev/null @@ -1,36 +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. - */ - -export default (kibana) => - new kibana.Plugin({ - config(Joi) { - return Joi.object() - .keys({ - enabled: Joi.boolean().default(true), - shared: Joi.string(), - }) - .default(); - }, - - uiExports: { - injectDefaultVars(server, options) { - return { shared: options.shared }; - }, - }, - }); diff --git a/src/legacy/ui/__tests__/fixtures/plugin_bar/package.json b/src/legacy/ui/__tests__/fixtures/plugin_bar/package.json deleted file mode 100644 index f79b807990dca..0000000000000 --- a/src/legacy/ui/__tests__/fixtures/plugin_bar/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "plugin_bar", - "version": "kibana" -} diff --git a/src/legacy/ui/__tests__/fixtures/plugin_foo/index.js b/src/legacy/ui/__tests__/fixtures/plugin_foo/index.js deleted file mode 100644 index 975a1dc7c92e7..0000000000000 --- a/src/legacy/ui/__tests__/fixtures/plugin_foo/index.js +++ /dev/null @@ -1,36 +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. - */ - -export default (kibana) => - new kibana.Plugin({ - config(Joi) { - return Joi.object() - .keys({ - enabled: Joi.boolean().default(true), - shared: Joi.string(), - }) - .default(); - }, - - uiExports: { - injectDefaultVars(server, options) { - return { shared: options.shared }; - }, - }, - }); diff --git a/src/legacy/ui/__tests__/fixtures/plugin_foo/package.json b/src/legacy/ui/__tests__/fixtures/plugin_foo/package.json deleted file mode 100644 index c1b7ddd35c9a2..0000000000000 --- a/src/legacy/ui/__tests__/fixtures/plugin_foo/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "plugin_foo", - "version": "kibana" -} diff --git a/src/legacy/ui/__tests__/fixtures/test_app/index.js b/src/legacy/ui/__tests__/fixtures/test_app/index.js deleted file mode 100644 index 3eddefd618ce0..0000000000000 --- a/src/legacy/ui/__tests__/fixtures/test_app/index.js +++ /dev/null @@ -1,39 +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. - */ - -export default (kibana) => - new kibana.Plugin({ - uiExports: { - app: { - name: 'test_app', - main: 'plugins/test_app/index.js', - }, - - injectDefaultVars() { - return { - from_defaults: true, - }; - }, - }, - init(server) { - server.injectUiAppVars('test_app', () => ({ - from_test_app: true, - })); - }, - }); diff --git a/src/legacy/ui/__tests__/fixtures/test_app/package.json b/src/legacy/ui/__tests__/fixtures/test_app/package.json deleted file mode 100644 index 3aeb029e4f4cc..0000000000000 --- a/src/legacy/ui/__tests__/fixtures/test_app/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "test_app", - "version": "kibana" -} diff --git a/src/legacy/ui/index.js b/src/legacy/ui/index.js index 05373fa5d1964..5c06cb4677347 100644 --- a/src/legacy/ui/index.js +++ b/src/legacy/ui/index.js @@ -18,4 +18,3 @@ */ export { uiMixin } from './ui_mixin'; -export { collectUiExports } from './ui_exports'; diff --git a/src/legacy/ui/ui_exports/README.md b/src/legacy/ui/ui_exports/README.md deleted file mode 100644 index 7fb117b1c25b9..0000000000000 --- a/src/legacy/ui/ui_exports/README.md +++ /dev/null @@ -1,95 +0,0 @@ -# UI Exports - -When defining a Plugin, the `uiExports` key can be used to define a map of export types to values that will be used to configure the UI system. A common use for `uiExports` is `uiExports.app`, which defines the configuration of a [`UiApp`][UiApp] and teaches the UI System how to render, bundle and tell the user about an application. - - -## `collectUiExports(pluginSpecs): { [type: string]: any }` - -This function produces the object commonly found at `kbnServer.uiExports`. This object is created by calling `collectPluginExports()` with a standard set of export type reducers and defaults for the UI System. - -### export type reducers - -The [`ui_export_types` module][UiExportTypes] defines the reducer used for each uiExports key (or `type`). The name of every export in [./ui_export_types/index.js][UiExportTypes] is a key that plugins can define in their `uiExports` specification and the value of those exports are reducers that `collectPluginExports()` will call to produce the merged result of all export specs. - -### example - UiApps - -Plugin authors can define a new UiApp in their plugin specification like so: - -```js -// a single app export -export default function (kibana) { - return new kibana.Plugin({ - //... - uiExports: { - app: { - // uiApp spec options go here - } - } - }) -} - -// apps can also export multiple apps -export default function (kibana) { - return new kibana.Plugin({ - //... - uiExports: { - apps: [ - { /* uiApp spec options */ }, - { /* second uiApp spec options */ }, - ] - } - }) -} -``` - -To handle this export type, the [ui_export_types][UiExportTypes] module exports two reducers, one named `app` and the other `apps`. - -```js -export const app = ... -export const apps = ... -``` - -These reducers are defined in [`ui_export_types/ui_apps`][UiAppExportType] and have the exact same definition: - -```js -// `wrap()` produces a reducer by wrapping a base reducer with modifiers. -// All but the last argument are modifiers that take a reducer and return -// an alternate reducer to use in it's place. -// -// Most wrappers call their target reducer with slightly different -// arguments. This allows composing standard reducer modifications for -// reuse, consistency, and easy reference (once you get the hang of it). -wrap( - // calls the next reducer with the `type` set to `uiAppSpecs`, ignoring - // the key the plugin author used to define this spec ("app" or "apps" - // in this example) - alias('uiAppSpecs'), - - // calls the next reducer with the `spec` set to the result of calling - // `applySpecDefaults(spec, type, pluginSpec)` which merges some defaults - // from the `PluginSpec` because we want uiAppSpecs to be useful individually - mapSpec(applySpecDefaults), - - // writes this spec to `acc[type]` (`acc.uiAppSpecs` in this example since - // the type was set to `uiAppSpecs` by `alias()`). It does this by concatenating - // the current value and the spec into an array. If either item is already - // an array its items are added to the result individually. If either item - // is undefined it is ignored. - // - // NOTE: since flatConcatAtType is last it isn't a wrapper, it's - // just a normal reducer - flatConcatAtType -) -``` - -This reducer format was chosen so that it will be easier to look back at these reducers and see that `app` and `apps` export specs are written to `kbnServer.uiExports.uiAppSpecs`, with defaults applied, in an array. - -### defaults - -The [`ui_exports/ui_export_defaults`][UiExportDefaults] module defines the default shape of the uiExports object produced by `collectUiExports()`. The defaults generally describe the `uiExports` from the UI System itself, like default visTypes and such. - -[UiExportDefaults]: ./ui_export_defaults.js "uiExport defaults definition" -[UiExportTypes]: ./ui_export_types/index.js "Index of default ui_export_types module" -[UiAppExportType]: ./ui_export_types/ui_apps.js "UiApp extension type definition" -[PluginSpec]: ../../plugin_discovery/plugin_spec/plugin_spec.js "PluginSpec class definition" -[PluginDiscovery]: '../../plugin_discovery' "plugin_discovery module" \ No newline at end of file diff --git a/src/legacy/ui/ui_exports/collect_ui_exports.ts b/src/legacy/ui/ui_exports/collect_ui_exports.ts deleted file mode 100644 index edb2a11dc0527..0000000000000 --- a/src/legacy/ui/ui_exports/collect_ui_exports.ts +++ /dev/null @@ -1,31 +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 { LegacyUiExports } from '../../../core/server'; - -// @ts-ignore -import { UI_EXPORT_DEFAULTS } from './ui_export_defaults'; -// @ts-ignore -import * as uiExportTypeReducers from './ui_export_types'; -// @ts-ignore -import { reduceExportSpecs } from '../../plugin_discovery'; - -export function collectUiExports(pluginSpecs: unknown[]): LegacyUiExports { - return reduceExportSpecs(pluginSpecs, uiExportTypeReducers, UI_EXPORT_DEFAULTS); -} diff --git a/src/legacy/ui/ui_exports/index.js b/src/legacy/ui/ui_exports/index.js deleted file mode 100644 index 56db698dc7b03..0000000000000 --- a/src/legacy/ui/ui_exports/index.js +++ /dev/null @@ -1,20 +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. - */ - -export { collectUiExports } from './collect_ui_exports'; diff --git a/src/legacy/ui/ui_exports/ui_export_defaults.js b/src/legacy/ui/ui_exports/ui_export_defaults.js deleted file mode 100644 index 227954155ce88..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_defaults.js +++ /dev/null @@ -1,20 +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. - */ - -export const UI_EXPORT_DEFAULTS = {}; diff --git a/src/legacy/ui/ui_exports/ui_export_types/index.js b/src/legacy/ui/ui_exports/ui_export_types/index.js deleted file mode 100644 index 9ff6a53f4afb9..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/index.js +++ /dev/null @@ -1,36 +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. - */ - -export { injectDefaultVars, replaceInjectedVars } from './modify_injected_vars'; - -export { - mappings, - migrations, - savedObjectSchemas, - savedObjectsManagement, - validations, -} from './saved_object'; - -export { taskDefinitions } from './task_definitions'; - -export { link, links } from './ui_nav_links'; - -export { uiSettingDefaults } from './ui_settings'; - -export { unknown } from './unknown'; diff --git a/src/legacy/ui/ui_exports/ui_export_types/modify_injected_vars.js b/src/legacy/ui/ui_exports/ui_export_types/modify_injected_vars.js deleted file mode 100644 index 4bb9f350bd959..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/modify_injected_vars.js +++ /dev/null @@ -1,32 +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 { flatConcatAtType } from './reduce'; -import { wrap, alias, mapSpec } from './modify_reduce'; - -export const replaceInjectedVars = wrap(alias('injectedVarsReplacers'), flatConcatAtType); - -export const injectDefaultVars = wrap( - alias('defaultInjectedVarProviders'), - mapSpec((spec, type, pluginSpec) => ({ - pluginSpec, - fn: spec, - })), - flatConcatAtType -); diff --git a/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/alias.js b/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/alias.js deleted file mode 100644 index a894e59a03c81..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/alias.js +++ /dev/null @@ -1,28 +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. - */ - -/** - * Creates a reducer wrapper which, when called with a reducer, creates a new - * reducer that replaces the `type` value with `newType` before delegating to - * the wrapped reducer - * @param {String} newType - * @return {Function} - */ -export const alias = (newType) => (next) => (acc, spec, type, pluginSpec) => - next(acc, spec, newType, pluginSpec); diff --git a/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/debug.js b/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/debug.js deleted file mode 100644 index c40bca59fe14c..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/debug.js +++ /dev/null @@ -1,31 +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 { mapSpec } from './map_spec'; - -/** - * Reducer wrapper which, replaces the `spec` with the details about the definition - * of that spec - * @type {Function} - */ -export const debug = mapSpec((spec, type, pluginSpec) => ({ - spec, - type, - pluginSpec, -})); diff --git a/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/index.js b/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/index.js deleted file mode 100644 index 54c81fefdd08a..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/index.js +++ /dev/null @@ -1,24 +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. - */ - -export { alias } from './alias'; -export { debug } from './debug'; -export { mapSpec } from './map_spec'; -export { wrap } from './wrap'; -export { uniqueKeys } from './unique_keys'; diff --git a/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/map_spec.js b/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/map_spec.js deleted file mode 100644 index 5970c45e7445e..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/map_spec.js +++ /dev/null @@ -1,29 +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. - */ - -/** - * Creates a reducer wrapper which, when called with a reducer, creates a new - * reducer that replaces the `specs` value with the result of calling - * `mapFn(spec, type, pluginSpec)` before delegating to the wrapped - * reducer - * @param {Function} mapFn receives `(specs, type, pluginSpec)` - * @return {Function} - */ -export const mapSpec = (mapFn) => (next) => (acc, spec, type, pluginSpec) => - next(acc, mapFn(spec, type, pluginSpec), type, pluginSpec); diff --git a/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/unique_keys.js b/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/unique_keys.js deleted file mode 100644 index dedcd057b09e3..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/unique_keys.js +++ /dev/null @@ -1,32 +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. - */ - -const pluginId = (pluginSpec) => (pluginSpec.id ? pluginSpec.id() : pluginSpec.getId()); - -export const uniqueKeys = (sourceType) => (next) => (acc, spec, type, pluginSpec) => { - const duplicates = Object.keys(spec).filter((key) => acc[type] && acc[type].hasOwnProperty(key)); - - if (duplicates.length) { - throw new Error( - `${pluginId(pluginSpec)} defined duplicate ${sourceType || type} values: ${duplicates}` - ); - } - - return next(acc, spec, type, pluginSpec); -}; diff --git a/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/wrap.js b/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/wrap.js deleted file mode 100644 index f84d83ed7c845..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/modify_reduce/wrap.js +++ /dev/null @@ -1,45 +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. - */ - -/** - * Wrap a function with any number of wrappers. Wrappers - * are functions that take a reducer and return a reducer - * that should be called in its place. The wrappers will - * be called in reverse order for setup and then in the - * order they are defined when the resulting reducer is - * executed. - * - * const reducer = wrap( - * next => (acc) => acc[1] = 'a', - * next => (acc) => acc[1] = 'b', - * next => (acc) => acc[1] = 'c' - * ) - * - * reducer('foo') //=> 'fco' - * - * @param {Function} ...wrappers - * @param {Function} reducer - * @return {Function} - */ -export function wrap(...args) { - const reducer = args[args.length - 1]; - const wrappers = args.slice(0, -1); - - return wrappers.reverse().reduce((acc, wrapper) => wrapper(acc), reducer); -} diff --git a/src/legacy/ui/ui_exports/ui_export_types/reduce/flat_concat_at_type.js b/src/legacy/ui/ui_exports/ui_export_types/reduce/flat_concat_at_type.js deleted file mode 100644 index 5fcbcac463392..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/reduce/flat_concat_at_type.js +++ /dev/null @@ -1,28 +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 { createTypeReducer, flatConcat } from './lib'; - -/** - * Reducer that merges two values concatenating all values - * into a flattened array - * @param {Any} [initial] - * @return {Function} - */ -export const flatConcatAtType = createTypeReducer(flatConcat); diff --git a/src/legacy/ui/ui_exports/ui_export_types/reduce/index.js b/src/legacy/ui/ui_exports/ui_export_types/reduce/index.js deleted file mode 100644 index 7dc1ba60fb3cb..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/reduce/index.js +++ /dev/null @@ -1,22 +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. - */ - -export { mergeAtType } from './merge_at_type'; -export { flatConcatValuesAtType } from './flat_concat_values_at_type'; -export { flatConcatAtType } from './flat_concat_at_type'; diff --git a/src/legacy/ui/ui_exports/ui_export_types/reduce/lib/create_type_reducer.js b/src/legacy/ui/ui_exports/ui_export_types/reduce/lib/create_type_reducer.js deleted file mode 100644 index bf4793c208308..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/reduce/lib/create_type_reducer.js +++ /dev/null @@ -1,32 +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. - */ - -/** - * Creates a reducer that reduces the values within `acc[type]` by calling - * reducer with signature: - * - * reducer(acc[type], spec, type, pluginSpec) - * - * @param {Function} reducer - * @return {Function} - */ -export const createTypeReducer = (reducer) => (acc, spec, type, pluginSpec) => ({ - ...acc, - [type]: reducer(acc[type], spec, type, pluginSpec), -}); diff --git a/src/legacy/ui/ui_exports/ui_export_types/reduce/lib/flat_concat.js b/src/legacy/ui/ui_exports/ui_export_types/reduce/lib/flat_concat.js deleted file mode 100644 index 1337c8a85d5b4..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/reduce/lib/flat_concat.js +++ /dev/null @@ -1,27 +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. - */ - -/** - * Concatenate two values into a single array, ignoring either - * value if it is undefined and flattening the value if it is an array - * @param {Array|T} a - * @param {Array} b - * @return {Array} - */ -export const flatConcat = (a, b) => [].concat(a === undefined ? [] : a, b === undefined ? [] : b); diff --git a/src/legacy/ui/ui_exports/ui_export_types/reduce/lib/index.js b/src/legacy/ui/ui_exports/ui_export_types/reduce/lib/index.js deleted file mode 100644 index e4281caebe245..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/reduce/lib/index.js +++ /dev/null @@ -1,22 +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. - */ - -export { flatConcat } from './flat_concat'; -export { mergeWith } from './merge_with'; -export { createTypeReducer } from './create_type_reducer'; diff --git a/src/legacy/ui/ui_exports/ui_export_types/reduce/lib/merge_with.js b/src/legacy/ui/ui_exports/ui_export_types/reduce/lib/merge_with.js deleted file mode 100644 index 6c7d31e6fd74d..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/reduce/lib/merge_with.js +++ /dev/null @@ -1,38 +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. - */ - -const uniqueConcat = (arrayA, arrayB) => - arrayB.reduce((acc, key) => (acc.includes(key) ? acc : acc.concat(key)), arrayA); - -/** - * Assign the keys from both objA and objB to target after passing the - * current and new value through merge as `(target[key], source[key])` - * @param {Object} objA - * @param {Object} objB - * @param {Function} merge - * @return {Object} target - */ -export function mergeWith(objA, objB, merge) { - const target = {}; - const keys = uniqueConcat(Object.keys(objA), Object.keys(objB)); - for (const key of keys) { - target[key] = merge(objA[key], objB[key]); - } - return target; -} diff --git a/src/legacy/ui/ui_exports/ui_export_types/reduce/merge_at_type.js b/src/legacy/ui/ui_exports/ui_export_types/reduce/merge_at_type.js deleted file mode 100644 index 4f5a501253851..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/reduce/merge_at_type.js +++ /dev/null @@ -1,25 +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 { createTypeReducer } from './lib'; - -export const mergeAtType = createTypeReducer((a, b) => ({ - ...a, - ...b, -})); diff --git a/src/legacy/ui/ui_exports/ui_export_types/saved_object.js b/src/legacy/ui/ui_exports/ui_export_types/saved_object.js deleted file mode 100644 index be6898d3e642c..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/saved_object.js +++ /dev/null @@ -1,65 +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 { flatConcatAtType, mergeAtType } from './reduce'; -import { alias, mapSpec, uniqueKeys, wrap } from './modify_reduce'; - -// mapping types -export const mappings = wrap( - alias('savedObjectMappings'), - mapSpec((spec, type, pluginSpec) => ({ - pluginId: pluginSpec.getId(), - properties: spec, - })), - flatConcatAtType -); - -const pluginId = (pluginSpec) => (pluginSpec.id ? pluginSpec.id() : pluginSpec.getId()); - -// Combines the `migrations` property of each plugin, -// ensuring that properties are unique across plugins -// and has migrations defined where the mappings are defined. -// See saved_objects/migrations for more details. -export const migrations = wrap( - alias('savedObjectMigrations'), - (next) => (acc, spec, type, pluginSpec) => { - const mappings = pluginSpec.getExportSpecs().mappings || {}; - const invalidMigrationTypes = Object.keys(spec).filter((type) => !mappings[type]); - if (invalidMigrationTypes.length) { - throw new Error( - 'Migrations and mappings must be defined together in the uiExports of a single plugin. ' + - `${pluginId(pluginSpec)} defines migrations for types ${invalidMigrationTypes.join( - ', ' - )} but does not define their mappings.` - ); - } - return next(acc, spec, type, pluginSpec); - }, - uniqueKeys(), - mergeAtType -); - -export const savedObjectSchemas = wrap(uniqueKeys(), mergeAtType); - -export const savedObjectsManagement = wrap(uniqueKeys(), mergeAtType); - -// Combines the `validations` property of each plugin, -// ensuring that properties are unique across plugins. -// See saved_objects/validation for more details. -export const validations = wrap(alias('savedObjectValidations'), uniqueKeys(), mergeAtType); diff --git a/src/legacy/ui/ui_exports/ui_export_types/task_definitions.js b/src/legacy/ui/ui_exports/ui_export_types/task_definitions.js deleted file mode 100644 index 8a0ed85d86f3e..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/task_definitions.js +++ /dev/null @@ -1,24 +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 { mergeAtType } from './reduce'; -import { alias, wrap, uniqueKeys } from './modify_reduce'; - -// How plugins define tasks that the task manager can run. -export const taskDefinitions = wrap(alias('taskDefinitions'), uniqueKeys(), mergeAtType); diff --git a/src/legacy/ui/ui_exports/ui_export_types/ui_nav_links.js b/src/legacy/ui/ui_exports/ui_export_types/ui_nav_links.js deleted file mode 100644 index 34aff7463a249..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/ui_nav_links.js +++ /dev/null @@ -1,24 +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 { flatConcatAtType } from './reduce'; -import { wrap, alias } from './modify_reduce'; - -export const links = wrap(alias('navLinkSpecs'), flatConcatAtType); -export const link = wrap(alias('navLinkSpecs'), flatConcatAtType); diff --git a/src/legacy/ui/ui_exports/ui_export_types/ui_settings.js b/src/legacy/ui/ui_exports/ui_export_types/ui_settings.js deleted file mode 100644 index 8d88490579c21..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/ui_settings.js +++ /dev/null @@ -1,23 +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 { mergeAtType } from './reduce'; -import { wrap, uniqueKeys } from './modify_reduce'; - -export const uiSettingDefaults = wrap(uniqueKeys(), mergeAtType); diff --git a/src/legacy/ui/ui_exports/ui_export_types/unknown.js b/src/legacy/ui/ui_exports/ui_export_types/unknown.js deleted file mode 100644 index a12a514d2e6bf..0000000000000 --- a/src/legacy/ui/ui_exports/ui_export_types/unknown.js +++ /dev/null @@ -1,23 +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 { flatConcatAtType } from './reduce'; -import { wrap, alias, debug } from './modify_reduce'; - -export const unknown = wrap(debug, alias('unknown'), flatConcatAtType); diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index e3b7c1e0c3ff9..2983dbbc28667 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -67,115 +67,108 @@ export function uiRenderMixin(kbnServer, server, config) { }, }); - // register the bootstrap.js route after plugins are initialized so that we can - // detect if any default auth strategies were registered - kbnServer.afterPluginsInit(() => { - const authEnabled = !!server.auth.settings.default; - - server.route({ - path: '/bootstrap.js', - method: 'GET', - config: { - tags: ['api'], - auth: authEnabled ? { mode: 'try' } : false, - }, - async handler(request, h) { - const soClient = kbnServer.newPlatform.start.core.savedObjects.getScopedClient( - KibanaRequest.from(request) - ); - const uiSettings = kbnServer.newPlatform.start.core.uiSettings.asScopedToClient(soClient); - - const darkMode = - !authEnabled || request.auth.isAuthenticated - ? await uiSettings.get('theme:darkMode') - : false; - - const themeVersion = - !authEnabled || request.auth.isAuthenticated - ? await uiSettings.get('theme:version') - : 'v7'; - - const themeTag = `${themeVersion === 'v7' ? 'v7' : 'v8'}${darkMode ? 'dark' : 'light'}`; - - const buildHash = server.newPlatform.env.packageInfo.buildNum; - const basePath = config.get('server.basePath'); - - const regularBundlePath = `${basePath}/${buildHash}/bundles`; - - const styleSheetPaths = [ - `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`, - ...(darkMode - ? [ - themeVersion === 'v7' - ? `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.darkCssDistFilename}` - : `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.darkV8CssDistFilename}`, - `${basePath}/node_modules/@kbn/ui-framework/dist/kui_dark.css`, - `${basePath}/ui/legacy_dark_theme.css`, - ] - : [ - themeVersion === 'v7' - ? `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.lightCssDistFilename}` - : `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.lightV8CssDistFilename}`, - `${basePath}/node_modules/@kbn/ui-framework/dist/kui_light.css`, - `${basePath}/ui/legacy_light_theme.css`, - ]), - ]; - - const kpUiPlugins = kbnServer.newPlatform.__internals.uiPlugins; - const kpPluginPublicPaths = new Map(); - const kpPluginBundlePaths = new Set(); - - // recursively iterate over the kpUiPlugin ids and their required bundles - // to populate kpPluginPublicPaths and kpPluginBundlePaths - (function readKpPlugins(ids) { - for (const id of ids) { - if (kpPluginPublicPaths.has(id)) { - continue; - } - - kpPluginPublicPaths.set(id, `${regularBundlePath}/plugin/${id}/`); - kpPluginBundlePaths.add(`${regularBundlePath}/plugin/${id}/${id}.plugin.js`); - readKpPlugins(kpUiPlugins.internal.get(id).requiredBundles); + const authEnabled = !!server.auth.settings.default; + server.route({ + path: '/bootstrap.js', + method: 'GET', + config: { + tags: ['api'], + auth: authEnabled ? { mode: 'try' } : false, + }, + async handler(request, h) { + const soClient = kbnServer.newPlatform.start.core.savedObjects.getScopedClient( + KibanaRequest.from(request) + ); + const uiSettings = kbnServer.newPlatform.start.core.uiSettings.asScopedToClient(soClient); + + const darkMode = + !authEnabled || request.auth.isAuthenticated + ? await uiSettings.get('theme:darkMode') + : false; + + const themeVersion = + !authEnabled || request.auth.isAuthenticated ? await uiSettings.get('theme:version') : 'v7'; + + const themeTag = `${themeVersion === 'v7' ? 'v7' : 'v8'}${darkMode ? 'dark' : 'light'}`; + + const buildHash = server.newPlatform.env.packageInfo.buildNum; + const basePath = config.get('server.basePath'); + + const regularBundlePath = `${basePath}/${buildHash}/bundles`; + + const styleSheetPaths = [ + `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.baseCssDistFilename}`, + ...(darkMode + ? [ + themeVersion === 'v7' + ? `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.darkCssDistFilename}` + : `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.darkV8CssDistFilename}`, + `${basePath}/node_modules/@kbn/ui-framework/dist/kui_dark.css`, + `${basePath}/ui/legacy_dark_theme.css`, + ] + : [ + themeVersion === 'v7' + ? `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.lightCssDistFilename}` + : `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.lightV8CssDistFilename}`, + `${basePath}/node_modules/@kbn/ui-framework/dist/kui_light.css`, + `${basePath}/ui/legacy_light_theme.css`, + ]), + ]; + + const kpUiPlugins = kbnServer.newPlatform.__internals.uiPlugins; + const kpPluginPublicPaths = new Map(); + const kpPluginBundlePaths = new Set(); + + // recursively iterate over the kpUiPlugin ids and their required bundles + // to populate kpPluginPublicPaths and kpPluginBundlePaths + (function readKpPlugins(ids) { + for (const id of ids) { + if (kpPluginPublicPaths.has(id)) { + continue; } - })(kpUiPlugins.public.keys()); - - const jsDependencyPaths = [ - ...UiSharedDeps.jsDepFilenames.map( - (filename) => `${regularBundlePath}/kbn-ui-shared-deps/${filename}` - ), - `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.jsFilename}`, - - `${regularBundlePath}/core/core.entry.js`, - ...kpPluginBundlePaths, - ]; - - // These paths should align with the bundle routes configured in - // src/optimize/bundles_route/bundles_route.ts - const publicPathMap = JSON.stringify({ - core: `${regularBundlePath}/core/`, - 'kbn-ui-shared-deps': `${regularBundlePath}/kbn-ui-shared-deps/`, - ...Object.fromEntries(kpPluginPublicPaths), - }); - - const bootstrap = new AppBootstrap({ - templateData: { - themeTag, - jsDependencyPaths, - styleSheetPaths, - publicPathMap, - }, - }); - - const body = await bootstrap.getJsFile(); - const etag = await bootstrap.getJsFileHash(); - - return h - .response(body) - .header('cache-control', 'must-revalidate') - .header('content-type', 'application/javascript') - .etag(etag); - }, - }); + + kpPluginPublicPaths.set(id, `${regularBundlePath}/plugin/${id}/`); + kpPluginBundlePaths.add(`${regularBundlePath}/plugin/${id}/${id}.plugin.js`); + readKpPlugins(kpUiPlugins.internal.get(id).requiredBundles); + } + })(kpUiPlugins.public.keys()); + + const jsDependencyPaths = [ + ...UiSharedDeps.jsDepFilenames.map( + (filename) => `${regularBundlePath}/kbn-ui-shared-deps/${filename}` + ), + `${regularBundlePath}/kbn-ui-shared-deps/${UiSharedDeps.jsFilename}`, + + `${regularBundlePath}/core/core.entry.js`, + ...kpPluginBundlePaths, + ]; + + // These paths should align with the bundle routes configured in + // src/optimize/bundles_route/bundles_route.ts + const publicPathMap = JSON.stringify({ + core: `${regularBundlePath}/core/`, + 'kbn-ui-shared-deps': `${regularBundlePath}/kbn-ui-shared-deps/`, + ...Object.fromEntries(kpPluginPublicPaths), + }); + + const bootstrap = new AppBootstrap({ + templateData: { + themeTag, + jsDependencyPaths, + styleSheetPaths, + publicPathMap, + }, + }); + + const body = await bootstrap.getJsFile(); + const etag = await bootstrap.getJsFileHash(); + + return h + .response(body) + .header('cache-control', 'must-revalidate') + .header('content-type', 'application/javascript') + .etag(etag); + }, }); server.route({ @@ -191,19 +184,17 @@ export function uiRenderMixin(kbnServer, server, config) { }); async function renderApp(h) { - const app = { getId: () => 'core' }; const { http } = kbnServer.newPlatform.setup.core; const { savedObjects } = kbnServer.newPlatform.start.core; - const { rendering, legacy } = kbnServer.newPlatform.__internals; + const { rendering } = kbnServer.newPlatform.__internals; const req = KibanaRequest.from(h.request); const uiSettings = kbnServer.newPlatform.start.core.uiSettings.asScopedToClient( savedObjects.getScopedClient(req) ); - const vars = await legacy.getVars(app.getId(), h.request, { + const vars = { apmConfig: getApmConfig(h.request.path), - }); + }; const content = await rendering.render(h.request, uiSettings, { - app, includeUserSettings: true, vars, }); diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx index fc88b31711b23..abef8afcc3985 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx @@ -182,6 +182,9 @@ function EditorUI({ initialTextValue }: EditorProps) { unsubscribeResizer(); clearSubscriptions(); window.removeEventListener('hashchange', onHashChange); + if (editorInstanceRef.current) { + editorInstanceRef.current.getCoreEditor().destroy(); + } }; }, [saveCurrentTextObject, initialTextValue, history, setInputEditor, settingsService]); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts index 469ef6d79fae5..393b7eee346f5 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts +++ b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts @@ -408,4 +408,8 @@ export class LegacyCoreEditor implements CoreEditor { }, ]); } + + destroy() { + this.editor.destroy(); + } } diff --git a/src/plugins/console/public/types/core_editor.ts b/src/plugins/console/public/types/core_editor.ts index b71f4fff44ca5..d88d8f86b874c 100644 --- a/src/plugins/console/public/types/core_editor.ts +++ b/src/plugins/console/public/types/core_editor.ts @@ -268,4 +268,9 @@ export interface CoreEditor { * detects a change */ registerAutocompleter(autocompleter: AutoCompleterFunction): void; + + /** + * Release any resources in use by the editor. + */ + destroy(): void; } diff --git a/src/plugins/data/common/index.ts b/src/plugins/data/common/index.ts index bc7080e7d450b..153b6a633b66d 100644 --- a/src/plugins/data/common/index.ts +++ b/src/plugins/data/common/index.ts @@ -27,3 +27,10 @@ export * from './query'; export * from './search'; export * from './types'; export * from './utils'; + +/** + * Use data plugin interface instead + * @deprecated + */ + +export { IndexPatternAttributes } from './types'; diff --git a/src/plugins/data/common/index_patterns/errors.ts b/src/plugins/data/common/index_patterns/errors.ts deleted file mode 100644 index 3d92bae1968fb..0000000000000 --- a/src/plugins/data/common/index_patterns/errors.ts +++ /dev/null @@ -1,29 +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 { FieldSpec } from './types'; - -export class FieldTypeUnknownError extends Error { - public readonly fieldSpec: FieldSpec; - constructor(message: string, spec: FieldSpec) { - super(message); - this.name = 'FieldTypeUnknownError'; - this.fieldSpec = spec; - } -} diff --git a/src/legacy/plugin_discovery/__tests__/fixtures/plugins/bar/index.js b/src/plugins/data/common/index_patterns/errors/duplicate_index_pattern.ts similarity index 83% rename from src/legacy/plugin_discovery/__tests__/fixtures/plugins/bar/index.js rename to src/plugins/data/common/index_patterns/errors/duplicate_index_pattern.ts index 0eef126f2255a..c42dcc1c6a24d 100644 --- a/src/legacy/plugin_discovery/__tests__/fixtures/plugins/bar/index.js +++ b/src/plugins/data/common/index_patterns/errors/duplicate_index_pattern.ts @@ -17,13 +17,9 @@ * under the License. */ -export default function (kibana) { - return [ - new kibana.Plugin({ - id: 'bar:one', - }), - new kibana.Plugin({ - id: 'bar:two', - }), - ]; +export class DuplicateIndexPatternError extends Error { + constructor(message: string) { + super(message); + this.name = 'DuplicateIndexPatternError'; + } } diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_object/index.js b/src/plugins/data/common/index_patterns/errors/index.ts similarity index 94% rename from src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_object/index.js rename to src/plugins/data/common/index_patterns/errors/index.ts index 59f4a2649f019..7cc39d93a2a18 100644 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_object/index.js +++ b/src/plugins/data/common/index_patterns/errors/index.ts @@ -17,6 +17,4 @@ * under the License. */ -export default { - foo: 'bar', -}; +export * from './duplicate_index_pattern'; diff --git a/src/plugins/data/common/index_patterns/fields/field_list.ts b/src/plugins/data/common/index_patterns/fields/field_list.ts index 4cf6075869851..c0eb55a15fead 100644 --- a/src/plugins/data/common/index_patterns/fields/field_list.ts +++ b/src/plugins/data/common/index_patterns/fields/field_list.ts @@ -20,7 +20,7 @@ import { findIndex } from 'lodash'; import { IFieldType } from './types'; import { IndexPatternField } from './index_pattern_field'; -import { OnNotification, FieldSpec } from '../types'; +import { FieldSpec, IndexPatternFieldMap } from '../types'; import { IndexPattern } from '../index_patterns'; import { shortenDottedString } from '../../utils'; @@ -35,16 +35,11 @@ export interface IIndexPatternFieldList extends Array { removeAll(): void; replaceAll(specs: FieldSpec[]): void; update(field: FieldSpec): void; - toSpec(options?: { getFormatterForField?: IndexPattern['getFormatterForField'] }): FieldSpec[]; + toSpec(options?: { + getFormatterForField?: IndexPattern['getFormatterForField']; + }): IndexPatternFieldMap; } -export type CreateIndexPatternFieldList = ( - indexPattern: IndexPattern, - specs?: FieldSpec[], - shortDotsEnable?: boolean, - onNotification?: OnNotification -) => IIndexPatternFieldList; - // extending the array class and using a constructor doesn't work well // when calling filter and similar so wrapping in a callback. // to be removed in the future @@ -105,7 +100,7 @@ export const fieldList = ( this.groups.clear(); }; - public readonly replaceAll = (spcs: FieldSpec[]) => { + public readonly replaceAll = (spcs: FieldSpec[] = []) => { this.removeAll(); spcs.forEach(this.add); }; @@ -115,7 +110,12 @@ export const fieldList = ( }: { getFormatterForField?: IndexPattern['getFormatterForField']; } = {}) { - return [...this.map((field) => field.toSpec({ getFormatterForField }))]; + return { + ...this.reduce((collector, field) => { + collector[field.name] = field.toSpec({ getFormatterForField }); + return collector; + }, {}), + }; } } diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts index 7f72bfe55c7cd..808afc3449c2a 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts @@ -17,12 +17,9 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; import { KbnFieldType, getKbnFieldType } from '../../kbn_field_types'; -import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import { IFieldType } from './types'; import { FieldSpec, IndexPattern } from '../..'; -import { FieldTypeUnknownError } from '../errors'; export class IndexPatternField implements IFieldType { readonly spec: FieldSpec; @@ -35,16 +32,12 @@ export class IndexPatternField implements IFieldType { this.displayName = displayName; this.kbnFieldType = getKbnFieldType(spec.type); - if (spec.type && this.kbnFieldType?.name === KBN_FIELD_TYPES.UNKNOWN) { - const msg = i18n.translate('data.indexPatterns.unknownFieldTypeErrorMsg', { - values: { type: spec.type, name: spec.name }, - defaultMessage: `Field '{name}' Unknown field type '{type}'`, - }); - throw new FieldTypeUnknownError(msg, spec); - } } // writable attrs + /** + * Count is used for field popularity + */ public get count() { return this.spec.count || 0; } @@ -53,6 +46,9 @@ export class IndexPatternField implements IFieldType { this.spec.count = count; } + /** + * Script field code + */ public get script() { return this.spec.script; } @@ -61,6 +57,9 @@ export class IndexPatternField implements IFieldType { this.spec.script = script; } + /** + * Script field language + */ public get lang() { return this.spec.lang; } @@ -69,6 +68,9 @@ export class IndexPatternField implements IFieldType { this.spec.lang = lang; } + /** + * Description of field type conflicts across different indices in the same index pattern + */ public get conflictDescriptions() { return this.spec.conflictDescriptions; } @@ -152,7 +154,7 @@ export class IndexPatternField implements IFieldType { getFormatterForField, }: { getFormatterForField?: IndexPattern['getFormatterForField']; - } = {}) { + } = {}): FieldSpec { return { count: this.count, script: this.script, diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap index 1871627da76de..ed84aceb60e5a 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap +++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap @@ -2,13 +2,13 @@ exports[`IndexPattern toSpec should match snapshot 1`] = ` Object { - "fields": Array [ - Object { + "fields": Object { + "@tags": Object { "aggregatable": true, "conflictDescriptions": undefined, - "count": 10, + "count": 0, "esTypes": Array [ - "long", + "keyword", ], "format": Object { "id": "number", @@ -17,20 +17,20 @@ Object { }, }, "lang": undefined, - "name": "bytes", + "name": "@tags", "readFromDocValues": true, "script": undefined, "scripted": false, "searchable": true, "subType": undefined, - "type": "number", + "type": "string", }, - Object { + "@timestamp": Object { "aggregatable": true, "conflictDescriptions": undefined, - "count": 20, + "count": 30, "esTypes": Array [ - "boolean", + "date", ], "format": Object { "id": "number", @@ -39,20 +39,20 @@ Object { }, }, "lang": undefined, - "name": "ssl", + "name": "@timestamp", "readFromDocValues": true, "script": undefined, "scripted": false, "searchable": true, "subType": undefined, - "type": "boolean", + "type": "date", }, - Object { + "_id": Object { "aggregatable": true, "conflictDescriptions": undefined, - "count": 30, + "count": 0, "esTypes": Array [ - "date", + "_id", ], "format": Object { "id": "number", @@ -61,20 +61,20 @@ Object { }, }, "lang": undefined, - "name": "@timestamp", - "readFromDocValues": true, + "name": "_id", + "readFromDocValues": false, "script": undefined, "scripted": false, "searchable": true, "subType": undefined, - "type": "date", + "type": "string", }, - Object { + "_source": Object { "aggregatable": true, "conflictDescriptions": undefined, - "count": 30, + "count": 0, "esTypes": Array [ - "date", + "_source", ], "format": Object { "id": "number", @@ -83,20 +83,20 @@ Object { }, }, "lang": undefined, - "name": "time", - "readFromDocValues": true, + "name": "_source", + "readFromDocValues": false, "script": undefined, "scripted": false, "searchable": true, "subType": undefined, - "type": "date", + "type": "_source", }, - Object { + "_type": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "keyword", + "_type", ], "format": Object { "id": "number", @@ -105,20 +105,20 @@ Object { }, }, "lang": undefined, - "name": "@tags", - "readFromDocValues": true, + "name": "_type", + "readFromDocValues": false, "script": undefined, "scripted": false, "searchable": true, "subType": undefined, "type": "string", }, - Object { + "area": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "date", + "geo_shape", ], "format": Object { "id": "number", @@ -127,20 +127,20 @@ Object { }, }, "lang": undefined, - "name": "utc_time", - "readFromDocValues": true, + "name": "area", + "readFromDocValues": false, "script": undefined, "scripted": false, "searchable": true, "subType": undefined, - "type": "date", + "type": "geo_shape", }, - Object { + "bytes": Object { "aggregatable": true, "conflictDescriptions": undefined, - "count": 0, + "count": 10, "esTypes": Array [ - "integer", + "long", ], "format": Object { "id": "number", @@ -149,7 +149,7 @@ Object { }, }, "lang": undefined, - "name": "phpmemory", + "name": "bytes", "readFromDocValues": true, "script": undefined, "scripted": false, @@ -157,12 +157,12 @@ Object { "subType": undefined, "type": "number", }, - Object { + "custom_user_field": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "ip", + "conflict", ], "format": Object { "id": "number", @@ -171,20 +171,20 @@ Object { }, }, "lang": undefined, - "name": "ip", + "name": "custom_user_field", "readFromDocValues": true, "script": undefined, "scripted": false, "searchable": true, "subType": undefined, - "type": "ip", + "type": "conflict", }, - Object { + "extension": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "attachment", + "text", ], "format": Object { "id": "number", @@ -193,15 +193,41 @@ Object { }, }, "lang": undefined, - "name": "request_body", - "readFromDocValues": true, + "name": "extension", + "readFromDocValues": false, "script": undefined, "scripted": false, "searchable": true, "subType": undefined, - "type": "attachment", + "type": "string", }, - Object { + "extension.keyword": Object { + "aggregatable": true, + "conflictDescriptions": undefined, + "count": 0, + "esTypes": Array [ + "keyword", + ], + "format": Object { + "id": "number", + "params": Object { + "pattern": "$0,0.[00]", + }, + }, + "lang": undefined, + "name": "extension.keyword", + "readFromDocValues": true, + "script": undefined, + "scripted": false, + "searchable": true, + "subType": Object { + "multi": Object { + "parent": "extension", + }, + }, + "type": "string", + }, + "geo.coordinates": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, @@ -215,7 +241,7 @@ Object { }, }, "lang": undefined, - "name": "point", + "name": "geo.coordinates", "readFromDocValues": true, "script": undefined, "scripted": false, @@ -223,12 +249,12 @@ Object { "subType": undefined, "type": "geo_point", }, - Object { + "geo.src": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "geo_shape", + "keyword", ], "format": Object { "id": "number", @@ -237,15 +263,15 @@ Object { }, }, "lang": undefined, - "name": "area", - "readFromDocValues": false, + "name": "geo.src", + "readFromDocValues": true, "script": undefined, "scripted": false, "searchable": true, "subType": undefined, - "type": "geo_shape", + "type": "string", }, - Object { + "hashed": Object { "aggregatable": false, "conflictDescriptions": undefined, "count": 0, @@ -267,12 +293,12 @@ Object { "subType": undefined, "type": "murmur3", }, - Object { + "ip": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "geo_point", + "ip", ], "format": Object { "id": "number", @@ -281,15 +307,15 @@ Object { }, }, "lang": undefined, - "name": "geo.coordinates", + "name": "ip", "readFromDocValues": true, "script": undefined, "scripted": false, "searchable": true, "subType": undefined, - "type": "geo_point", + "type": "ip", }, - Object { + "machine.os": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, @@ -303,7 +329,7 @@ Object { }, }, "lang": undefined, - "name": "extension", + "name": "machine.os", "readFromDocValues": false, "script": undefined, "scripted": false, @@ -311,7 +337,7 @@ Object { "subType": undefined, "type": "string", }, - Object { + "machine.os.raw": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, @@ -325,19 +351,19 @@ Object { }, }, "lang": undefined, - "name": "extension.keyword", + "name": "machine.os.raw", "readFromDocValues": true, "script": undefined, "scripted": false, "searchable": true, "subType": Object { "multi": Object { - "parent": "extension", + "parent": "machine.os", }, }, "type": "string", }, - Object { + "non-filterable": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, @@ -351,20 +377,20 @@ Object { }, }, "lang": undefined, - "name": "machine.os", + "name": "non-filterable", "readFromDocValues": false, "script": undefined, "scripted": false, - "searchable": true, + "searchable": false, "subType": undefined, "type": "string", }, - Object { - "aggregatable": true, + "non-sortable": Object { + "aggregatable": false, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "keyword", + "text", ], "format": Object { "id": "number", @@ -373,24 +399,20 @@ Object { }, }, "lang": undefined, - "name": "machine.os.raw", - "readFromDocValues": true, + "name": "non-sortable", + "readFromDocValues": false, "script": undefined, "scripted": false, - "searchable": true, - "subType": Object { - "multi": Object { - "parent": "machine.os", - }, - }, + "searchable": false, + "subType": undefined, "type": "string", }, - Object { + "phpmemory": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "keyword", + "integer", ], "format": Object { "id": "number", @@ -399,20 +421,20 @@ Object { }, }, "lang": undefined, - "name": "geo.src", + "name": "phpmemory", "readFromDocValues": true, "script": undefined, "scripted": false, "searchable": true, "subType": undefined, - "type": "string", + "type": "number", }, - Object { + "point": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "_id", + "geo_point", ], "format": Object { "id": "number", @@ -421,20 +443,20 @@ Object { }, }, "lang": undefined, - "name": "_id", - "readFromDocValues": false, + "name": "point", + "readFromDocValues": true, "script": undefined, "scripted": false, "searchable": true, "subType": undefined, - "type": "string", + "type": "geo_point", }, - Object { + "request_body": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "_type", + "attachment", ], "format": Object { "id": "number", @@ -443,20 +465,20 @@ Object { }, }, "lang": undefined, - "name": "_type", - "readFromDocValues": false, + "name": "request_body", + "readFromDocValues": true, "script": undefined, "scripted": false, "searchable": true, "subType": undefined, - "type": "string", + "type": "attachment", }, - Object { + "script date": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "_source", + "date", ], "format": Object { "id": "number", @@ -464,43 +486,21 @@ Object { "pattern": "$0,0.[00]", }, }, - "lang": undefined, - "name": "_source", + "lang": "painless", + "name": "script date", "readFromDocValues": false, - "script": undefined, - "scripted": false, + "script": "1234", + "scripted": true, "searchable": true, "subType": undefined, - "type": "_source", + "type": "date", }, - Object { + "script murmur3": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "text", - ], - "format": Object { - "id": "number", - "params": Object { - "pattern": "$0,0.[00]", - }, - }, - "lang": undefined, - "name": "non-filterable", - "readFromDocValues": false, - "script": undefined, - "scripted": false, - "searchable": false, - "subType": undefined, - "type": "string", - }, - Object { - "aggregatable": false, - "conflictDescriptions": undefined, - "count": 0, - "esTypes": Array [ - "text", + "murmur3", ], "format": Object { "id": "number", @@ -508,21 +508,21 @@ Object { "pattern": "$0,0.[00]", }, }, - "lang": undefined, - "name": "non-sortable", + "lang": "expression", + "name": "script murmur3", "readFromDocValues": false, - "script": undefined, - "scripted": false, - "searchable": false, + "script": "1234", + "scripted": true, + "searchable": true, "subType": undefined, - "type": "string", + "type": "murmur3", }, - Object { + "script number": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "conflict", + "long", ], "format": Object { "id": "number", @@ -530,16 +530,16 @@ Object { "pattern": "$0,0.[00]", }, }, - "lang": undefined, - "name": "custom_user_field", - "readFromDocValues": true, - "script": undefined, - "scripted": false, + "lang": "expression", + "name": "script number", + "readFromDocValues": false, + "script": "1234", + "scripted": true, "searchable": true, "subType": undefined, - "type": "conflict", + "type": "number", }, - Object { + "script string": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, @@ -561,12 +561,12 @@ Object { "subType": undefined, "type": "string", }, - Object { + "ssl": Object { "aggregatable": true, "conflictDescriptions": undefined, - "count": 0, + "count": 20, "esTypes": Array [ - "long", + "boolean", ], "format": Object { "id": "number", @@ -574,19 +574,19 @@ Object { "pattern": "$0,0.[00]", }, }, - "lang": "expression", - "name": "script number", - "readFromDocValues": false, - "script": "1234", - "scripted": true, + "lang": undefined, + "name": "ssl", + "readFromDocValues": true, + "script": undefined, + "scripted": false, "searchable": true, "subType": undefined, - "type": "number", + "type": "boolean", }, - Object { + "time": Object { "aggregatable": true, "conflictDescriptions": undefined, - "count": 0, + "count": 30, "esTypes": Array [ "date", ], @@ -596,21 +596,21 @@ Object { "pattern": "$0,0.[00]", }, }, - "lang": "painless", - "name": "script date", - "readFromDocValues": false, - "script": "1234", - "scripted": true, + "lang": undefined, + "name": "time", + "readFromDocValues": true, + "script": undefined, + "scripted": false, "searchable": true, "subType": undefined, "type": "date", }, - Object { + "utc_time": Object { "aggregatable": true, "conflictDescriptions": undefined, "count": 0, "esTypes": Array [ - "murmur3", + "date", ], "format": Object { "id": "number", @@ -618,21 +618,22 @@ Object { "pattern": "$0,0.[00]", }, }, - "lang": "expression", - "name": "script murmur3", - "readFromDocValues": false, - "script": "1234", - "scripted": true, + "lang": undefined, + "name": "utc_time", + "readFromDocValues": true, + "script": undefined, + "scripted": false, "searchable": true, "subType": undefined, - "type": "murmur3", + "type": "date", }, - ], + }, "id": "test-pattern", "sourceFilters": undefined, "timeFieldName": "timestamp", "title": "title", + "type": "index-pattern", "typeMeta": undefined, - "version": 2, + "version": "2", } `; diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap new file mode 100644 index 0000000000000..752fdcf11991c --- /dev/null +++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_patterns.test.ts.snap @@ -0,0 +1,22 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`IndexPatterns savedObjectToSpec 1`] = ` +Object { + "fields": Object {}, + "id": "id", + "intervalName": undefined, + "sourceFilters": Array [ + Object { + "value": "item1", + }, + Object { + "value": "item2", + }, + ], + "timeFieldName": "@timestamp", + "title": "kibana-*", + "type": "", + "typeMeta": Object {}, + "version": "version", +} +`; diff --git a/src/plugins/data/common/index_patterns/index_patterns/_fields_fetcher.ts b/src/plugins/data/common/index_patterns/index_patterns/_fields_fetcher.ts deleted file mode 100644 index 4eba0576ff235..0000000000000 --- a/src/plugins/data/common/index_patterns/index_patterns/_fields_fetcher.ts +++ /dev/null @@ -1,48 +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 { IndexPattern } from '.'; -import { GetFieldsOptions, IIndexPatternsApiClient } from '../types'; - -/** @internal */ -export const createFieldsFetcher = ( - indexPattern: IndexPattern, - apiClient: IIndexPatternsApiClient, - metaFields: string[] = [] -) => { - const fieldFetcher = { - fetch: (options: GetFieldsOptions) => { - return fieldFetcher.fetchForWildcard(indexPattern.title, { - ...options, - type: indexPattern.type, - params: indexPattern.typeMeta && indexPattern.typeMeta.params, - }); - }, - fetchForWildcard: (pattern: string, options: GetFieldsOptions = {}) => { - return apiClient.getFieldsForWildcard({ - pattern, - metaFields, - type: options.type, - params: options.params || {}, - }); - }, - }; - - return fieldFetcher; -}; diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts index f49897c47d562..91286a38f16a0 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts @@ -17,19 +17,18 @@ * under the License. */ -import { defaults, map, last } from 'lodash'; +import { map, last } from 'lodash'; import { IndexPattern } from './index_pattern'; import { DuplicateField } from '../../../../kibana_utils/common'; -// @ts-ignore +// @ts-expect-error import mockLogStashFields from '../../../../../fixtures/logstash_fields'; -// @ts-ignore import { stubbedSavedObjectIndexPattern } from '../../../../../fixtures/stubbed_saved_object_index_pattern'; import { IndexPatternField } from '../fields'; import { fieldFormatsMock } from '../../field_formats/mocks'; -import { FieldFormat, IndexPatternsService } from '../..'; +import { FieldFormat } from '../..'; class MockFieldFormatter {} @@ -63,90 +62,33 @@ jest.mock('../../field_mapping', () => { }; }); -let mockFieldsFetcherResponse: any[] = []; - -jest.mock('./_fields_fetcher', () => ({ - createFieldsFetcher: jest.fn().mockImplementation(() => ({ - fetch: jest.fn().mockImplementation(() => { - return new Promise((resolve) => resolve(mockFieldsFetcherResponse)); - }), - every: jest.fn(), - })), -})); - -let object: any = {}; - -const savedObjectsClient = { - create: jest.fn(), - get: jest.fn().mockImplementation(() => object), - update: jest.fn().mockImplementation(async (type, id, body, { version }) => { - if (object.version !== version) { - throw new Object({ - res: { - status: 409, - }, - }); - } - object.attributes.title = body.title; - object.version += 'a'; - return { - id: object.id, - version: object.version, - }; - }), -}; - -const patternCache = { - clear: jest.fn(), - get: jest.fn(), - set: jest.fn(), - clearAll: jest.fn(), -}; - -const apiClient = { - _getUrl: jest.fn(), - getFieldsForTimePattern: jest.fn(), - getFieldsForWildcard: jest.fn(), -}; - // helper function to create index patterns -function create(id: string, payload?: any): Promise { - const indexPattern = new IndexPattern(id, { - savedObjectsClient: savedObjectsClient as any, - apiClient, - patternCache, +function create(id: string) { + const { + type, + version, + attributes: { timeFieldName, fields, title }, + } = stubbedSavedObjectIndexPattern(id); + + return new IndexPattern({ + spec: { id, type, version, timeFieldName, fields, title }, + savedObjectsClient: {} as any, fieldFormats: fieldFormatsMock, - indexPatternsService: {} as IndexPatternsService, - onNotification: () => {}, - onError: () => {}, shortDotsEnable: false, metaFields: [], }); - - setDocsourcePayload(id, payload); - - return indexPattern.init(); -} - -function setDocsourcePayload(id: string | null, providedPayload: any) { - object = defaults(providedPayload || {}, stubbedSavedObjectIndexPattern(id)); } describe('IndexPattern', () => { - const indexPatternId = 'test-pattern'; - let indexPattern: IndexPattern; // create an indexPattern instance for each test beforeEach(() => { - return create(indexPatternId).then((pattern: IndexPattern) => { - indexPattern = pattern; - }); + indexPattern = create('test-pattern'); }); describe('api', () => { test('should have expected properties', () => { - expect(indexPattern).toHaveProperty('refreshFields'); expect(indexPattern).toHaveProperty('popularizeField'); expect(indexPattern).toHaveProperty('getScriptedFields'); expect(indexPattern).toHaveProperty('getNonScriptedFields'); @@ -158,13 +100,6 @@ describe('IndexPattern', () => { }); }); - describe('init', () => { - test('should append the found fields', () => { - expect(savedObjectsClient.get).toHaveBeenCalled(); - expect(indexPattern.fields).toHaveLength(mockLogStashFields().length); - }); - }); - describe('fields', () => { test('should have expected properties on fields', function () { expect(indexPattern.fields[0]).toHaveProperty('displayName'); @@ -229,43 +164,9 @@ describe('IndexPattern', () => { }); }); - describe('refresh fields', () => { - test('should fetch fields from the fieldsFetcher', async () => { - expect(indexPattern.fields.length).toBeGreaterThan(2); - - mockFieldsFetcherResponse = [{ name: 'foo' }, { name: 'bar' }]; - - await indexPattern.refreshFields(); - - mockFieldsFetcherResponse = []; - - const newFields = indexPattern.getNonScriptedFields(); - - expect(newFields).toHaveLength(2); - expect([...newFields.map((f) => f.name)]).toEqual(['foo', 'bar']); - }); - - test('should preserve the scripted fields', async () => { - // add spy to indexPattern.getScriptedFields - // sinon.spy(indexPattern, 'getScriptedFields'); - - // refresh fields, which will fetch - await indexPattern.refreshFields(); - - // called to append scripted fields to the response from mapper.getFieldsForIndexPattern - // sinon.assert.calledOnce(indexPattern.getScriptedFields); - expect(indexPattern.getScriptedFields().map((f) => f.name)).toEqual( - mockLogStashFields() - .filter((f: IndexPatternField) => f.scripted) - .map((f: IndexPatternField) => f.name) - ); - }); - }); - describe('add and remove scripted fields', () => { test('should append the scripted field', async () => { // keep a copy of the current scripted field count - // const saveSpy = sinon.spy(indexPattern, 'save'); const oldCount = indexPattern.getScriptedFields().length; // add a new scripted field @@ -283,7 +184,6 @@ describe('IndexPattern', () => { ); const scriptedFields = indexPattern.getScriptedFields(); - // expect(saveSpy.callCount).to.equal(1); expect(scriptedFields).toHaveLength(oldCount + 1); expect((indexPattern.fields.getByName(scriptedField.name) as IndexPatternField).name).toEqual( scriptedField.name @@ -291,14 +191,12 @@ describe('IndexPattern', () => { }); test('should remove scripted field, by name', async () => { - // const saveSpy = sinon.spy(indexPattern, 'save'); const scriptedFields = indexPattern.getScriptedFields(); const oldCount = scriptedFields.length; const scriptedField = last(scriptedFields)!; await indexPattern.removeScriptedField(scriptedField.name); - // expect(saveSpy.callCount).to.equal(1); expect(indexPattern.getScriptedFields().length).toEqual(oldCount - 1); expect(indexPattern.fields.getByName(scriptedField.name)).toEqual(undefined); }); @@ -330,8 +228,13 @@ describe('IndexPattern', () => { } as FieldFormat; indexPattern.getFormatterForField = () => formatter; const spec = indexPattern.toSpec(); - const restoredPattern = await create(spec.id as string); - restoredPattern.initFromSpec(spec); + const restoredPattern = new IndexPattern({ + spec, + savedObjectsClient: {} as any, + fieldFormats: fieldFormatsMock, + shortDotsEnable: false, + metaFields: [], + }); expect(restoredPattern.id).toEqual(indexPattern.id); expect(restoredPattern.title).toEqual(indexPattern.title); expect(restoredPattern.timeFieldName).toEqual(indexPattern.timeFieldName); @@ -342,26 +245,22 @@ describe('IndexPattern', () => { describe('popularizeField', () => { test('should increment the popularity count by default', () => { - // const saveSpy = sinon.stub(indexPattern, 'save'); indexPattern.fields.forEach(async (field) => { const oldCount = field.count || 0; await indexPattern.popularizeField(field.name); - // expect(saveSpy.callCount).to.equal(i + 1); expect(field.count).toEqual(oldCount + 1); }); }); test('should increment the popularity count', () => { - // const saveSpy = sinon.stub(indexPattern, 'save'); indexPattern.fields.forEach(async (field) => { const oldCount = field.count || 0; const incrementAmount = 4; await indexPattern.popularizeField(field.name, incrementAmount); - // expect(saveSpy.callCount).to.equal(i + 1); expect(field.count).toEqual(oldCount + incrementAmount); }); }); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index 76f1a5e59d0ee..882235889b55c 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -18,211 +18,94 @@ */ import _, { each, reject } from 'lodash'; -import { i18n } from '@kbn/i18n'; import { SavedObjectsClientCommon } from '../..'; -import { DuplicateField, SavedObjectNotFound } from '../../../../kibana_utils/common'; +import { DuplicateField } from '../../../../kibana_utils/common'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES, IIndexPattern, - FieldTypeUnknownError, FieldFormatNotFoundError, + IFieldType, } from '../../../common'; -import { findByTitle } from '../utils'; -import { IndexPatternMissingIndices } from '../lib'; import { IndexPatternField, IIndexPatternFieldList, fieldList } from '../fields'; -import { createFieldsFetcher } from './_fields_fetcher'; import { formatHitProvider } from './format_hit'; import { flattenHitWrapper } from './flatten_hit'; -import { OnNotification, OnError, IIndexPatternsApiClient, IndexPatternAttributes } from '../types'; import { FieldFormatsStartCommon, FieldFormat } from '../../field_formats'; -import { PatternCache } from './_pattern_cache'; -import { expandShorthand, FieldMappingSpec, MappingObject } from '../../field_mapping'; -import { IndexPatternSpec, TypeMeta, FieldSpec, SourceFilter } from '../types'; +import { IndexPatternSpec, TypeMeta, SourceFilter, IndexPatternFieldMap } from '../types'; import { SerializedFieldFormat } from '../../../../expressions/common'; -import { IndexPatternsService } from '..'; - -const savedObjectType = 'index-pattern'; interface IndexPatternDeps { + spec?: IndexPatternSpec; savedObjectsClient: SavedObjectsClientCommon; - apiClient: IIndexPatternsApiClient; - patternCache: PatternCache; fieldFormats: FieldFormatsStartCommon; - indexPatternsService: IndexPatternsService; - onNotification: OnNotification; - onError: OnError; shortDotsEnable: boolean; metaFields: string[]; } +interface SavedObjectBody { + title?: string; + timeFieldName?: string; + intervalName?: string; + fields?: string; + sourceFilters?: string; + fieldFormatMap?: string; + typeMeta?: string; + type?: string; +} + +type FormatFieldFn = (hit: Record, fieldName: string) => any; + export class IndexPattern implements IIndexPattern { public id?: string; public title: string = ''; - public fieldFormatMap: any; + public fieldFormatMap: Record; public typeMeta?: TypeMeta; - public fields: IIndexPatternFieldList & { toSpec: () => FieldSpec[] }; + public fields: IIndexPatternFieldList & { toSpec: () => IndexPatternFieldMap }; public timeFieldName: string | undefined; public intervalName: string | undefined; public type: string | undefined; - public formatHit: any; - public formatField: any; - public flattenHit: any; + public formatHit: { + (hit: Record, type?: string): any; + formatField: FormatFieldFn; + }; + public formatField: FormatFieldFn; + public flattenHit: (hit: Record, deep?: boolean) => Record; public metaFields: string[]; - + // savedObject version public version: string | undefined; private savedObjectsClient: SavedObjectsClientCommon; - private patternCache: PatternCache; public sourceFilters?: SourceFilter[]; - // todo make read only, update via method or factor out - public originalBody: { [key: string]: any } = {}; - public fieldsFetcher: any; // probably want to factor out any direct usage and change to private - private indexPatternsService: IndexPatternsService; + private originalSavedObjectBody: SavedObjectBody = {}; private shortDotsEnable: boolean = false; private fieldFormats: FieldFormatsStartCommon; - private onNotification: OnNotification; - private onError: OnError; - - private mapping: MappingObject = expandShorthand({ - title: ES_FIELD_TYPES.TEXT, - timeFieldName: ES_FIELD_TYPES.KEYWORD, - intervalName: ES_FIELD_TYPES.KEYWORD, - fields: 'json', - sourceFilters: 'json', - fieldFormatMap: { - type: ES_FIELD_TYPES.TEXT, - _serialize: (map = {}) => { - const serialized = _.transform(map, this.serializeFieldFormatMap); - return _.isEmpty(serialized) ? undefined : JSON.stringify(serialized); - }, - _deserialize: (map = '{}') => { - return _.mapValues(JSON.parse(map), (mapping) => { - return this.deserializeFieldFormatMap(mapping); - }); - }, - }, - type: ES_FIELD_TYPES.KEYWORD, - typeMeta: 'json', - }); - - constructor( - id: string | undefined, - { - savedObjectsClient, - apiClient, - patternCache, - fieldFormats, - indexPatternsService, - onNotification, - onError, - shortDotsEnable = false, - metaFields = [], - }: IndexPatternDeps - ) { - this.id = id; + + constructor({ + spec = {}, + savedObjectsClient, + fieldFormats, + shortDotsEnable = false, + metaFields = [], + }: IndexPatternDeps) { + // set dependencies this.savedObjectsClient = savedObjectsClient; - this.patternCache = patternCache; this.fieldFormats = fieldFormats; - this.indexPatternsService = indexPatternsService; - this.onNotification = onNotification; - this.onError = onError; - + // set config this.shortDotsEnable = shortDotsEnable; this.metaFields = metaFields; + // initialize functionality this.fields = fieldList([], this.shortDotsEnable); - this.fieldsFetcher = createFieldsFetcher(this, apiClient, metaFields); this.flattenHit = flattenHitWrapper(this, metaFields); this.formatHit = formatHitProvider( this, fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.STRING) ); this.formatField = this.formatHit.formatField; - } - - private unknownFieldErrorNotification( - fieldType: string, - fieldName: string, - indexPatternTitle: string - ) { - const title = i18n.translate('data.indexPatterns.unknownFieldHeader', { - values: { type: fieldType }, - defaultMessage: 'Unknown field type {type}', - }); - const text = i18n.translate('data.indexPatterns.unknownFieldErrorMessage', { - values: { name: fieldName, title: indexPatternTitle }, - defaultMessage: 'Field {name} in indexPattern {title} is using an unknown field type.', - }); - this.onNotification({ title, text, color: 'danger', iconType: 'alert' }); - } - - private serializeFieldFormatMap(flat: any, format: string, field: string | undefined) { - if (format && field) { - flat[field] = format; - } - } - - private deserializeFieldFormatMap(mapping: any) { - try { - return this.fieldFormats.getInstance(mapping.id, mapping.params); - } catch (err) { - if (err instanceof FieldFormatNotFoundError) { - return undefined; - } else { - throw err; - } - } - } - - private isFieldRefreshRequired(specs?: FieldSpec[]): boolean { - if (!specs) { - return true; - } - - return specs.every((spec) => { - // See https://github.com/elastic/kibana/pull/8421 - const hasFieldCaps = 'aggregatable' in spec && 'searchable' in spec; - - // See https://github.com/elastic/kibana/pull/11969 - const hasDocValuesFlag = 'readFromDocValues' in spec; - - return !hasFieldCaps || !hasDocValuesFlag; - }); - } - - private async indexFields(specs?: FieldSpec[]) { - if (!this.id) { - return; - } - - if (this.isFieldRefreshRequired(specs)) { - await this.refreshFields(); - } else { - if (specs) { - try { - this.fields.replaceAll(specs); - } catch (err) { - if (err instanceof FieldTypeUnknownError) { - this.unknownFieldErrorNotification(err.fieldSpec.name, err.fieldSpec.type, this.title); - } else { - throw err; - } - } - } - } - } - public initFromSpec(spec: IndexPatternSpec) { - // create fieldFormatMap from field list - const fieldFormatMap: Record = {}; - if (_.isArray(spec.fields)) { - spec.fields.forEach((field: FieldSpec) => { - if (field.format) { - fieldFormatMap[field.name as string] = { ...field.format }; - } - }); - } + // set values + this.id = spec.id; + const fieldFormatMap = this.fieldSpecsToFieldFormatMap(spec.fields); this.version = spec.version; @@ -230,53 +113,55 @@ export class IndexPattern implements IIndexPattern { this.timeFieldName = spec.timeFieldName; this.sourceFilters = spec.sourceFilters; - try { - this.fields.replaceAll(spec.fields || []); - } catch (err) { - if (err instanceof FieldTypeUnknownError) { - this.unknownFieldErrorNotification(err.fieldSpec.name, err.fieldSpec.type, this.title); - } else { - throw err; - } - } + this.fields.replaceAll(Object.values(spec.fields || {})); + this.type = spec.type; this.typeMeta = spec.typeMeta; this.fieldFormatMap = _.mapValues(fieldFormatMap, (mapping) => { return this.deserializeFieldFormatMap(mapping); }); - - return this; } - private updateFromElasticSearch(response: any) { - if (!response.found) { - throw new SavedObjectNotFound(savedObjectType, this.id, 'management/kibana/indexPatterns'); - } - - _.forOwn(this.mapping, (fieldMapping: FieldMappingSpec, name: string | undefined) => { - if (!fieldMapping._deserialize || !name) { - return; + /** + * Get last saved saved object fields + */ + getOriginalSavedObjectBody = () => ({ ...this.originalSavedObjectBody }); + + /** + * Reset last saved saved object fields. used after saving + */ + resetOriginalSavedObjectBody = () => { + this.originalSavedObjectBody = this.getAsSavedObjectBody(); + }; + + /** + * Converts field format spec to field format instance + * @param mapping + */ + private deserializeFieldFormatMap(mapping: SerializedFieldFormat>) { + try { + return this.fieldFormats.getInstance(mapping.id as string, mapping.params); + } catch (err) { + if (err instanceof FieldFormatNotFoundError) { + return undefined; + } else { + throw err; } - - response[name] = fieldMapping._deserialize(response[name]); - }); - - this.title = response.title; - this.timeFieldName = response.timeFieldName; - this.intervalName = response.intervalName; - this.sourceFilters = response.sourceFilters; - this.fieldFormatMap = response.fieldFormatMap; - this.type = response.type; - this.typeMeta = response.typeMeta; - - if (!this.title && this.id) { - this.title = this.id; } - this.version = response.version; - - return this.indexFields(response.fields); } + /** + * Extracts FieldFormatMap from FieldSpec map + * @param fldList FieldSpec map + */ + private fieldSpecsToFieldFormatMap = (fldList: IndexPatternSpec['fields'] = {}) => + Object.values(fldList).reduce>((col, fieldSpec) => { + if (fieldSpec.format) { + col[fieldSpec.name] = { ...fieldSpec.format }; + } + return col; + }, {}); + getComputedFields() { const scriptFields: any = {}; if (!this.fields) { @@ -318,37 +203,6 @@ export class IndexPattern implements IIndexPattern { }; } - async init() { - if (!this.id) { - return this; // no id === no elasticsearch document - } - - const savedObject = await this.savedObjectsClient.get( - savedObjectType, - this.id - ); - - const response = { - version: savedObject.version, - found: savedObject.version ? true : false, - title: savedObject.attributes.title, - timeFieldName: savedObject.attributes.timeFieldName, - intervalName: savedObject.attributes.intervalName, - fields: savedObject.attributes.fields, - sourceFilters: savedObject.attributes.sourceFilters, - fieldFormatMap: savedObject.attributes.fieldFormatMap, - typeMeta: savedObject.attributes.typeMeta, - type: savedObject.attributes.type, - }; - // Do this before we attempt to update from ES since that call can potentially perform a save - this.originalBody = this.prepBody(); - await this.updateFromElasticSearch(response); - // Do it after to ensure we have the most up to date information - this.originalBody = this.prepBody(); - - return this; - } - public toSpec(): IndexPatternSpec { return { id: this.id, @@ -359,17 +213,33 @@ export class IndexPattern implements IIndexPattern { sourceFilters: this.sourceFilters, fields: this.fields.toSpec({ getFormatterForField: this.getFormatterForField.bind(this) }), typeMeta: this.typeMeta, + type: this.type, }; } - // Get the source filtering configuration for that index. + /** + * Get the source filtering configuration for that index. + */ getSourceFiltering() { return { excludes: (this.sourceFilters && this.sourceFilters.map((filter: any) => filter.value)) || [], }; } - async addScriptedField(name: string, script: string, fieldType: string = 'string', lang: string) { + /** + * Add scripted field to field list + * + * @param name field name + * @param script script code + * @param fieldType + * @param lang + */ + async addScriptedField( + name: string, + script: string, + fieldType: string = 'string', + lang: string = 'painless' + ) { const scriptedFields = this.getScriptedFields(); const names = _.map(scriptedFields, 'name'); @@ -377,27 +247,24 @@ export class IndexPattern implements IIndexPattern { throw new DuplicateField(name); } - try { - this.fields.add({ - name, - script, - type: fieldType, - scripted: true, - lang, - aggregatable: true, - searchable: true, - count: 0, - readFromDocValues: false, - }); - } catch (err) { - if (err instanceof FieldTypeUnknownError) { - this.unknownFieldErrorNotification(err.fieldSpec.name, err.fieldSpec.type, this.title); - } else { - throw err; - } - } + this.fields.add({ + name, + script, + type: fieldType, + scripted: true, + lang, + aggregatable: true, + searchable: true, + count: 0, + readFromDocValues: false, + }); } + /** + * Remove scripted field from field list + * @param fieldName + */ + removeScriptedField(fieldName: string) { const field = this.fields.getByName(fieldName); if (field) { @@ -424,9 +291,14 @@ export class IndexPattern implements IIndexPattern { field.count = count; try { - const res = await this.savedObjectsClient.update(savedObjectType, this.id, this.prepBody(), { - version: this.version, - }); + const res = await this.savedObjectsClient.update( + 'index-pattern', + this.id, + this.getAsSavedObjectBody(), + { + version: this.version, + } + ); this.version = res.version; } catch (e) { // no need for an error message here @@ -468,24 +340,48 @@ export class IndexPattern implements IIndexPattern { return this.typeMeta?.aggs; } - isWildcard() { + /** + * Does this index pattern title include a '*' + */ + private isWildcard() { return _.includes(this.title, '*'); } - prepBody() { + /** + * Returns index pattern as saved object body for saving + */ + getAsSavedObjectBody() { + const serializeFieldFormatMap = ( + flat: any, + format: FieldFormat | undefined, + field: string | undefined + ) => { + if (format && field) { + flat[field] = format; + } + }; + const serialized = _.transform(this.fieldFormatMap, serializeFieldFormatMap); + const fieldFormatMap = _.isEmpty(serialized) ? undefined : JSON.stringify(serialized); + return { title: this.title, timeFieldName: this.timeFieldName, intervalName: this.intervalName, - sourceFilters: this.mapping.sourceFilters._serialize!(this.sourceFilters), - fields: this.mapping.fields._serialize!(this.fields), - fieldFormatMap: this.mapping.fieldFormatMap._serialize!(this.fieldFormatMap), + sourceFilters: this.sourceFilters ? JSON.stringify(this.sourceFilters) : undefined, + fields: this.fields ? JSON.stringify(this.fields) : undefined, + fieldFormatMap, type: this.type, - typeMeta: this.mapping.typeMeta._serialize!(this.typeMeta), + typeMeta: this.typeMeta ? JSON.stringify(this.typeMeta) : undefined, }; } - getFormatterForField(field: IndexPatternField | IndexPatternField['spec']): FieldFormat { + /** + * Provide a field, get its formatter + * @param field + */ + getFormatterForField( + field: IndexPatternField | IndexPatternField['spec'] | IFieldType + ): FieldFormat { return ( this.fieldFormatMap[field.name] || this.fieldFormats.getDefaultInstance( @@ -494,81 +390,4 @@ export class IndexPattern implements IIndexPattern { ) ); } - - async create(allowOverride: boolean = false) { - const _create = async (duplicateId?: string) => { - if (duplicateId) { - this.patternCache.clear(duplicateId); - await this.savedObjectsClient.delete(savedObjectType, duplicateId); - } - - const body = this.prepBody(); - const response = await this.savedObjectsClient.create(savedObjectType, body, { id: this.id }); - - this.id = response.id; - return response.id; - }; - - const potentialDuplicateByTitle = await findByTitle(this.savedObjectsClient, this.title); - // If there is potentially duplicate title, just create it - if (!potentialDuplicateByTitle) { - return await _create(); - } - - // We found a duplicate but we aren't allowing override, show the warn modal - if (!allowOverride) { - return false; - } - - return await _create(potentialDuplicateByTitle.id); - } - - async _fetchFields() { - const fields = await this.fieldsFetcher.fetch(this); - const scripted = this.getScriptedFields().map((field) => field.spec); - try { - this.fields.replaceAll([...fields, ...scripted]); - } catch (err) { - if (err instanceof FieldTypeUnknownError) { - this.unknownFieldErrorNotification(err.fieldSpec.name, err.fieldSpec.type, this.title); - } else { - throw err; - } - } - } - - refreshFields() { - return ( - this._fetchFields() - // todo - .then(() => this.indexPatternsService.save(this)) - .catch((err) => { - // https://github.com/elastic/kibana/issues/9224 - // This call will attempt to remap fields from the matching - // ES index which may not actually exist. In that scenario, - // we still want to notify the user that there is a problem - // but we do not want to potentially make any pages unusable - // so do not rethrow the error here - - if (err instanceof IndexPatternMissingIndices) { - this.onNotification({ - title: (err as any).message, - color: 'danger', - iconType: 'alert', - }); - return []; - } - - this.onError(err, { - title: i18n.translate('data.indexPatterns.fetchFieldErrorTitle', { - defaultMessage: 'Error fetching fields for index pattern {title} (ID: {id})', - values: { - id: this.id, - title: this.title, - }, - }), - }); - }) - ); - } } diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts index d3b3a73a4b50f..b22437ebbdb4e 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts @@ -18,7 +18,7 @@ */ import { defaults } from 'lodash'; -import { IndexPatternsService } from '.'; +import { IndexPatternsService, IndexPattern } from '.'; import { fieldFormatsMock } from '../../field_formats/mocks'; import { stubbedSavedObjectIndexPattern } from '../../../../../fixtures/stubbed_saved_object_index_pattern'; import { UiSettingsCommon, SavedObjectsClientCommon, SavedObject } from '../types'; @@ -31,7 +31,6 @@ const createFieldsFetcher = jest.fn().mockImplementation(() => ({ })); const fieldFormats = fieldFormatsMock; - let object: any = {}; function setDocsourcePayload(id: string | null, providedPayload: any) { @@ -43,16 +42,18 @@ describe('IndexPatterns', () => { let savedObjectsClient: SavedObjectsClientCommon; beforeEach(() => { + const indexPatternObj = { id: 'id', version: 'a', attributes: { title: 'title' } }; savedObjectsClient = {} as SavedObjectsClientCommon; savedObjectsClient.find = jest.fn( - () => - Promise.resolve([{ id: 'id', attributes: { title: 'title' } }]) as Promise< - Array> - > + () => Promise.resolve([indexPatternObj]) as Promise>> ); savedObjectsClient.delete = jest.fn(() => Promise.resolve({}) as Promise); - savedObjectsClient.get = jest.fn().mockImplementation(() => object); savedObjectsClient.create = jest.fn(); + savedObjectsClient.get = jest.fn().mockImplementation(async (type, id) => ({ + id: object.id, + version: object.version, + attributes: object.attributes, + })); savedObjectsClient.update = jest .fn() .mockImplementation(async (type, id, body, { version }) => { @@ -141,30 +142,73 @@ describe('IndexPatterns', () => { }); // Create a normal index patterns - const pattern = await indexPatterns.make('foo'); + const pattern = await indexPatterns.get('foo'); expect(pattern.version).toBe('fooa'); + indexPatterns.clearCache(); // Create the same one - we're going to handle concurrency - const samePattern = await indexPatterns.make('foo'); + const samePattern = await indexPatterns.get('foo'); expect(samePattern.version).toBe('fooaa'); // This will conflict because samePattern did a save (from refreshFields) // but the resave should work fine pattern.title = 'foo2'; - await indexPatterns.save(pattern); + await indexPatterns.updateSavedObject(pattern); // This should not be able to recover samePattern.title = 'foo3'; let result; try { - await indexPatterns.save(samePattern); + await indexPatterns.updateSavedObject(samePattern); } catch (err) { result = err; } expect(result.res.status).toBe(409); }); + + test('create', async () => { + const title = 'kibana-*'; + indexPatterns.refreshFields = jest.fn(); + + const indexPattern = await indexPatterns.create({ title }, true); + expect(indexPattern).toBeInstanceOf(IndexPattern); + expect(indexPattern.title).toBe(title); + expect(indexPatterns.refreshFields).not.toBeCalled(); + + await indexPatterns.create({ title }); + expect(indexPatterns.refreshFields).toBeCalled(); + }); + + test('createAndSave', async () => { + const title = 'kibana-*'; + indexPatterns.createSavedObject = jest.fn(); + indexPatterns.setDefault = jest.fn(); + await indexPatterns.createAndSave({ title }); + expect(indexPatterns.createSavedObject).toBeCalled(); + expect(indexPatterns.setDefault).toBeCalled(); + }); + + test('savedObjectToSpec', () => { + const savedObject = { + id: 'id', + version: 'version', + attributes: { + title: 'kibana-*', + timeFieldName: '@timestamp', + fields: '[]', + sourceFilters: '[{"value":"item1"},{"value":"item2"}]', + fieldFormatMap: '{"field":{}}', + typeMeta: '{}', + type: '', + }, + type: 'index-pattern', + references: [], + }; + + expect(indexPatterns.savedObjectToSpec(savedObject)).toMatchSnapshot(); + }); }); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 47484f8ec75bb..c56954ba6a29b 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -33,9 +33,17 @@ import { IIndexPatternsApiClient, GetFieldsOptions, IndexPatternSpec, + IndexPatternAttributes, + FieldSpec, + FieldFormatMap, + IndexPatternFieldMap, } from '../types'; import { FieldFormatsStartCommon } from '../../field_formats'; import { UI_SETTINGS, SavedObject } from '../../../common'; +import { SavedObjectNotFound } from '../../../../kibana_utils/common'; +import { IndexPatternMissingIndices } from '../lib'; +import { findByTitle } from '../utils'; +import { DuplicateIndexPatternError } from '../errors'; const indexPatternCache = createIndexPatternCache(); const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3; @@ -86,6 +94,9 @@ export class IndexPatternsService { ); } + /** + * Refresh cache of index pattern ids and titles + */ private async refreshSavedObjectsCache() { this.savedObjectsCache = await this.savedObjectsClient.find({ type: 'index-pattern', @@ -94,6 +105,10 @@ export class IndexPatternsService { }); } + /** + * Get list of index pattern ids + * @param refresh Force refresh of index pattern list + */ getIds = async (refresh: boolean = false) => { if (!this.savedObjectsCache || refresh) { await this.refreshSavedObjectsCache(); @@ -104,6 +119,10 @@ export class IndexPatternsService { return this.savedObjectsCache.map((obj) => obj?.id); }; + /** + * Get list of index pattern titles + * @param refresh Force refresh of index pattern list + */ getTitles = async (refresh: boolean = false): Promise => { if (!this.savedObjectsCache || refresh) { await this.refreshSavedObjectsCache(); @@ -114,14 +133,10 @@ export class IndexPatternsService { return this.savedObjectsCache.map((obj) => obj?.attributes?.title); }; - getFieldsForTimePattern = (options: GetFieldsOptions = {}) => { - return this.apiClient.getFieldsForTimePattern(options); - }; - - getFieldsForWildcard = (options: GetFieldsOptions = {}) => { - return this.apiClient.getFieldsForWildcard(options); - }; - + /** + * Clear index pattern list cache + * @param id optionally clear a single id + */ clearCache = (id?: string) => { this.savedObjectsCache = null; if (id) { @@ -130,6 +145,7 @@ export class IndexPatternsService { indexPatternCache.clearAll(); } }; + getCache = async () => { if (!this.savedObjectsCache) { await this.refreshSavedObjectsCache(); @@ -137,6 +153,9 @@ export class IndexPatternsService { return this.savedObjectsCache; }; + /** + * Get default index pattern + */ getDefault = async () => { const defaultIndexPatternId = await this.config.get('defaultIndex'); if (defaultIndexPatternId) { @@ -146,47 +165,350 @@ export class IndexPatternsService { return null; }; + /** + * Optionally set default index pattern, unless force = true + * @param id + * @param force + */ + setDefault = async (id: string, force = false) => { + if (force || !this.config.get('defaultIndex')) { + await this.config.set('defaultIndex', id); + } + }; + + private isFieldRefreshRequired(specs?: IndexPatternFieldMap): boolean { + if (!specs) { + return true; + } + + return Object.values(specs).every((spec) => { + // See https://github.com/elastic/kibana/pull/8421 + const hasFieldCaps = 'aggregatable' in spec && 'searchable' in spec; + + // See https://github.com/elastic/kibana/pull/11969 + const hasDocValuesFlag = 'readFromDocValues' in spec; + + return !hasFieldCaps || !hasDocValuesFlag; + }); + } + + /** + * Get field list by providing { pattern } + * @param options + */ + getFieldsForWildcard = async (options: GetFieldsOptions = {}) => { + const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS); + return this.apiClient.getFieldsForWildcard({ + pattern: options.pattern, + metaFields, + type: options.type, + params: options.params || {}, + }); + }; + + /** + * Get field list by providing an index patttern (or spec) + * @param options + */ + getFieldsForIndexPattern = async ( + indexPattern: IndexPattern | IndexPatternSpec, + options: GetFieldsOptions = {} + ) => + this.getFieldsForWildcard({ + pattern: indexPattern.title as string, + ...options, + type: indexPattern.type, + params: indexPattern.typeMeta && indexPattern.typeMeta.params, + }); + + /** + * Refresh field list for a given index pattern + * @param indexPattern + */ + refreshFields = async (indexPattern: IndexPattern) => { + try { + const fields = await this.getFieldsForIndexPattern(indexPattern); + const scripted = indexPattern.getScriptedFields().map((field) => field.spec); + indexPattern.fields.replaceAll([...fields, ...scripted]); + } catch (err) { + if (err instanceof IndexPatternMissingIndices) { + this.onNotification({ title: (err as any).message, color: 'danger', iconType: 'alert' }); + } + + this.onError(err, { + title: i18n.translate('data.indexPatterns.fetchFieldErrorTitle', { + defaultMessage: 'Error fetching fields for index pattern {title} (ID: {id})', + values: { id: indexPattern.id, title: indexPattern.title }, + }), + }); + } + }; + + /** + * Refreshes a field list from a spec before an index pattern instance is created + * @param fields + * @param id + * @param title + * @param options + */ + private refreshFieldSpecMap = async ( + fields: IndexPatternFieldMap, + id: string, + title: string, + options: GetFieldsOptions + ) => { + const scriptdFields = Object.values(fields).filter((field) => field.scripted); + try { + const newFields = await this.getFieldsForWildcard(options); + return this.fieldArrayToMap([...newFields, ...scriptdFields]); + } catch (err) { + if (err instanceof IndexPatternMissingIndices) { + this.onNotification({ title: (err as any).message, color: 'danger', iconType: 'alert' }); + return {}; + } + + this.onError(err, { + title: i18n.translate('data.indexPatterns.fetchFieldErrorTitle', { + defaultMessage: 'Error fetching fields for index pattern {title} (ID: {id})', + values: { id, title }, + }), + }); + } + return fields; + }; + + /** + * Applies a set of formats to a set of fields + * @param fieldSpecs + * @param fieldFormatMap + */ + private addFormatsToFields = (fieldSpecs: FieldSpec[], fieldFormatMap: FieldFormatMap) => { + Object.entries(fieldFormatMap).forEach(([fieldName, value]) => { + const field = fieldSpecs.find((fld: FieldSpec) => fld.name === fieldName); + if (field) { + field.format = value; + } + }); + }; + + /** + * Converts field array to map + * @param fields + */ + fieldArrayToMap = (fields: FieldSpec[]) => + fields.reduce((collector, field) => { + collector[field.name] = field; + return collector; + }, {}); + + /** + * Converts index pattern saved object to index pattern spec + * @param savedObject + */ + + savedObjectToSpec = (savedObject: SavedObject): IndexPatternSpec => { + const { + id, + version, + attributes: { + title, + timeFieldName, + intervalName, + fields, + sourceFilters, + fieldFormatMap, + typeMeta, + type, + }, + } = savedObject; + + const parsedSourceFilters = sourceFilters ? JSON.parse(sourceFilters) : undefined; + const parsedTypeMeta = typeMeta ? JSON.parse(typeMeta) : undefined; + const parsedFieldFormatMap = fieldFormatMap ? JSON.parse(fieldFormatMap) : {}; + const parsedFields: FieldSpec[] = fields ? JSON.parse(fields) : []; + + this.addFormatsToFields(parsedFields, parsedFieldFormatMap); + return { + id, + version, + title, + intervalName, + timeFieldName, + sourceFilters: parsedSourceFilters, + fields: this.fieldArrayToMap(parsedFields), + typeMeta: parsedTypeMeta, + type, + }; + }; + + /** + * Get an index pattern by id. Cache optimized + * @param id + */ + get = async (id: string): Promise => { const cache = indexPatternCache.get(id); if (cache) { return cache; } - const indexPattern = await this.make(id); + const savedObject = await this.savedObjectsClient.get( + savedObjectType, + id + ); + + if (!savedObject.version) { + throw new SavedObjectNotFound(savedObjectType, id, 'management/kibana/indexPatterns'); + } + + const spec = this.savedObjectToSpec(savedObject); + const { title, type, typeMeta } = spec; + const parsedFieldFormats: FieldFormatMap = savedObject.attributes.fieldFormatMap + ? JSON.parse(savedObject.attributes.fieldFormatMap) + : {}; + + const isFieldRefreshRequired = this.isFieldRefreshRequired(spec.fields); + let isSaveRequired = isFieldRefreshRequired; + try { + spec.fields = isFieldRefreshRequired + ? await this.refreshFieldSpecMap(spec.fields || {}, id, spec.title as string, { + pattern: title, + metaFields: await this.config.get(UI_SETTINGS.META_FIELDS), + type, + params: typeMeta && typeMeta.params, + }) + : spec.fields; + } catch (err) { + isSaveRequired = false; + if (err instanceof IndexPatternMissingIndices) { + this.onNotification({ + title: (err as any).message, + color: 'danger', + iconType: 'alert', + }); + } else { + this.onError(err, { + title: i18n.translate('data.indexPatterns.fetchFieldErrorTitle', { + defaultMessage: 'Error fetching fields for index pattern {title} (ID: {id})', + values: { id, title }, + }), + }); + } + } + + Object.entries(parsedFieldFormats).forEach(([fieldName, value]) => { + const field = spec.fields?.[fieldName]; + if (field) { + field.format = value; + } + }); + + const indexPattern = await this.create(spec, true); + indexPatternCache.set(id, indexPattern); + if (isSaveRequired) { + try { + this.updateSavedObject(indexPattern); + } catch (err) { + this.onError(err, { + title: i18n.translate('data.indexPatterns.fetchFieldSaveErrorTitle', { + defaultMessage: + 'Error saving after fetching fields for index pattern {title} (ID: {id})', + values: { + id: indexPattern.id, + title: indexPattern.title, + }, + }), + }); + } + } - return indexPatternCache.set(id, indexPattern); + indexPattern.resetOriginalSavedObjectBody(); + return indexPattern; }; - async specToIndexPattern(spec: IndexPatternSpec) { + /** + * Create a new index pattern instance + * @param spec + * @param skipFetchFields + */ + async create(spec: IndexPatternSpec, skipFetchFields = false): Promise { const shortDotsEnable = await this.config.get(UI_SETTINGS.SHORT_DOTS_ENABLE); const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS); - const indexPattern = new IndexPattern(spec.id, { + const indexPattern = new IndexPattern({ + spec, savedObjectsClient: this.savedObjectsClient, - apiClient: this.apiClient, - patternCache: indexPatternCache, fieldFormats: this.fieldFormats, - indexPatternsService: this, - onNotification: this.onNotification, - onError: this.onError, shortDotsEnable, metaFields, }); - indexPattern.initFromSpec(spec); + if (!skipFetchFields) { + await this.refreshFields(indexPattern); + } + + return indexPattern; + } + + /** + * Create a new index pattern and save it right away + * @param spec + * @param override Overwrite if existing index pattern exists + * @param skipFetchFields + */ + + async createAndSave(spec: IndexPatternSpec, override = false, skipFetchFields = false) { + const indexPattern = await this.create(spec, skipFetchFields); + await this.createSavedObject(indexPattern, override); + await this.setDefault(indexPattern.id as string); return indexPattern; } - async save(indexPattern: IndexPattern, saveAttempts: number = 0): Promise { + /** + * Save a new index pattern + * @param indexPattern + * @param override Overwrite if existing index pattern exists + */ + + async createSavedObject(indexPattern: IndexPattern, override = false) { + const dupe = await findByTitle(this.savedObjectsClient, indexPattern.title); + if (dupe) { + if (override) { + await this.delete(dupe.id); + } else { + throw new DuplicateIndexPatternError(`Duplicate index pattern: ${indexPattern.title}`); + } + } + + const body = indexPattern.getAsSavedObjectBody(); + const response = await this.savedObjectsClient.create(savedObjectType, body, { + id: indexPattern.id, + }); + indexPattern.id = response.id; + indexPatternCache.set(indexPattern.id, indexPattern); + return indexPattern; + } + + /** + * Save existing index pattern. Will attempt to merge differences if there are conflicts + * @param indexPattern + * @param saveAttempts + */ + + async updateSavedObject( + indexPattern: IndexPattern, + saveAttempts: number = 0 + ): Promise { if (!indexPattern.id) return; - const shortDotsEnable = await this.config.get(UI_SETTINGS.SHORT_DOTS_ENABLE); - const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS); - const body = indexPattern.prepBody(); + // get the list of attributes + const body = indexPattern.getAsSavedObjectBody(); + const originalBody = indexPattern.getOriginalSavedObjectBody(); + // get changed keys const originalChangedKeys: string[] = []; Object.entries(body).forEach(([key, value]) => { - if (value !== indexPattern.originalBody[key]) { + if (value !== (originalBody as any)[key]) { originalChangedKeys.push(key); } }); @@ -197,92 +519,60 @@ export class IndexPatternsService { indexPattern.id = resp.id; indexPattern.version = resp.version; }) - .catch((err) => { + .catch(async (err) => { if (err?.res?.status === 409 && saveAttempts++ < MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS) { - const samePattern = new IndexPattern(indexPattern.id, { - savedObjectsClient: this.savedObjectsClient, - apiClient: this.apiClient, - patternCache: indexPatternCache, - fieldFormats: this.fieldFormats, - indexPatternsService: this, - onNotification: this.onNotification, - onError: this.onError, - shortDotsEnable, - metaFields, + const samePattern = await this.get(indexPattern.id as string); + // What keys changed from now and what the server returned + const updatedBody = samePattern.getAsSavedObjectBody(); + + // Build a list of changed keys from the server response + // and ensure we ignore the key if the server response + // is the same as the original response (since that is expected + // if we made a change in that key) + + const serverChangedKeys: string[] = []; + Object.entries(updatedBody).forEach(([key, value]) => { + if (value !== (body as any)[key] && value !== (originalBody as any)[key]) { + serverChangedKeys.push(key); + } }); - return samePattern.init().then(() => { - // What keys changed from now and what the server returned - const updatedBody = samePattern.prepBody(); - - // Build a list of changed keys from the server response - // and ensure we ignore the key if the server response - // is the same as the original response (since that is expected - // if we made a change in that key) - - const serverChangedKeys: string[] = []; - Object.entries(updatedBody).forEach(([key, value]) => { - if (value !== (body as any)[key] && value !== indexPattern.originalBody[key]) { - serverChangedKeys.push(key); - } - }); - - let unresolvedCollision = false; - for (const originalKey of originalChangedKeys) { - for (const serverKey of serverChangedKeys) { - if (originalKey === serverKey) { - unresolvedCollision = true; - break; - } + let unresolvedCollision = false; + for (const originalKey of originalChangedKeys) { + for (const serverKey of serverChangedKeys) { + if (originalKey === serverKey) { + unresolvedCollision = true; + break; } } + } - if (unresolvedCollision) { - const title = i18n.translate('data.indexPatterns.unableWriteLabel', { - defaultMessage: - 'Unable to write index pattern! Refresh the page to get the most up to date changes for this index pattern.', - }); - - this.onNotification({ title, color: 'danger' }); - throw err; - } - - // Set the updated response on this object - serverChangedKeys.forEach((key) => { - (indexPattern as any)[key] = (samePattern as any)[key]; + if (unresolvedCollision) { + const title = i18n.translate('data.indexPatterns.unableWriteLabel', { + defaultMessage: + 'Unable to write index pattern! Refresh the page to get the most up to date changes for this index pattern.', }); - indexPattern.version = samePattern.version; - // Clear cache - indexPatternCache.clear(indexPattern.id!); + this.onNotification({ title, color: 'danger' }); + throw err; + } - // Try the save again - return this.save(indexPattern, saveAttempts); + // Set the updated response on this object + serverChangedKeys.forEach((key) => { + (indexPattern as any)[key] = (samePattern as any)[key]; }); + indexPattern.version = samePattern.version; + + // Clear cache + indexPatternCache.clear(indexPattern.id!); + + // Try the save again + return this.updateSavedObject(indexPattern, saveAttempts); } throw err; }); } - async make(id?: string): Promise { - const shortDotsEnable = await this.config.get(UI_SETTINGS.SHORT_DOTS_ENABLE); - const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS); - - const indexPattern = new IndexPattern(id, { - savedObjectsClient: this.savedObjectsClient, - apiClient: this.apiClient, - patternCache: indexPatternCache, - fieldFormats: this.fieldFormats, - indexPatternsService: this, - onNotification: this.onNotification, - onError: this.onError, - shortDotsEnable, - metaFields, - }); - - return indexPattern.init(); - } - /** * Deletes an index pattern from .kibana index * @param indexPatternId: Id of kibana Index Pattern to delete diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 7a230c20f6cd0..cb0c3aa0de38e 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -22,29 +22,23 @@ import { ToastInputFields, ErrorToastOptions } from 'src/core/public/notificatio import type { SavedObject } from 'src/core/server'; import { IFieldType } from './fields'; import { SerializedFieldFormat } from '../../../expressions/common'; -import { KBN_FIELD_TYPES } from '..'; +import { KBN_FIELD_TYPES, IndexPatternField, FieldFormat } from '..'; + +export type FieldFormatMap = Record; export interface IIndexPattern { - [key: string]: any; fields: IFieldType[]; title: string; id?: string; type?: string; timeFieldName?: string; getTimeField?(): IFieldType | undefined; - fieldFormatMap?: Record< - string, - { - id: string; - params: unknown; - } - >; + fieldFormatMap?: Record | undefined>; + getFormatterForField?: ( + field: IndexPatternField | IndexPatternField['spec'] | IFieldType + ) => FieldFormat; } -/** - * Use data plugin interface instead - * @deprecated - */ export interface IndexPatternAttributes { type: string; fields: string; @@ -166,15 +160,18 @@ export interface FieldSpec { indexed?: boolean; } +export type IndexPatternFieldMap = Record; + export interface IndexPatternSpec { id?: string; version?: string; - - title: string; + title?: string; + intervalName?: string; timeFieldName?: string; sourceFilters?: SourceFilter[]; - fields?: FieldSpec[]; + fields?: IndexPatternFieldMap; typeMeta?: TypeMeta; + type?: string; } export interface SourceFilter { diff --git a/src/plugins/data/common/search/aggs/buckets/histogram.ts b/src/plugins/data/common/search/aggs/buckets/histogram.ts index 4b631e1fd7cd7..c3d3f041dd0c7 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram.ts @@ -158,6 +158,7 @@ export const getHistogramBucketAgg = ({ maxBucketsUiSettings: getConfig(UI_SETTINGS.HISTOGRAM_MAX_BARS), maxBucketsUserInput: aggConfig.params.maxBars, intervalBase: aggConfig.params.intervalBase, + esTypes: aggConfig.params.field?.spec?.esTypes || [], }); }, }, diff --git a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts index d3a95b32cd425..7e5e20e5917aa 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts @@ -21,6 +21,7 @@ import { calculateHistogramInterval, CalculateHistogramIntervalParams, } from './histogram_calculate_interval'; +import { ES_FIELD_TYPES } from '../../../../types'; describe('calculateHistogramInterval', () => { describe('auto calculating mode', () => { @@ -36,10 +37,91 @@ describe('calculateHistogramInterval', () => { min: 0, max: 1, }, + esTypes: [], }; }); describe('maxBucketsUserInput is defined', () => { + test('should set 1 as an interval for integer numbers that are less than maxBuckets #1', () => { + const p = { + ...params, + maxBucketsUserInput: 100, + values: { + min: 1, + max: 50, + }, + esTypes: [ES_FIELD_TYPES.INTEGER], + }; + expect(calculateHistogramInterval(p)).toEqual(1); + }); + + test('should set 1 as an interval for integer numbers that are less than maxBuckets #2', () => { + const p = { + ...params, + maxBucketsUiSettings: 1000, + maxBucketsUserInput: 258, + values: { + min: 521, + max: 689, + }, + esTypes: [ES_FIELD_TYPES.INTEGER], + }; + expect(calculateHistogramInterval(p)).toEqual(1); + }); + + test('should set correct interval for integer numbers that are greater than maxBuckets #1', () => { + const p = { + ...params, + maxBucketsUserInput: 100, + values: { + min: 400, + max: 790, + }, + esTypes: [ES_FIELD_TYPES.INTEGER, ES_FIELD_TYPES.SHORT], + }; + expect(calculateHistogramInterval(p)).toEqual(5); + }); + + test('should set correct interval for integer numbers that are greater than maxBuckets #2', () => { + // diff === 3456211; interval === 50000; buckets === 69 + const p = { + ...params, + maxBucketsUserInput: 100, + values: { + min: 567, + max: 3456778, + }, + esTypes: [ES_FIELD_TYPES.LONG], + }; + expect(calculateHistogramInterval(p)).toEqual(50000); + }); + + test('should not set integer interval if the field type is float #1', () => { + const p = { + ...params, + maxBucketsUserInput: 100, + values: { + min: 0, + max: 1, + }, + esTypes: [ES_FIELD_TYPES.FLOAT], + }; + expect(calculateHistogramInterval(p)).toEqual(0.01); + }); + + test('should not set integer interval if the field type is float #2', () => { + const p = { + ...params, + maxBucketsUserInput: 100, + values: { + min: 0, + max: 1, + }, + esTypes: [ES_FIELD_TYPES.INTEGER, ES_FIELD_TYPES.FLOAT], + }; + expect(calculateHistogramInterval(p)).toEqual(0.01); + }); + test('should not set interval which more than largest possible', () => { const p = { ...params, @@ -48,6 +130,7 @@ describe('calculateHistogramInterval', () => { min: 150, max: 250, }, + esTypes: [ES_FIELD_TYPES.SHORT], }; expect(calculateHistogramInterval(p)).toEqual(1); }); @@ -61,6 +144,7 @@ describe('calculateHistogramInterval', () => { min: 0.1, max: 0.9, }, + esTypes: [ES_FIELD_TYPES.FLOAT], }) ).toBe(0.02); }); @@ -74,6 +158,7 @@ describe('calculateHistogramInterval', () => { min: 10.45, max: 1000.05, }, + esTypes: [ES_FIELD_TYPES.FLOAT], }) ).toBe(100); }); @@ -88,6 +173,7 @@ describe('calculateHistogramInterval', () => { min: 0, max: 100, }, + esTypes: [ES_FIELD_TYPES.BYTE], }) ).toEqual(1); }); @@ -100,8 +186,9 @@ describe('calculateHistogramInterval', () => { min: 1, max: 10, }, + esTypes: [ES_FIELD_TYPES.INTEGER], }) - ).toEqual(0.1); + ).toEqual(1); }); test('should set intervals for integer numbers (diff more than maxBucketsUiSettings)', () => { @@ -113,6 +200,7 @@ describe('calculateHistogramInterval', () => { min: 45678, max: 90123, }, + esTypes: [ES_FIELD_TYPES.INTEGER], }) ).toEqual(500); }); @@ -127,6 +215,7 @@ describe('calculateHistogramInterval', () => { min: 1.245, max: 2.9, }, + esTypes: [ES_FIELD_TYPES.FLOAT], }) ).toEqual(0.02); expect( @@ -136,6 +225,7 @@ describe('calculateHistogramInterval', () => { min: 0.5, max: 2.3, }, + esTypes: [ES_FIELD_TYPES.FLOAT], }) ).toEqual(0.02); }); @@ -149,6 +239,7 @@ describe('calculateHistogramInterval', () => { min: 0.1, max: 0.9, }, + esTypes: [ES_FIELD_TYPES.FLOAT], }) ).toBe(0.01); }); diff --git a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts index 378340e876296..313ecf1000f41 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts @@ -18,6 +18,7 @@ */ import { isAutoInterval } from '../_interval_options'; +import { ES_FIELD_TYPES } from '../../../../types'; interface IntervalValuesRange { min: number; @@ -28,6 +29,7 @@ export interface CalculateHistogramIntervalParams { interval: string; maxBucketsUiSettings: number; maxBucketsUserInput?: number | ''; + esTypes: ES_FIELD_TYPES[]; intervalBase?: number; values?: IntervalValuesRange; } @@ -77,11 +79,27 @@ const calculateForGivenInterval = ( - The lower power of 10, times 2 - The lower power of 10, times 5 **/ -const calculateAutoInterval = (diff: number, maxBars: number) => { +const calculateAutoInterval = (diff: number, maxBars: number, esTypes: ES_FIELD_TYPES[]) => { const exactInterval = diff / maxBars; - const lowerPower = Math.pow(10, Math.floor(Math.log10(exactInterval))); + // For integer fields that are less than maxBars, we should use 1 as the value of interval + // Elastic has 4 integer data types: long, integer, short, byte + // see: https://www.elastic.co/guide/en/elasticsearch/reference/current/number.html + if ( + diff < maxBars && + esTypes.every((esType) => + [ + ES_FIELD_TYPES.INTEGER, + ES_FIELD_TYPES.LONG, + ES_FIELD_TYPES.SHORT, + ES_FIELD_TYPES.BYTE, + ].includes(esType) + ) + ) { + return 1; + } + const lowerPower = Math.pow(10, Math.floor(Math.log10(exactInterval))); const autoBuckets = diff / lowerPower; if (autoBuckets > maxBars) { @@ -103,6 +121,7 @@ export const calculateHistogramInterval = ({ maxBucketsUserInput, intervalBase, values, + esTypes, }: CalculateHistogramIntervalParams) => { const isAuto = isAutoInterval(interval); let calculatedInterval = isAuto ? 0 : parseFloat(interval); @@ -119,8 +138,10 @@ export const calculateHistogramInterval = ({ calculatedInterval = isAuto ? calculateAutoInterval( diff, + // Mind maxBucketsUserInput can be an empty string, hence we need to ensure it here - Math.min(maxBucketsUiSettings, maxBucketsUserInput || maxBucketsUiSettings) + Math.min(maxBucketsUiSettings, maxBucketsUserInput || maxBucketsUiSettings), + esTypes ) : calculateForGivenInterval(diff, calculatedInterval, maxBucketsUiSettings); } diff --git a/src/plugins/data/common/search/es_search/index.ts b/src/plugins/data/common/search/es_search/index.ts index d8f7b5091eb8f..8e8897c7d7517 100644 --- a/src/plugins/data/common/search/es_search/index.ts +++ b/src/plugins/data/common/search/es_search/index.ts @@ -18,3 +18,4 @@ */ export * from './types'; +export * from './utils'; diff --git a/src/legacy/ui/ui_exports/ui_export_types/reduce/flat_concat_values_at_type.js b/src/plugins/data/common/search/es_search/utils.ts similarity index 54% rename from src/legacy/ui/ui_exports/ui_export_types/reduce/flat_concat_values_at_type.js rename to src/plugins/data/common/search/es_search/utils.ts index 229c5be24aac5..517a0c03cf5c8 100644 --- a/src/legacy/ui/ui_exports/ui_export_types/reduce/flat_concat_values_at_type.js +++ b/src/plugins/data/common/search/es_search/utils.ts @@ -17,14 +17,25 @@ * under the License. */ -import { createTypeReducer, flatConcat, mergeWith } from './lib'; +import { IEsSearchResponse } from './types'; /** - * Reducer that merges specs by concatenating the values of - * all keys in accumulator and spec with the same logic as concat - * @param {[type]} initial [description] - * @return {[type]} [description] + * @returns true if response had an error while executing in ES */ -export const flatConcatValuesAtType = createTypeReducer((objectA, objectB) => - mergeWith(objectA || {}, objectB || {}, flatConcat) -); +export const isErrorResponse = (response?: IEsSearchResponse) => { + return !response || (!response.isRunning && response.isPartial); +}; + +/** + * @returns true if response is completed successfully + */ +export const isCompleteResponse = (response?: IEsSearchResponse) => { + return response && !response.isRunning && !response.isPartial; +}; + +/** + * @returns true if request is still running an/d response contains partial results + */ +export const isPartialResponse = (response?: IEsSearchResponse) => { + return response && response.isRunning && response.isPartial; +}; diff --git a/src/plugins/data/common/search/index.ts b/src/plugins/data/common/search/index.ts index 2ee0db384cf06..2ec4afbc60d96 100644 --- a/src/plugins/data/common/search/index.ts +++ b/src/plugins/data/common/search/index.ts @@ -23,3 +23,4 @@ export * from './expressions'; export * from './search_source'; export * from './tabify'; export * from './types'; +export * from './es_search'; diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 5038af9409316..57865f05871a1 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -230,6 +230,8 @@ import { formatHitProvider, } from './index_patterns'; +export type { IndexPatternsService } from './index_patterns'; + // Index patterns namespace: export const indexPatterns = { ILLEGAL_CHARACTERS_KEY, @@ -262,9 +264,12 @@ export { UI_SETTINGS, TypeMeta as IndexPatternTypeMeta, AggregationRestrictions as IndexPatternAggRestrictions, + IndexPatternSpec, fieldList, } from '../common'; +export { DuplicateIndexPatternError } from '../common/index_patterns/errors'; + /* * Autocomplete query suggestions: */ @@ -374,7 +379,7 @@ export { export type { SearchSource } from './search'; -export { ISearchOptions } from '../common'; +export { ISearchOptions, isErrorResponse, isCompleteResponse, isPartialResponse } from '../common'; // Search namespace export const search = { diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index db8d9dba4e0c7..5919c1e294b2f 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -454,6 +454,13 @@ export interface DataPublicPluginStartUi { SearchBar: React.ComponentType; } +// Warning: (ae-missing-release-tag) "DuplicateIndexPatternError" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class DuplicateIndexPatternError extends Error { + constructor(message: string); +} + // @public (undocumented) export enum ES_FIELD_TYPES { // (undocumented) @@ -1004,16 +1011,15 @@ export interface IFieldType { // // @public (undocumented) export interface IIndexPattern { + // Warning: (ae-forgotten-export) The symbol "SerializedFieldFormat" needs to be exported by the entry point index.d.ts + // // (undocumented) - [key: string]: any; - // (undocumented) - fieldFormatMap?: Record; + fieldFormatMap?: Record | undefined>; // (undocumented) fields: IFieldType[]; // (undocumented) + getFormatterForField?: (field: IndexPatternField | IndexPatternField['spec'] | IFieldType) => FieldFormat; + // (undocumented) getTimeField?(): IFieldType | undefined; // (undocumented) id?: string; @@ -1043,10 +1049,12 @@ export interface IIndexPatternFieldList extends Array { removeAll(): void; // (undocumented) replaceAll(specs: FieldSpec[]): void; + // Warning: (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts + // // (undocumented) toSpec(options?: { getFormatterForField?: IndexPattern['getFormatterForField']; - }): FieldSpec[]; + }): IndexPatternFieldMap; // (undocumented) update(field: FieldSpec): void; } @@ -1079,27 +1087,23 @@ export type IMetricAggType = MetricAggType; // @public (undocumented) export class IndexPattern implements IIndexPattern { // Warning: (ae-forgotten-export) The symbol "IndexPatternDeps" needs to be exported by the entry point index.d.ts - constructor(id: string | undefined, { savedObjectsClient, apiClient, patternCache, fieldFormats, indexPatternsService, onNotification, onError, shortDotsEnable, metaFields, }: IndexPatternDeps); - // (undocumented) - addScriptedField(name: string, script: string, fieldType: string | undefined, lang: string): Promise; - // (undocumented) - create(allowOverride?: boolean): Promise; - // (undocumented) - _fetchFields(): Promise; + constructor({ spec, savedObjectsClient, fieldFormats, shortDotsEnable, metaFields, }: IndexPatternDeps); + addScriptedField(name: string, script: string, fieldType?: string, lang?: string): Promise; // (undocumented) - fieldFormatMap: any; + fieldFormatMap: Record; // (undocumented) fields: IIndexPatternFieldList & { - toSpec: () => FieldSpec[]; + toSpec: () => IndexPatternFieldMap; }; // (undocumented) - fieldsFetcher: any; + flattenHit: (hit: Record, deep?: boolean) => Record; // (undocumented) - flattenHit: any; + formatField: FormatFieldFn; // (undocumented) - formatField: any; - // (undocumented) - formatHit: any; + formatHit: { + (hit: Record, type?: string): any; + formatField: FormatFieldFn; + }; // (undocumented) getAggregationRestrictions(): Record> | undefined; + getAsSavedObjectBody(): { + title: string; + timeFieldName: string | undefined; + intervalName: string | undefined; + sourceFilters: string | undefined; + fields: string | undefined; + fieldFormatMap: string | undefined; + type: string | undefined; + typeMeta: string | undefined; + }; // (undocumented) getComputedFields(): { storedFields: string[]; @@ -1120,13 +1134,21 @@ export class IndexPattern implements IIndexPattern { }; // (undocumented) getFieldByName(name: string): IndexPatternField | undefined; - // (undocumented) - getFormatterForField(field: IndexPatternField | IndexPatternField['spec']): FieldFormat; + getFormatterForField(field: IndexPatternField | IndexPatternField['spec'] | IFieldType): FieldFormat; // (undocumented) getNonScriptedFields(): IndexPatternField[]; + getOriginalSavedObjectBody: () => { + title?: string | undefined; + timeFieldName?: string | undefined; + intervalName?: string | undefined; + fields?: string | undefined; + sourceFilters?: string | undefined; + fieldFormatMap?: string | undefined; + typeMeta?: string | undefined; + type?: string | undefined; + }; // (undocumented) getScriptedFields(): IndexPatternField[]; - // (undocumented) getSourceFiltering(): { excludes: any[]; }; @@ -1135,12 +1157,6 @@ export class IndexPattern implements IIndexPattern { // (undocumented) id?: string; // (undocumented) - init(): Promise; - // Warning: (ae-forgotten-export) The symbol "IndexPatternSpec" needs to be exported by the entry point index.d.ts - // - // (undocumented) - initFromSpec(spec: IndexPatternSpec): this; - // (undocumented) intervalName: string | undefined; // (undocumented) isTimeBased(): boolean; @@ -1149,30 +1165,11 @@ export class IndexPattern implements IIndexPattern { // (undocumented) isTimeNanosBased(): boolean; // (undocumented) - isWildcard(): boolean; - // (undocumented) metaFields: string[]; // (undocumented) - originalBody: { - [key: string]: any; - }; - // (undocumented) popularizeField(fieldName: string, unit?: number): Promise; - // (undocumented) - prepBody(): { - title: string; - timeFieldName: string | undefined; - intervalName: string | undefined; - sourceFilters: string | undefined; - fields: string | undefined; - fieldFormatMap: string | undefined; - type: string | undefined; - typeMeta: string | undefined; - }; - // (undocumented) - refreshFields(): Promise; - // (undocumented) removeScriptedField(fieldName: string): void; + resetOriginalSavedObjectBody: () => void; // Warning: (ae-forgotten-export) The symbol "SourceFilter" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1205,7 +1202,7 @@ export type IndexPatternAggRestrictions = Record | undefined; set conflictDescriptions(conflictDescriptions: Record | undefined); - // (undocumented) get count(): number; set count(count: number); // (undocumented) @@ -1244,14 +1239,12 @@ export class IndexPatternField implements IFieldType { get esTypes(): string[] | undefined; // (undocumented) get filterable(): boolean; - // (undocumented) get lang(): string | undefined; set lang(lang: string | undefined); // (undocumented) get name(): string; // (undocumented) get readFromDocValues(): boolean; - // (undocumented) get script(): string | undefined; set script(script: string | undefined); // (undocumented) @@ -1282,24 +1275,7 @@ export class IndexPatternField implements IFieldType { // (undocumented) toSpec({ getFormatterForField, }?: { getFormatterForField?: IndexPattern['getFormatterForField']; - }): { - count: number; - script: string | undefined; - lang: string | undefined; - conflictDescriptions: Record | undefined; - name: string; - type: string; - esTypes: string[] | undefined; - scripted: boolean; - searchable: boolean; - aggregatable: boolean; - readFromDocValues: boolean; - subType: import("../types").IFieldSubType | undefined; - format: { - id: any; - params: any; - } | undefined; - }; + }): FieldSpec; // (undocumented) get type(): string; // (undocumented) @@ -1323,7 +1299,6 @@ export const indexPatterns: { formatHitProvider: typeof formatHitProvider; }; -// Warning: (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "IndexPatternsContract" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1356,6 +1331,63 @@ export class IndexPatternSelect extends Component { UNSAFE_componentWillReceiveProps(nextProps: IndexPatternSelectProps): void; } +// Warning: (ae-missing-release-tag) "IndexPatternSpec" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface IndexPatternSpec { + // (undocumented) + fields?: IndexPatternFieldMap; + // (undocumented) + id?: string; + // (undocumented) + intervalName?: string; + // (undocumented) + sourceFilters?: SourceFilter[]; + // (undocumented) + timeFieldName?: string; + // (undocumented) + title?: string; + // (undocumented) + type?: string; + // (undocumented) + typeMeta?: IndexPatternTypeMeta; + // (undocumented) + version?: string; +} + +// Warning: (ae-missing-release-tag) "IndexPatternsService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class IndexPatternsService { + // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceDeps" needs to be exported by the entry point index.d.ts + constructor({ uiSettings, savedObjectsClient, apiClient, fieldFormats, onNotification, onError, onRedirectNoIndexPattern, }: IndexPatternsServiceDeps); + clearCache: (id?: string | undefined) => void; + create(spec: IndexPatternSpec, skipFetchFields?: boolean): Promise; + createAndSave(spec: IndexPatternSpec, override?: boolean, skipFetchFields?: boolean): Promise; + createSavedObject(indexPattern: IndexPattern, override?: boolean): Promise; + delete(indexPatternId: string): Promise<{}>; + // Warning: (ae-forgotten-export) The symbol "EnsureDefaultIndexPattern" needs to be exported by the entry point index.d.ts + // + // (undocumented) + ensureDefaultIndexPattern: EnsureDefaultIndexPattern; + fieldArrayToMap: (fields: FieldSpec[]) => Record; + get: (id: string) => Promise; + // Warning: (ae-forgotten-export) The symbol "IndexPatternSavedObjectAttrs" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getCache: () => Promise[] | null | undefined>; + getDefault: () => Promise; + getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions) => Promise; + // Warning: (ae-forgotten-export) The symbol "GetFieldsOptions" needs to be exported by the entry point index.d.ts + getFieldsForWildcard: (options?: GetFieldsOptions) => Promise; + getIds: (refresh?: boolean) => Promise; + getTitles: (refresh?: boolean) => Promise; + refreshFields: (indexPattern: IndexPattern) => Promise; + savedObjectToSpec: (savedObject: SavedObject) => IndexPatternSpec; + setDefault: (id: string, force?: boolean) => Promise; + updateSavedObject(indexPattern: IndexPattern, saveAttempts?: number): Promise; +} + // Warning: (ae-missing-release-tag) "TypeMeta" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1381,6 +1413,11 @@ export type InputTimeRange = TimeRange | { to: Moment; }; +// Warning: (ae-missing-release-tag) "isCompleteResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const isCompleteResponse: (response?: IEsSearchResponse | undefined) => boolean | undefined; + // Warning: (ae-missing-release-tag) "ISearch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1433,6 +1470,11 @@ export interface ISearchStartSearchSource { createEmpty: () => ISearchSource; } +// Warning: (ae-missing-release-tag) "isErrorResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const isErrorResponse: (response?: IEsSearchResponse | undefined) => boolean | undefined; + // Warning: (ae-missing-release-tag) "isFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1443,6 +1485,11 @@ export const isFilter: (x: unknown) => x is Filter; // @public (undocumented) export const isFilters: (x: unknown) => x is Filter[]; +// Warning: (ae-missing-release-tag) "isPartialResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const isPartialResponse: (response?: IEsSearchResponse | undefined) => boolean | undefined; + // Warning: (ae-missing-release-tag) "isQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2189,6 +2236,7 @@ export const UI_SETTINGS: { // src/plugins/data/common/es_query/filters/meta_filter.ts:54:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrase_filter.ts:33:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrases_filter.ts:31:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:70:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts // src/plugins/data/common/search/aggs/types.ts:98:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts // src/plugins/data/public/field_formats/field_formats_service.ts:67:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FilterLabel" needs to be exported by the entry point index.d.ts @@ -2217,27 +2265,27 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "UrlFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "StringFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:178:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:380:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:380:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:380:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:380:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:382:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:383:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:392:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:393:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:394:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:395:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:403:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:404:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:236:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:385:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:385:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:385:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:385:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:387:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:388:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:397:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:398:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:404:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:405:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:408:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:409:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:412: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 // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 03baff4910309..43080cc5a5989 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -133,16 +133,17 @@ export { IndexPatternsFetcher, FieldDescriptor as IndexPatternFieldDescriptor, shouldReadFieldFromDocValues, // used only in logstash_fields fixture + FieldDescriptor, } from './index_patterns'; export { - IIndexPattern, IFieldType, IFieldSubType, ES_FIELD_TYPES, KBN_FIELD_TYPES, IndexPatternAttributes, UI_SETTINGS, + IndexPattern, } from '../common'; /** @@ -289,3 +290,5 @@ export const config: PluginConfigDescriptor = { }, schema: configSchema, }; + +export type { IndexPatternsService } from './index_patterns'; diff --git a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts index ff9d67152e268..57c636a9e3c69 100644 --- a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts +++ b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts @@ -55,9 +55,10 @@ export class IndexPatternsFetcher { async getFieldsForWildcard(options: { pattern: string | string[]; metaFields?: string[]; + fieldCapsOptions?: { allowNoIndices: boolean }; }): Promise { - const { pattern, metaFields } = options; - return await getFieldCapabilities(this._callDataCluster, pattern, metaFields); + const { pattern, metaFields, fieldCapsOptions } = options; + return await getFieldCapabilities(this._callDataCluster, pattern, metaFields, fieldCapsOptions); } /** diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/es_api.ts b/src/plugins/data/server/index_patterns/fetcher/lib/es_api.ts index 0738a16034d46..27ce14f9a3597 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/es_api.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/es_api.ts @@ -69,15 +69,20 @@ export async function callIndexAliasApi( * * @param {Function} callCluster bound function for accessing an es client * @param {Array|String} indices + * @param {Object} fieldCapsOptions * @return {Promise} */ -export async function callFieldCapsApi(callCluster: LegacyAPICaller, indices: string[] | string) { +export async function callFieldCapsApi( + callCluster: LegacyAPICaller, + indices: string[] | string, + fieldCapsOptions: { allowNoIndices: boolean } = { allowNoIndices: false } +) { try { return (await callCluster('fieldCaps', { index: indices, fields: '*', ignoreUnavailable: true, - allowNoIndices: false, + ...fieldCapsOptions, })) as FieldCapsResponse; } catch (error) { throw convertEsError(indices, error); diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.test.js b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.test.js index a0af7582ac6f3..0e5757b7b782b 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.test.js +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.test.js @@ -61,7 +61,7 @@ describe('index_patterns/field_capabilities/field_capabilities', () => { await getFieldCapabilities(footballs[0], footballs[1]); sinon.assert.calledOnce(callFieldCapsApi); - calledWithExactly(callFieldCapsApi, [footballs[0], footballs[1]]); + calledWithExactly(callFieldCapsApi, [footballs[0], footballs[1], undefined]); }); }); diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts index 6b26c82dc95e7..62e77e0adad66 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts @@ -32,14 +32,20 @@ import { FieldDescriptor } from '../../index_patterns_fetcher'; * @param {Function} callCluster bound function for accessing an es client * @param {Array} [indices=[]] the list of indexes to check * @param {Array} [metaFields=[]] the list of internal fields to include + * @param {Object} fieldCapsOptions * @return {Promise>} */ export async function getFieldCapabilities( callCluster: LegacyAPICaller, indices: string | string[] = [], - metaFields: string[] = [] + metaFields: string[] = [], + fieldCapsOptions?: { allowNoIndices: boolean } ) { - const esFieldCaps: FieldCapsResponse = await callFieldCapsApi(callCluster, indices); + const esFieldCaps: FieldCapsResponse = await callFieldCapsApi( + callCluster, + indices, + fieldCapsOptions + ); const fieldsFromFieldCapsByName = keyBy(readFieldCapsResponse(esFieldCaps), 'name'); const allFieldsUnsorted = Object.keys(fieldsFromFieldCapsByName) diff --git a/src/plugins/data/server/index_patterns/utils.ts b/src/plugins/data/server/index_patterns/utils.ts index e841097fe49c2..1e7a85599612c 100644 --- a/src/plugins/data/server/index_patterns/utils.ts +++ b/src/plugins/data/server/index_patterns/utils.ts @@ -18,11 +18,11 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; -import { IIndexPattern, IFieldType } from '../../common'; +import { IFieldType, IndexPatternAttributes, SavedObject } from '../../common'; export const getFieldByName = ( fieldName: string, - indexPattern: IIndexPattern + indexPattern: SavedObject ): IFieldType | undefined => { const fields: IFieldType[] = indexPattern && JSON.parse(indexPattern.attributes.fields); const field = fields && fields.find((f) => f.name === fieldName); @@ -33,8 +33,8 @@ export const getFieldByName = ( export const findIndexPatternById = async ( savedObjectsClient: SavedObjectsClientContract, index: string -): Promise => { - const savedObjectsResponse = await savedObjectsClient.find({ +): Promise | undefined> => { + const savedObjectsResponse = await savedObjectsClient.find({ type: 'index-pattern', fields: ['fields'], search: `"${index}"`, @@ -42,6 +42,6 @@ export const findIndexPatternById = async ( }); if (savedObjectsResponse.total > 0) { - return (savedObjectsResponse.saved_objects[0] as unknown) as IIndexPattern; + return savedObjectsResponse.saved_objects[0]; } }; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index c48aa8397dc83..6d4112543ce0e 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -36,6 +36,7 @@ import { ConfigDeprecationProvider } from '@kbn/config'; import { CoreSetup } from 'src/core/server'; import { CoreSetup as CoreSetup_2 } from 'kibana/server'; import { CoreStart } from 'src/core/server'; +import { CoreStart as CoreStart_2 } from 'kibana/server'; import { CountParams } from 'elasticsearch'; import { CreateDocumentParams } from 'elasticsearch'; import { DeleteDocumentByQueryParams } from 'elasticsearch'; @@ -126,6 +127,7 @@ import { PackageInfo } from '@kbn/config'; import { PathConfigType } from '@kbn/utils'; import { PingParams } from 'elasticsearch'; import { Plugin as Plugin_2 } from 'src/core/server'; +import { Plugin as Plugin_3 } from 'kibana/server'; import { PluginInitializerContext as PluginInitializerContext_2 } from 'src/core/server'; import { PutScriptParams } from 'elasticsearch'; import { PutTemplateParams } from 'elasticsearch'; @@ -396,6 +398,32 @@ export interface EsQueryConfig { queryStringOptions: Record; } +// Warning: (ae-missing-release-tag) "FieldDescriptor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +interface FieldDescriptor { + // (undocumented) + aggregatable: boolean; + // (undocumented) + esTypes: string[]; + // (undocumented) + name: string; + // (undocumented) + readFromDocValues: boolean; + // (undocumented) + searchable: boolean; + // Warning: (ae-forgotten-export) The symbol "FieldSubType" needs to be exported by the entry point index.d.ts + // + // (undocumented) + subType?: FieldSubType; + // (undocumented) + type: string; +} + +export { FieldDescriptor } + +export { FieldDescriptor as IndexPatternFieldDescriptor } + // Warning: (ae-missing-release-tag) "FieldFormatConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -467,6 +495,7 @@ export function getShardTimeout(config: SharedGlobalConfig): { timeout?: undefined; }; +// Warning: (ae-forgotten-export) The symbol "IIndexPattern" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "getTime" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -590,40 +619,129 @@ export interface IFieldType { visualizable?: boolean; } -// Warning: (ae-missing-release-tag) "IIndexPattern" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-forgotten-export) The symbol "MetricAggType" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "IMetricAggType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface IIndexPattern { +export type IMetricAggType = MetricAggType; + +// Warning: (ae-missing-release-tag) "IndexPattern" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class IndexPattern implements IIndexPattern { + // Warning: (ae-forgotten-export) The symbol "IndexPatternDeps" needs to be exported by the entry point index.d.ts + constructor({ spec, savedObjectsClient, fieldFormats, shortDotsEnable, metaFields, }: IndexPatternDeps); + addScriptedField(name: string, script: string, fieldType?: string, lang?: string): Promise; // (undocumented) - [key: string]: any; + fieldFormatMap: Record; + // Warning: (ae-forgotten-export) The symbol "IIndexPatternFieldList" needs to be exported by the entry point index.d.ts + // + // (undocumented) + fields: IIndexPatternFieldList & { + toSpec: () => IndexPatternFieldMap; + }; + // (undocumented) + flattenHit: (hit: Record, deep?: boolean) => Record; + // (undocumented) + formatField: FormatFieldFn; + // (undocumented) + formatHit: { + (hit: Record, type?: string): any; + formatField: FormatFieldFn; + }; // (undocumented) - fieldFormatMap?: Record; + getAggregationRestrictions(): Record> | undefined; + getAsSavedObjectBody(): { + title: string; + timeFieldName: string | undefined; + intervalName: string | undefined; + sourceFilters: string | undefined; + fields: string | undefined; + fieldFormatMap: string | undefined; + type: string | undefined; + typeMeta: string | undefined; + }; + // (undocumented) + getComputedFields(): { + storedFields: string[]; + scriptFields: any; + docvalueFields: { + field: any; + format: string; + }[]; + }; + // (undocumented) + getFieldByName(name: string): IndexPatternField | undefined; + getFormatterForField(field: IndexPatternField | IndexPatternField['spec'] | IFieldType): FieldFormat; + // Warning: (ae-forgotten-export) The symbol "IndexPatternField" needs to be exported by the entry point index.d.ts + // // (undocumented) - fields: IFieldType[]; + getNonScriptedFields(): IndexPatternField[]; + getOriginalSavedObjectBody: () => { + title?: string | undefined; + timeFieldName?: string | undefined; + intervalName?: string | undefined; + fields?: string | undefined; + sourceFilters?: string | undefined; + fieldFormatMap?: string | undefined; + typeMeta?: string | undefined; + type?: string | undefined; + }; + // (undocumented) + getScriptedFields(): IndexPatternField[]; + getSourceFiltering(): { + excludes: any[]; + }; // (undocumented) - getTimeField?(): IFieldType | undefined; + getTimeField(): IndexPatternField | undefined; // (undocumented) id?: string; // (undocumented) - timeFieldName?: string; + intervalName: string | undefined; + // (undocumented) + isTimeBased(): boolean; + // (undocumented) + isTimeBasedWildcard(): boolean; + // (undocumented) + isTimeNanosBased(): boolean; + // (undocumented) + metaFields: string[]; + // (undocumented) + popularizeField(fieldName: string, unit?: number): Promise; + removeScriptedField(fieldName: string): void; + resetOriginalSavedObjectBody: () => void; + // Warning: (ae-forgotten-export) The symbol "SourceFilter" needs to be exported by the entry point index.d.ts + // + // (undocumented) + sourceFilters?: SourceFilter[]; + // (undocumented) + timeFieldName: string | undefined; // (undocumented) title: string; + // Warning: (ae-forgotten-export) The symbol "IndexPatternSpec" needs to be exported by the entry point index.d.ts + // // (undocumented) - type?: string; + toSpec(): IndexPatternSpec; + // (undocumented) + type: string | undefined; + // Warning: (ae-forgotten-export) The symbol "TypeMeta" needs to be exported by the entry point index.d.ts + // + // (undocumented) + typeMeta?: TypeMeta; + // (undocumented) + version: string | undefined; } -// Warning: (ae-forgotten-export) The symbol "MetricAggType" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "IMetricAggType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type IMetricAggType = MetricAggType; - // Warning: (ae-missing-release-tag) "IndexPatternAttributes" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public @deprecated +// @public (undocumented) export interface IndexPatternAttributes { // (undocumented) fieldFormatMap?: string; @@ -643,28 +761,6 @@ export interface IndexPatternAttributes { typeMeta: string; } -// Warning: (ae-missing-release-tag) "FieldDescriptor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface IndexPatternFieldDescriptor { - // (undocumented) - aggregatable: boolean; - // (undocumented) - esTypes: string[]; - // (undocumented) - name: string; - // (undocumented) - readFromDocValues: boolean; - // (undocumented) - searchable: boolean; - // Warning: (ae-forgotten-export) The symbol "FieldSubType" needs to be exported by the entry point index.d.ts - // - // (undocumented) - subType?: FieldSubType; - // (undocumented) - type: string; -} - // Warning: (ae-missing-release-tag) "indexPatterns" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -683,11 +779,29 @@ export class IndexPatternsFetcher { metaFields: string[]; lookBack: number; interval: string; - }): Promise; + }): Promise; getFieldsForWildcard(options: { pattern: string | string[]; metaFields?: string[]; - }): Promise; + fieldCapsOptions?: { + allowNoIndices: boolean; + }; + }): Promise; +} + +// Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceStart" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "IndexPatternsService" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class IndexPatternsService implements Plugin_3 { + // (undocumented) + setup(core: CoreSetup_2): void; + // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceStartDeps" needs to be exported by the entry point index.d.ts + // + // (undocumented) + start(core: CoreStart_2, { fieldFormats, logger }: IndexPatternsServiceStartDeps): { + indexPatternsServiceFactory: (kibanaRequest: KibanaRequest) => Promise; + }; } // Warning: (ae-missing-release-tag) "ISearchOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -889,7 +1003,7 @@ export class Plugin implements Plugin_2 Promise; }; indexPatterns: { - indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; + indexPatternsServiceFactory: (kibanaRequest: import("../../../core/server").KibanaRequest) => Promise; }; }; // (undocumented) @@ -923,8 +1037,6 @@ export interface PluginStart { // // (undocumented) fieldFormats: FieldFormatsStart; - // Warning: (ae-forgotten-export) The symbol "IndexPatternsServiceStart" needs to be exported by the entry point index.d.ts - // // (undocumented) indexPatterns: IndexPatternsServiceStart; // (undocumented) @@ -1110,7 +1222,8 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // // src/plugins/data/common/es_query/filters/meta_filter.ts:53:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/meta_filter.ts:54:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/index_patterns/fields/types.ts:41:25 - (ae-forgotten-export) The symbol "IndexPattern" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:64:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:70:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildCustomFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:40:23 - (ae-forgotten-export) The symbol "buildFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:71:21 - (ae-forgotten-export) The symbol "getEsQueryConfig" needs to be exported by the entry point index.d.ts @@ -1132,19 +1245,20 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:127:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:127:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:225:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:225:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:225:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:225:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:227:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:228:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:237:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:238:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:239:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:243:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:244:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:248:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:251:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:226:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:226:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:226:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:226:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:228:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:229:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:238:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:239:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:240:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:244:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:245:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:249:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:252:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index_patterns/index_patterns_service.ts:50:14 - (ae-forgotten-export) The symbol "IndexPatternsService" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:88:66 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/discover/public/application/angular/get_painless_error.ts b/src/plugins/discover/public/application/angular/get_painless_error.ts index e1e98d9df27b1..162dacd3ac3b7 100644 --- a/src/plugins/discover/public/application/angular/get_painless_error.ts +++ b/src/plugins/discover/public/application/angular/get_painless_error.ts @@ -18,20 +18,77 @@ */ import { i18n } from '@kbn/i18n'; -import { get } from 'lodash'; -export function getPainlessError(error: Error) { - const rootCause: Array<{ lang: string; script: string }> | undefined = get( - error, - 'body.attributes.error.root_cause' - ); - const message: string = get(error, 'body.message'); +interface FailedShards { + shard: number; + index: string; + node: string; + reason: { + type: string; + reason: string; + script_stack: string[]; + script: string; + lang: string; + position: { + offset: number; + start: number; + end: number; + }; + caused_by: { + type: string; + reason: string; + }; + }; +} + +interface EsError { + body: { + statusCode: number; + error: string; + message: string; + attributes?: { + error?: { + root_cause?: [ + { + lang: string; + script: string; + } + ]; + type: string; + reason: string; + caused_by: { + type: string; + reason: string; + phase: string; + grouped: boolean; + failed_shards: FailedShards[]; + }; + }; + }; + }; +} + +export function getCause(error: EsError) { + const cause = error.body?.attributes?.error?.root_cause; + if (cause) { + return cause[0]; + } + + const failedShards = error.body?.attributes?.error?.caused_by?.failed_shards; + + if (failedShards && failedShards[0] && failedShards[0].reason) { + return error.body?.attributes?.error?.caused_by?.failed_shards[0].reason; + } +} + +export function getPainlessError(error: EsError) { + const cause = getCause(error); - if (!rootCause) { + if (!cause) { return; } - const [{ lang, script }] = rootCause; + const { lang, script } = cause; if (lang !== 'painless') { return; @@ -44,6 +101,6 @@ export function getPainlessError(error: Error) { defaultMessage: "Error with Painless scripted field '{script}'.", values: { script }, }), - error: message, + error: error.body?.message, }; } diff --git a/src/plugins/discover/public/application/components/discover_legacy.tsx b/src/plugins/discover/public/application/components/discover_legacy.tsx index 1a98843649259..9c3d833d73b23 100644 --- a/src/plugins/discover/public/application/components/discover_legacy.tsx +++ b/src/plugins/discover/public/application/components/discover_legacy.tsx @@ -25,7 +25,7 @@ import { IUiSettingsClient, MountPoint } from 'kibana/public'; import { HitsCounter } from './hits_counter'; import { TimechartHeader } from './timechart_header'; import { DiscoverSidebar } from './sidebar'; -import { getServices, IIndexPattern } from '../../kibana_services'; +import { getServices, IndexPattern } from '../../kibana_services'; // @ts-ignore import { DiscoverNoResults } from '../angular/directives/no_results'; import { DiscoverUninitialized } from '../angular/directives/uninitialized'; @@ -58,7 +58,7 @@ export interface DiscoverLegacyProps { fieldCounts: Record; histogramData: Chart; hits: number; - indexPattern: IIndexPattern; + indexPattern: IndexPattern; minimumVisibleRows: number; onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; onChangeInterval: (interval: string) => void; diff --git a/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts b/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts index 8746883a5d968..dad208c815675 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/field_calculator.test.ts @@ -142,7 +142,7 @@ describe('fieldCalculator', function () { let hits: any; beforeEach(function () { - hits = _.each(_.cloneDeep(realHits), indexPattern.flattenHit); + hits = _.each(_.cloneDeep(realHits), (hit) => indexPattern.flattenHit(hit)); }); it('Should return an array of values for _source fields', function () { diff --git a/src/plugins/discover/public/application/components/table/table.test.tsx b/src/plugins/discover/public/application/components/table/table.test.tsx index 07e9e0a129a26..2874e2483275b 100644 --- a/src/plugins/discover/public/application/components/table/table.test.tsx +++ b/src/plugins/discover/public/application/components/table/table.test.tsx @@ -22,7 +22,7 @@ import { findTestSubject } from '@elastic/eui/lib/test'; import { DocViewTable } from './table'; import { indexPatterns, IndexPattern } from '../../../../../data/public'; -const indexPattern = { +const indexPattern = ({ fields: { getAll: () => [ { @@ -60,7 +60,7 @@ const indexPattern = { metaFields: ['_index', '_score'], flattenHit: undefined, formatHit: jest.fn((hit) => hit._source), -} as IndexPattern; +} as unknown) as IndexPattern; indexPattern.fields.getByName = (name: string) => { return indexPattern.fields.getAll().find((field) => field.name === name); diff --git a/src/plugins/embeddable/public/bootstrap.ts b/src/plugins/embeddable/public/bootstrap.ts index 33cf210763b10..5c95214ef591b 100644 --- a/src/plugins/embeddable/public/bootstrap.ts +++ b/src/plugins/embeddable/public/bootstrap.ts @@ -19,7 +19,6 @@ import { UiActionsSetup } from '../../ui_actions/public'; import { contextMenuTrigger, - createFilterAction, panelBadgeTrigger, EmbeddableContext, CONTEXT_MENU_TRIGGER, @@ -29,8 +28,6 @@ import { ACTION_INSPECT_PANEL, REMOVE_PANEL_ACTION, ACTION_EDIT_PANEL, - FilterActionContext, - ACTION_APPLY_FILTER, panelNotificationTrigger, PANEL_NOTIFICATION_TRIGGER, } from './lib'; @@ -48,7 +45,6 @@ declare module '../../ui_actions/public' { [ACTION_INSPECT_PANEL]: EmbeddableContext; [REMOVE_PANEL_ACTION]: EmbeddableContext; [ACTION_EDIT_PANEL]: EmbeddableContext; - [ACTION_APPLY_FILTER]: FilterActionContext; } } @@ -60,8 +56,4 @@ export const bootstrap = (uiActions: UiActionsSetup) => { uiActions.registerTrigger(contextMenuTrigger); uiActions.registerTrigger(panelBadgeTrigger); uiActions.registerTrigger(panelNotificationTrigger); - - const actionApplyFilter = createFilterAction(); - - uiActions.registerAction(actionApplyFilter); }; diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index c5d8853ada5e8..7609f07d660bc 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -24,7 +24,6 @@ import { EmbeddablePublicPlugin } from './plugin'; export { ACTION_ADD_PANEL, - ACTION_APPLY_FILTER, ACTION_EDIT_PANEL, Adapters, AddPanelAction, diff --git a/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts b/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts deleted file mode 100644 index 88c1a5917e609..0000000000000 --- a/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts +++ /dev/null @@ -1,134 +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 { createFilterAction } from './apply_filter_action'; -import { expectErrorAsync } from '../../tests/helpers'; -import { defaultTrigger } from '../../../../ui_actions/public/triggers'; - -test('has ACTION_APPLY_FILTER type and id', () => { - const action = createFilterAction(); - expect(action.id).toBe('ACTION_APPLY_FILTER'); - expect(action.type).toBe('ACTION_APPLY_FILTER'); -}); - -test('has expected display name', () => { - const action = createFilterAction(); - expect(action.getDisplayName({} as any)).toMatchInlineSnapshot(`"Apply filter to current view"`); -}); - -describe('getIconType()', () => { - test('returns "filter" icon', async () => { - const action = createFilterAction(); - const result = action.getIconType({} as any); - expect(result).toBe('filter'); - }); -}); - -describe('isCompatible()', () => { - test('when embeddable filters and filters exist, returns true', async () => { - const action = createFilterAction(); - const result = await action.isCompatible({ - embeddable: { - getRoot: () => ({ - getInput: () => ({ - filters: [], - }), - }), - } as any, - filters: [], - trigger: defaultTrigger, - }); - expect(result).toBe(true); - }); - - test('when embeddable filters not set, returns false', async () => { - const action = createFilterAction(); - const result = await action.isCompatible({ - embeddable: { - getRoot: () => ({ - getInput: () => ({ - // filters: [], - }), - }), - } as any, - filters: [], - trigger: defaultTrigger, - }); - expect(result).toBe(false); - }); - - test('when triggerContext or filters are not set, returns false', async () => { - const action = createFilterAction(); - - const result1 = await action.isCompatible({ - embeddable: { - getRoot: () => ({ - getInput: () => ({ - filters: [], - }), - }), - } as any, - } as any); - expect(result1).toBe(false); - }); -}); - -const getEmbeddable = () => { - const root = { - getInput: jest.fn(() => ({ - filters: [], - })), - updateInput: jest.fn(), - }; - const embeddable = { - getRoot: () => root, - } as any; - return [embeddable, root]; -}; - -describe('execute()', () => { - describe('when no filters are given', () => { - test('throws an error', async () => { - const action = createFilterAction(); - const error = await expectErrorAsync(() => - action.execute({ - embeddable: getEmbeddable(), - } as any) - ); - expect(error).toBeInstanceOf(Error); - expect(error.message).toBe('Applying a filter requires a filter and embeddable as context'); - }); - - test('updates filter input on success', async () => { - const action = createFilterAction(); - const [embeddable, root] = getEmbeddable(); - - await action.execute({ - embeddable, - filters: ['FILTER' as any], - trigger: defaultTrigger, - }); - - expect(root.updateInput).toHaveBeenCalledTimes(1); - expect(root.updateInput.mock.calls[0][0]).toMatchObject({ - filters: ['FILTER'], - }); - }); - }); -}); diff --git a/src/plugins/embeddable/public/lib/actions/apply_filter_action.ts b/src/plugins/embeddable/public/lib/actions/apply_filter_action.ts deleted file mode 100644 index 3460203aac29c..0000000000000 --- a/src/plugins/embeddable/public/lib/actions/apply_filter_action.ts +++ /dev/null @@ -1,69 +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 { i18n } from '@kbn/i18n'; -import { ActionByType, createAction, IncompatibleActionError } from '../ui_actions'; -import { IEmbeddable, EmbeddableInput } from '../embeddables'; -import { Filter } from '../../../../../plugins/data/public'; - -export const ACTION_APPLY_FILTER = 'ACTION_APPLY_FILTER'; - -type RootEmbeddable = IEmbeddable; -export interface FilterActionContext { - embeddable: IEmbeddable; - filters: Filter[]; -} - -async function isCompatible(context: FilterActionContext) { - if (context.embeddable === undefined) { - return false; - } - const root = context.embeddable.getRoot() as RootEmbeddable; - return Boolean(root.getInput().filters !== undefined && context.filters !== undefined); -} - -export function createFilterAction(): ActionByType { - return createAction({ - type: ACTION_APPLY_FILTER, - id: ACTION_APPLY_FILTER, - order: 100, - getIconType: () => 'filter', - getDisplayName: () => { - return i18n.translate('embeddableApi.actions.applyFilterActionTitle', { - defaultMessage: 'Apply filter to current view', - }); - }, - isCompatible, - execute: async ({ embeddable, filters }) => { - if (!filters || !embeddable) { - throw new Error('Applying a filter requires a filter and embeddable as context'); - } - - if (!(await isCompatible({ embeddable, filters }))) { - throw new IncompatibleActionError(); - } - - const root = embeddable.getRoot() as RootEmbeddable; - - root.updateInput({ - filters, - }); - }, - }); -} diff --git a/src/plugins/embeddable/public/lib/actions/index.ts b/src/plugins/embeddable/public/lib/actions/index.ts index ea32c6aa2d455..8be2c3f5df450 100644 --- a/src/plugins/embeddable/public/lib/actions/index.ts +++ b/src/plugins/embeddable/public/lib/actions/index.ts @@ -17,5 +17,4 @@ * under the License. */ -export * from './apply_filter_action'; export * from './edit_panel_action'; diff --git a/src/plugins/embeddable/public/tests/apply_filter_action.test.ts b/src/plugins/embeddable/public/tests/apply_filter_action.test.ts deleted file mode 100644 index f8c4a4a7e4b72..0000000000000 --- a/src/plugins/embeddable/public/tests/apply_filter_action.test.ts +++ /dev/null @@ -1,153 +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 { testPlugin } from './test_plugin'; -import { EmbeddableOutput, isErrorEmbeddable, createFilterAction } from '../lib'; -import { - FilterableContainer, - FilterableContainerInput, - FILTERABLE_CONTAINER, - FilterableEmbeddableFactory, - HelloWorldContainer, - FILTERABLE_EMBEDDABLE, - FilterableEmbeddable, - FilterableContainerFactory, - FilterableEmbeddableInput, -} from '../lib/test_samples'; -import { esFilters } from '../../../data/public'; -import { applyFilterTrigger } from '../../../ui_actions/public'; - -test('ApplyFilterAction applies the filter to the root of the container tree', async () => { - const { doStart, setup } = testPlugin(); - - const factory2 = new FilterableEmbeddableFactory(); - const factory1 = new FilterableContainerFactory(async () => await api.getEmbeddableFactory); - setup.registerEmbeddableFactory(factory2.type, factory2); - setup.registerEmbeddableFactory(factory1.type, factory1); - - const api = doStart(); - - const applyFilterAction = createFilterAction(); - - const root = new FilterableContainer( - { id: 'root', panels: {}, filters: [] }, - api.getEmbeddableFactory - ); - - const node1 = await root.addNewEmbeddable< - FilterableContainerInput, - EmbeddableOutput, - FilterableContainer - >(FILTERABLE_CONTAINER, { panels: {}, id: 'node1' }); - - const node2 = await root.addNewEmbeddable< - FilterableContainerInput, - EmbeddableOutput, - FilterableContainer - >(FILTERABLE_CONTAINER, { panels: {}, id: 'Node2' }); - - if (isErrorEmbeddable(node1) || isErrorEmbeddable(node2)) { - throw new Error(); - } - - const embeddable = await node2.addNewEmbeddable< - FilterableEmbeddableInput, - EmbeddableOutput, - FilterableEmbeddable - >(FILTERABLE_EMBEDDABLE, { id: 'leaf' }); - - if (isErrorEmbeddable(embeddable)) { - throw new Error(); - } - - const filter: any = { - $state: { store: esFilters.FilterStateStore.APP_STATE }, - meta: { - disabled: false, - negate: false, - alias: '', - }, - query: { match: { extension: { query: 'foo' } } }, - }; - - await applyFilterAction.execute({ embeddable, filters: [filter], trigger: applyFilterTrigger }); - expect(root.getInput().filters.length).toBe(1); - expect(node1.getInput().filters.length).toBe(1); - expect(embeddable.getInput().filters.length).toBe(1); - expect(node2.getInput().filters.length).toBe(1); -}); - -test('ApplyFilterAction is incompatible if the root container does not accept a filter as input', async () => { - const { doStart, setup } = testPlugin(); - - const factory = new FilterableEmbeddableFactory(); - setup.registerEmbeddableFactory(factory.type, factory); - const api = doStart(); - const applyFilterAction = createFilterAction(); - - const parent = new HelloWorldContainer({ id: 'root', panels: {} }, { - getEmbeddableFactory: api.getEmbeddableFactory, - } as any); - const embeddable = await parent.addNewEmbeddable< - FilterableContainerInput, - EmbeddableOutput, - FilterableContainer - >(FILTERABLE_EMBEDDABLE, { id: 'leaf' }); - - if (isErrorEmbeddable(embeddable)) { - throw new Error(); - } - - // @ts-ignore - expect(await applyFilterAction.isCompatible({ embeddable })).toBe(false); -}); - -test('trying to execute on incompatible context throws an error ', async () => { - const { doStart, setup } = testPlugin(); - - const factory = new FilterableEmbeddableFactory(); - setup.registerEmbeddableFactory(factory.type, factory); - - const api = doStart(); - const applyFilterAction = createFilterAction(); - - const parent = new HelloWorldContainer({ id: 'root', panels: {} }, { - getEmbeddableFactory: api.getEmbeddableFactory, - } as any); - - const embeddable = await parent.addNewEmbeddable< - FilterableContainerInput, - EmbeddableOutput, - FilterableContainer - >(FILTERABLE_EMBEDDABLE, { id: 'leaf' }); - - if (isErrorEmbeddable(embeddable)) { - throw new Error(); - } - - async function check() { - await applyFilterAction.execute({ embeddable } as any); - } - await expect(check()).rejects.toThrow(Error); -}); - -test('gets title', async () => { - const applyFilterAction = createFilterAction(); - expect(applyFilterAction.getDisplayName({} as any)).toBeDefined(); -}); diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/__snapshots__/step_time_field.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/__snapshots__/step_time_field.test.tsx.snap index 6cc92d20cfdcc..544e3ba983122 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/__snapshots__/step_time_field.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/__snapshots__/step_time_field.test.tsx.snap @@ -27,7 +27,7 @@ exports[`StepTimeField should render "Custom index pattern ID already exists" wh /> ({ - fieldsFetcher: { - fetchForWildcard: jest.fn().mockReturnValue(Promise.resolve(fields)), - }, - }), + create: () => ({}), + getFieldsForWildcard: jest.fn().mockReturnValue(Promise.resolve(fields)), } as any; describe('StepTimeField', () => { diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/step_time_field.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/step_time_field.tsx index 5d33a08557fed..cacabb6d7623b 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/step_time_field.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/step_time_field/step_time_field.tsx @@ -108,12 +108,12 @@ export class StepTimeField extends Component { }); test('invokes the provided services when creating an index pattern', async () => { - const create = jest.fn().mockImplementation(() => 'id'); + const newIndexPatternAndSave = jest.fn().mockImplementation(async () => { + return indexPattern; + }); const clear = jest.fn(); mockContext.data.indexPatterns.clearCache = clear; const indexPattern = ({ @@ -151,11 +153,10 @@ describe('CreateIndexPatternWizard', () => { title: 'my-fake-index-pattern', timeFieldName: 'timestamp', fields: [], - create, + _fetchFields: jest.fn(), } as unknown) as IndexPattern; - mockContext.data.indexPatterns.make = async () => { - return indexPattern; - }; + mockContext.data.indexPatterns.createAndSave = newIndexPatternAndSave; + mockContext.data.indexPatterns.setDefault = jest.fn(); const component = createComponentWithContext( CreateIndexPatternWizard, @@ -165,9 +166,8 @@ describe('CreateIndexPatternWizard', () => { component.setState({ indexPattern: 'foo' }); await (component.instance() as CreateIndexPatternWizard).createIndexPattern(undefined, 'id'); - expect(mockContext.uiSettings.get).toBeCalled(); - expect(create).toBeCalled(); - expect(clear).toBeCalledWith('id'); - expect(routeComponentPropsMock.history.push).toBeCalledWith(`/patterns/id`); + expect(newIndexPatternAndSave).toBeCalled(); + expect(clear).toBeCalledWith('1'); + expect(routeComponentPropsMock.history.push).toBeCalledWith(`/patterns/1`); }); }); diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx index a789ebbfadbce..aa97c21d766b9 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/create_index_pattern_wizard.tsx @@ -40,6 +40,7 @@ import { ensureMinimumTime, getIndices } from './lib'; import { IndexPatternCreationConfig } from '../..'; import { IndexPatternManagmentContextValue } from '../../types'; import { MatchedItem } from './types'; +import { DuplicateIndexPatternError, IndexPattern } from '../../../../data/public'; interface CreateIndexPatternWizardState { step: number; @@ -156,50 +157,50 @@ export class CreateIndexPatternWizard extends Component< }; createIndexPattern = async (timeFieldName: string | undefined, indexPatternId: string) => { + let emptyPattern: IndexPattern; const { history } = this.props; const { indexPattern } = this.state; - const emptyPattern = await this.context.services.data.indexPatterns.make(); - - Object.assign(emptyPattern, { - id: indexPatternId, - title: indexPattern, - timeFieldName, - ...this.state.indexPatternCreationType.getIndexPatternMappings(), - }); - - const createdId = await emptyPattern.create(); - if (!createdId) { - const confirmMessage = i18n.translate( - 'indexPatternManagement.indexPattern.titleExistsLabel', - { - values: { title: emptyPattern.title }, - defaultMessage: "An index pattern with the title '{title}' already exists.", - } - ); - - const isConfirmed = await this.context.services.overlays.openConfirm(confirmMessage, { - confirmButtonText: i18n.translate( - 'indexPatternManagement.indexPattern.goToPatternButtonLabel', + try { + emptyPattern = await this.context.services.data.indexPatterns.createAndSave({ + id: indexPatternId, + title: indexPattern, + timeFieldName, + ...this.state.indexPatternCreationType.getIndexPatternMappings(), + }); + } catch (err) { + if (err instanceof DuplicateIndexPatternError) { + const confirmMessage = i18n.translate( + 'indexPatternManagement.indexPattern.titleExistsLabel', { - defaultMessage: 'Go to existing pattern', + values: { title: emptyPattern!.title }, + defaultMessage: "An index pattern with the title '{title}' already exists.", } - ), - }); + ); + + const isConfirmed = await this.context.services.overlays.openConfirm(confirmMessage, { + confirmButtonText: i18n.translate( + 'indexPatternManagement.indexPattern.goToPatternButtonLabel', + { + defaultMessage: 'Go to existing pattern', + } + ), + }); - if (isConfirmed) { - return history.push(`/patterns/${indexPatternId}`); + if (isConfirmed) { + return history.push(`/patterns/${indexPatternId}`); + } else { + return; + } } else { - return; + throw err; } } - if (!this.context.services.uiSettings.get('defaultIndex')) { - await this.context.services.uiSettings.set('defaultIndex', createdId); - } + await this.context.services.data.indexPatterns.setDefault(emptyPattern.id as string); - this.context.services.data.indexPatterns.clearCache(createdId); - history.push(`/patterns/${createdId}`); + this.context.services.data.indexPatterns.clearCache(emptyPattern.id as string); + history.push(`/patterns/${emptyPattern.id}`); }; goToTimeFieldStep = (indexPattern: string, selectedTimeField?: string) => { diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx index 13be9ca6c9c25..08edf42df60d8 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx @@ -96,7 +96,7 @@ export const CreateEditField = withRouter( indexPattern={indexPattern} spec={spec} services={{ - saveIndexPattern: data.indexPatterns.save.bind(data.indexPatterns), + saveIndexPattern: data.indexPatterns.updateSavedObject.bind(data.indexPatterns), redirectAway, }} /> diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx index d09836019b0bc..67a20c428040f 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -121,7 +121,8 @@ export const EditIndexPattern = withRouter( const refreshFields = () => { overlays.openConfirm(confirmMessage, confirmModalOptionsRefresh).then(async (isConfirmed) => { if (isConfirmed) { - await indexPattern.refreshFields(); + await data.indexPatterns.refreshFields(indexPattern); + await data.indexPatterns.updateSavedObject(indexPattern); setFields(indexPattern.getNonScriptedFields()); } }); @@ -236,7 +237,7 @@ export const EditIndexPattern = withRouter( ({ @@ -43,7 +43,7 @@ const helpers = { const indexPattern = ({ getNonScriptedFields: () => fields, -} as unknown) as IIndexPattern; +} as unknown) as IndexPattern; const mockFieldToIndexPatternField = (spec: Record) => { return new IndexPatternField( diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx index 23977aac7fa7a..7be420e2af50d 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx @@ -19,23 +19,19 @@ import React, { Component } from 'react'; import { createSelector } from 'reselect'; -import { - IndexPatternField, - IIndexPattern, - IFieldType, -} from '../../../../../../plugins/data/public'; +import { IndexPatternField, IndexPattern, IFieldType } from '../../../../../../plugins/data/public'; import { Table } from './components/table'; import { getFieldFormat } from './lib'; import { IndexedFieldItem } from './types'; interface IndexedFieldsTableProps { fields: IndexPatternField[]; - indexPattern: IIndexPattern; + indexPattern: IndexPattern; fieldFilter?: string; indexedFieldTypeFilter?: string; helpers: { redirectToRoute: (obj: any) => void; - getFieldInfo: (indexPattern: IIndexPattern, field: IFieldType) => string[]; + getFieldInfo: (indexPattern: IndexPattern, field: IFieldType) => string[]; }; fieldWildcardMatcher: (filters: any[]) => (val: any) => boolean; } diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx index 08cc90faf75fa..c7ea20c700086 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx @@ -39,7 +39,7 @@ interface ScriptedFieldsTableProps { }; onRemoveField?: () => void; painlessDocLink: string; - saveIndexPattern: DataPublicPluginStart['indexPatterns']['save']; + saveIndexPattern: DataPublicPluginStart['indexPatterns']['updateSavedObject']; } interface ScriptedFieldsTableState { diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx index e43ee2e55eeca..2d3a61b42c3a4 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx @@ -22,13 +22,13 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { Table, TableProps, TableState } from './table'; import { EuiTableFieldDataColumnType, keys } from '@elastic/eui'; -import { IIndexPattern } from 'src/plugins/data/public'; +import { IndexPattern } from 'src/plugins/data/public'; import { SourceFiltersTableFilter } from '../../types'; -const indexPattern = {} as IIndexPattern; +const indexPattern = {} as IndexPattern; const items: SourceFiltersTableFilter[] = [{ value: 'tim*', clientId: '' }]; -const getIndexPatternMock = (mockedFields: any = {}) => ({ ...mockedFields } as IIndexPattern); +const getIndexPatternMock = (mockedFields: any = {}) => ({ ...mockedFields } as IndexPattern); const getTableColumnRender = ( component: ShallowWrapper, diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx index f73d756f28116..c5b09961f25fc 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx @@ -30,7 +30,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { IIndexPattern } from 'src/plugins/data/public'; +import { IndexPattern } from 'src/plugins/data/public'; import { SourceFiltersTableFilter } from '../../types'; const filterHeader = i18n.translate( @@ -80,7 +80,7 @@ const cancelAria = i18n.translate( ); export interface TableProps { - indexPattern: IIndexPattern; + indexPattern: IndexPattern; items: SourceFiltersTableFilter[]; deleteFilter: Function; fieldWildcardMatcher: Function; diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.tsx index b00648f124716..cd311db513c09 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.tsx @@ -30,7 +30,7 @@ export interface SourceFiltersTableProps { filterFilter: string; fieldWildcardMatcher: Function; onAddOrRemoveFilter?: Function; - saveIndexPattern: DataPublicPluginStart['indexPatterns']['save']; + saveIndexPattern: DataPublicPluginStart['indexPatterns']['updateSavedObject']; } export interface SourceFiltersTableState { diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx index 101399ef02b73..5c29dfafd3c07 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx @@ -49,7 +49,7 @@ import { getTabs, getPath, convertToEuiSelectOption } from './utils'; interface TabsProps extends Pick { indexPattern: IndexPattern; fields: IndexPatternField[]; - saveIndexPattern: DataPublicPluginStart['indexPatterns']['save']; + saveIndexPattern: DataPublicPluginStart['indexPatterns']['updateSavedObject']; } const searchAriaLabel = i18n.translate( diff --git a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx index 2b484d1d837bf..4fae91e78f8f9 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx @@ -133,7 +133,7 @@ export interface FieldEdiorProps { spec: IndexPatternField['spec']; services: { redirectAway: () => void; - saveIndexPattern: DataPublicPluginStart['indexPatterns']['save']; + saveIndexPattern: DataPublicPluginStart['indexPatterns']['updateSavedObject']; }; } @@ -825,7 +825,7 @@ export class FieldEditor extends PureComponent { + .catch(() => { if (oldField) { indexPattern.fields.update(oldField); } else { diff --git a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_string/index.js b/src/plugins/management/common/contants.ts similarity index 94% rename from src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_string/index.js rename to src/plugins/management/common/contants.ts index 8900db15321ae..6ff585510dab1 100644 --- a/src/legacy/plugin_discovery/plugin_pack/__tests__/fixtures/plugins/exports_string/index.js +++ b/src/plugins/management/common/contants.ts @@ -17,4 +17,4 @@ * under the License. */ -export default 'foo'; +export const MANAGEMENT_APP_ID = 'management'; diff --git a/src/plugins/management/public/index.ts b/src/plugins/management/public/index.ts index f6c23ccf0143f..f3e25b90b73c7 100644 --- a/src/plugins/management/public/index.ts +++ b/src/plugins/management/public/index.ts @@ -32,3 +32,5 @@ export { ManagementStart, DefinedSections, } from './types'; + +export { MANAGEMENT_APP_ID } from '../common/contants'; diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index 808578c470ae1..122e73796753c 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -33,6 +33,7 @@ import { AppNavLinkStatus, } from '../../../core/public'; +import { MANAGEMENT_APP_ID } from '../common/contants'; import { ManagementSectionsService, getSectionsServiceStartPrivate, @@ -72,7 +73,7 @@ export class ManagementPlugin implements Plugin { const closeFlyout = useCallback(() => setFlyoutVisible(false), [setFlyoutVisible]); return ( - - - -

- -

-
-
- - {!newsFetchResult ? ( - - ) : newsFetchResult.feedItems.length > 0 ? ( - newsFetchResult.feedItems.map((item: NewsfeedItem) => { - return ( - - {item.linkText} - - } - date={item.publishOn.format('DD MMMM YYYY')} - badge={item.badge ? {item.badge} : undefined} + + + + +

+ - ); - }) - ) : ( - - )} - - - - - - - - - - {newsFetchResult ? ( - -

- -

-
- ) : null} -
-
-
- +

+
+
+ + {!newsFetchResult ? ( + + ) : newsFetchResult.feedItems.length > 0 ? ( + newsFetchResult.feedItems.map((item: NewsfeedItem) => { + return ( + + {item.linkText} + + } + date={item.publishOn.format('DD MMMM YYYY')} + badge={item.badge ? {item.badge} : undefined} + /> + ); + }) + ) : ( + + )} + + + + + + + + + + {newsFetchResult ? ( + +

+ +

+
+ ) : null} +
+
+
+
+
); }; diff --git a/src/plugins/newsfeed/public/components/newsfeed_header_nav_button.tsx b/src/plugins/newsfeed/public/components/newsfeed_header_nav_button.tsx index 628cfde18b0d5..7924544640086 100644 --- a/src/plugins/newsfeed/public/components/newsfeed_header_nav_button.tsx +++ b/src/plugins/newsfeed/public/components/newsfeed_header_nav_button.tsx @@ -19,7 +19,8 @@ import React, { useState, Fragment, useEffect } from 'react'; import * as Rx from 'rxjs'; -import { EuiHeaderSectionItemButton, EuiIcon, EuiNotificationBadge } from '@elastic/eui'; +import { EuiHeaderSectionItemButton, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { NewsfeedFlyout } from './flyout_list'; import { FetchResult } from '../types'; @@ -65,15 +66,19 @@ export const NewsfeedNavButton = ({ apiFetchResult }: Props) => { aria-controls="keyPadMenu" aria-expanded={flyoutVisible} aria-haspopup="true" - aria-label="Newsfeed menu" + aria-label={ + showBadge + ? i18n.translate('newsfeed.headerButton.unreadAriaLabel', { + defaultMessage: 'Newsfeed menu - unread items available', + }) + : i18n.translate('newsfeed.headerButton.readAriaLabel', { + defaultMessage: 'Newsfeed menu - all items read', + }) + } + notification={showBadge ? true : null} onClick={showFlyout} > - {showBadge ? ( - - ▪ - - ) : null} {flyoutVisible ? : null} diff --git a/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts b/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts index 679ea5ffc23ee..eb95c213e680d 100644 --- a/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts +++ b/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts @@ -24,10 +24,11 @@ import { SavedObject, SavedObjectLoader } from '../../../saved_objects/public'; import { DataPublicPluginStart, IndexPatternsContract, - IIndexPattern, injectSearchSourceReferences, + IndexPatternSpec, } from '../../../data/public'; import { FailedImport } from './process_import_response'; +import { DuplicateIndexPatternError, IndexPattern } from '../../../data/public'; type SavedObjectsRawDoc = Record; @@ -70,11 +71,10 @@ function addJsonFieldToIndexPattern( async function importIndexPattern( doc: SavedObjectsRawDoc, indexPatterns: IndexPatternsContract, - overwriteAll: boolean, + overwriteAll: boolean = false, openConfirm: OverlayStart['openConfirm'] ) { // TODO: consolidate this is the code in create_index_pattern_wizard.js - const emptyPattern = await indexPatterns.make(); const { title, timeFieldName, @@ -84,50 +84,53 @@ async function importIndexPattern( type, typeMeta, } = doc._source; - const importedIndexPattern = { + const indexPatternSpec: IndexPatternSpec = { id: doc._id, title, timeFieldName, - } as IIndexPattern; + }; + let emptyPattern: IndexPattern; if (type) { - importedIndexPattern.type = type; + indexPatternSpec.type = type; } - addJsonFieldToIndexPattern(importedIndexPattern, fields, 'fields', title); - addJsonFieldToIndexPattern(importedIndexPattern, fieldFormatMap, 'fieldFormatMap', title); - addJsonFieldToIndexPattern(importedIndexPattern, sourceFilters, 'sourceFilters', title); - addJsonFieldToIndexPattern(importedIndexPattern, typeMeta, 'typeMeta', title); - Object.assign(emptyPattern, importedIndexPattern); - - let newId = await emptyPattern.create(overwriteAll); - if (!newId) { - // We can override and we want to prompt for confirmation - const isConfirmed = await openConfirm( - i18n.translate('savedObjectsManagement.indexPattern.confirmOverwriteLabel', { - values: { title }, - defaultMessage: "Are you sure you want to overwrite '{title}'?", - }), - { - title: i18n.translate('savedObjectsManagement.indexPattern.confirmOverwriteTitle', { - defaultMessage: 'Overwrite {type}?', - values: { type }, + addJsonFieldToIndexPattern(indexPatternSpec, fields, 'fields', title); + addJsonFieldToIndexPattern(indexPatternSpec, fieldFormatMap, 'fieldFormatMap', title); + addJsonFieldToIndexPattern(indexPatternSpec, sourceFilters, 'sourceFilters', title); + addJsonFieldToIndexPattern(indexPatternSpec, typeMeta, 'typeMeta', title); + try { + emptyPattern = await indexPatterns.createAndSave(indexPatternSpec, overwriteAll, true); + } catch (err) { + if (err instanceof DuplicateIndexPatternError) { + // We can override and we want to prompt for confirmation + const isConfirmed = await openConfirm( + i18n.translate('savedObjectsManagement.indexPattern.confirmOverwriteLabel', { + values: { title }, + defaultMessage: "Are you sure you want to overwrite '{title}'?", }), - confirmButtonText: i18n.translate( - 'savedObjectsManagement.indexPattern.confirmOverwriteButton', - { - defaultMessage: 'Overwrite', - } - ), - } - ); + { + title: i18n.translate('savedObjectsManagement.indexPattern.confirmOverwriteTitle', { + defaultMessage: 'Overwrite {type}?', + values: { type }, + }), + confirmButtonText: i18n.translate( + 'savedObjectsManagement.indexPattern.confirmOverwriteButton', + { + defaultMessage: 'Overwrite', + } + ), + } + ); - if (isConfirmed) { - newId = (await emptyPattern.create(true)) as string; - } else { - return; + if (isConfirmed) { + emptyPattern = await indexPatterns.createAndSave(indexPatternSpec, true, true); + } else { + return; + } } } - indexPatterns.clearCache(newId); - return newId; + + indexPatterns.clearCache(emptyPattern!.id); + return emptyPattern!.id; } async function importDocument(obj: SavedObject, doc: SavedObjectsRawDoc, overwriteAll: boolean) { diff --git a/src/plugins/timelion/public/plugin.ts b/src/plugins/timelion/public/plugin.ts index 24d1b9eb3fb65..7656a808dfb00 100644 --- a/src/plugins/timelion/public/plugin.ts +++ b/src/plugins/timelion/public/plugin.ts @@ -28,6 +28,7 @@ import { AppMountParameters, AppUpdater, ScopedHistory, + AppNavLinkStatus, } from '../../../core/public'; import { Panel } from './panels/panel'; import { initAngularBootstrap, KibanaLegacyStart } from '../../kibana_legacy/public'; @@ -35,7 +36,10 @@ import { createKbnUrlTracker } from '../../kibana_utils/public'; import { DataPublicPluginStart, esFilters, DataPublicPluginSetup } from '../../data/public'; import { NavigationPublicPluginStart } from '../../navigation/public'; import { VisualizationsStart } from '../../visualizations/public'; -import { VisTypeTimelionPluginStart } from '../../vis_type_timelion/public'; +import { + VisTypeTimelionPluginStart, + VisTypeTimelionPluginSetup, +} from '../../vis_type_timelion/public'; export interface TimelionPluginDependencies { data: DataPublicPluginStart; @@ -55,7 +59,13 @@ export class TimelionPlugin implements Plugin { this.initializerContext = initializerContext; } - public setup(core: CoreSetup, { data }: { data: DataPublicPluginSetup }) { + public setup( + core: CoreSetup, + { + data, + visTypeTimelion, + }: { data: DataPublicPluginSetup; visTypeTimelion: VisTypeTimelionPluginSetup } + ) { const timelionPanels: Map = new Map(); const { appMounted, appUnMounted, stop: stopUrlTracker } = createKbnUrlTracker({ @@ -93,7 +103,8 @@ export class TimelionPlugin implements Plugin { defaultPath: '#/', euiIconType: 'logoKibana', category: DEFAULT_APP_CATEGORIES.kibana, - updater$: this.appStateUpdater.asObservable(), + navLinkStatus: + visTypeTimelion.isUiEnabled === false ? AppNavLinkStatus.hidden : AppNavLinkStatus.default, mount: async (params: AppMountParameters) => { const [coreStart, pluginsStart] = await core.getStartServices(); this.currentHistory = params.history; diff --git a/src/plugins/ui_actions/public/mocks.ts b/src/plugins/ui_actions/public/mocks.ts index 3522ac4941ba0..759430169b613 100644 --- a/src/plugins/ui_actions/public/mocks.ts +++ b/src/plugins/ui_actions/public/mocks.ts @@ -48,6 +48,7 @@ const createStartContract = (): Start => { executeTriggerActions: jest.fn(), fork: jest.fn(), getAction: jest.fn(), + hasAction: jest.fn(), getTrigger: jest.fn(), getTriggerActions: jest.fn((id: TriggerId) => []), getTriggerCompatibleActions: jest.fn(), diff --git a/src/plugins/ui_actions/public/service/ui_actions_service.ts b/src/plugins/ui_actions/public/service/ui_actions_service.ts index 6028177964fb7..ec5f3afa19c94 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_service.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_service.ts @@ -99,6 +99,10 @@ export class UiActionsService { this.actions.delete(actionId); }; + public readonly hasAction = (actionId: string): boolean => { + return this.actions.has(actionId); + }; + public readonly attachAction = (triggerId: T, actionId: string): void => { const trigger = this.triggers.get(triggerId); diff --git a/src/plugins/vis_type_timelion/public/index.ts b/src/plugins/vis_type_timelion/public/index.ts index abfe345d8c672..f5e781b95f2e2 100644 --- a/src/plugins/vis_type_timelion/public/index.ts +++ b/src/plugins/vis_type_timelion/public/index.ts @@ -31,4 +31,4 @@ export { generateTicksProvider } from './helpers/tick_generator'; export { DEFAULT_TIME_FORMAT, calculateInterval } from '../common/lib'; -export { VisTypeTimelionPluginStart } from './plugin'; +export { VisTypeTimelionPluginStart, VisTypeTimelionPluginSetup } from './plugin'; diff --git a/src/plugins/vis_type_timelion/public/plugin.ts b/src/plugins/vis_type_timelion/public/plugin.ts index 060fec04deb3f..e2c7efec34c7f 100644 --- a/src/plugins/vis_type_timelion/public/plugin.ts +++ b/src/plugins/vis_type_timelion/public/plugin.ts @@ -66,11 +66,16 @@ export interface VisTypeTimelionPluginStart { getArgValueSuggestions: typeof getArgValueSuggestions; } +/** @public */ +export interface VisTypeTimelionPluginSetup { + isUiEnabled: boolean; +} + /** @internal */ export class TimelionVisPlugin implements Plugin< - void, + VisTypeTimelionPluginSetup, VisTypeTimelionPluginStart, TimelionVisSetupDependencies, TimelionVisStartDependencies @@ -89,14 +94,15 @@ export class TimelionVisPlugin expressions.registerFunction(() => getTimelionVisualizationConfig(dependencies)); visualizations.createReactVisualization(getTimelionVisDefinition(dependencies)); + + return { + isUiEnabled: this.initializerContext.config.get().ui.enabled, + }; } public start(core: CoreStart, plugins: TimelionVisStartDependencies) { setIndexPatterns(plugins.data.indexPatterns); setSavedObjectsClient(core.savedObjects.client); - if (this.initializerContext.config.get().ui.enabled === false) { - core.chrome.navLinks.update('timelion', { hidden: true }); - } return { getArgValueSuggestions, diff --git a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts index 777de89672bbe..26a1792e3ec70 100644 --- a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts +++ b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts @@ -16,15 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -import { uniqBy } from 'lodash'; +import { uniqBy, get } from 'lodash'; import { first, map } from 'rxjs/operators'; import { KibanaRequest, RequestHandlerContext } from 'kibana/server'; -// @ts-ignore -import { getIndexPatternObject } from './vis_data/helpers/get_index_pattern'; -import { indexPatterns } from '../../../data/server'; import { Framework } from '../plugin'; -import { IndexPatternFieldDescriptor, IndexPatternsFetcher } from '../../../data/server'; +import { + indexPatterns, + IndexPatternFieldDescriptor, + IndexPatternsFetcher, +} from '../../../data/server'; import { ReqFacade } from './search_strategies/strategies/abstract_search_strategy'; export async function getFields( @@ -58,7 +59,15 @@ export async function getFields( .toPromise(); }, }; - const { indexPatternString } = await getIndexPatternObject(reqFacade, indexPattern); + let indexPatternString = indexPattern; + + if (!indexPatternString) { + const [, { data }] = await framework.core.getStartServices(); + const indexPatternsService = await data.indexPatterns.indexPatternsServiceFactory(request); + const defaultIndexPattern = await indexPatternsService.getDefault(); + indexPatternString = get(defaultIndexPattern, 'title', ''); + } + const { searchStrategy, capabilities, diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js index 6773ee482b098..4dcc67dc46976 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js @@ -49,6 +49,7 @@ describe('AbstractSearchStrategy', () => { expect(fields).toBe(mockedFields); expect(req.pre.indexPatternsService.getFieldsForWildcard).toHaveBeenCalledWith({ pattern: indexPattern, + fieldCapsOptions: { allowNoIndices: true }, }); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts index 92b7e6976962e..2eb92b2b777e8 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts @@ -84,6 +84,7 @@ export class AbstractSearchStrategy { return await indexPatternsService!.getFieldsForWildcard({ pattern: indexPattern, + fieldCapsOptions: { allowNoIndices: true }, }); } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.js index e4bda194299df..82a2ef66cb1c0 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.js @@ -17,12 +17,10 @@ * under the License. */ -import { get } from 'lodash'; - const DEFAULT_TIME_FIELD = '@timestamp'; export function getIntervalAndTimefield(panel, series = {}, indexPatternObject) { - const getDefaultTimeField = () => get(indexPatternObject, 'timeFieldName', DEFAULT_TIME_FIELD); + const getDefaultTimeField = () => indexPatternObject?.timeFieldName ?? DEFAULT_TIME_FIELD; const timeField = (series.override_index_pattern && series.series_time_field) || diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index 18ae68ec40fe5..c091d396b4924 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -243,12 +243,6 @@ export class VisualizeEmbeddable dirty = true; } - // propagate the title to the output embeddable - // but only when the visualization is in edit/Visualize mode - if (!this.parent && this.vis.title !== this.output.title) { - this.updateOutput({ title: this.vis.title }); - } - if (this.vis.description && this.domNode) { this.domNode.setAttribute('data-description', this.vis.description); } diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index 0423e48bfb41e..65c9a5410d226 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -267,6 +267,7 @@ export const getTopNavConfig = ( } const currentTitle = savedVis.title; savedVis.title = newTitle; + embeddableHandler.updateInput({ title: newTitle }); savedVis.copyOnSave = newCopyOnSave; savedVis.description = newDescription; const saveOptions = { diff --git a/test/accessibility/apps/dashboard_panel.ts b/test/accessibility/apps/dashboard_panel.ts index 03fa76387da1f..1a817ce6b7a1c 100644 --- a/test/accessibility/apps/dashboard_panel.ts +++ b/test/accessibility/apps/dashboard_panel.ts @@ -18,7 +18,6 @@ */ import { FtrProviderContext } from '../ftr_provider_context'; - export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'home', 'settings']); const a11y = getService('a11y'); @@ -31,6 +30,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { useActualUrl: true, }); + await PageObjects.home.addSampleDataSet('flights'); await PageObjects.common.navigateToApp('dashboard'); await testSubjects.click('dashboardListingTitleLink-[Flights]-Global-Flight-Dashboard'); diff --git a/test/functional/apps/discover/_errors.js b/test/functional/apps/discover/_errors.ts similarity index 92% rename from test/functional/apps/discover/_errors.js rename to test/functional/apps/discover/_errors.ts index 614059dc8ac94..9520d652a65d5 100644 --- a/test/functional/apps/discover/_errors.js +++ b/test/functional/apps/discover/_errors.ts @@ -18,8 +18,9 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { +export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); diff --git a/test/functional/apps/management/_index_pattern_popularity.js b/test/functional/apps/management/_index_pattern_popularity.js index e2fcf50ef2c12..530b8e1111a0c 100644 --- a/test/functional/apps/management/_index_pattern_popularity.js +++ b/test/functional/apps/management/_index_pattern_popularity.js @@ -60,7 +60,7 @@ export default function ({ getService, getPageObjects }) { // check that it is 0 (previous increase was cancelled const popularity = await PageObjects.settings.getPopularity(); log.debug('popularity = ' + popularity); - expect(popularity).to.be(''); + expect(popularity).to.be('0'); }); it('can be saved', async function () { diff --git a/test/functional/page_objects/newsfeed_page.ts b/test/functional/page_objects/newsfeed_page.ts index 52267138819ab..6e5e801589295 100644 --- a/test/functional/page_objects/newsfeed_page.ts +++ b/test/functional/page_objects/newsfeed_page.ts @@ -21,6 +21,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export function NewsfeedPageProvider({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); + const find = getService('find'); const retry = getService('retry'); const flyout = getService('flyout'); const testSubjects = getService('testSubjects'); @@ -49,7 +50,7 @@ export function NewsfeedPageProvider({ getService, getPageObjects }: FtrProvider } async getRedButtonSign() { - return await testSubjects.exists('showBadgeNews'); + return await find.existsByCssSelector('.euiHeaderSectionItemButton__notification--dot'); } async getNewsfeedList() { diff --git a/test/plugin_functional/plugins/index_patterns/server/plugin.ts b/test/plugin_functional/plugins/index_patterns/server/plugin.ts index 1c85f226623cb..ddf9acb259983 100644 --- a/test/plugin_functional/plugins/index_patterns/server/plugin.ts +++ b/test/plugin_functional/plugins/index_patterns/server/plugin.ts @@ -78,7 +78,7 @@ export class IndexPatternsTestPlugin const id = (req.params as Record).id; const service = await data.indexPatterns.indexPatternsServiceFactory(req); const ip = await service.get(id); - await service.save(ip); + await service.updateSavedObject(ip); return res.ok(); } ); diff --git a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts index 2db9eb733f805..7e736ea7a066f 100644 --- a/test/plugin_functional/test_suites/data_plugin/index_patterns.ts +++ b/test/plugin_functional/test_suites/data_plugin/index_patterns.ts @@ -46,14 +46,14 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const body = await ( await supertest.get(`/api/index-patterns-plugin/get/${indexPatternId}`).expect(200) ).body; - expect(body.fields.length > 0).to.equal(true); + expect(typeof body.id).to.equal('string'); }); it('can update index pattern', async () => { - const body = await ( - await supertest.get(`/api/index-patterns-plugin/update/${indexPatternId}`).expect(200) - ).body; - expect(body).to.eql({}); + const resp = await supertest + .get(`/api/index-patterns-plugin/update/${indexPatternId}`) + .expect(200); + expect(resp.body).to.eql({}); }); it('can delete index pattern', async () => { diff --git a/x-pack/.gitignore b/x-pack/.gitignore index d73b6f64f036a..99e33dbb88e92 100644 --- a/x-pack/.gitignore +++ b/x-pack/.gitignore @@ -6,13 +6,9 @@ /test/functional/apps/reporting/reports/session /test/reporting/configs/failure_debug/ /plugins/reporting/.chromium/ -/legacy/plugins/reporting/.chromium/ -/legacy/plugins/reporting/.phantom/ /plugins/reporting/chromium/ /plugins/reporting/.phantom/ /.aws-config.json /.env /.kibana-plugin-helpers.dev.* -!/legacy/plugins/infra/**/target .cache -!/legacy/plugins/security_solution/**/target diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index a700781438706..66ae478b86828 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -9,8 +9,8 @@ "xpack.alerts": "plugins/alerts", "xpack.eventLog": "plugins/event_log", "xpack.alertingBuiltins": "plugins/alerting_builtins", - "xpack.apm": ["legacy/plugins/apm", "plugins/apm"], - "xpack.beatsManagement": ["legacy/plugins/beats_management", "plugins/beats_management"], + "xpack.apm": "plugins/apm", + "xpack.beatsManagement": "plugins/beats_management", "xpack.canvas": "plugins/canvas", "xpack.cloud": "plugins/cloud", "xpack.dashboard": "plugins/dashboard_enhanced", @@ -35,15 +35,15 @@ "xpack.lens": "plugins/lens", "xpack.licenseMgmt": "plugins/license_management", "xpack.licensing": "plugins/licensing", - "xpack.logstash": ["plugins/logstash", "legacy/plugins/logstash"], + "xpack.logstash": ["plugins/logstash"], "xpack.main": "legacy/plugins/xpack_main", - "xpack.maps": ["plugins/maps", "legacy/plugins/maps"], - "xpack.ml": ["plugins/ml", "legacy/plugins/ml"], + "xpack.maps": ["plugins/maps"], + "xpack.ml": ["plugins/ml"], "xpack.monitoring": ["plugins/monitoring"], "xpack.remoteClusters": "plugins/remote_clusters", "xpack.painlessLab": "plugins/painless_lab", "xpack.reporting": ["plugins/reporting"], - "xpack.rollupJobs": ["legacy/plugins/rollup", "plugins/rollup"], + "xpack.rollupJobs": ["plugins/rollup"], "xpack.searchProfiler": "plugins/searchprofiler", "xpack.security": "plugins/security", "xpack.server": "legacy/server", @@ -55,6 +55,7 @@ "xpack.triggersActionsUI": "plugins/triggers_actions_ui", "xpack.upgradeAssistant": "plugins/upgrade_assistant", "xpack.uptime": ["plugins/uptime"], + "xpack.urlDrilldown": "plugins/drilldowns/url_drilldown", "xpack.watcher": "plugins/watcher", "xpack.observability": "plugins/observability" }, diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index e6f160ce8c654..eec7b0246d026 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -8,17 +8,15 @@ export function createJestConfig({ kibanaDirectory, rootDir, xPackKibanaDirector const fileMockPath = `${kibanaDirectory}/src/dev/jest/mocks/file_mock.js`; return { rootDir, - roots: ['/plugins', '/legacy/plugins', '/legacy/server'], + roots: ['/plugins'], moduleFileExtensions: ['js', 'mjs', 'json', 'ts', 'tsx', 'node'], moduleNameMapper: { '@elastic/eui$': `${kibanaDirectory}/node_modules/@elastic/eui/test-env`, '@elastic/eui/lib/(.*)?': `${kibanaDirectory}/node_modules/@elastic/eui/test-env/$1`, '^fixtures/(.*)': `${kibanaDirectory}/src/fixtures/$1`, - 'uiExports/(.*)': fileMockPath, '^src/core/(.*)': `${kibanaDirectory}/src/core/$1`, '^src/legacy/(.*)': `${kibanaDirectory}/src/legacy/$1`, '^src/plugins/(.*)': `${kibanaDirectory}/src/plugins/$1`, - '^legacy/plugins/xpack_main/(.*);': `${xPackKibanaDirectory}/legacy/plugins/xpack_main/public/$1`, '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': fileMockPath, '\\.module.(css|scss)$': `${kibanaDirectory}/src/dev/jest/mocks/css_module_mock.js`, '\\.(css|less|scss)$': `${kibanaDirectory}/src/dev/jest/mocks/style_mock.js`, @@ -30,8 +28,6 @@ export function createJestConfig({ kibanaDirectory, rootDir, xPackKibanaDirector '^(!!)?file-loader!': fileMockPath, }, collectCoverageFrom: [ - 'legacy/plugins/**/*.{js,mjs,jsx,ts,tsx}', - 'legacy/server/**/*.{js,mjs,jsx,ts,tsx}', 'plugins/**/*.{js,mjs,jsx,ts,tsx}', '!**/{__test__,__snapshots__,__examples__,integration_tests,tests}/**', '!**/*.test.{js,mjs,ts,tsx}', diff --git a/x-pack/legacy/common/__tests__/poller.js b/x-pack/legacy/common/__tests__/poller.js deleted file mode 100644 index 24558502a8d02..0000000000000 --- a/x-pack/legacy/common/__tests__/poller.js +++ /dev/null @@ -1,240 +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 expect from '@kbn/expect'; -import sinon from 'sinon'; -import { Poller } from '../poller'; - -describe('Poller', () => { - const pollFrequencyInMillis = 20; - let functionToPoll; - let successFunction; - let errorFunction; - let poller; - let clock; - - beforeEach(() => { - clock = sinon.useFakeTimers(); - }); - - afterEach(() => { - clock.restore(); - if (poller) { - poller.stop(); - } - }); - - // Allowing the Poller to poll requires intimate knowledge of the inner workings of the Poller. - // We have to ensure that the Promises internal to the `_poll` method are resolved to queue up - // the next setTimeout before incrementing the clock. The order of this differs slightly when the - // `trailing` is set, hence the different `allowPoll` and `allowDelayPoll` functions. - const queueNextPoll = async () => { - await Promise.resolve(); - await Promise.resolve(); - }; - - const allowPoll = async (interval) => { - await queueNextPoll(); - clock.tick(interval); - }; - - const allowDelayPoll = async (interval) => { - clock.tick(interval); - await queueNextPoll(); - }; - - describe('start()', () => { - beforeEach(() => { - functionToPoll = sinon.spy(() => { - return Promise.resolve(42); - }); - successFunction = sinon.spy(); - errorFunction = sinon.spy(); - poller = new Poller({ - functionToPoll, - successFunction, - errorFunction, - pollFrequencyInMillis, - }); - }); - - describe(`when trailing isn't set`, () => { - it(`polls immediately`, () => { - poller.start(); - expect(functionToPoll.callCount).to.be(1); - }); - }); - - describe(`when trailing is set to true`, () => { - beforeEach(() => { - poller = new Poller({ - functionToPoll, - successFunction, - errorFunction, - pollFrequencyInMillis, - trailing: true, - }); - }); - - it('waits for pollFrequencyInMillis before polling', async () => { - poller.start(); - expect(functionToPoll.callCount).to.be(0); - allowDelayPoll(pollFrequencyInMillis); - expect(functionToPoll.callCount).to.be(1); - }); - }); - - it('polls the functionToPoll multiple times', async () => { - poller.start(); - await allowPoll(pollFrequencyInMillis * 2); - expect(functionToPoll.callCount).to.be.greaterThan(1); - }); - - describe('when the function to poll succeeds', () => { - it('calls the successFunction multiple times', async () => { - poller.start(); - await allowPoll(pollFrequencyInMillis * 2); - expect(successFunction.callCount).to.be.greaterThan(1); - expect(errorFunction.callCount).to.be(0); - }); - }); - - describe('when the function to poll fails', () => { - beforeEach(() => { - functionToPoll = sinon.spy(() => { - return Promise.reject(42); - }); - }); - - describe('when the continuePollingOnError option has not been set', () => { - beforeEach(() => { - poller = new Poller({ - functionToPoll, - successFunction, - errorFunction, - pollFrequencyInMillis, - }); - }); - - it('calls the errorFunction exactly once and polling is stopped', async () => { - poller.start(); - await allowPoll(pollFrequencyInMillis * 4); - expect(poller.isRunning()).to.be(false); - expect(successFunction.callCount).to.be(0); - expect(errorFunction.callCount).to.be(1); - }); - }); - - describe('when the continuePollingOnError option has been set to true', () => { - beforeEach(() => { - poller = new Poller({ - functionToPoll, - successFunction, - errorFunction, - pollFrequencyInMillis, - continuePollingOnError: true, - }); - }); - - it('calls the errorFunction multiple times', async () => { - poller.start(); - await allowPoll(pollFrequencyInMillis); - await allowPoll(pollFrequencyInMillis); - expect(successFunction.callCount).to.be(0); - expect(errorFunction.callCount).to.be.greaterThan(1); - }); - - describe('when pollFrequencyErrorMultiplier has been set', () => { - beforeEach(() => { - poller = new Poller({ - functionToPoll, - successFunction, - errorFunction, - pollFrequencyInMillis, - continuePollingOnError: true, - pollFrequencyErrorMultiplier: 2, - }); - }); - - it('waits for the multiplier * the pollFrequency', async () => { - poller.start(); - await queueNextPoll(); - expect(functionToPoll.callCount).to.be(1); - await allowPoll(pollFrequencyInMillis); - expect(functionToPoll.callCount).to.be(1); - await allowPoll(pollFrequencyInMillis); - expect(functionToPoll.callCount).to.be(2); - }); - }); - }); - }); - }); - - describe('isRunning()', () => { - beforeEach(() => { - functionToPoll = sinon.spy(() => { - return Promise.resolve(42); - }); - poller = new Poller({ - functionToPoll, - }); - }); - - it('returns true immediately after invoking start()', () => { - poller.start(); - expect(poller.isRunning()).to.be(true); - }); - - it('returns false after invoking stop', () => { - poller.start(); - poller.stop(); - expect(poller.isRunning()).to.be(false); - }); - }); - - describe('stop()', () => { - describe(`when successFunction isn't set`, () => { - beforeEach(() => { - functionToPoll = sinon.spy(() => { - return Promise.resolve(42); - }); - poller = new Poller({ - functionToPoll, - pollFrequencyInMillis, - }); - }); - - it(`doesn't poll again`, async () => { - poller.start(); - expect(functionToPoll.callCount).to.be(1); - poller.stop(); - await allowPoll(pollFrequencyInMillis); - expect(functionToPoll.callCount).to.be(1); - }); - }); - - describe(`when successFunction is a Promise`, () => { - beforeEach(() => { - functionToPoll = sinon.spy(() => { - return Promise.resolve(42); - }); - poller = new Poller({ - functionToPoll, - successFunction: Promise.resolve(), - pollFrequencyInMillis, - }); - }); - - it(`doesn't poll again when successFunction is a Promise`, async () => { - poller.start(); - expect(functionToPoll.callCount).to.be(1); - poller.stop(); - await allowPoll(pollFrequencyInMillis); - expect(functionToPoll.callCount).to.be(1); - }); - }); - }); -}); diff --git a/x-pack/legacy/common/constants/index.ts b/x-pack/legacy/common/constants/index.ts deleted file mode 100644 index 4db0f994fd47e..0000000000000 --- a/x-pack/legacy/common/constants/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { - LICENSE_STATUS_UNAVAILABLE, - LICENSE_STATUS_INVALID, - LICENSE_STATUS_EXPIRED, - LICENSE_STATUS_VALID, -} from './license_status'; - -export { - LICENSE_TYPE_BASIC, - LICENSE_TYPE_STANDARD, - LICENSE_TYPE_GOLD, - LICENSE_TYPE_PLATINUM, - LICENSE_TYPE_ENTERPRISE, - LICENSE_TYPE_TRIAL, - RANKED_LICENSE_TYPES, - LicenseType, -} from './license_types'; diff --git a/x-pack/legacy/common/constants/license_status.ts b/x-pack/legacy/common/constants/license_status.ts deleted file mode 100644 index 5fdfa08d73959..0000000000000 --- a/x-pack/legacy/common/constants/license_status.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const LICENSE_STATUS_UNAVAILABLE = 'UNAVAILABLE'; -export const LICENSE_STATUS_INVALID = 'INVALID'; -export const LICENSE_STATUS_EXPIRED = 'EXPIRED'; -export const LICENSE_STATUS_VALID = 'VALID'; diff --git a/x-pack/legacy/common/constants/license_types.ts b/x-pack/legacy/common/constants/license_types.ts deleted file mode 100644 index 8c329df2f85f7..0000000000000 --- a/x-pack/legacy/common/constants/license_types.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const LICENSE_TYPE_BASIC = 'basic'; -export const LICENSE_TYPE_STANDARD = 'standard'; -export const LICENSE_TYPE_GOLD = 'gold'; -export const LICENSE_TYPE_PLATINUM = 'platinum'; -export const LICENSE_TYPE_ENTERPRISE = 'enterprise'; -export const LICENSE_TYPE_TRIAL = 'trial'; - -export type LicenseType = - | typeof LICENSE_TYPE_BASIC - | typeof LICENSE_TYPE_STANDARD - | typeof LICENSE_TYPE_GOLD - | typeof LICENSE_TYPE_PLATINUM - | typeof LICENSE_TYPE_ENTERPRISE - | typeof LICENSE_TYPE_TRIAL; - -// These are ordered from least featureful to most featureful, so we can assume that someone holding -// a license at a particular index cannot access any features unlocked by the licenses that follow it. -export const RANKED_LICENSE_TYPES = [ - LICENSE_TYPE_BASIC, - LICENSE_TYPE_STANDARD, - LICENSE_TYPE_GOLD, - LICENSE_TYPE_PLATINUM, - LICENSE_TYPE_ENTERPRISE, - LICENSE_TYPE_TRIAL, -]; diff --git a/x-pack/legacy/common/eui_draggable/index.d.ts b/x-pack/legacy/common/eui_draggable/index.d.ts deleted file mode 100644 index 322966b3c982e..0000000000000 --- a/x-pack/legacy/common/eui_draggable/index.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { EuiDraggable, EuiDragDropContext } from '@elastic/eui'; - -type PropsOf = T extends React.ComponentType ? ComponentProps : never; -type FirstArgumentOf = Func extends (arg1: infer FirstArgument, ...rest: any[]) => any - ? FirstArgument - : never; -export type DragHandleProps = FirstArgumentOf< - Exclude['children'], React.ReactElement> ->['dragHandleProps']; -export type DropResult = FirstArgumentOf['onDragEnd']>; diff --git a/x-pack/legacy/common/eui_styled_components/index.ts b/x-pack/legacy/common/eui_styled_components/index.ts deleted file mode 100644 index 9b3ed903627b4..0000000000000 --- a/x-pack/legacy/common/eui_styled_components/index.ts +++ /dev/null @@ -1,20 +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 { - css, - euiStyled, - EuiTheme, - EuiThemeProvider, - createGlobalStyle, - keyframes, - withTheme, -} from './eui_styled_components'; - -export { css, euiStyled, EuiTheme, EuiThemeProvider, createGlobalStyle, keyframes, withTheme }; -// In order to to mimic the styled-components module we need to ignore the following -// eslint-disable-next-line import/no-default-export -export default euiStyled; diff --git a/x-pack/legacy/common/poller.js b/x-pack/legacy/common/poller.js deleted file mode 100644 index 09824ce9d6d23..0000000000000 --- a/x-pack/legacy/common/poller.js +++ /dev/null @@ -1,79 +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 _ from 'lodash'; - -export class Poller { - constructor(options) { - this.functionToPoll = options.functionToPoll; // Must return a Promise - this.successFunction = options.successFunction || _.noop; - this.errorFunction = options.errorFunction || _.noop; - this.pollFrequencyInMillis = options.pollFrequencyInMillis; - this.trailing = options.trailing || false; - this.continuePollingOnError = options.continuePollingOnError || false; - this.pollFrequencyErrorMultiplier = options.pollFrequencyErrorMultiplier || 1; - this._timeoutId = null; - this._isRunning = false; - } - - getPollFrequency() { - return this.pollFrequencyInMillis; - } - - _poll() { - return this.functionToPoll() - .then(this.successFunction) - .then(() => { - if (!this._isRunning) { - return; - } - - this._timeoutId = setTimeout(this._poll.bind(this), this.pollFrequencyInMillis); - }) - .catch((e) => { - this.errorFunction(e); - if (!this._isRunning) { - return; - } - - if (this.continuePollingOnError) { - this._timeoutId = setTimeout( - this._poll.bind(this), - this.pollFrequencyInMillis * this.pollFrequencyErrorMultiplier - ); - } else { - this.stop(); - } - }); - } - - start() { - if (this._isRunning) { - return; - } - - this._isRunning = true; - if (this.trailing) { - this._timeoutId = setTimeout(this._poll.bind(this), this.pollFrequencyInMillis); - } else { - this._poll(); - } - } - - stop() { - if (!this._isRunning) { - return; - } - - this._isRunning = false; - clearTimeout(this._timeoutId); - this._timeoutId = null; - } - - isRunning() { - return this._isRunning; - } -} diff --git a/x-pack/legacy/plugins/xpack_main/index.js b/x-pack/legacy/plugins/xpack_main/index.js deleted file mode 100644 index a3bd66e744fda..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/index.js +++ /dev/null @@ -1,31 +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 { resolve } from 'path'; -import { setupXPackMain } from './server/lib/setup_xpack_main'; -import { xpackInfoRoute } from './server/routes/api/v1'; - -export const xpackMain = (kibana) => { - return new kibana.Plugin({ - id: 'xpack_main', - configPrefix: 'xpack.xpack_main', - publicDir: resolve(__dirname, 'public'), - require: [], - - config(Joi) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - - init(server) { - setupXPackMain(server); - - // register routes - xpackInfoRoute(server); - }, - }); -}; diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js deleted file mode 100644 index f49f44bed97a7..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js +++ /dev/null @@ -1,68 +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 { BehaviorSubject } from 'rxjs'; -import sinon from 'sinon'; -import { XPackInfo } from '../xpack_info'; -import { setupXPackMain } from '../setup_xpack_main'; - -describe('setupXPackMain()', () => { - const sandbox = sinon.createSandbox(); - - let mockServer; - let mockStatusObservable; - let mockElasticsearchPlugin; - - beforeEach(() => { - sandbox.useFakeTimers(); - - mockElasticsearchPlugin = { - getCluster: sinon.stub(), - }; - - mockStatusObservable = sinon.stub({ subscribe() {} }); - - mockServer = sinon.stub({ - plugins: { - elasticsearch: mockElasticsearchPlugin, - }, - newPlatform: { - setup: { - core: { - status: { - core$: { - pipe() { - return mockStatusObservable; - }, - }, - }, - }, - plugins: { features: {}, licensing: { license$: new BehaviorSubject() } }, - }, - }, - events: { on() {} }, - log() {}, - config() {}, - expose() {}, - ext() {}, - }); - - // Make sure plugins doesn't consume config - const configGetStub = sinon - .stub() - .throws(new Error('`config.get` is called with unexpected key.')); - mockServer.config.returns({ get: configGetStub }); - }); - - afterEach(() => sandbox.restore()); - - it('all extension hooks should be properly initialized.', () => { - setupXPackMain(mockServer); - - sinon.assert.calledWithExactly(mockServer.expose, 'info', sinon.match.instanceOf(XPackInfo)); - sinon.assert.calledWithExactly(mockStatusObservable.subscribe, sinon.match.func); - }); -}); diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/xpack_info.js b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/xpack_info.js deleted file mode 100644 index 81fb822882817..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/xpack_info.js +++ /dev/null @@ -1,398 +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 { createHash } from 'crypto'; -import { BehaviorSubject } from 'rxjs'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; -import { XPackInfo } from '../xpack_info'; -import { licensingMock } from '../../../../../../plugins/licensing/server/mocks'; - -function createLicense(license = {}, features = {}) { - return licensingMock.createLicense({ - license: { - uid: 'custom-uid', - type: 'gold', - mode: 'gold', - status: 'active', - expiryDateInMillis: 1286575200000, - ...license, - }, - features: { - security: { - description: 'Security for the Elastic Stack', - isAvailable: true, - isEnabled: true, - }, - watcher: { - description: 'Alerting, Notification and Automation for the Elastic Stack', - isAvailable: true, - isEnabled: false, - }, - ...features, - }, - }); -} - -function getSignature(object) { - return createHash('md5').update(JSON.stringify(object)).digest('hex'); -} - -describe('XPackInfo', () => { - let mockServer; - let mockElasticsearchPlugin; - - beforeEach(() => { - mockServer = sinon.stub({ - plugins: { elasticsearch: mockElasticsearchPlugin }, - events: { on() {} }, - newPlatform: { - setup: { - plugins: { - licensing: {}, - }, - }, - }, - }); - }); - - describe('refreshNow()', () => { - it('delegates to the new platform licensing plugin', async () => { - const refresh = sinon.spy(); - - const xPackInfo = new XPackInfo(mockServer, { - licensing: { - license$: new BehaviorSubject(createLicense()), - refresh: refresh, - }, - }); - - await xPackInfo.refreshNow(); - - sinon.assert.calledOnce(refresh); - }); - }); - - describe('license', () => { - let xPackInfo; - let license$; - beforeEach(async () => { - license$ = new BehaviorSubject(createLicense()); - xPackInfo = new XPackInfo(mockServer, { - licensing: { - license$, - refresh: () => null, - }, - }); - }); - - it('getUid() shows license uid returned from the license$.', async () => { - expect(xPackInfo.license.getUid()).to.be('custom-uid'); - - license$.next(createLicense({ uid: 'new-custom-uid' })); - - expect(xPackInfo.license.getUid()).to.be('new-custom-uid'); - - license$.next(createLicense({ uid: undefined, error: 'error-reason' })); - - expect(xPackInfo.license.getUid()).to.be(undefined); - }); - - it('isActive() is based on the status returned from the backend.', async () => { - expect(xPackInfo.license.isActive()).to.be(true); - - license$.next(createLicense({ status: 'expired' })); - expect(xPackInfo.license.isActive()).to.be(false); - - license$.next(createLicense({ status: 'some other value' })); - expect(xPackInfo.license.isActive()).to.be(false); - - license$.next(createLicense({ status: 'active' })); - expect(xPackInfo.license.isActive()).to.be(true); - - license$.next(createLicense({ status: undefined, error: 'error-reason' })); - expect(xPackInfo.license.isActive()).to.be(false); - }); - - it('getExpiryDateInMillis() is based on the value returned from the backend.', async () => { - expect(xPackInfo.license.getExpiryDateInMillis()).to.be(1286575200000); - - license$.next(createLicense({ expiryDateInMillis: 10203040 })); - expect(xPackInfo.license.getExpiryDateInMillis()).to.be(10203040); - - license$.next(createLicense({ expiryDateInMillis: undefined, error: 'error-reason' })); - expect(xPackInfo.license.getExpiryDateInMillis()).to.be(undefined); - }); - - it('getType() is based on the value returned from the backend.', async () => { - expect(xPackInfo.license.getType()).to.be('gold'); - - license$.next(createLicense({ type: 'basic' })); - expect(xPackInfo.license.getType()).to.be('basic'); - - license$.next(createLicense({ type: undefined, error: 'error-reason' })); - expect(xPackInfo.license.getType()).to.be(undefined); - }); - - it('isOneOf() correctly determines if current license is presented in the specified list.', async () => { - expect(xPackInfo.license.isOneOf('gold')).to.be(true); - expect(xPackInfo.license.isOneOf(['gold', 'basic'])).to.be(true); - expect(xPackInfo.license.isOneOf(['platinum', 'basic'])).to.be(false); - expect(xPackInfo.license.isOneOf('standard')).to.be(false); - - license$.next(createLicense({ mode: 'basic' })); - - expect(xPackInfo.license.isOneOf('basic')).to.be(true); - expect(xPackInfo.license.isOneOf(['gold', 'basic'])).to.be(true); - expect(xPackInfo.license.isOneOf(['platinum', 'gold'])).to.be(false); - expect(xPackInfo.license.isOneOf('standard')).to.be(false); - }); - }); - - describe('feature', () => { - let xPackInfo; - let license$; - beforeEach(async () => { - license$ = new BehaviorSubject( - createLicense( - {}, - { - feature: { - isAvailable: false, - isEnabled: true, - }, - } - ) - ); - xPackInfo = new XPackInfo(mockServer, { - licensing: { - license$, - refresh: () => null, - }, - }); - }); - - it('isAvailable() checks whether particular feature is available.', async () => { - const availableFeatureOne = xPackInfo.feature('security'); - const availableFeatureTwo = xPackInfo.feature('watcher'); - const unavailableFeatureOne = xPackInfo.feature('feature'); - const unavailableFeatureTwo = xPackInfo.feature('non-existing-feature'); - - expect(availableFeatureOne.isAvailable()).to.be(true); - expect(availableFeatureTwo.isAvailable()).to.be(true); - expect(unavailableFeatureOne.isAvailable()).to.be(false); - expect(unavailableFeatureTwo.isAvailable()).to.be(false); - }); - - it('isEnabled() checks whether particular feature is enabled.', async () => { - const enabledFeatureOne = xPackInfo.feature('security'); - const enabledFeatureTwo = xPackInfo.feature('feature'); - const disabledFeatureOne = xPackInfo.feature('watcher'); - const disabledFeatureTwo = xPackInfo.feature('non-existing-feature'); - - expect(enabledFeatureOne.isEnabled()).to.be(true); - expect(enabledFeatureTwo.isEnabled()).to.be(true); - expect(disabledFeatureOne.isEnabled()).to.be(false); - expect(disabledFeatureTwo.isEnabled()).to.be(false); - }); - - it('registerLicenseCheckResultsGenerator() allows to fill in XPack Info feature specific info.', async () => { - const securityFeature = xPackInfo.feature('security'); - const watcherFeature = xPackInfo.feature('watcher'); - - expect(xPackInfo.toJSON().features.security).to.be(undefined); - expect(xPackInfo.toJSON().features.watcher).to.be(undefined); - - securityFeature.registerLicenseCheckResultsGenerator((info) => { - return { - isXPackInfo: info instanceof XPackInfo, - license: info.license.getType(), - someCustomValue: 100500, - }; - }); - - expect(xPackInfo.toJSON().features.security).to.eql({ - isXPackInfo: true, - license: 'gold', - someCustomValue: 100500, - }); - expect(xPackInfo.toJSON().features.watcher).to.be(undefined); - - watcherFeature.registerLicenseCheckResultsGenerator((info) => { - return { - isXPackInfo: info instanceof XPackInfo, - license: info.license.getType(), - someAnotherCustomValue: 500100, - }; - }); - - expect(xPackInfo.toJSON().features.security).to.eql({ - isXPackInfo: true, - license: 'gold', - someCustomValue: 100500, - }); - expect(xPackInfo.toJSON().features.watcher).to.eql({ - isXPackInfo: true, - license: 'gold', - someAnotherCustomValue: 500100, - }); - - license$.next(createLicense({ type: 'platinum' })); - - expect(xPackInfo.toJSON().features.security).to.eql({ - isXPackInfo: true, - license: 'platinum', - someCustomValue: 100500, - }); - expect(xPackInfo.toJSON().features.watcher).to.eql({ - isXPackInfo: true, - license: 'platinum', - someAnotherCustomValue: 500100, - }); - }); - - it('getLicenseCheckResults() correctly returns feature specific info.', async () => { - const securityFeature = xPackInfo.feature('security'); - const watcherFeature = xPackInfo.feature('watcher'); - - expect(securityFeature.getLicenseCheckResults()).to.be(undefined); - expect(watcherFeature.getLicenseCheckResults()).to.be(undefined); - - securityFeature.registerLicenseCheckResultsGenerator((info) => { - return { - isXPackInfo: info instanceof XPackInfo, - license: info.license.getType(), - someCustomValue: 100500, - }; - }); - - expect(securityFeature.getLicenseCheckResults()).to.eql({ - isXPackInfo: true, - license: 'gold', - someCustomValue: 100500, - }); - expect(watcherFeature.getLicenseCheckResults()).to.be(undefined); - - watcherFeature.registerLicenseCheckResultsGenerator((info) => { - return { - isXPackInfo: info instanceof XPackInfo, - license: info.license.getType(), - someAnotherCustomValue: 500100, - }; - }); - - expect(securityFeature.getLicenseCheckResults()).to.eql({ - isXPackInfo: true, - license: 'gold', - someCustomValue: 100500, - }); - expect(watcherFeature.getLicenseCheckResults()).to.eql({ - isXPackInfo: true, - license: 'gold', - someAnotherCustomValue: 500100, - }); - - license$.next(createLicense({ type: 'platinum' })); - - expect(securityFeature.getLicenseCheckResults()).to.eql({ - isXPackInfo: true, - license: 'platinum', - someCustomValue: 100500, - }); - expect(watcherFeature.getLicenseCheckResults()).to.eql({ - isXPackInfo: true, - license: 'platinum', - someAnotherCustomValue: 500100, - }); - }); - }); - - it('onLicenseInfoChange() allows to subscribe to license update', async () => { - const license$ = new BehaviorSubject(createLicense()); - - const xPackInfo = new XPackInfo(mockServer, { - licensing: { - license$, - refresh: () => null, - }, - }); - - const watcherFeature = xPackInfo.feature('watcher'); - watcherFeature.registerLicenseCheckResultsGenerator((info) => ({ - type: info.license.getType(), - })); - - const statuses = []; - xPackInfo.onLicenseInfoChange(() => statuses.push(watcherFeature.getLicenseCheckResults())); - - license$.next(createLicense({ type: 'basic' })); - expect(statuses).to.eql([{ type: 'basic' }]); - - license$.next(createLicense({ type: 'trial' })); - expect(statuses).to.eql([{ type: 'basic' }, { type: 'trial' }]); - }); - - it('refreshNow() leads to onLicenseInfoChange()', async () => { - const license$ = new BehaviorSubject(createLicense()); - - const xPackInfo = new XPackInfo(mockServer, { - licensing: { - license$, - refresh: () => license$.next({ type: 'basic' }), - }, - }); - - const watcherFeature = xPackInfo.feature('watcher'); - - watcherFeature.registerLicenseCheckResultsGenerator((info) => ({ - type: info.license.getType(), - })); - - const statuses = []; - xPackInfo.onLicenseInfoChange(() => statuses.push(watcherFeature.getLicenseCheckResults())); - - await xPackInfo.refreshNow(); - expect(statuses).to.eql([{ type: 'basic' }]); - }); - - it('getSignature() returns correct signature.', async () => { - const license$ = new BehaviorSubject(createLicense()); - const xPackInfo = new XPackInfo(mockServer, { - licensing: { - license$, - refresh: () => null, - }, - }); - - expect(xPackInfo.getSignature()).to.be( - getSignature({ - license: { - type: 'gold', - isActive: true, - expiryDateInMillis: 1286575200000, - }, - features: {}, - }) - ); - - license$.next(createLicense({ type: 'platinum', expiryDateInMillis: 20304050 })); - - const expectedSignature = getSignature({ - license: { - type: 'platinum', - isActive: true, - expiryDateInMillis: 20304050, - }, - features: {}, - }); - expect(xPackInfo.getSignature()).to.be(expectedSignature); - - // Should stay the same after refresh if nothing changed. - license$.next(createLicense({ type: 'platinum', expiryDateInMillis: 20304050 })); - - expect(xPackInfo.getSignature()).to.be(expectedSignature); - }); -}); diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js b/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js deleted file mode 100644 index fd4e3c86d0ca7..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js +++ /dev/null @@ -1,33 +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 { pairwise } from 'rxjs/operators'; -import { XPackInfo } from './xpack_info'; - -/** - * Setup the X-Pack Main plugin. This is fired every time that the Elasticsearch plugin becomes Green. - * - * This will ensure that X-Pack is installed on the Elasticsearch cluster, as well as trigger the initial - * polling for _xpack/info. - * - * @param server {Object} The Kibana server object. - */ -export function setupXPackMain(server) { - const info = new XPackInfo(server, { licensing: server.newPlatform.setup.plugins.licensing }); - - server.expose('info', info); - - // trigger an xpack info refresh whenever the elasticsearch plugin status changes - server.newPlatform.setup.core.status.core$ - .pipe(pairwise()) - .subscribe(async ([coreLast, coreCurrent]) => { - if (coreLast.elasticsearch.level !== coreCurrent.elasticsearch.level) { - await info.refreshNow(); - } - }); - - return info; -} diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.ts b/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.ts deleted file mode 100644 index aa66532a2897d..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info.ts +++ /dev/null @@ -1,240 +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 { createHash } from 'crypto'; -import { Legacy } from 'kibana'; - -import { XPackInfoLicense } from './xpack_info_license'; - -import { LicensingPluginSetup, ILicense } from '../../../../../plugins/licensing/server'; - -export interface XPackInfoOptions { - clusterSource?: string; - pollFrequencyInMillis: number; -} - -type LicenseGeneratorCheck = (xpackInfo: XPackInfo) => any; - -export interface XPackFeature { - isAvailable(): boolean; - isEnabled(): boolean; - registerLicenseCheckResultsGenerator(generator: LicenseGeneratorCheck): void; - getLicenseCheckResults(): any; -} - -interface Deps { - licensing: LicensingPluginSetup; -} - -/** - * A helper that provides a convenient way to access XPack Info returned by Elasticsearch. - */ -export class XPackInfo { - /** - * XPack License object. - * @type {XPackInfoLicense} - * @private - */ - _license: XPackInfoLicense; - - /** - * Feature name <-> feature license check generator function mapping. - * @type {Map} - * @private - */ - _featureLicenseCheckResultsGenerators = new Map(); - - /** - * Set of listener functions that will be called whenever the license - * info changes - * @type {Set} - */ - _licenseInfoChangedListeners = new Set<() => void>(); - - /** - * Cache that may contain last xpack info API response or error, json representation - * of xpack info and xpack info signature. - * @type {{response: Object|undefined, error: Object|undefined, json: Object|undefined, signature: string|undefined}} - * @private - */ - private _cache: { - license?: ILicense; - error?: string; - json?: Record; - signature?: string; - }; - - /** - * XPack License instance. - * @returns {XPackInfoLicense} - */ - public get license() { - return this._license; - } - - private readonly licensingPlugin: LicensingPluginSetup; - - /** - * Constructs XPack info object. - * @param {Hapi.Server} server HapiJS server instance. - */ - constructor(server: Legacy.Server, deps: Deps) { - if (!deps.licensing) { - throw new Error('XPackInfo requires enabled Licensing plugin'); - } - this.licensingPlugin = deps.licensing; - - this._cache = {}; - - this.licensingPlugin.license$.subscribe((license: ILicense) => { - if (license.isActive) { - this._cache = { - license, - error: undefined, - }; - } else { - this._cache = { - license, - error: license.error, - }; - } - - this._licenseInfoChangedListeners.forEach((fn) => fn()); - }); - - this._license = new XPackInfoLicense(() => this._cache.license); - } - - /** - * Checks whether XPack info is available. - * @returns {boolean} - */ - isAvailable() { - return Boolean(this._cache.license?.isAvailable); - } - - /** - * Checks whether ES was available - * @returns {boolean} - */ - isXpackUnavailable() { - return ( - this._cache.error && - this._cache.error === 'X-Pack plugin is not installed on the Elasticsearch cluster.' - ); - } - - /** - * If present, describes the reason why XPack info is not available. - * @returns {Error|string} - */ - unavailableReason() { - return this._cache.license?.getUnavailableReason(); - } - - onLicenseInfoChange(handler: () => void) { - this._licenseInfoChangedListeners.add(handler); - } - - /** - * Queries server to get the updated XPack info. - * @returns {Promise.} - */ - async refreshNow() { - await this.licensingPlugin.refresh(); - return this; - } - - /** - * Returns a wrapper around XPack info that gives an access to the properties of - * the specific feature. - * @param {string} name Name of the feature to get a wrapper for. - * @returns {Object} - */ - feature(name: string): XPackFeature { - return { - /** - * Checks whether feature is available (permitted by the current license). - * @returns {boolean} - */ - isAvailable: () => { - return Boolean(this._cache.license?.getFeature(name).isAvailable); - }, - - /** - * Checks whether feature is enabled (not disabled by the configuration specifically). - * @returns {boolean} - */ - isEnabled: () => { - return Boolean(this._cache.license?.getFeature(name).isEnabled); - }, - - /** - * Registers a `generator` function that will be called with XPackInfo instance as - * argument whenever XPack info changes. Whatever `generator` returns will be stored - * in XPackInfo JSON representation and can be accessed with `getLicenseCheckResults`. - * @param {Function} generator Function to call whenever XPackInfo changes. - */ - registerLicenseCheckResultsGenerator: (generator: LicenseGeneratorCheck) => { - this._featureLicenseCheckResultsGenerators.set(name, generator); - - // Since JSON representation and signature are cached we should invalidate them to - // include results from newly registered generator when they are requested. - this._cache.json = undefined; - this._cache.signature = undefined; - }, - - /** - * Returns license check results that were previously produced by the `generator` function. - * @returns {Object} - */ - getLicenseCheckResults: () => this.toJSON().features[name], - }; - } - - /** - * Extracts string md5 hash from the stringified version of license JSON representation. - * @returns {string} - */ - getSignature() { - if (this._cache.signature) { - return this._cache.signature; - } - - this._cache.signature = createHash('md5').update(JSON.stringify(this.toJSON())).digest('hex'); - - return this._cache.signature; - } - - /** - * Returns JSON representation of the license object that is suitable for serialization. - * @returns {Object} - */ - toJSON() { - if (this._cache.json) { - return this._cache.json; - } - - this._cache.json = { - license: { - type: this.license.getType(), - isActive: this.license.isActive(), - expiryDateInMillis: this.license.getExpiryDateInMillis(), - }, - features: {}, - }; - - // Set response elements specific to each feature. To do this, - // call the license check results generator for each feature, passing them - // the xpack info object - for (const [feature, licenseChecker] of this._featureLicenseCheckResultsGenerators) { - // return value expected to be a dictionary object. - this._cache.json.features[feature] = licenseChecker(this); - } - - return this._cache.json; - } -} diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.test.js b/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.test.js deleted file mode 100644 index ccb5742216ca7..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.test.js +++ /dev/null @@ -1,207 +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 { licensingMock } from '../../../../../plugins/licensing/server/mocks'; -import { XPackInfoLicense } from './xpack_info_license'; - -function getXPackInfoLicense(getRawLicense) { - return new XPackInfoLicense(getRawLicense); -} - -describe('XPackInfoLicense', () => { - const xpackInfoLicenseUndefined = getXPackInfoLicense(() => {}); - let xpackInfoLicense; - let getRawLicense; - - beforeEach(() => { - getRawLicense = jest.fn(); - xpackInfoLicense = getXPackInfoLicense(getRawLicense); - }); - - test('getUid returns uid field', () => { - const uid = 'abc123'; - - getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { uid } })); - - expect(xpackInfoLicense.getUid()).toBe(uid); - expect(getRawLicense).toHaveBeenCalledTimes(1); - - expect(xpackInfoLicenseUndefined.getUid()).toBe(undefined); - }); - - test('isActive returns true if status is active', () => { - getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'active' } })); - - expect(xpackInfoLicense.isActive()).toBe(true); - expect(getRawLicense).toHaveBeenCalledTimes(1); - }); - - test('isActive returns false if status is not active', () => { - getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { status: 'aCtIvE' } })); // needs to match exactly - - expect(xpackInfoLicense.isActive()).toBe(false); - expect(getRawLicense).toHaveBeenCalledTimes(1); - - expect(xpackInfoLicenseUndefined.isActive()).toBe(false); - }); - - test('getExpiryDateInMillis returns expiry_date_in_millis', () => { - getRawLicense.mockReturnValue( - licensingMock.createLicense({ license: { expiryDateInMillis: 123 } }) - ); - - expect(xpackInfoLicense.getExpiryDateInMillis()).toBe(123); - expect(getRawLicense).toHaveBeenCalledTimes(1); - - expect(xpackInfoLicenseUndefined.getExpiryDateInMillis()).toBe(undefined); - }); - - test('isOneOf returns true of the mode includes one of the types', () => { - getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { mode: 'platinum' } })); - - expect(xpackInfoLicense.isOneOf('platinum')).toBe(true); - expect(getRawLicense).toHaveBeenCalledTimes(1); - - expect(xpackInfoLicense.isOneOf(['platinum'])).toBe(true); - expect(getRawLicense).toHaveBeenCalledTimes(2); - expect(xpackInfoLicense.isOneOf(['gold', 'platinum'])).toBe(true); - expect(getRawLicense).toHaveBeenCalledTimes(3); - expect(xpackInfoLicense.isOneOf(['platinum', 'gold'])).toBe(true); - expect(getRawLicense).toHaveBeenCalledTimes(4); - expect(xpackInfoLicense.isOneOf(['basic', 'gold'])).toBe(false); - expect(getRawLicense).toHaveBeenCalledTimes(5); - expect(xpackInfoLicense.isOneOf(['basic'])).toBe(false); - expect(getRawLicense).toHaveBeenCalledTimes(6); - - expect(xpackInfoLicenseUndefined.isOneOf(['platinum', 'gold'])).toBe(false); - }); - - test('getType returns the type', () => { - getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { type: 'basic' } })); - - expect(xpackInfoLicense.getType()).toBe('basic'); - expect(getRawLicense).toHaveBeenCalledTimes(1); - - getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { type: 'gold' } })); - - expect(xpackInfoLicense.getType()).toBe('gold'); - expect(getRawLicense).toHaveBeenCalledTimes(2); - - expect(xpackInfoLicenseUndefined.getType()).toBe(undefined); - }); - - test('getMode returns the mode', () => { - getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { mode: 'basic' } })); - - expect(xpackInfoLicense.getMode()).toBe('basic'); - expect(getRawLicense).toHaveBeenCalledTimes(1); - - getRawLicense.mockReturnValue(licensingMock.createLicense({ license: { mode: 'gold' } })); - - expect(xpackInfoLicense.getMode()).toBe('gold'); - expect(getRawLicense).toHaveBeenCalledTimes(2); - - expect(xpackInfoLicenseUndefined.getMode()).toBe(undefined); - }); - - test('isActiveLicense returns the true if active and typeChecker matches', () => { - const expectAbc123 = (type) => type === 'abc123'; - - getRawLicense.mockReturnValue( - licensingMock.createLicense({ license: { status: 'active', mode: 'abc123' } }) - ); - - expect(xpackInfoLicense.isActiveLicense(expectAbc123)).toBe(true); - expect(getRawLicense).toHaveBeenCalledTimes(1); - - getRawLicense.mockReturnValue( - licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'abc123' } }) - ); - - expect(xpackInfoLicense.isActiveLicense(expectAbc123)).toBe(false); - expect(getRawLicense).toHaveBeenCalledTimes(2); - - getRawLicense.mockReturnValue( - licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'NOTabc123' } }) - ); - - expect(xpackInfoLicense.isActiveLicense(expectAbc123)).toBe(false); - expect(getRawLicense).toHaveBeenCalledTimes(3); - - getRawLicense.mockReturnValue( - licensingMock.createLicense({ license: { status: 'active', mode: 'NOTabc123' } }) - ); - - expect(xpackInfoLicense.isActiveLicense(expectAbc123)).toBe(false); - expect(getRawLicense).toHaveBeenCalledTimes(4); - - expect(xpackInfoLicenseUndefined.isActive(expectAbc123)).toBe(false); - }); - - test('isBasic returns the true if active and basic', () => { - getRawLicense.mockReturnValue( - licensingMock.createLicense({ license: { status: 'active', mode: 'basic' } }) - ); - - expect(xpackInfoLicense.isBasic()).toBe(true); - expect(getRawLicense).toHaveBeenCalledTimes(1); - - getRawLicense.mockReturnValue( - licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'gold' } }) - ); - - expect(xpackInfoLicense.isBasic()).toBe(false); - expect(getRawLicense).toHaveBeenCalledTimes(2); - - getRawLicense.mockReturnValue( - licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'trial' } }) - ); - - expect(xpackInfoLicense.isBasic()).toBe(false); - expect(getRawLicense).toHaveBeenCalledTimes(3); - - getRawLicense.mockReturnValue( - licensingMock.createLicense({ license: { status: 'active', mode: 'platinum' } }) - ); - - expect(xpackInfoLicense.isBasic()).toBe(false); - expect(getRawLicense).toHaveBeenCalledTimes(4); - - expect(xpackInfoLicenseUndefined.isBasic()).toBe(false); - }); - - test('isNotBasic returns the true if active and not basic', () => { - getRawLicense.mockReturnValue( - licensingMock.createLicense({ license: { status: 'active', mode: 'platinum' } }) - ); - - expect(xpackInfoLicense.isNotBasic()).toBe(true); - expect(getRawLicense).toHaveBeenCalledTimes(1); - - getRawLicense.mockReturnValue( - licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'gold' } }) - ); - - expect(xpackInfoLicense.isNotBasic()).toBe(false); - expect(getRawLicense).toHaveBeenCalledTimes(2); - - getRawLicense.mockReturnValue( - licensingMock.createLicense({ license: { status: 'NOTactive', mode: 'trial' } }) - ); - - expect(xpackInfoLicense.isNotBasic()).toBe(false); - expect(getRawLicense).toHaveBeenCalledTimes(3); - - getRawLicense.mockReturnValue( - licensingMock.createLicense({ license: { status: 'active', mode: 'basic' } }) - ); - - expect(xpackInfoLicense.isNotBasic()).toBe(false); - expect(getRawLicense).toHaveBeenCalledTimes(4); - - expect(xpackInfoLicenseUndefined.isNotBasic()).toBe(false); - }); -}); diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.ts b/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.ts deleted file mode 100644 index dd53f63909475..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/lib/xpack_info_license.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ILicense } from '../../../../../plugins/licensing/server'; - -/** - * "View" for XPack Info license information. - */ -export class XPackInfoLicense { - /** - * Function that retrieves license information from the XPack info object. - * @type {Function} - * @private - */ - _getRawLicense: () => ILicense | undefined; - - constructor(getRawLicense: () => ILicense | undefined) { - this._getRawLicense = getRawLicense; - } - - /** - * Returns unique identifier of the license. - * @returns {string|undefined} - */ - getUid() { - return this._getRawLicense()?.uid; - } - - /** - * Indicates whether license is still active. - * @returns {boolean} - */ - isActive() { - return Boolean(this._getRawLicense()?.isActive); - } - - /** - * Returns license expiration date in ms. - * - * Note: A basic license created after 6.3 will have no expiration, thus returning undefined. - * - * @returns {number|undefined} - */ - getExpiryDateInMillis() { - return this._getRawLicense()?.expiryDateInMillis; - } - - /** - * Checks if the license is represented in a specified license list. - * @param {String} candidateLicenses List of the licenses to check against. - * @returns {boolean} - */ - isOneOf(candidateLicenses: string | string[]) { - const candidates = Array.isArray(candidateLicenses) ? candidateLicenses : [candidateLicenses]; - const mode = this._getRawLicense()?.mode; - return Boolean(mode && candidates.includes(mode)); - } - - /** - * Returns type of the license (basic, gold etc.). - * @returns {string|undefined} - */ - getType() { - return this._getRawLicense()?.type; - } - - /** - * Returns mode of the license (basic, gold etc.). This is the "effective" type of the license. - * @returns {string|undefined} - */ - getMode() { - return this._getRawLicense()?.mode; - } - - /** - * Determine if the current license is active and the supplied {@code type}. - * - * @param {Function} typeChecker The license type checker. - * @returns {boolean} - */ - isActiveLicense(typeChecker: (mode: string) => boolean) { - const license = this._getRawLicense(); - - return Boolean(license?.isActive && typeChecker(license.mode as any)); - } - - /** - * Determine if the license is an active, basic license. - * - * Note: This also verifies that the license is active. Therefore it is not safe to assume that !isBasic() === isNotBasic(). - * - * @returns {boolean} - */ - isBasic() { - return this.isActiveLicense((mode) => mode === 'basic'); - } - - /** - * Determine if the license is an active, non-basic license (e.g., standard, gold, platinum, or trial). - * - * Note: This also verifies that the license is active. Therefore it is not safe to assume that !isBasic() === isNotBasic(). - * - * @returns {boolean} - */ - isNotBasic() { - return this.isActiveLicense((mode) => mode !== 'basic'); - } -} diff --git a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/__tests__/xpack_info.js b/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/__tests__/xpack_info.js deleted file mode 100644 index 540d9f63ea6c8..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/__tests__/xpack_info.js +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import sinon from 'sinon'; - -import { xpackInfoRoute } from '../xpack_info'; - -describe('XPackInfo routes', () => { - let serverStub; - beforeEach(() => { - serverStub = { - route: sinon.stub(), - plugins: { - xpack_main: { - info: sinon.stub({ isAvailable() {}, toJSON() {} }), - }, - }, - }; - - xpackInfoRoute(serverStub); - }); - - it('correctly initialize XPack Info route.', () => { - sinon.assert.calledWithExactly(serverStub.route, { - method: 'GET', - path: '/api/xpack/v1/info', - handler: sinon.match.func, - }); - }); - - it('replies with `Not Found` Boom error if `xpackInfo` is not available.', () => { - serverStub.plugins.xpack_main.info.isAvailable.returns(false); - - const onRouteHandler = serverStub.route.firstCall.args[0].handler; - const response = onRouteHandler(); - - expect(response.isBoom).to.be(true); - expect(response.message).to.be('Not Found'); - expect(response.output.statusCode).to.be(404); - }); - - it('replies with pre-processed `xpackInfo` if it is available.', () => { - serverStub.plugins.xpack_main.info.isAvailable.returns(true); - serverStub.plugins.xpack_main.info.toJSON.returns({ - license: { - type: 'gold', - isActive: true, - expiryDateInMillis: 1509368280381, - }, - features: { - security: { - showLogin: true, - allowLogin: true, - showLinks: false, - allowRoleDocumentLevelSecurity: false, - allowRoleFieldLevelSecurity: false, - }, - }, - }); - - const onRouteHandler = serverStub.route.firstCall.args[0].handler; - const response = onRouteHandler(); - - expect(response).to.eql({ - license: { - type: 'gold', - is_active: true, - expiry_date_in_millis: 1509368280381, - }, - features: { - security: { - show_login: true, - allow_login: true, - show_links: false, - allow_role_document_level_security: false, - allow_role_field_level_security: false, - }, - }, - }); - }); -}); diff --git a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/xpack_info.js b/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/xpack_info.js deleted file mode 100644 index 3cc57ae9fcab4..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/xpack_info.js +++ /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 Boom from 'boom'; -import { convertKeysToSnakeCaseDeep } from '../../../../../../server/lib/key_case_converter'; - -/* - * A route to provide the basic XPack info for the production cluster - */ -export function xpackInfoRoute(server) { - server.route({ - method: 'GET', - path: '/api/xpack/v1/info', - handler() { - const xPackInfo = server.plugins.xpack_main.info; - - return xPackInfo.isAvailable() - ? convertKeysToSnakeCaseDeep(xPackInfo.toJSON()) - : Boom.notFound(); - }, - }); -} diff --git a/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts b/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts deleted file mode 100644 index c2ec5662ad12e..0000000000000 --- a/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts +++ /dev/null @@ -1,14 +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 KbnServer from 'src/legacy/server/kbn_server'; -import { KibanaFeature } from '../../../../plugins/features/server'; -import { XPackInfo, XPackInfoOptions } from './lib/xpack_info'; -export { XPackFeature } from './lib/xpack_info'; - -export interface XPackMainPlugin { - info: XPackInfo; -} diff --git a/x-pack/legacy/server/lib/__tests__/key_case_converter.js b/x-pack/legacy/server/lib/__tests__/key_case_converter.js deleted file mode 100644 index 7ed9fa668ae66..0000000000000 --- a/x-pack/legacy/server/lib/__tests__/key_case_converter.js +++ /dev/null @@ -1,117 +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 expect from '@kbn/expect'; -import { convertKeysToSnakeCaseDeep, convertKeysToCamelCaseDeep } from '../key_case_converter'; - -describe('key_case_converter', () => { - let testObject; - - beforeEach(() => { - testObject = { - topLevelKey1: { - innerLevelKey1: 17, - inner_level_key2: [19, 31], - }, - top_level_key2: { - innerLevelKey1: 'foo_fooFoo', - inner_level_key2: [{ foo_bar: 29 }, { barBar: 37 }], - }, - }; - }); - - describe('convertKeysToSnakeCaseDeep', () => { - it('should recursively convert camelCase keys to snake_case keys', () => { - const expectedResultObject = { - top_level_key_1: { - inner_level_key_1: 17, - inner_level_key_2: [19, 31], - }, - top_level_key_2: { - inner_level_key_1: 'foo_fooFoo', - inner_level_key_2: [{ foo_bar: 29 }, { bar_bar: 37 }], - }, - }; - expect(convertKeysToSnakeCaseDeep(testObject)).to.eql(expectedResultObject); - }); - - it('should not modify original object', () => { - convertKeysToSnakeCaseDeep(testObject); - expect(Object.keys(testObject)).to.contain('topLevelKey1'); - expect(Object.keys(testObject.topLevelKey1)).to.contain('innerLevelKey1'); - }); - - it('should preserve inner arrays', () => { - const result = convertKeysToSnakeCaseDeep(testObject); - expect(testObject.topLevelKey1.inner_level_key2).to.be.an(Array); - expect(result.top_level_key_1.inner_level_key_2).to.be.an(Array); - }); - - it('should preserve top-level arrays', () => { - testObject = [{ foo_bar: 17 }, [19, { barBaz: 'qux' }]]; - const expectedResultObject = [{ foo_bar: 17 }, [19, { bar_baz: 'qux' }]]; - const result = convertKeysToSnakeCaseDeep(testObject); - expect(testObject).to.be.an(Array); - expect(testObject[1]).to.be.an(Array); - expect(result).to.be.an(Array); - expect(result[1]).to.be.an(Array); - expect(result).to.eql(expectedResultObject); - }); - - it('should throw an error if something other an object or array is passed in', () => { - const expectedErrorMessageRegexp = /Specified object should be an Object or Array/; - expect(convertKeysToSnakeCaseDeep) - .withArgs('neither an object nor an array') - .to.throwException(expectedErrorMessageRegexp); - }); - }); - - describe('convertKeysToCamelCaseDeep', () => { - it('should recursively convert snake_case keys to camelCase keys', () => { - const expectedResultObject = { - topLevelKey1: { - innerLevelKey1: 17, - innerLevelKey2: [19, 31], - }, - topLevelKey2: { - innerLevelKey1: 'foo_fooFoo', - innerLevelKey2: [{ fooBar: 29 }, { barBar: 37 }], - }, - }; - expect(convertKeysToCamelCaseDeep(testObject)).to.eql(expectedResultObject); - }); - - it('should not modify original object', () => { - convertKeysToCamelCaseDeep(testObject); - expect(Object.keys(testObject)).to.contain('top_level_key2'); - expect(Object.keys(testObject.topLevelKey1)).to.contain('inner_level_key2'); - }); - - it('should preserve inner arrays', () => { - const result = convertKeysToCamelCaseDeep(testObject); - expect(testObject.topLevelKey1.inner_level_key2).to.be.an(Array); - expect(result.topLevelKey1.innerLevelKey2).to.be.an(Array); - }); - - it('should preserve top-level arrays', () => { - testObject = [{ foo_bar: 17 }, [19, { barBaz: 'qux' }]]; - const expectedResultObject = [{ fooBar: 17 }, [19, { barBaz: 'qux' }]]; - const result = convertKeysToCamelCaseDeep(testObject); - expect(testObject).to.be.an(Array); - expect(testObject[1]).to.be.an(Array); - expect(result).to.be.an(Array); - expect(result[1]).to.be.an(Array); - expect(result).to.eql(expectedResultObject); - }); - - it('should throw an error if something other an object or array is passed in', () => { - const expectedErrorMessageRegexp = /Specified object should be an Object or Array/; - expect(convertKeysToCamelCaseDeep) - .withArgs('neither an object nor an array') - .to.throwException(expectedErrorMessageRegexp); - }); - }); -}); diff --git a/x-pack/legacy/server/lib/__tests__/kibana_state.js b/x-pack/legacy/server/lib/__tests__/kibana_state.js deleted file mode 100644 index d1b4142b10446..0000000000000 --- a/x-pack/legacy/server/lib/__tests__/kibana_state.js +++ /dev/null @@ -1,129 +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 expect from '@kbn/expect'; -import rison from 'rison-node'; -import { parseKibanaState } from '../parse_kibana_state'; - -const stateIndices = { - global: '_g', - app: '_a', -}; -const globalTime = - '(refreshInterval:(display:Off,pause:!f,value:0),time:(from:now-15m,mode:quick,to:now))'; - -describe('Kibana state', function () { - describe('type checking', function () { - it('should throw if not given an object', function () { - const fn = () => parseKibanaState('i am not an object', 'global'); - const fn2 = () => parseKibanaState(['arrays are not valid either'], 'global'); - expect(fn).to.throwException(/must be an object/i); - expect(fn2).to.throwException(/must be an object/i); - }); - - it('should throw with invalid type', function () { - const fn = () => parseKibanaState({}, 'this is an invalid state type'); - expect(fn).to.throwException(/unknown state type/i); - }); - }); - - describe('value of exists', function () { - it('should be false if state does not exist', function () { - const state = parseKibanaState({}, 'global'); - expect(state.exists).to.equal(false); - }); - - it('should be true if state exists', function () { - const query = {}; - query[stateIndices.global] = rison.encode({ hello: 'world' }); - const state = parseKibanaState(query, 'global'); - expect(state.exists).to.equal(true); - }); - }); - - describe('instance methods', function () { - let query; - - beforeEach(function () { - query = {}; - query[stateIndices.global] = globalTime; - }); - - describe('get', function () { - it('should return the value', function () { - const state = parseKibanaState(query, 'global'); - const { refreshInterval } = rison.decode(globalTime); - expect(state.get('refreshInterval')).to.eql(refreshInterval); - }); - - it('should use the default value for missing props', function () { - const defaultValue = 'default value'; - const state = parseKibanaState(query, 'global'); - expect(state.get('no such value', defaultValue)).to.equal(defaultValue); - }); - }); - - describe('set', function () { - it('should update the value of the state', function () { - const state = parseKibanaState(query, 'global'); - expect(state.get('refreshInterval.pause')).to.equal(false); - - state.set(['refreshInterval', 'pause'], true); - expect(state.get('refreshInterval.pause')).to.equal(true); - }); - - it('should create new properties', function () { - const prop = 'newProp'; - const value = 12345; - const state = parseKibanaState(query, 'global'); - expect(state.get(prop)).to.be(undefined); - - state.set(prop, value); - expect(state.get(prop)).to.not.be(undefined); - expect(state.get(prop)).to.equal(value); - }); - }); - - describe('removing properties', function () { - it('should remove a single value', function () { - const state = parseKibanaState(query, 'global'); - expect(state.get('refreshInterval')).to.be.an('object'); - - state.removeProps('refreshInterval'); - expect(state.get('refreshInterval')).to.be(undefined); - }); - - it('should remove multiple values', function () { - const state = parseKibanaState(query, 'global'); - expect(state.get('refreshInterval')).to.be.an('object'); - expect(state.get('time')).to.be.an('object'); - - state.removeProps(['refreshInterval', 'time']); - expect(state.get('refreshInterval')).to.be(undefined); - expect(state.get('time')).to.be(undefined); - }); - }); - - describe('toString', function () { - it('should rison encode the state', function () { - const state = parseKibanaState(query, 'global'); - expect(state.toString()).to.equal(globalTime); - }); - }); - - describe('toQuery', function () { - it('should return an object', function () { - const state = parseKibanaState(query, 'global'); - expect(state.toQuery()).to.be.an('object'); - }); - - it('should contain the kibana state property', function () { - const state = parseKibanaState(query, 'global'); - expect(state.toQuery()).to.have.property(stateIndices.global, globalTime); - }); - }); - }); -}); diff --git a/x-pack/legacy/server/lib/check_license/check_license.js b/x-pack/legacy/server/lib/check_license/check_license.js deleted file mode 100644 index 7695755622310..0000000000000 --- a/x-pack/legacy/server/lib/check_license/check_license.js +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { - LICENSE_STATUS_UNAVAILABLE, - LICENSE_STATUS_INVALID, - LICENSE_STATUS_EXPIRED, - LICENSE_STATUS_VALID, - RANKED_LICENSE_TYPES, -} from '../../../common/constants'; - -export function checkLicense(pluginName, minimumLicenseRequired, xpackLicenseInfo) { - if (!minimumLicenseRequired) { - throw new Error( - `Error checking license for plugin "${pluginName}". The minimum license required has not been provided.` - ); - } - - if (!RANKED_LICENSE_TYPES.includes(minimumLicenseRequired)) { - throw new Error(`Invalid license type supplied to checkLicense: ${minimumLicenseRequired}`); - } - - // If, for some reason, we cannot get the license information - // from Elasticsearch, assume worst case and disable - if (!xpackLicenseInfo || !xpackLicenseInfo.isAvailable()) { - return { - status: LICENSE_STATUS_UNAVAILABLE, - message: i18n.translate('xpack.server.checkLicense.errorUnavailableMessage', { - defaultMessage: - 'You cannot use {pluginName} because license information is not available at this time.', - values: { pluginName }, - }), - }; - } - - const { license } = xpackLicenseInfo; - const isLicenseModeValid = license.isOneOf( - [...RANKED_LICENSE_TYPES].splice(RANKED_LICENSE_TYPES.indexOf(minimumLicenseRequired)) - ); - const isLicenseActive = license.isActive(); - const licenseType = license.getType(); - - // License is not valid - if (!isLicenseModeValid) { - return { - status: LICENSE_STATUS_INVALID, - message: i18n.translate('xpack.server.checkLicense.errorUnsupportedMessage', { - defaultMessage: - 'Your {licenseType} license does not support {pluginName}. Please upgrade your license.', - values: { licenseType, pluginName }, - }), - }; - } - - // License is valid but not active - if (!isLicenseActive) { - return { - status: LICENSE_STATUS_EXPIRED, - message: i18n.translate('xpack.server.checkLicense.errorExpiredMessage', { - defaultMessage: - 'You cannot use {pluginName} because your {licenseType} license has expired.', - values: { licenseType, pluginName }, - }), - }; - } - - // License is valid and active - return { - status: LICENSE_STATUS_VALID, - }; -} diff --git a/x-pack/legacy/server/lib/check_license/check_license.test.js b/x-pack/legacy/server/lib/check_license/check_license.test.js deleted file mode 100644 index 65b599ed4a5f6..0000000000000 --- a/x-pack/legacy/server/lib/check_license/check_license.test.js +++ /dev/null @@ -1,132 +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 { set } from '@elastic/safer-lodash-set'; -import { checkLicense } from './check_license'; -import { - LICENSE_STATUS_UNAVAILABLE, - LICENSE_STATUS_EXPIRED, - LICENSE_STATUS_VALID, - LICENSE_TYPE_BASIC, -} from '../../../common/constants'; - -describe('check_license', function () { - const pluginName = 'Foo'; - const minimumLicenseRequired = LICENSE_TYPE_BASIC; - let mockLicenseInfo; - beforeEach(() => (mockLicenseInfo = {})); - - describe('license information is undefined', () => { - beforeEach(() => (mockLicenseInfo = undefined)); - - it('should set status to unavailable', () => { - expect(checkLicense(pluginName, minimumLicenseRequired, mockLicenseInfo).status).toBe( - LICENSE_STATUS_UNAVAILABLE - ); - }); - - it('should set a message', () => { - expect(checkLicense(pluginName, minimumLicenseRequired, mockLicenseInfo).message).not.toBe( - undefined - ); - }); - }); - - describe('license information is not available', () => { - beforeEach(() => (mockLicenseInfo.isAvailable = () => false)); - - it('should set status to unavailable', () => { - expect(checkLicense(pluginName, minimumLicenseRequired, mockLicenseInfo).status).toBe( - LICENSE_STATUS_UNAVAILABLE - ); - }); - - it('should set a message', () => { - expect(checkLicense(pluginName, minimumLicenseRequired, mockLicenseInfo).message).not.toBe( - undefined - ); - }); - }); - - describe('license information is available', () => { - beforeEach(() => { - mockLicenseInfo.isAvailable = () => true; - set(mockLicenseInfo, 'license.getType', () => LICENSE_TYPE_BASIC); - }); - - describe('& license is trial, standard, gold, platinum', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isOneOf', () => true)); - - describe('& license is active', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true)); - - it('should set status to valid', () => { - expect(checkLicense(pluginName, minimumLicenseRequired, mockLicenseInfo).status).toBe( - LICENSE_STATUS_VALID - ); - }); - - it('should not set a message', () => { - expect(checkLicense(pluginName, minimumLicenseRequired, mockLicenseInfo).message).toBe( - undefined - ); - }); - }); - - describe('& license is expired', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false)); - - it('should set status to inactive', () => { - expect(checkLicense(pluginName, minimumLicenseRequired, mockLicenseInfo).status).toBe( - LICENSE_STATUS_EXPIRED - ); - }); - - it('should set a message', () => { - expect( - checkLicense(pluginName, minimumLicenseRequired, mockLicenseInfo).message - ).not.toBe(undefined); - }); - }); - }); - - describe('& license is basic', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isOneOf', () => true)); - - describe('& license is active', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => true)); - - it('should set status to valid', () => { - expect(checkLicense(pluginName, minimumLicenseRequired, mockLicenseInfo).status).toBe( - LICENSE_STATUS_VALID - ); - }); - - it('should not set a message', () => { - expect(checkLicense(pluginName, minimumLicenseRequired, mockLicenseInfo).message).toBe( - undefined - ); - }); - }); - - describe('& license is expired', () => { - beforeEach(() => set(mockLicenseInfo, 'license.isActive', () => false)); - - it('should set status to inactive', () => { - expect(checkLicense(pluginName, minimumLicenseRequired, mockLicenseInfo).status).toBe( - LICENSE_STATUS_EXPIRED - ); - }); - - it('should set a message', () => { - expect( - checkLicense(pluginName, minimumLicenseRequired, mockLicenseInfo).message - ).not.toBe(undefined); - }); - }); - }); - }); -}); diff --git a/x-pack/legacy/server/lib/create_router/call_with_request_factory/call_with_request_factory.js b/x-pack/legacy/server/lib/create_router/call_with_request_factory/call_with_request_factory.js deleted file mode 100644 index df1ce95b31655..0000000000000 --- a/x-pack/legacy/server/lib/create_router/call_with_request_factory/call_with_request_factory.js +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const callWithRequestFactory = (server, pluginId, config) => { - const { callWithRequest } = config - ? server.plugins.elasticsearch.createCluster(pluginId, config) - : server.plugins.elasticsearch.getCluster('data'); - return callWithRequest; -}; diff --git a/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.d.ts b/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.d.ts deleted file mode 100644 index 3537d1bf42079..0000000000000 --- a/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.d.ts +++ /dev/null @@ -1,18 +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 { Legacy } from 'kibana'; -import { LegacyAPICaller } from '../../../../../../src/core/server'; - -export type CallWithRequest = (...args: any[]) => LegacyAPICaller; - -export declare function callWithRequestFactory( - server: Legacy.Server, - pluginId: string, - config?: { - plugins: any[]; - } -): CallWithRequest; diff --git a/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.js b/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.js deleted file mode 100644 index 787814d87dff9..0000000000000 --- a/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { callWithRequestFactory } from './call_with_request_factory'; diff --git a/x-pack/legacy/server/lib/create_router/error_wrappers/__tests__/wrap_custom_error.js b/x-pack/legacy/server/lib/create_router/error_wrappers/__tests__/wrap_custom_error.js deleted file mode 100644 index f9c102be7a1ff..0000000000000 --- a/x-pack/legacy/server/lib/create_router/error_wrappers/__tests__/wrap_custom_error.js +++ /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 expect from '@kbn/expect'; -import { wrapCustomError } from '../wrap_custom_error'; - -describe('wrap_custom_error', () => { - describe('#wrapCustomError', () => { - it('should return a Boom object', () => { - const originalError = new Error('I am an error'); - const statusCode = 404; - const wrappedError = wrapCustomError(originalError, statusCode); - - expect(wrappedError.isBoom).to.be(true); - expect(wrappedError.output.statusCode).to.equal(statusCode); - }); - }); -}); diff --git a/x-pack/legacy/server/lib/create_router/error_wrappers/__tests__/wrap_es_error.js b/x-pack/legacy/server/lib/create_router/error_wrappers/__tests__/wrap_es_error.js deleted file mode 100644 index 8241dc4329137..0000000000000 --- a/x-pack/legacy/server/lib/create_router/error_wrappers/__tests__/wrap_es_error.js +++ /dev/null @@ -1,39 +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 expect from '@kbn/expect'; -import { wrapEsError } from '../wrap_es_error'; - -describe('wrap_es_error', () => { - describe('#wrapEsError', () => { - let originalError; - beforeEach(() => { - originalError = new Error('I am an error'); - originalError.statusCode = 404; - originalError.response = '{}'; - }); - - it('should return a Boom object', () => { - const wrappedError = wrapEsError(originalError); - - expect(wrappedError.isBoom).to.be(true); - }); - - it('should return the correct Boom object', () => { - const wrappedError = wrapEsError(originalError); - - expect(wrappedError.output.statusCode).to.be(originalError.statusCode); - expect(wrappedError.output.payload.message).to.be(originalError.message); - }); - - it('should return the correct Boom object with custom message', () => { - const wrappedError = wrapEsError(originalError, { 404: 'No encontrado!' }); - - expect(wrappedError.output.statusCode).to.be(originalError.statusCode); - expect(wrappedError.output.payload.message).to.be('No encontrado!'); - }); - }); -}); diff --git a/x-pack/legacy/server/lib/create_router/error_wrappers/__tests__/wrap_unknown_error.js b/x-pack/legacy/server/lib/create_router/error_wrappers/__tests__/wrap_unknown_error.js deleted file mode 100644 index 85e0b2b3033ad..0000000000000 --- a/x-pack/legacy/server/lib/create_router/error_wrappers/__tests__/wrap_unknown_error.js +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { wrapUnknownError } from '../wrap_unknown_error'; - -describe('wrap_unknown_error', () => { - describe('#wrapUnknownError', () => { - it('should return a Boom object', () => { - const originalError = new Error('I am an error'); - const wrappedError = wrapUnknownError(originalError); - - expect(wrappedError.isBoom).to.be(true); - }); - }); -}); diff --git a/x-pack/legacy/server/lib/create_router/error_wrappers/index.d.ts b/x-pack/legacy/server/lib/create_router/error_wrappers/index.d.ts deleted file mode 100644 index 1aaefb4e3727c..0000000000000 --- a/x-pack/legacy/server/lib/create_router/error_wrappers/index.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import Boom from 'boom'; - -export declare function wrapCustomError(error: Error, statusCode: number): Boom; - -export declare function wrapEsError(error: Error, statusCodeToMessageMap?: object): Boom; - -export declare function wrapUnknownError(error: Error): Boom; diff --git a/x-pack/legacy/server/lib/create_router/error_wrappers/index.js b/x-pack/legacy/server/lib/create_router/error_wrappers/index.js deleted file mode 100644 index f275f15637091..0000000000000 --- a/x-pack/legacy/server/lib/create_router/error_wrappers/index.js +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { wrapCustomError } from './wrap_custom_error'; -export { wrapEsError } from './wrap_es_error'; -export { wrapUnknownError } from './wrap_unknown_error'; diff --git a/x-pack/legacy/server/lib/create_router/error_wrappers/wrap_custom_error.js b/x-pack/legacy/server/lib/create_router/error_wrappers/wrap_custom_error.js deleted file mode 100644 index 3295113d38ee5..0000000000000 --- a/x-pack/legacy/server/lib/create_router/error_wrappers/wrap_custom_error.js +++ /dev/null @@ -1,18 +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 Boom from 'boom'; - -/** - * Wraps a custom error into a Boom error response and returns it - * - * @param err Object error - * @param statusCode Error status code - * @return Object Boom error response - */ -export function wrapCustomError(err, statusCode) { - return Boom.boomify(err, { statusCode }); -} diff --git a/x-pack/legacy/server/lib/create_router/error_wrappers/wrap_es_error.js b/x-pack/legacy/server/lib/create_router/error_wrappers/wrap_es_error.js deleted file mode 100644 index 72be6321af8a2..0000000000000 --- a/x-pack/legacy/server/lib/create_router/error_wrappers/wrap_es_error.js +++ /dev/null @@ -1,59 +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 Boom from 'boom'; - -function extractCausedByChain(causedBy = {}, accumulator = []) { - const { reason, caused_by } = causedBy; // eslint-disable-line camelcase - - if (reason) { - accumulator.push(reason); - } - - // eslint-disable-next-line camelcase - if (caused_by) { - return extractCausedByChain(caused_by, accumulator); - } - - return accumulator; -} - -/** - * Wraps an error thrown by the ES JS client into a Boom error response and returns it - * - * @param err Object Error thrown by ES JS client - * @param statusCodeToMessageMap Object Optional map of HTTP status codes => error messages - * @return Object Boom error response - */ -export function wrapEsError(err, statusCodeToMessageMap = {}) { - const { statusCode, response } = err; - - const { - error: { - root_cause = [], // eslint-disable-line camelcase - caused_by, // eslint-disable-line camelcase - } = {}, - } = JSON.parse(response); - - // If no custom message if specified for the error's status code, just - // wrap the error as a Boom error response, include the additional information from ES, and return it - if (!statusCodeToMessageMap[statusCode]) { - const boomError = Boom.boomify(err, { statusCode }); - - // The caused_by chain has the most information so use that if it's available. If not then - // settle for the root_cause. - const causedByChain = extractCausedByChain(caused_by); - const defaultCause = root_cause.length ? extractCausedByChain(root_cause[0]) : undefined; - - boomError.output.payload.cause = causedByChain.length ? causedByChain : defaultCause; - return boomError; - } - - // Otherwise, use the custom message to create a Boom error response and - // return it - const message = statusCodeToMessageMap[statusCode]; - return new Boom(message, { statusCode }); -} diff --git a/x-pack/legacy/server/lib/create_router/error_wrappers/wrap_unknown_error.js b/x-pack/legacy/server/lib/create_router/error_wrappers/wrap_unknown_error.js deleted file mode 100644 index ffd915c513362..0000000000000 --- a/x-pack/legacy/server/lib/create_router/error_wrappers/wrap_unknown_error.js +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; - -/** - * Wraps an unknown error into a Boom error response and returns it - * - * @param err Object Unknown error - * @return Object Boom error response - */ -export function wrapUnknownError(err) { - return Boom.boomify(err); -} diff --git a/x-pack/legacy/server/lib/create_router/index.d.ts b/x-pack/legacy/server/lib/create_router/index.d.ts deleted file mode 100644 index 76e5f4b599708..0000000000000 --- a/x-pack/legacy/server/lib/create_router/index.d.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { Request, ResponseToolkit } from 'hapi'; -import { Legacy } from 'kibana'; -import { CallWithRequest } from './call_with_request_factory'; - -export * from './error_wrappers'; - -export type RouterRouteHandler = ( - req: Request, - callWithRequest: ReturnType, - responseToolkit: ResponseToolkit -) => Promise; - -export type RouterRoute = (path: string, handler: RouterRouteHandler) => Router; - -export interface Router { - get: RouterRoute; - post: RouterRoute; - put: RouterRoute; - delete: RouterRoute; - patch: RouterRoute; - isEsError: any; -} - -export declare function createRouter( - server: Legacy.Server, - pluginId: string, - apiBasePath: string, - config?: { - plugins: any[]; - } -): Router; - -export declare function isEsErrorFactory(server: Legacy.Server): any; diff --git a/x-pack/legacy/server/lib/create_router/index.js b/x-pack/legacy/server/lib/create_router/index.js deleted file mode 100644 index e4d66bdb5a48b..0000000000000 --- a/x-pack/legacy/server/lib/create_router/index.js +++ /dev/null @@ -1,61 +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 Boom from 'boom'; -import { callWithRequestFactory } from './call_with_request_factory'; -import { isEsErrorFactory as createIsEsError } from './is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from './error_wrappers'; -import { licensePreRoutingFactory } from './license_pre_routing_factory'; - -export { wrapEsError, wrapUnknownError, wrapCustomError } from './error_wrappers'; - -// Sometimes consumers will need to check if errors are ES errors, too. -export const isEsErrorFactory = (server) => { - return createIsEsError(server); -}; - -export const createRouter = (server, pluginId, apiBasePath = '', config) => { - const isEsError = isEsErrorFactory(server); - - // NOTE: The license-checking logic depends on the xpack_main plugin, so if your plugin - // consumes this helper, make sure it declares 'xpack_main' as a dependency. - const licensePreRouting = licensePreRoutingFactory(server, pluginId); - - const callWithRequestInstance = callWithRequestFactory(server, pluginId, config); - - const requestHandler = (handler) => async (request, h) => { - try { - const callWithRequest = (...args) => { - return callWithRequestInstance(request, ...args); - }; - return await handler(request, callWithRequest, h); - } catch (err) { - if (err instanceof Boom) { - throw err; - } - - if (isEsError(err)) { - throw wrapEsError(err); - } - - throw wrapUnknownError(err); - } - }; - - // Decorate base router with HTTP methods. - return ['get', 'post', 'put', 'delete', 'patch'].reduce((router, methodName) => { - router[methodName] = (subPath, handler) => { - const method = methodName.toUpperCase(); - const path = apiBasePath + subPath; - server.route({ - path, - method, - handler: requestHandler(handler), - config: { pre: [licensePreRouting] }, - }); - }; - return router; - }, {}); -}; diff --git a/x-pack/legacy/server/lib/create_router/is_es_error_factory/__tests__/is_es_error_factory.js b/x-pack/legacy/server/lib/create_router/is_es_error_factory/__tests__/is_es_error_factory.js deleted file mode 100644 index ef6fbaf9c53d0..0000000000000 --- a/x-pack/legacy/server/lib/create_router/is_es_error_factory/__tests__/is_es_error_factory.js +++ /dev/null @@ -1,44 +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 expect from '@kbn/expect'; -import { isEsErrorFactory } from '../is_es_error_factory'; -import { set } from '@elastic/safer-lodash-set'; - -class MockAbstractEsError {} - -describe('is_es_error_factory', () => { - let mockServer; - let isEsError; - - beforeEach(() => { - const mockEsErrors = { - _Abstract: MockAbstractEsError, - }; - mockServer = {}; - set(mockServer, 'plugins.elasticsearch.getCluster', () => ({ errors: mockEsErrors })); - - isEsError = isEsErrorFactory(mockServer); - }); - - describe('#isEsErrorFactory', () => { - it('should return a function', () => { - expect(isEsError).to.be.a(Function); - }); - - describe('returned function', () => { - it('should return true if passed-in err is a known esError', () => { - const knownEsError = new MockAbstractEsError(); - expect(isEsError(knownEsError)).to.be(true); - }); - - it('should return false if passed-in err is not a known esError', () => { - const unknownEsError = {}; - expect(isEsError(unknownEsError)).to.be(false); - }); - }); - }); -}); diff --git a/x-pack/legacy/server/lib/create_router/is_es_error_factory/index.js b/x-pack/legacy/server/lib/create_router/is_es_error_factory/index.js deleted file mode 100644 index 441648a8701e0..0000000000000 --- a/x-pack/legacy/server/lib/create_router/is_es_error_factory/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { isEsErrorFactory } from './is_es_error_factory'; diff --git a/x-pack/legacy/server/lib/create_router/is_es_error_factory/is_es_error_factory.js b/x-pack/legacy/server/lib/create_router/is_es_error_factory/is_es_error_factory.js deleted file mode 100644 index 80daac5bd496d..0000000000000 --- a/x-pack/legacy/server/lib/create_router/is_es_error_factory/is_es_error_factory.js +++ /dev/null @@ -1,18 +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 { memoize } from 'lodash'; - -const esErrorsFactory = memoize((server) => { - return server.plugins.elasticsearch.getCluster('admin').errors; -}); - -export function isEsErrorFactory(server) { - const esErrors = esErrorsFactory(server); - return function isEsError(err) { - return err instanceof esErrors._Abstract; - }; -} diff --git a/x-pack/legacy/server/lib/create_router/license_pre_routing_factory/__tests__/license_pre_routing_factory.js b/x-pack/legacy/server/lib/create_router/license_pre_routing_factory/__tests__/license_pre_routing_factory.js deleted file mode 100644 index dde18a0ccd7dd..0000000000000 --- a/x-pack/legacy/server/lib/create_router/license_pre_routing_factory/__tests__/license_pre_routing_factory.js +++ /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 expect from '@kbn/expect'; -import { licensePreRoutingFactory } from '../license_pre_routing_factory'; -import { LICENSE_STATUS_INVALID, LICENSE_STATUS_VALID } from '../../../../../common/constants'; - -describe('license_pre_routing_factory', () => { - describe('#reportingFeaturePreRoutingFactory', () => { - let mockServer; - let mockLicenseCheckResults; - - beforeEach(() => { - mockServer = { - plugins: { - xpack_main: { - info: { - feature: () => ({ - getLicenseCheckResults: () => mockLicenseCheckResults, - }), - }, - }, - }, - }; - }); - - it('instantiates a new instance per plugin', () => { - const firstInstance = licensePreRoutingFactory(mockServer, 'foo'); - const secondInstance = licensePreRoutingFactory(mockServer, 'bar'); - - expect(firstInstance).to.not.be(secondInstance); - }); - - describe('status is invalid', () => { - beforeEach(() => { - mockLicenseCheckResults = { - status: LICENSE_STATUS_INVALID, - }; - }); - - it('replies with 403', () => { - const licensePreRouting = licensePreRoutingFactory(mockServer); - const stubRequest = {}; - expect(() => licensePreRouting(stubRequest)).to.throwException((response) => { - expect(response).to.be.an(Error); - expect(response.isBoom).to.be(true); - expect(response.output.statusCode).to.be(403); - }); - }); - }); - - describe('status is valid', () => { - beforeEach(() => { - mockLicenseCheckResults = { - status: LICENSE_STATUS_VALID, - }; - }); - - it('replies with nothing', () => { - const licensePreRouting = licensePreRoutingFactory(mockServer); - const stubRequest = {}; - const response = licensePreRouting(stubRequest); - expect(response).to.be(null); - }); - }); - }); -}); diff --git a/x-pack/legacy/server/lib/create_router/license_pre_routing_factory/index.js b/x-pack/legacy/server/lib/create_router/license_pre_routing_factory/index.js deleted file mode 100644 index 0743e443955f4..0000000000000 --- a/x-pack/legacy/server/lib/create_router/license_pre_routing_factory/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { licensePreRoutingFactory } from './license_pre_routing_factory'; diff --git a/x-pack/legacy/server/lib/create_router/license_pre_routing_factory/license_pre_routing_factory.js b/x-pack/legacy/server/lib/create_router/license_pre_routing_factory/license_pre_routing_factory.js deleted file mode 100644 index 81640ebb35ea9..0000000000000 --- a/x-pack/legacy/server/lib/create_router/license_pre_routing_factory/license_pre_routing_factory.js +++ /dev/null @@ -1,26 +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 { wrapCustomError } from '../error_wrappers'; -import { LICENSE_STATUS_VALID } from '../../../../common/constants'; - -export const licensePreRoutingFactory = (server, pluginId) => { - return () => { - const xpackMainPlugin = server.plugins.xpack_main; - const licenseCheckResults = xpackMainPlugin.info.feature(pluginId).getLicenseCheckResults(); - - // Apps which don't have any license restrictions will return undefined license check results. - if (licenseCheckResults) { - if (licenseCheckResults.status !== LICENSE_STATUS_VALID) { - const error = new Error(licenseCheckResults.message); - const statusCode = 403; - throw wrapCustomError(error, statusCode); - } - } - - return null; - }; -}; diff --git a/x-pack/legacy/server/lib/key_case_converter.js b/x-pack/legacy/server/lib/key_case_converter.js deleted file mode 100644 index a2a5452b3a1d9..0000000000000 --- a/x-pack/legacy/server/lib/key_case_converter.js +++ /dev/null @@ -1,52 +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 _ from 'lodash'; - -// Note: This function uses _.clone. This will clone objects created by constructors other than Object -// to plain Object objects. Uncloneable values such as functions, DOM nodes, Maps, Sets, and WeakMaps -// will be cloned to the empty object. -function convertKeysToSpecifiedCaseDeep(object, caseConversionFunction) { - // Base case - if (!(_.isPlainObject(object) || _.isArray(object))) { - return object; - } - - // Clone (so we don't modify the original object that was passed in) - let newObject; - if (Array.isArray(object)) { - newObject = object.slice(0); - } else { - newObject = _.clone(object); - - // Convert top-level keys - newObject = _.mapKeys(newObject, (value, key) => caseConversionFunction(key)); - } - - // Recursively convert nested object keys - _.forEach( - newObject, - (value, key) => (newObject[key] = convertKeysToSpecifiedCaseDeep(value, caseConversionFunction)) - ); - - return newObject; -} - -function validateObject(object) { - if (!(_.isPlainObject(object) || _.isArray(object))) { - throw new Error('Specified object should be an Object or Array'); - } -} - -export function convertKeysToSnakeCaseDeep(object) { - validateObject(object); - return convertKeysToSpecifiedCaseDeep(object, _.snakeCase); -} - -export function convertKeysToCamelCaseDeep(object) { - validateObject(object); - return convertKeysToSpecifiedCaseDeep(object, _.camelCase); -} diff --git a/x-pack/legacy/server/lib/parse_kibana_state.js b/x-pack/legacy/server/lib/parse_kibana_state.js deleted file mode 100644 index a6c9bfbb511c1..0000000000000 --- a/x-pack/legacy/server/lib/parse_kibana_state.js +++ /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 { set } from '@elastic/safer-lodash-set'; -import { isPlainObject, omit, get } from 'lodash'; -import rison from 'rison-node'; - -const stateTypeKeys = { - global: '_g', - app: '_a', -}; - -class KibanaState { - constructor(query, type = 'global') { - const propId = stateTypeKeys[type]; - if (!isPlainObject(query)) throw new TypeError('Query parameter must be an object'); - if (!propId) throw new TypeError(`Unknown state type: '${type}'`); - - const queryValue = query[propId]; - - this.exists = Boolean(queryValue); - this.state = queryValue ? rison.decode(queryValue) : {}; - this.type = type; - } - - removeProps(props) { - this.state = omit(this.state, props); - } - - get(prop, defVal) { - return get(this.state, prop, defVal); - } - - set(prop, val) { - return set(this.state, prop, val); - } - - toString() { - return rison.encode(this.state); - } - - toQuery() { - const index = stateTypeKeys[this.type]; - const output = {}; - output[index] = this.toString(); - return output; - } -} - -export function parseKibanaState(query, type) { - return new KibanaState(query, type); -} diff --git a/x-pack/legacy/server/lib/register_license_checker/index.d.ts b/x-pack/legacy/server/lib/register_license_checker/index.d.ts deleted file mode 100644 index 555008921df42..0000000000000 --- a/x-pack/legacy/server/lib/register_license_checker/index.d.ts +++ /dev/null @@ -1,15 +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 { Legacy } from 'kibana'; -import { LicenseType } from '../../../common/constants'; - -export declare function registerLicenseChecker( - server: Legacy.Server, - pluginId: string, - pluginName: string, - minimumLicenseRequired: LicenseType -): void; diff --git a/x-pack/legacy/server/lib/register_license_checker/index.js b/x-pack/legacy/server/lib/register_license_checker/index.js deleted file mode 100644 index 7b0f97c38d129..0000000000000 --- a/x-pack/legacy/server/lib/register_license_checker/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { registerLicenseChecker } from './register_license_checker'; diff --git a/x-pack/legacy/server/lib/register_license_checker/register_license_checker.js b/x-pack/legacy/server/lib/register_license_checker/register_license_checker.js deleted file mode 100644 index 57cbe30c25cb2..0000000000000 --- a/x-pack/legacy/server/lib/register_license_checker/register_license_checker.js +++ /dev/null @@ -1,34 +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 { pairwise } from 'rxjs/operators'; - -import { ServiceStatusLevels } from '../../../../../src/core/server'; -import { checkLicense } from '../check_license'; - -export function registerLicenseChecker(server, pluginId, pluginName, minimumLicenseRequired) { - const xpackMainPlugin = server.plugins.xpack_main; - const subscription = server.newPlatform.setup.core.status.core$ - .pipe(pairwise()) - .subscribe(([coreLast, coreCurrent]) => { - if ( - !subscription.closed && - coreLast.elasticsearch.level !== ServiceStatusLevels.available && - coreCurrent.elasticsearch.level === ServiceStatusLevels.available - ) { - // Unsubscribe as soon as ES becomes available so this function only runs once - subscription.unsubscribe(); - - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results for this plugin - xpackMainPlugin.info - .feature(pluginId) - .registerLicenseCheckResultsGenerator((xpackLicenseInfo) => { - return checkLicense(pluginName, minimumLicenseRequired, xpackLicenseInfo); - }); - } - }); -} diff --git a/x-pack/legacy/server/lib/watch_status_and_license_to_initialize.js b/x-pack/legacy/server/lib/watch_status_and_license_to_initialize.js deleted file mode 100644 index 109dbbb20e35d..0000000000000 --- a/x-pack/legacy/server/lib/watch_status_and_license_to_initialize.js +++ /dev/null @@ -1,83 +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 Rx from 'rxjs'; -import { catchError, mergeMap, map, switchMap, tap } from 'rxjs/operators'; - -export const RETRY_SCALE_DURATION = 100; -export const RETRY_DURATION_MAX = 10000; - -const calculateDuration = (i) => { - const duration = i * RETRY_SCALE_DURATION; - if (duration > RETRY_DURATION_MAX) { - return RETRY_DURATION_MAX; - } - - return duration; -}; - -// we can't use a retryWhen here, because we want to propagate the red status and then retry -const propagateRedStatusAndScaleRetry = () => { - let i = 0; - return (err, caught) => - Rx.concat( - Rx.of({ - state: 'red', - message: err.message, - }), - Rx.timer(calculateDuration(++i)).pipe(mergeMap(() => caught)) - ); -}; - -export function watchStatusAndLicenseToInitialize(xpackMainPlugin, downstreamPlugin, initialize) { - const xpackInfo = xpackMainPlugin.info; - const xpackInfoFeature = xpackInfo.feature(downstreamPlugin.id); - - const upstreamStatus = xpackMainPlugin.status; - const currentStatus$ = Rx.of({ - state: upstreamStatus.state, - message: upstreamStatus.message, - }); - const newStatus$ = Rx.fromEvent( - upstreamStatus, - 'change', - null, - (previousState, previousMsg, state, message) => { - return { - state, - message, - }; - } - ); - const status$ = Rx.merge(currentStatus$, newStatus$); - - const currentLicense$ = Rx.of(xpackInfoFeature.getLicenseCheckResults()); - const newLicense$ = Rx.fromEventPattern(xpackInfo.onLicenseInfoChange.bind(xpackInfo)).pipe( - map(() => xpackInfoFeature.getLicenseCheckResults()) - ); - const license$ = Rx.merge(currentLicense$, newLicense$); - - Rx.combineLatest(status$, license$) - .pipe( - map(([status, license]) => ({ status, license })), - switchMap(({ status, license }) => { - if (status.state !== 'green') { - return Rx.of({ state: status.state, message: status.message }); - } - - return Rx.defer(() => initialize(license)).pipe( - map(() => ({ - state: 'green', - message: 'Ready', - })), - catchError(propagateRedStatusAndScaleRetry()) - ); - }), - tap(({ state, message }) => { - downstreamPlugin.status[state](message); - }) - ) - .subscribe(); -} diff --git a/x-pack/legacy/server/lib/watch_status_and_license_to_initialize.test.js b/x-pack/legacy/server/lib/watch_status_and_license_to_initialize.test.js deleted file mode 100644 index 33282b7591db7..0000000000000 --- a/x-pack/legacy/server/lib/watch_status_and_license_to_initialize.test.js +++ /dev/null @@ -1,301 +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 { EventEmitter } from 'events'; -import { - watchStatusAndLicenseToInitialize, - RETRY_SCALE_DURATION, - RETRY_DURATION_MAX, -} from './watch_status_and_license_to_initialize'; - -const createMockXpackMainPluginAndFeature = (featureId) => { - const licenseChangeCallbacks = []; - - const mockFeature = { - getLicenseCheckResults: jest.fn(), - mock: { - triggerLicenseChange: () => { - for (const callback of licenseChangeCallbacks) { - callback(); - } - }, - setLicenseCheckResults: (value) => { - mockFeature.getLicenseCheckResults.mockReturnValue(value); - }, - }, - }; - - const mockXpackMainPlugin = { - info: { - onLicenseInfoChange: (callback) => { - licenseChangeCallbacks.push(callback); - }, - feature: (id) => { - if (id === featureId) { - return mockFeature; - } - throw new Error('Unexpected feature'); - }, - }, - status: new EventEmitter(), - mock: { - setStatus: (state, message) => { - mockXpackMainPlugin.status.state = state; - mockXpackMainPlugin.status.message = message; - mockXpackMainPlugin.status.emit('change', null, null, state, message); - }, - }, - }; - - return { mockXpackMainPlugin, mockFeature }; -}; - -const createMockDownstreamPlugin = (id) => { - const defaultImplementation = () => { - throw new Error('Not implemented'); - }; - return { - id, - status: { - disabled: jest.fn().mockImplementation(defaultImplementation), - yellow: jest.fn().mockImplementation(defaultImplementation), - green: jest.fn().mockImplementation(defaultImplementation), - red: jest.fn().mockImplementation(defaultImplementation), - }, - }; -}; - -const advanceRetry = async (initializeCount) => { - await Promise.resolve(); - let duration = initializeCount * RETRY_SCALE_DURATION; - if (duration > RETRY_DURATION_MAX) { - duration = RETRY_DURATION_MAX; - } - jest.advanceTimersByTime(duration); -}; - -['red', 'yellow', 'disabled'].forEach((state) => { - test(`mirrors ${state} immediately`, () => { - const pluginId = 'foo-plugin'; - const message = `${state} is now the state`; - const { mockXpackMainPlugin } = createMockXpackMainPluginAndFeature(pluginId); - mockXpackMainPlugin.mock.setStatus(state, message); - const downstreamPlugin = createMockDownstreamPlugin(pluginId); - const initializeMock = jest.fn(); - downstreamPlugin.status[state].mockImplementation(() => {}); - - watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock); - - expect(initializeMock).not.toHaveBeenCalled(); - expect(downstreamPlugin.status[state]).toHaveBeenCalledTimes(1); - expect(downstreamPlugin.status[state]).toHaveBeenCalledWith(message); - }); -}); - -test(`calls initialize and doesn't immediately set downstream status when the initial status is green`, () => { - const pluginId = 'foo-plugin'; - const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId); - mockXpackMainPlugin.mock.setStatus('green', 'green is now the state'); - const licenseCheckResults = Symbol(); - mockFeature.mock.setLicenseCheckResults(licenseCheckResults); - const downstreamPlugin = createMockDownstreamPlugin(pluginId); - const initializeMock = jest.fn().mockImplementation(() => new Promise(() => {})); - - watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock); - - expect(initializeMock).toHaveBeenCalledTimes(1); - expect(initializeMock).toHaveBeenCalledWith(licenseCheckResults); - expect(downstreamPlugin.status.green).toHaveBeenCalledTimes(0); -}); - -test(`sets downstream plugin's status to green when initialize resolves`, (done) => { - const pluginId = 'foo-plugin'; - const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId); - mockXpackMainPlugin.mock.setStatus('green', 'green is now the state'); - const licenseCheckResults = Symbol(); - mockFeature.mock.setLicenseCheckResults(licenseCheckResults); - const downstreamPlugin = createMockDownstreamPlugin(pluginId); - const initializeMock = jest.fn().mockImplementation(() => Promise.resolve()); - - watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock); - - expect(initializeMock).toHaveBeenCalledTimes(1); - expect(initializeMock).toHaveBeenCalledWith(licenseCheckResults); - downstreamPlugin.status.green.mockImplementation((actualMessage) => { - expect(actualMessage).toBe('Ready'); - done(); - }); -}); - -test(`sets downstream plugin's status to red when initialize initially rejects, and continually polls initialize`, (done) => { - jest.useFakeTimers(); - - const pluginId = 'foo-plugin'; - const errorMessage = 'the error message'; - const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId); - mockXpackMainPlugin.mock.setStatus('green'); - const licenseCheckResults = Symbol(); - mockFeature.mock.setLicenseCheckResults(licenseCheckResults); - const downstreamPlugin = createMockDownstreamPlugin(pluginId); - - let isRed = false; - let initializeCount = 0; - const initializeMock = jest.fn().mockImplementation(() => { - ++initializeCount; - - // on the second retry, ensure we already set the status to red - if (initializeCount === 2) { - expect(isRed).toBe(true); - } - - // this should theoretically continue indefinitely, but we only have so long to run the tests - if (initializeCount === 100) { - done(); - } - - // everytime this is called, we have to wait for a new promise to be resolved - // allowing the Promise the we return below to run, and then advance the timers - setImmediate(() => { - advanceRetry(initializeCount); - }); - return Promise.reject(new Error(errorMessage)); - }); - - watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock); - - expect(initializeMock).toHaveBeenCalledTimes(1); - expect(initializeMock).toHaveBeenCalledWith(licenseCheckResults); - downstreamPlugin.status.red.mockImplementation((message) => { - isRed = true; - expect(message).toBe(errorMessage); - }); -}); - -test(`sets downstream plugin's status to green when initialize resolves after rejecting 10 times`, (done) => { - jest.useFakeTimers(); - - const pluginId = 'foo-plugin'; - const errorMessage = 'the error message'; - const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId); - mockXpackMainPlugin.mock.setStatus('green'); - const licenseCheckResults = Symbol(); - mockFeature.mock.setLicenseCheckResults(licenseCheckResults); - const downstreamPlugin = createMockDownstreamPlugin(pluginId); - - let initializeCount = 0; - const initializeMock = jest.fn().mockImplementation(() => { - ++initializeCount; - - // everytime this is called, we have to wait for a new promise to be resolved - // allowing the Promise the we return below to run, and then advance the timers - setImmediate(() => { - advanceRetry(initializeCount); - }); - - if (initializeCount >= 10) { - return Promise.resolve(); - } - - return Promise.reject(new Error(errorMessage)); - }); - - watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock); - - expect(initializeMock).toHaveBeenCalledTimes(1); - expect(initializeMock).toHaveBeenCalledWith(licenseCheckResults); - downstreamPlugin.status.red.mockImplementation((message) => { - expect(initializeCount).toBeLessThan(10); - expect(message).toBe(errorMessage); - }); - downstreamPlugin.status.green.mockImplementation((message) => { - expect(initializeCount).toBe(10); - expect(message).toBe('Ready'); - done(); - }); -}); - -test(`calls initialize twice when it gets a new license and the status is green`, (done) => { - const pluginId = 'foo-plugin'; - const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId); - mockXpackMainPlugin.mock.setStatus('green'); - const firstLicenseCheckResults = Symbol(); - const secondLicenseCheckResults = Symbol(); - mockFeature.mock.setLicenseCheckResults(firstLicenseCheckResults); - const downstreamPlugin = createMockDownstreamPlugin(pluginId); - const initializeMock = jest.fn().mockImplementation(() => Promise.resolve()); - - let count = 0; - downstreamPlugin.status.green.mockImplementation((message) => { - expect(message).toBe('Ready'); - ++count; - if (count === 1) { - mockFeature.mock.setLicenseCheckResults(secondLicenseCheckResults); - mockFeature.mock.triggerLicenseChange(); - } - if (count === 2) { - expect(initializeMock).toHaveBeenCalledWith(firstLicenseCheckResults); - expect(initializeMock).toHaveBeenCalledWith(secondLicenseCheckResults); - expect(initializeMock).toHaveBeenCalledTimes(2); - done(); - } - }); - - watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock); -}); - -test(`doesn't call initialize twice when it gets a new license when the status isn't green`, (done) => { - const pluginId = 'foo-plugin'; - const redMessage = 'the red message'; - const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId); - mockXpackMainPlugin.mock.setStatus('green'); - const firstLicenseCheckResults = Symbol(); - const secondLicenseCheckResults = Symbol(); - mockFeature.mock.setLicenseCheckResults(firstLicenseCheckResults); - const downstreamPlugin = createMockDownstreamPlugin(pluginId); - const initializeMock = jest.fn().mockImplementation(() => Promise.resolve()); - - downstreamPlugin.status.green.mockImplementation((message) => { - expect(message).toBe('Ready'); - mockXpackMainPlugin.mock.setStatus('red', redMessage); - mockFeature.mock.setLicenseCheckResults(secondLicenseCheckResults); - mockFeature.mock.triggerLicenseChange(); - }); - - downstreamPlugin.status.red.mockImplementation((message) => { - expect(message).toBe(redMessage); - expect(initializeMock).toHaveBeenCalledTimes(1); - expect(initializeMock).toHaveBeenCalledWith(firstLicenseCheckResults); - done(); - }); - - watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock); -}); - -test(`calls initialize twice when the status changes to green twice`, (done) => { - const pluginId = 'foo-plugin'; - const { mockXpackMainPlugin, mockFeature } = createMockXpackMainPluginAndFeature(pluginId); - mockXpackMainPlugin.mock.setStatus('green'); - const licenseCheckResults = Symbol(); - mockFeature.mock.setLicenseCheckResults(licenseCheckResults); - const downstreamPlugin = createMockDownstreamPlugin(pluginId); - const initializeMock = jest.fn().mockImplementation(() => Promise.resolve()); - - let count = 0; - downstreamPlugin.status.green.mockImplementation((message) => { - expect(message).toBe('Ready'); - ++count; - if (count === 1) { - mockXpackMainPlugin.mock.setStatus('green'); - } - if (count === 2) { - expect(initializeMock).toHaveBeenCalledWith(licenseCheckResults); - expect(initializeMock).toHaveBeenCalledTimes(2); - done(); - } - }); - - watchStatusAndLicenseToInitialize(mockXpackMainPlugin, downstreamPlugin, initializeMock); -}); diff --git a/x-pack/legacy/server/lib/xpack_usage.js b/x-pack/legacy/server/lib/xpack_usage.js deleted file mode 100644 index 50b50ba18c37f..0000000000000 --- a/x-pack/legacy/server/lib/xpack_usage.js +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export function xpackUsage(client) { - /* - * Get an object over the Usage API that as available/enabled data and some - * select metadata for each of the X-Pack UI plugins - */ - return client.transport.request({ - method: 'GET', - path: '/_xpack/usage', - }); -} diff --git a/x-pack/package.json b/x-pack/package.json index 0560b1bebe42b..3af97ed16ed6f 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -30,7 +30,7 @@ }, "devDependencies": { "@cypress/webpack-preprocessor": "^4.1.0", - "@elastic/apm-rum-react": "^1.2.3", + "@elastic/apm-rum-react": "^1.2.4", "@elastic/maki": "6.3.0", "@kbn/dev-utils": "1.0.0", "@kbn/es": "1.0.0", @@ -111,7 +111,7 @@ "@types/proper-lockfile": "^3.0.1", "@types/puppeteer": "^1.20.1", "@types/react": "^16.9.36", - "@types/react-beautiful-dnd": "^12.1.1", + "@types/react-beautiful-dnd": "^13.0.0", "@types/react-dom": "^16.9.8", "@types/react-redux": "^7.1.9", "@types/react-router-dom": "^5.1.5", @@ -221,7 +221,7 @@ "proxyquire": "1.8.0", "re-resizable": "^6.1.1", "react-apollo": "^2.1.4", - "react-beautiful-dnd": "^12.2.0", + "react-beautiful-dnd": "^13.0.0", "react-docgen-typescript-loader": "^3.1.1", "react-dropzone": "^4.2.9", "react-fast-compare": "^2.0.4", diff --git a/x-pack/plugins/actions/common/types.ts b/x-pack/plugins/actions/common/types.ts index 49e8f3e80b14a..41ec4d2a88e9f 100644 --- a/x-pack/plugins/actions/common/types.ts +++ b/x-pack/plugins/actions/common/types.ts @@ -24,3 +24,13 @@ export interface ActionResult { config: Record; isPreconfigured: boolean; } + +// the result returned from an action type executor function +export interface ActionTypeExecutorResult { + actionId: string; + status: 'ok' | 'error'; + message?: string; + serviceMessage?: string; + data?: Data; + retry?: null | boolean | Date; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts index 7a0e24521a1c6..3d92d5ebf33fc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts @@ -284,4 +284,47 @@ describe('execute()', () => { ] `); }); + + test('resolves with an error when an error occurs in the indexing operation', async () => { + const secrets = {}; + // minimal params + const config = { index: 'index-value', refresh: false, executionTimeField: null }; + const params = { + documents: [{ '': 'bob' }], + }; + + const actionId = 'some-id'; + + services.callCluster.mockResolvedValue({ + took: 0, + errors: true, + items: [ + { + index: { + _index: 'indexme', + _id: '7buTjHQB0SuNSiS9Hayt', + status: 400, + error: { + type: 'mapper_parsing_exception', + reason: 'failed to parse', + caused_by: { + type: 'illegal_argument_exception', + reason: 'field name cannot be an empty string', + }, + }, + }, + }, + ], + }); + + expect(await actionType.executor({ actionId, config, secrets, params, services })) + .toMatchInlineSnapshot(` + Object { + "actionId": "some-id", + "message": "error indexing documents", + "serviceMessage": "failed to parse (field name cannot be an empty string)", + "status": "error", + } + `); + }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts index 53bf75651b1e5..868c07b775c78 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { curry } from 'lodash'; +import { curry, find } from 'lodash'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; @@ -85,21 +85,39 @@ async function executor( refresh: config.refresh, }; - let result; try { - result = await services.callCluster('bulk', bulkParams); + const result = await services.callCluster('bulk', bulkParams); + + const err = find(result.items, 'index.error.reason'); + if (err) { + return wrapErr( + `${err.index.error!.reason}${ + err.index.error?.caused_by ? ` (${err.index.error?.caused_by?.reason})` : '' + }`, + actionId, + logger + ); + } + + return { status: 'ok', data: result, actionId }; } catch (err) { - const message = i18n.translate('xpack.actions.builtin.esIndex.errorIndexingErrorMessage', { - defaultMessage: 'error indexing documents', - }); - logger.error(`error indexing documents: ${err.message}`); - return { - status: 'error', - actionId, - message, - serviceMessage: err.message, - }; + return wrapErr(err.message, actionId, logger); } +} - return { status: 'ok', data: result, actionId }; +function wrapErr( + errMessage: string, + actionId: string, + logger: Logger +): ActionTypeExecutorResult { + const message = i18n.translate('xpack.actions.builtin.esIndex.errorIndexingErrorMessage', { + defaultMessage: 'error indexing documents', + }); + logger.error(`error indexing documents: ${errMessage}`); + return { + status: 'error', + actionId, + message, + serviceMessage: errMessage, + }; } diff --git a/x-pack/plugins/actions/server/constants/plugin.ts b/x-pack/plugins/actions/server/constants/plugin.ts index 7d20eb6990247..b82464bd92a18 100644 --- a/x-pack/plugins/actions/server/constants/plugin.ts +++ b/x-pack/plugins/actions/server/constants/plugin.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LICENSE_TYPE_BASIC, LicenseType } from '../../../../legacy/common/constants'; +import { LicenseType } from '../../../licensing/server'; export const PLUGIN = { ID: 'actions', - MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_BASIC as LicenseType, // TODO: supposed to be changed up on requirements + MINIMUM_LICENSE_REQUIRED: 'basic' as LicenseType, // TODO: supposed to be changed up on requirements // eslint-disable-next-line @typescript-eslint/no-explicit-any getI18nName: (i18n: any): string => i18n.translate('xpack.actions.appName', { diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index 3e92ca331bb93..a23a2b0893261 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -15,6 +15,8 @@ import { SavedObjectsClientContract, SavedObjectAttributes, } from '../../../../src/core/server'; +import { ActionTypeExecutorResult } from '../common'; +export { ActionTypeExecutorResult } from '../common'; export type WithoutQueryAndParams = Pick>; export type GetServicesFunction = (request: KibanaRequest) => Services; @@ -80,16 +82,6 @@ export interface FindActionResult extends ActionResult { referencedByCount: number; } -// the result returned from an action type executor function -export interface ActionTypeExecutorResult { - actionId: string; - status: 'ok' | 'error'; - message?: string; - serviceMessage?: string; - data?: Data; - retry?: null | boolean | Date; -} - // signature of the action type executor function export type ExecutorType = ( options: ActionTypeExecutorOptions diff --git a/x-pack/plugins/alerts/server/constants/plugin.ts b/x-pack/plugins/alerts/server/constants/plugin.ts index c180b68680841..4e1e0c59e0b48 100644 --- a/x-pack/plugins/alerts/server/constants/plugin.ts +++ b/x-pack/plugins/alerts/server/constants/plugin.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LICENSE_TYPE_BASIC, LicenseType } from '../../../../legacy/common/constants'; +import { LicenseType } from '../../../licensing/server'; export const PLUGIN = { ID: 'alerts', - MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_BASIC as LicenseType, // TODO: supposed to be changed up on requirements + MINIMUM_LICENSE_REQUIRED: 'basic' as LicenseType, // TODO: supposed to be changed up on requirements // all plugins seem to use getI18nName with any // eslint-disable-next-line @typescript-eslint/no-explicit-any getI18nName: (i18n: any): string => 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 5be684eca4651..7ea3f83d747c0 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pickBy, mapValues, omit, without } from 'lodash'; +import { pickBy, mapValues, without } from 'lodash'; import { Logger, KibanaRequest } from '../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; import { ConcreteTaskInstance } from '../../../task_manager/server'; @@ -228,12 +228,13 @@ export class TaskRunner { }); if (!muteAll) { - const enabledAlertInstances = omit(instancesWithScheduledActions, ...mutedInstanceIds); + const mutedInstanceIdsSet = new Set(mutedInstanceIds); await Promise.all( - Object.entries(enabledAlertInstances) + Object.entries(instancesWithScheduledActions) .filter( - ([, alertInstance]: [string, AlertInstance]) => !alertInstance.isThrottled(throttle) + ([alertInstanceName, alertInstance]: [string, AlertInstance]) => + !alertInstance.isThrottled(throttle) && !mutedInstanceIdsSet.has(alertInstanceName) ) .map(([id, alertInstance]: [string, AlertInstance]) => this.executeAlertInstance(id, alertInstance, executionHandler) diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap index 2044053e049f1..2962a5fd2df3b 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/__snapshots__/index.test.ts.snap @@ -172,7 +172,7 @@ Array [ }, Object { "key": "transaction_max_spans", - "max": 32000, + "max": undefined, "min": 0, "type": "integer", "validationName": "integerRt", diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts index 95892f435e8f9..e777e1fd09d0b 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts @@ -177,7 +177,6 @@ export const generalSettings: RawSettingDefinition[] = [ key: 'transaction_max_spans', type: 'integer', min: 0, - max: 32000, defaultValue: '500', label: i18n.translate('xpack.apm.agentConfig.transactionMaxSpans.label', { defaultMessage: 'Transaction max spans', diff --git a/x-pack/plugins/apm/common/service_health_status.ts b/x-pack/plugins/apm/common/service_health_status.ts index 468f06ab97af8..1d4bcfb3b0e07 100644 --- a/x-pack/plugins/apm/common/service_health_status.ts +++ b/x-pack/plugins/apm/common/service_health_status.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { ANOMALY_SEVERITY } from '../../ml/common'; -import { EuiTheme } from '../../../legacy/common/eui_styled_components'; +import { EuiTheme } from '../../xpack_legacy/common'; export enum ServiceHealthStatus { healthy = 'healthy', diff --git a/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature b/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature index ac4188a598458..7b894b6ca7aac 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature +++ b/x-pack/plugins/apm/e2e/cypress/integration/csm_dashboard.feature @@ -27,3 +27,11 @@ Feature: CSM Dashboard Given a user clicks the page load breakdown filter When the user selected the breakdown Then breakdown series should appear in chart + + Scenario: Search by url filter focus + When a user clicks inside url search field + Then it displays top pages in the suggestion popover + + Scenario: Search by url filter + When a user enters a query in url search field + Then it should filter results based on query diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/url_search_filter.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/url_search_filter.ts new file mode 100644 index 0000000000000..3b5dd70065055 --- /dev/null +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/url_search_filter.ts @@ -0,0 +1,65 @@ +/* + * 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 { When, Then } from 'cypress-cucumber-preprocessor/steps'; +import { DEFAULT_TIMEOUT } from './csm_dashboard'; + +When(`a user clicks inside url search field`, () => { + // wait for all loading to finish + cy.get('kbnLoadingIndicator').should('not.be.visible'); + cy.get('.euiStat__title-isLoading').should('not.be.visible'); + cy.get('span[data-cy=csmUrlFilter]', DEFAULT_TIMEOUT).within(() => { + cy.get('input.euiFieldSearch').click(); + }); +}); + +Then(`it displays top pages in the suggestion popover`, () => { + cy.get('kbnLoadingIndicator').should('not.be.visible'); + + cy.get('div.euiPopover__panel-isOpen', DEFAULT_TIMEOUT).within(() => { + const listOfUrls = cy.get('li.euiSelectableListItem'); + listOfUrls.should('have.length', 5); + + const actualUrlsText = [ + 'http://opbeans-node:3000/dashboardPage views: 17Page load duration: 109 ms ', + 'http://opbeans-node:3000/ordersPage views: 14Page load duration: 72 ms', + ]; + + cy.get('li.euiSelectableListItem') + .eq(0) + .should('have.text', actualUrlsText[0]); + cy.get('li.euiSelectableListItem') + .eq(1) + .should('have.text', actualUrlsText[1]); + }); +}); + +When(`a user enters a query in url search field`, () => { + cy.get('kbnLoadingIndicator').should('not.be.visible'); + + cy.get('[data-cy=csmUrlFilter]').within(() => { + cy.get('input.euiSelectableSearch').type('cus'); + }); + + cy.get('kbnLoadingIndicator').should('not.be.visible'); +}); + +Then(`it should filter results based on query`, () => { + cy.get('kbnLoadingIndicator').should('not.be.visible'); + + cy.get('div.euiPopover__panel-isOpen', DEFAULT_TIMEOUT).within(() => { + const listOfUrls = cy.get('li.euiSelectableListItem'); + listOfUrls.should('have.length', 1); + + const actualUrlsText = [ + 'http://opbeans-node:3000/customersPage views: 10Page load duration: 76 ms ', + ]; + + cy.get('li.euiSelectableListItem') + .eq(0) + .should('have.text', actualUrlsText[0]); + }); +}); diff --git a/x-pack/plugins/apm/e2e/ingest-data/replay.js b/x-pack/plugins/apm/e2e/ingest-data/replay.js index 6bab95635f558..74c86b1b09ab4 100644 --- a/x-pack/plugins/apm/e2e/ingest-data/replay.js +++ b/x-pack/plugins/apm/e2e/ingest-data/replay.js @@ -70,34 +70,40 @@ function incrementSpinnerCount({ success }) { } let iterIndex = 0; +function setItemMetaAndHeaders(item) { + const headers = { + 'content-type': 'application/x-ndjson', + }; + + if (SECRET_TOKEN) { + headers.Authorization = `Bearer ${SECRET_TOKEN}`; + } + + if (item.url === '/intake/v2/rum/events') { + if (iterIndex === userAgents.length) { + // set some event agent to opbean + setRumAgent(item); + iterIndex = 0; + } + headers['User-Agent'] = userAgents[iterIndex]; + headers['X-Forwarded-For'] = userIps[iterIndex]; + iterIndex++; + } + return headers; +} + function setRumAgent(item) { - item.body = item.body.replace( - '"name":"client"', - '"name":"opbean-client-rum"' - ); + if (item.body) { + item.body = item.body.replace( + '"name":"client"', + '"name":"opbean-client-rum"' + ); + } } -async function insertItem(item) { +async function insertItem(item, headers) { try { const url = `${APM_SERVER_URL}${item.url}`; - const headers = { - 'content-type': 'application/x-ndjson', - }; - - if (item.url === '/intake/v2/rum/events') { - if (iterIndex === userAgents.length) { - // set some event agent to opbean - setRumAgent(item); - iterIndex = 0; - } - headers['User-Agent'] = userAgents[iterIndex]; - headers['X-Forwarded-For'] = userIps[iterIndex]; - iterIndex++; - } - - if (SECRET_TOKEN) { - headers.Authorization = `Bearer ${SECRET_TOKEN}`; - } await axios({ method: item.method, @@ -133,8 +139,9 @@ async function init() { await Promise.all( items.map(async (item) => { try { + const headers = setItemMetaAndHeaders(item); // retry 5 times with exponential backoff - await pRetry(() => limit(() => insertItem(item)), { + await pRetry(() => limit(() => insertItem(item, headers)), { retries: 5, }); incrementSpinnerCount({ success: true }); 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 4aa2d841e8deb..0d61ca8e39845 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 @@ -62,15 +62,6 @@ export function renderAsRedirectTo(to: string) { // If you provide an inline function to the component prop, you would create a // new component every render. This results in the existing component unmounting // and the new component mounting instead of just updating the existing component. -// -// This means you should use `render` if you're providing an inline function. -// However, the `ApmRoute` component from @elastic/apm-rum-react, only supports -// `component`, and will give you a large console warning if you use `render`. -// -// This warning cannot be turned off -// (see https://github.com/elastic/apm-agent-rum-js/issues/881) so while this is -// slightly more code, it provides better performance without causing console -// warnings to appear. function HomeServices() { return ; } @@ -153,7 +144,7 @@ export const routes: APMRouteDefinition[] = [ { exact: true, path: '/', - component: renderAsRedirectTo('/services'), + render: renderAsRedirectTo('/services'), breadcrumb: 'APM', }, { @@ -175,7 +166,7 @@ export const routes: APMRouteDefinition[] = [ { exact: true, path: '/settings', - component: renderAsRedirectTo('/settings/agent-configuration'), + render: renderAsRedirectTo('/settings/agent-configuration'), breadcrumb: i18n.translate('xpack.apm.breadcrumb.listSettingsTitle', { defaultMessage: 'Settings', }), @@ -219,7 +210,7 @@ export const routes: APMRouteDefinition[] = [ exact: true, path: '/services/:serviceName', breadcrumb: ({ match }) => match.params.serviceName, - component: (props: RouteComponentProps<{ serviceName: string }>) => + render: (props: RouteComponentProps<{ serviceName: string }>) => renderAsRedirectTo( `/services/${props.match.params.serviceName}/transactions` )(props), diff --git a/x-pack/plugins/apm/public/components/app/Main/route_config/route_config.test.tsx b/x-pack/plugins/apm/public/components/app/Main/route_config/route_config.test.tsx index 21a162111bc79..ba3641cc4dadd 100644 --- a/x-pack/plugins/apm/public/components/app/Main/route_config/route_config.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/route_config/route_config.test.tsx @@ -14,7 +14,7 @@ describe('routes', () => { it('redirects to /services', () => { const location = { hash: '', pathname: '/', search: '' }; expect( - (route as any).component({ location } as any).props.to.pathname + (route!.render!({ location } as any) as any).props.to.pathname ).toEqual('/services'); }); }); @@ -28,9 +28,7 @@ describe('routes', () => { search: '', }; - expect( - ((route as any).component({ location }) as any).props.to - ).toEqual({ + expect((route!.render!({ location } as any) as any).props.to).toEqual({ hash: '', pathname: '/services/opbeans-python/transactions/view', search: 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 c76be19edfe47..904144dec6de9 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 @@ -33,7 +33,10 @@ import { ChartWrapper } from '../ChartWrapper'; import { I18LABELS } from '../translations'; interface Props { - data?: Array>; + data?: { + topItems: string[]; + items: Array>; + }; loading: boolean; } @@ -68,15 +71,9 @@ export function PageViewsChart({ data, loading }: Props) { }); }; - let breakdownAccessors: Set = new Set(); - if (data && data.length > 0) { - data.forEach((item) => { - breakdownAccessors = new Set([ - ...Array.from(breakdownAccessors), - ...Object.keys(item).filter((key) => key !== 'x'), - ]); - }); - } + const breakdownAccessors = data?.topItems?.length ? data?.topItems : ['y']; + + const [darkMode] = useUiSetting$('theme:darkMode'); const customSeriesNaming: SeriesNameFn = ({ yAccessor }) => { if (yAccessor === 'y') { @@ -86,8 +83,6 @@ export function PageViewsChart({ data, loading }: Props) { return yAccessor; }; - const [darkMode] = useUiSetting$('theme:darkMode'); - return ( {(!loading || data) && ( @@ -115,7 +110,8 @@ export function PageViewsChart({ data, loading }: Props) { id="page_views" title={I18LABELS.pageViews} position={Position.Left} - tickFormat={(d) => numeral(d).format('0a')} + tickFormat={(d) => numeral(d).format('0')} + labelFormat={(d) => numeral(d).format('0a')} /> 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 1edfd724dadd7..a77d27c4bc883 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 @@ -22,7 +22,7 @@ const ClFlexGroup = styled(EuiFlexGroup)` export function ClientMetrics() { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, searchTerm } = urlParams; const { data, status } = useFetcher( (callApmApi) => { @@ -31,13 +31,18 @@ export function ClientMetrics() { return callApmApi({ pathname: '/api/apm/rum/client-metrics', params: { - query: { start, end, uiFilters: JSON.stringify(uiFilters) }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + urlQuery: searchTerm, + }, }, }); } return Promise.resolve(null); }, - [start, end, uiFilters] + [start, end, uiFilters, searchTerm] ); const STAT_STYLE = { width: '240px' }; 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 f97db3b42eecb..c8e45d2b2f672 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 @@ -22,7 +22,7 @@ export interface PercentileRange { export function PageLoadDistribution() { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, searchTerm } = urlParams; const [percentileRange, setPercentileRange] = useState({ min: null, @@ -41,6 +41,7 @@ export function PageLoadDistribution() { start, end, uiFilters: JSON.stringify(uiFilters), + urlQuery: searchTerm, ...(percentileRange.min && percentileRange.max ? { minPercentile: String(percentileRange.min), @@ -53,7 +54,14 @@ export function PageLoadDistribution() { } return Promise.resolve(null); }, - [end, start, uiFilters, percentileRange.min, percentileRange.max] + [ + end, + start, + uiFilters, + percentileRange.min, + percentileRange.max, + searchTerm, + ] ); const onPercentileChange = (min: number, max: number) => { 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 814cf977c9569..d6a544333531f 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 @@ -17,7 +17,7 @@ interface Props { export const useBreakdowns = ({ percentileRange, field, value }: Props) => { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, searchTerm } = urlParams; const { min: minP, max: maxP } = percentileRange ?? {}; @@ -32,6 +32,7 @@ export const useBreakdowns = ({ percentileRange, field, value }: Props) => { end, breakdown: value, uiFilters: JSON.stringify(uiFilters), + urlQuery: searchTerm, ...(minP && maxP ? { minPercentile: String(minP), @@ -43,6 +44,6 @@ export const useBreakdowns = ({ percentileRange, field, value }: Props) => { }); } }, - [end, start, uiFilters, field, value, minP, maxP] + [end, start, uiFilters, field, value, minP, maxP, searchTerm] ); }; 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 2991f9a15f085..f2da0955412e7 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 @@ -16,7 +16,7 @@ import { BreakdownItem } from '../../../../../typings/ui_filters'; export function PageViewsTrend() { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, searchTerm } = urlParams; const [breakdown, setBreakdown] = useState(null); @@ -30,6 +30,7 @@ export function PageViewsTrend() { start, end, uiFilters: JSON.stringify(uiFilters), + urlQuery: searchTerm, ...(breakdown ? { breakdowns: JSON.stringify(breakdown), @@ -41,7 +42,7 @@ export function PageViewsTrend() { } return Promise.resolve(undefined); }, - [end, start, uiFilters, breakdown] + [end, start, uiFilters, breakdown, searchTerm] ); return ( diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/ServiceNameFilter/index.tsx similarity index 94% rename from x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx rename to x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/ServiceNameFilter/index.tsx index cbf9ba009dce2..f10c9e888a193 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/ServiceNameFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/ServiceNameFilter/index.tsx @@ -13,8 +13,8 @@ import { import { i18n } from '@kbn/i18n'; import React, { useEffect, useCallback } from 'react'; import { useHistory } from 'react-router-dom'; -import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { fromQuery, toQuery } from '../../Links/url_helpers'; +import { useUrlParams } from '../../../../../hooks/useUrlParams'; +import { fromQuery, toQuery } from '../../../../shared/Links/url_helpers'; interface Props { serviceNames: string[]; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/RenderOption.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/RenderOption.tsx new file mode 100644 index 0000000000000..1a6f4e25fc315 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/RenderOption.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, { ReactNode } from 'react'; +import classNames from 'classnames'; +import { EuiHighlight, EuiSelectableOption } from '@elastic/eui'; +import styled from 'styled-components'; +import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; + +const StyledSpan = styled.span` + color: ${euiLightVars.euiColorSecondaryText}; + font-weight: 500; + :not(:last-of-type)::after { + content: '•'; + margin: 0 4px; + } +`; + +const StyledListSpan = styled.span` + display: block; + margin-top: 4px; + font-size: 12px; +`; +export type UrlOption = { + meta?: string[]; +} & EuiSelectableOption; + +export const formatOptions = (options: EuiSelectableOption[]) => { + return options.map((item: EuiSelectableOption) => ({ + title: item.label, + ...item, + className: classNames( + 'euiSelectableTemplateSitewide__listItem', + item.className + ), + })); +}; + +export function selectableRenderOptions( + option: UrlOption, + searchValue: string +) { + return ( + <> + + {option.label} + + {renderOptionMeta(option.meta)} + + ); +} + +function renderOptionMeta(meta?: string[]): ReactNode { + if (!meta || meta.length < 1) return; + return ( + + {meta.map((item: string) => ( + {item} + ))} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx new file mode 100644 index 0000000000000..298ec15b8480b --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx @@ -0,0 +1,164 @@ +/* + * 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, { FormEvent, useRef, useState } from 'react'; +import { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiPopover, + EuiPopoverTitle, + EuiSelectable, + EuiSelectableMessage, +} from '@elastic/eui'; +import { + formatOptions, + selectableRenderOptions, + UrlOption, +} from './RenderOption'; +import { I18LABELS } from '../../translations'; + +interface Props { + data: { + items: UrlOption[]; + total?: number; + }; + loading: boolean; + onInputChange: (e: FormEvent) => void; + onTermChange: () => void; + onChange: (updatedOptions: UrlOption[]) => void; + searchValue: string; + onClose: () => void; +} + +export function SelectableUrlList({ + data, + loading, + onInputChange, + onTermChange, + onChange, + searchValue, + onClose, +}: Props) { + const [popoverIsOpen, setPopoverIsOpen] = useState(false); + const [popoverRef, setPopoverRef] = useState(null); + const [searchRef, setSearchRef] = useState(null); + + const titleRef = useRef(null); + + const searchOnFocus = (e: React.FocusEvent) => { + setPopoverIsOpen(true); + }; + + const onSearchInput = (e: React.FormEvent) => { + onInputChange(e); + setPopoverIsOpen(true); + }; + + const searchOnBlur = (e: React.FocusEvent) => { + if ( + !popoverRef?.contains(e.relatedTarget as HTMLElement) && + !popoverRef?.contains(titleRef.current as HTMLDivElement) + ) { + setPopoverIsOpen(false); + } + }; + + const formattedOptions = formatOptions(data.items ?? []); + + const closePopover = () => { + setPopoverIsOpen(false); + onClose(); + if (searchRef) { + searchRef.blur(); + } + }; + + const loadingMessage = ( + + +
+

{I18LABELS.loadingResults}

+
+ ); + + const emptyMessage = ( + +

{I18LABELS.noResults}

+
+ ); + + const titleText = searchValue + ? I18LABELS.getSearchResultsLabel(data?.total ?? 0) + : I18LABELS.topPages; + + function PopOverTitle() { + return ( + + + + {loading ? : titleText} + + + { + onTermChange(); + setPopoverIsOpen(false); + }} + > + {I18LABELS.matchThisQuery} + + + + + ); + } + + return ( + + {(list, search) => ( + +
+ + {list} +
+
+ )} +
+ ); +} 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 new file mode 100644 index 0000000000000..b88cf29740dcd --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/index.tsx @@ -0,0 +1,132 @@ +/* + * 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 { EuiTitle } from '@elastic/eui'; +import useDebounce from 'react-use/lib/useDebounce'; +import React, { useEffect, useState, FormEvent, useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; +import { useUrlParams } from '../../../../../hooks/useUrlParams'; +import { useFetcher } from '../../../../../hooks/useFetcher'; +import { I18LABELS } from '../../translations'; +import { fromQuery, toQuery } from '../../../../shared/Links/url_helpers'; +import { formatToSec } from '../../UXMetrics/KeyUXMetrics'; +import { SelectableUrlList } from './SelectableUrlList'; +import { UrlOption } from './RenderOption'; + +interface Props { + onChange: (value: string[]) => void; +} + +export function URLSearch({ onChange: onFilterChange }: Props) { + const history = useHistory(); + + const { urlParams, uiFilters } = useUrlParams(); + + const { start, end, serviceName } = urlParams; + const [searchValue, setSearchValue] = useState(''); + + const [debouncedValue, setDebouncedValue] = useState(''); + + useDebounce( + () => { + setSearchValue(debouncedValue); + }, + 250, + [debouncedValue] + ); + + const updateSearchTerm = useCallback( + (searchTermN: string) => { + const newLocation = { + ...history.location, + search: fromQuery({ + ...toQuery(history.location.search), + searchTerm: searchTermN, + }), + }; + history.push(newLocation); + }, + [history] + ); + + const [checkedUrls, setCheckedUrls] = useState([]); + + const { data, status } = useFetcher( + (callApmApi) => { + if (start && end && serviceName) { + const { transactionUrl, ...restFilters } = uiFilters; + + return callApmApi({ + pathname: '/api/apm/rum-client/url-search', + params: { + query: { + start, + end, + uiFilters: JSON.stringify(restFilters), + urlQuery: searchValue, + }, + }, + }); + } + return Promise.resolve(null); + }, + [start, end, serviceName, uiFilters, searchValue] + ); + + useEffect(() => { + setCheckedUrls(uiFilters.transactionUrl || []); + }, [uiFilters]); + + const onChange = (updatedOptions: UrlOption[]) => { + const clickedItems = updatedOptions.filter( + (option) => option.checked === 'on' + ); + + setCheckedUrls(clickedItems.map((item) => item.url)); + }; + + const items: UrlOption[] = (data?.items ?? []).map((item) => ({ + label: item.url, + key: item.url, + meta: [ + I18LABELS.pageViews + ': ' + item.count, + I18LABELS.pageLoadDuration + ': ' + formatToSec(item.pld), + ], + url: item.url, + checked: checkedUrls?.includes(item.url) ? 'on' : undefined, + })); + + const onInputChange = (e: FormEvent) => { + setDebouncedValue(e.currentTarget.value); + }; + + const isLoading = status !== 'success'; + + const onTermChange = () => { + updateSearchTerm(searchValue); + }; + + const onClose = () => { + onFilterChange(checkedUrls); + }; + + return ( + <> + +

{I18LABELS.url}

+
+ + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/UrlList.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/UrlList.tsx new file mode 100644 index 0000000000000..437c005db37b0 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/UrlList.tsx @@ -0,0 +1,74 @@ +/* + * 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 { EuiFlexGrid, EuiFlexItem, EuiBadge } from '@elastic/eui'; +import styled from 'styled-components'; +import { i18n } from '@kbn/i18n'; +import { px, truncate, unit } from '../../../../style/variables'; + +const BadgeText = styled.div` + display: inline-block; + ${truncate(px(unit * 12))}; + vertical-align: middle; +`; + +interface Props { + value: string[]; + onRemove: (val: string) => void; +} + +const formatUrlValue = (val: string) => { + const maxUrlToDisplay = 30; + const urlLength = val.length; + if (urlLength < maxUrlToDisplay) { + return val; + } + const urlObj = new URL(val); + if (urlObj.pathname === '/') { + return val; + } + const domainVal = urlObj.hostname; + const extraLength = urlLength - maxUrlToDisplay; + const extraDomain = domainVal.substring(0, extraLength); + + if (urlObj.pathname.length + 7 > maxUrlToDisplay) { + return val.replace(domainVal, '..'); + } + + return val.replace(extraDomain, '..'); +}; + +const removeFilterLabel = i18n.translate( + 'xpack.apm.uifilter.badge.removeFilter', + { defaultMessage: 'Remove filter' } +); + +export function UrlList({ onRemove, value }: Props) { + return ( + + {value.map((val) => ( + + { + onRemove(val); + }} + onClickAriaLabel={removeFilterLabel} + iconOnClick={() => { + onRemove(val); + }} + iconOnClickAriaLabel={removeFilterLabel} + iconType="cross" + iconSide="right" + > + {formatUrlValue(val)} + + + ))} + + ); +} 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 new file mode 100644 index 0000000000000..9d3c8d012871f --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/index.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import React, { useCallback, useMemo } from 'react'; +import { EuiSpacer, EuiBadge } from '@elastic/eui'; +import { useHistory } from 'react-router-dom'; +import { Projection } from '../../../../../common/projections'; +import { useLocalUIFilters } from '../../../../hooks/useLocalUIFilters'; +import { URLSearch } from './URLSearch'; +import { LocalUIFilters } from '../../../shared/LocalUIFilters'; +import { UrlList } from './UrlList'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { fromQuery, toQuery } from '../../../shared/Links/url_helpers'; + +const removeSearchTermLabel = i18n.translate( + 'xpack.apm.uiFilter.url.removeSearchTerm', + { defaultMessage: 'Clear url query' } +); + +export function URLFilter() { + const history = useHistory(); + + const { + urlParams: { searchTerm }, + } = useUrlParams(); + + const localUIFiltersConfig = useMemo(() => { + const config: React.ComponentProps = { + filterNames: ['transactionUrl'], + projection: Projection.rumOverview, + }; + + return config; + }, []); + + const { filters, setFilterValue } = useLocalUIFilters({ + ...localUIFiltersConfig, + }); + + const updateSearchTerm = useCallback( + (searchTermN?: string) => { + const newLocation = { + ...history.location, + search: fromQuery({ + ...toQuery(history.location.search), + searchTerm: searchTermN, + }), + }; + history.push(newLocation); + }, + [history] + ); + + const { name, value: filterValue } = filters[0]; + + return ( + + + { + setFilterValue('transactionUrl', value); + }} + /> + + {searchTerm && ( + <> + { + updateSearchTerm(); + }} + onClickAriaLabel={removeSearchTermLabel} + iconOnClick={() => { + updateSearchTerm(); + }} + iconOnClickAriaLabel={removeSearchTermLabel} + iconType="cross" + iconSide="right" + > + *{searchTerm}* + + + + )} + {filterValue.length > 0 && ( + { + setFilterValue( + name, + filterValue.filter((v) => val !== v) + ); + }} + value={filterValue} + /> + )} + + + ); +} 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 5c9a636adec8f..1d8360872afba 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 @@ -38,7 +38,7 @@ interface Props { export function KeyUXMetrics({ data, loading }: Props) { const { urlParams, uiFilters } = useUrlParams(); - const { start, end, serviceName } = urlParams; + const { start, end, serviceName, searchTerm } = urlParams; const { data: longTaskData, status } = useFetcher( (callApmApi) => { @@ -46,13 +46,18 @@ export function KeyUXMetrics({ data, loading }: Props) { return callApmApi({ pathname: '/api/apm/rum-client/long-task-metrics', params: { - query: { start, end, uiFilters: JSON.stringify(uiFilters) }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + urlQuery: searchTerm, + }, }, }); } return Promise.resolve(null); }, - [start, end, serviceName, uiFilters] + [start, end, serviceName, uiFilters, searchTerm] ); // Note: FCP value is in ms unit 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 94c3acfaa9727..3c7b4e39401de 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 @@ -33,7 +33,7 @@ export interface UXMetrics { export function UXMetrics() { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, searchTerm } = urlParams; const { data, status } = useFetcher( (callApmApi) => { @@ -42,13 +42,18 @@ export function UXMetrics() { return callApmApi({ pathname: '/api/apm/rum-client/web-core-vitals', params: { - query: { start, end, uiFilters: JSON.stringify(uiFilters) }, + query: { + start, + end, + uiFilters: JSON.stringify(uiFilters), + urlQuery: searchTerm, + }, }, }); } return Promise.resolve(null); }, - [start, end, uiFilters] + [start, end, uiFilters, searchTerm] ); return ( 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 245f58370d3d7..2db6ef8fa6c06 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 @@ -14,7 +14,7 @@ import { useUrlParams } from '../../../../hooks/useUrlParams'; export function VisitorBreakdown() { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { start, end, searchTerm } = urlParams; const { data, status } = useFetcher( (callApmApi) => { @@ -26,13 +26,14 @@ export function VisitorBreakdown() { start, end, uiFilters: JSON.stringify(uiFilters), + urlQuery: searchTerm, }, }, }); } return Promise.resolve(null); }, - [end, start, uiFilters] + [end, start, uiFilters, searchTerm] ); return ( diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx index fa0551252b6a1..588831d55771d 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/index.tsx @@ -12,14 +12,15 @@ import { EuiSpacer, } from '@elastic/eui'; import { useTrackPageview } from '../../../../../observability/public'; -import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { Projection } from '../../../../common/projections'; import { RumDashboard } from './RumDashboard'; -import { ServiceNameFilter } from '../../shared/LocalUIFilters/ServiceNameFilter'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { useFetcher } from '../../../hooks/useFetcher'; import { RUM_AGENTS } from '../../../../common/agent_name'; import { EnvironmentFilter } from '../../shared/EnvironmentFilter'; +import { URLFilter } from './URLFilter'; +import { LocalUIFilters } from '../../shared/LocalUIFilters'; +import { ServiceNameFilter } from './URLFilter/ServiceNameFilter'; export function RumOverview() { useTrackPageview({ app: 'apm', path: 'rum_overview' }); @@ -27,7 +28,7 @@ export function RumOverview() { const localUIFiltersConfig = useMemo(() => { const config: React.ComponentProps = { - filterNames: ['transactionUrl', 'location', 'device', 'os', 'browser'], + filterNames: ['location', 'device', 'os', 'browser'], projection: Projection.rumOverview, }; @@ -63,6 +64,7 @@ export function RumOverview() { + <> + {' '} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts index 1fafb7d1ed4d0..714788ef468c6 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts @@ -79,6 +79,32 @@ export const I18LABELS = { defaultMessage: 'Page load duration by region', } ), + searchByUrl: i18n.translate('xpack.apm.rum.filters.searchByUrl', { + defaultMessage: 'Search by url', + }), + getSearchResultsLabel: (total: number) => + i18n.translate('xpack.apm.rum.filters.searchResults', { + defaultMessage: '{total} Search results', + values: { total }, + }), + topPages: i18n.translate('xpack.apm.rum.filters.topPages', { + defaultMessage: 'Top pages', + }), + select: i18n.translate('xpack.apm.rum.filters.select', { + defaultMessage: 'Select', + }), + url: i18n.translate('xpack.apm.rum.filters.url', { + defaultMessage: 'Url', + }), + matchThisQuery: i18n.translate('xpack.apm.rum.filters.url.matchThisQuery', { + defaultMessage: 'Match this query', + }), + loadingResults: i18n.translate('xpack.apm.rum.filters.url.loadingResults', { + defaultMessage: 'Loading results', + }), + noResults: i18n.translate('xpack.apm.rum.filters.url.noResults', { + defaultMessage: 'No results available', + }), }; export const VisitorBreakdownLabel = i18n.translate( diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/BetaBadge.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/BetaBadge.tsx deleted file mode 100644 index b468470e3a17d..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/BetaBadge.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiBetaBadge } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import styled from 'styled-components'; - -const BetaBadgeContainer = styled.div` - right: ${({ theme }) => theme.eui.gutterTypes.gutterMedium}; - position: absolute; - top: ${({ theme }) => theme.eui.gutterTypes.gutterSmall}; - z-index: 1; /* The element containing the cytoscape canvas has z-index = 0. */ -`; - -export function BetaBadge() { - return ( - - - - ); -} 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 cb5a57e9ab9fb..bb450131bdfb8 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -4,15 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useTheme } from '../../../hooks/useTheme'; +import React from 'react'; +import { useTrackPageview } from '../../../../../observability/public'; import { invalidLicenseMessage, isActivePlatinumLicense, } from '../../../../common/service_map'; import { useFetcher } from '../../../hooks/useFetcher'; import { useLicense } from '../../../hooks/useLicense'; +import { useTheme } from '../../../hooks/useTheme'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { callApmApi } from '../../../services/rest/createCallApmApi'; import { LicensePrompt } from '../../shared/LicensePrompt'; @@ -22,8 +23,6 @@ import { getCytoscapeDivStyle } from './cytoscapeOptions'; import { EmptyBanner } from './EmptyBanner'; import { Popover } from './Popover'; import { useRefDimensions } from './useRefDimensions'; -import { BetaBadge } from './BetaBadge'; -import { useTrackPageview } from '../../../../../observability/public'; interface ServiceMapProps { serviceName?: string; @@ -80,7 +79,6 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { style={getCytoscapeDivStyle(theme)} > - {serviceName && } @@ -96,7 +94,7 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { grow={false} style={{ width: 600, textAlign: 'center' as const }} > - + ); diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/ClientSideMonitoringCallout.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/ClientSideMonitoringCallout.tsx new file mode 100644 index 0000000000000..b6938b211994d --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/ClientSideMonitoringCallout.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiButton, EuiCallOut, EuiSpacer, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; + +export function ClientSideMonitoringCallout() { + const { core } = useApmPluginContext(); + const clientSideMonitoringHref = core.http.basePath.prepend(`/app/csm`); + + return ( + + + {i18n.translate( + 'xpack.apm.transactionOverview.clientSideMonitoring.calloutText', + { + defaultMessage: + 'We are beyond excited to introduce a new experience for analyzing the user experience metrics specifically for your RUM services. It provides insights into the core vitals and visitor breakdown by browser and location. The app is always available in the left sidebar among the other Observability views.', + } + )} + + + + {i18n.translate( + 'xpack.apm.transactionOverview.clientSideMonitoring.linkLabel', + { defaultMessage: 'Take me there' } + )} + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx index 3e32b0ec23b13..7c887da6dc5e6 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/index.tsx @@ -35,6 +35,8 @@ import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { TransactionTypeFilter } from '../../shared/LocalUIFilters/TransactionTypeFilter'; import { TransactionList } from './TransactionList'; import { useRedirect } from './useRedirect'; +import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types'; +import { ClientSideMonitoringCallout } from './ClientSideMonitoringCallout'; function getRedirectLocation({ urlParams, @@ -125,6 +127,12 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) { + {transactionType === TRANSACTION_PAGE_LOAD && ( + <> + + + + )} { ); expect(href).toMatchInlineSnapshot( - `"/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now-4h))&_a=(mlTimeSeriesExplorer:(entities:(service.name:opbeans-test,transaction.type:request),zoom:(from:now/w,to:now-4h)))"` + `"/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:!t,value:0),time:(from:now/w,to:now-4h))&_a=(mlTimeSeriesExplorer:(entities:(service.name:opbeans-test,transaction.type:request)))"` ); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.test.ts b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.test.ts index 66f3903ba849b..d84f55af993aa 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.test.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.test.ts @@ -28,7 +28,7 @@ describe('useTimeSeriesExplorerHref', () => { }); expect(href).toMatchInlineSnapshot( - `"/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(apm-production-485b-high_mean_transaction_duration)),refreshInterval:(pause:!t,value:10000),time:(from:'2020-07-29T17:27:29.000Z',to:'2020-07-29T18:45:00.000Z'))&_a=(mlTimeSeriesExplorer:(entities:(service.name:opbeans-java,transaction.type:request),zoom:(from:'2020-07-29T17:27:29.000Z',to:'2020-07-29T18:45:00.000Z')))"` + `"/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(apm-production-485b-high_mean_transaction_duration)),refreshInterval:(pause:!t,value:10000),time:(from:'2020-07-29T17:27:29.000Z',to:'2020-07-29T18:45:00.000Z'))&_a=(mlTimeSeriesExplorer:(entities:(service.name:opbeans-java,transaction.type:request)))"` ); }); }); 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 3b60962d797ed..0cb87a4f515b6 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 @@ -39,7 +39,6 @@ export function useTimeSeriesExplorerHref({ 'service.name': serviceName, 'transaction.type': transactionType, }, - zoom: time, }, }), } diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.test.tsx deleted file mode 100644 index 9f112475a4a78..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.test.tsx +++ /dev/null @@ -1,24 +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 from 'react'; -import { shallow } from 'enzyme'; -import { BrowserLineChart } from './BrowserLineChart'; -import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; - -describe('BrowserLineChart', () => { - describe('render', () => { - it('renders', () => { - expect(() => - shallow( - - - - ) - ).not.toThrowError(); - }); - }); -}); diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.tsx deleted file mode 100644 index 40caf35155918..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/BrowserLineChart.tsx +++ /dev/null @@ -1,45 +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 { EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { useAvgDurationByBrowser } from '../../../../hooks/useAvgDurationByBrowser'; -import { getDurationFormatter } from '../../../../utils/formatters'; -import { - getResponseTimeTickFormatter, - getResponseTimeTooltipFormatter, - getMaxY, -} from './helper'; -import { TransactionLineChart } from './TransactionLineChart'; - -export function BrowserLineChart() { - const { data } = useAvgDurationByBrowser(); - const maxY = getMaxY(data); - const formatter = getDurationFormatter(maxY); - const formatTooltipValue = getResponseTimeTooltipFormatter(formatter); - const tickFormatY = getResponseTimeTickFormatter(formatter); - - return ( - <> - - - {i18n.translate( - 'xpack.apm.metrics.pageLoadCharts.avgPageLoadByBrowser', - { - defaultMessage: 'Avg. page load duration distribution by browser', - } - )} - - - - - ); -} diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx deleted file mode 100644 index 69d4e8109dfbf..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/ChoroplethToolTip.tsx +++ /dev/null @@ -1,48 +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 from 'react'; -import { i18n } from '@kbn/i18n'; -import { asDuration, asInteger } from '../../../../../utils/formatters'; -import { fontSizes } from '../../../../../style/variables'; - -export function ChoroplethToolTip({ - name, - value, - docCount, -}: { - name: string; - value: number; - docCount: number; -}) { - return ( -
-
{name}
-
- {i18n.translate( - 'xpack.apm.metrics.durationByCountryMap.RegionMapChart.ToolTip.avgPageLoadDuration', - { - defaultMessage: 'Avg. page load duration:', - } - )} -
-
- {asDuration(value)} -
-
- ( - {i18n.translate( - 'xpack.apm.metrics.durationByCountryMap.RegionMapChart.ToolTip.countPageLoads', - { - values: { docCount: asInteger(docCount) }, - defaultMessage: '{docCount} page loads', - } - )} - ) -
-
- ); -} diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/index.tsx deleted file mode 100644 index 965cb2ae4f50a..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/ChoroplethMap/index.tsx +++ /dev/null @@ -1,270 +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, { - useState, - useEffect, - useRef, - useCallback, - useMemo, -} from 'react'; -import { Map, NavigationControl, Popup } from 'mapbox-gl'; -import 'mapbox-gl/dist/mapbox-gl.css'; -import { shade, tint } from 'polished'; -import { EuiTheme } from '../../../../../../../observability/public'; -import { useTheme } from '../../../../../hooks/useTheme'; -import { ChoroplethToolTip } from './ChoroplethToolTip'; - -interface ChoroplethItem { - key: string; - value: number; - docCount: number; -} - -interface Tooltip { - name: string; - value: number; - docCount: number; -} - -interface WorldCountryFeatureProperties { - name: string; - iso2: string; - iso3: string; -} - -interface Props { - items: ChoroplethItem[]; -} - -const CHOROPLETH_LAYER_ID = 'choropleth_layer'; -const CHOROPLETH_POLYGONS_SOURCE_ID = 'choropleth_polygons'; -const GEOJSON_KEY_PROPERTY = 'iso2'; -const MAPBOX_STYLE = - 'https://tiles.maps.elastic.co/styles/osm-bright-desaturated/style.json'; -const GEOJSON_SOURCE = - 'https://vector.maps.elastic.co/files/world_countries_v1.geo.json?elastic_tile_service_tos=agree&my_app_name=ems-landing&my_app_version=7.2.0'; - -export function getProgressionColor(scale: number, theme: EuiTheme) { - const baseColor = theme.eui.euiColorPrimary; - const adjustedScale = 0.75 * scale + 0.05; // prevents pure black & white as min/max colors. - if (adjustedScale < 0.5) { - return tint(adjustedScale * 2, baseColor); - } - if (adjustedScale > 0.5) { - return shade(1 - (adjustedScale - 0.5) * 2, baseColor); - } - return baseColor; -} - -const getMin = (items: ChoroplethItem[]) => - Math.min(...items.map((item) => item.value)); - -const getMax = (items: ChoroplethItem[]) => - Math.max(...items.map((item) => item.value)); - -export function ChoroplethMap(props: Props) { - const theme = useTheme(); - const { items } = props; - const containerRef = useRef(null); - const [map, setMap] = useState(null); - const popupRef = useRef(null); - const popupContainerRef = useRef(null); - const [tooltipState, setTooltipState] = useState(null); - const [min, max] = useMemo(() => [getMin(items), getMax(items)], [items]); - - // converts an item value to a scaled value between 0 and 1 - const getValueScale = useCallback( - (value: number) => (value - min) / (max - min), - [max, min] - ); - - const controlScrollZoomOnWheel = useCallback((event: WheelEvent) => { - if (event.ctrlKey || event.metaKey) { - event.preventDefault(); - } else { - event.stopPropagation(); - } - }, []); - - // side effect creates a new mouseover handler referencing new component state - // and replaces the old one stored in `updateTooltipStateOnMousemoveRef` - useEffect(() => { - const updateTooltipStateOnMousemove = (event: mapboxgl.MapMouseEvent) => { - const isMapQueryable = - map && - popupRef.current && - items.length && - map.getLayer(CHOROPLETH_LAYER_ID); - - if (!isMapQueryable) { - return; - } - (popupRef.current as Popup).setLngLat(event.lngLat); - const hoverFeatures = (map as Map).queryRenderedFeatures(event.point, { - layers: [CHOROPLETH_LAYER_ID], - }); - - if (tooltipState && hoverFeatures.length === 0) { - return setTooltipState(null); - } - - const featureProperties = hoverFeatures[0] - .properties as WorldCountryFeatureProperties; - - if (tooltipState && tooltipState.name === featureProperties.name) { - return; - } - - const item = items.find( - ({ key }) => - featureProperties && key === featureProperties[GEOJSON_KEY_PROPERTY] - ); - - if (item) { - return setTooltipState({ - name: featureProperties.name, - value: item.value, - docCount: item.docCount, - }); - } - - setTooltipState(null); - }; - updateTooltipStateOnMousemoveRef.current = updateTooltipStateOnMousemove; - }, [map, items, tooltipState]); - - const updateTooltipStateOnMousemoveRef = useRef( - (_event: mapboxgl.MapMouseEvent & mapboxgl.EventData) => {} - ); - - // initialization side effect, only runs once - useEffect(() => { - if (containerRef.current === null) { - return; - } - - // set up Map object - const mapboxMap = new Map({ - attributionControl: false, - container: containerRef.current, - dragRotate: false, - touchZoomRotate: false, - zoom: 0.85, - center: { lng: 0, lat: 30 }, - style: MAPBOX_STYLE, - }); - - mapboxMap.addControl( - new NavigationControl({ showCompass: false }), - 'top-left' - ); - - // set up Popup object - popupRef.current = new Popup({ - closeButton: false, - closeOnClick: false, - }); - - // always use the current handler which changes with component state - mapboxMap.on('mousemove', (...args) => - updateTooltipStateOnMousemoveRef.current(...args) - ); - mapboxMap.on('mouseout', () => { - setTooltipState(null); - }); - - // only scroll zoom when key is pressed - const canvasElement = mapboxMap.getCanvas(); - canvasElement.addEventListener('wheel', controlScrollZoomOnWheel); - - mapboxMap.on('load', () => { - mapboxMap.addSource(CHOROPLETH_POLYGONS_SOURCE_ID, { - type: 'geojson', - data: GEOJSON_SOURCE, - }); - setMap(mapboxMap); - }); - - // cleanup function called when component unmounts - return () => { - canvasElement.removeEventListener('wheel', controlScrollZoomOnWheel); - }; - }, [controlScrollZoomOnWheel]); - - // side effect replaces choropleth layer with new one on items changes - useEffect(() => { - if (!map) { - return; - } - - // find first symbol layer to place new layer in correct order - const symbolLayer = (map.getStyle().layers || []).find( - ({ type }) => type === 'symbol' - ); - - if (map.getLayer(CHOROPLETH_LAYER_ID)) { - map.removeLayer(CHOROPLETH_LAYER_ID); - } - - if (items.length === 0) { - return; - } - - const stops = items.map(({ key, value }) => [ - key, - getProgressionColor(getValueScale(value), theme), - ]); - - const fillColor: mapboxgl.FillPaint['fill-color'] = { - property: GEOJSON_KEY_PROPERTY, - stops, - type: 'categorical', - default: 'transparent', - }; - - map.addLayer( - { - id: CHOROPLETH_LAYER_ID, - type: 'fill', - source: CHOROPLETH_POLYGONS_SOURCE_ID, - layout: {}, - paint: { - 'fill-opacity': 0.75, - 'fill-color': fillColor, - }, - }, - symbolLayer ? symbolLayer.id : undefined - ); - }, [map, items, theme, getValueScale]); - - // side effect to only render the Popup when hovering a region with a matching item - useEffect(() => { - if (!(popupContainerRef.current && map && popupRef.current)) { - return; - } - if (tooltipState) { - popupRef.current.setDOMContent(popupContainerRef.current).addTo(map); - if (popupContainerRef.current.parentElement) { - popupContainerRef.current.parentElement.style.pointerEvents = 'none'; - } - } else { - popupRef.current.remove(); - } - }, [map, tooltipState]); - - // render map container and tooltip in a hidden container - return ( -
-
-
-
- {tooltipState ? : null} -
-
-
- ); -} diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx deleted file mode 100644 index 2dd3d058e98b8..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/DurationByCountryMap/index.tsx +++ /dev/null @@ -1,33 +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 { EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { useAvgDurationByCountry } from '../../../../../hooks/useAvgDurationByCountry'; - -import { ChoroplethMap } from '../ChoroplethMap'; - -export function DurationByCountryMap() { - const { data } = useAvgDurationByCountry(); - - return ( - <> - {' '} - - - {i18n.translate( - 'xpack.apm.metrics.durationByCountryMap.avgPageLoadByCountryLabel', - { - defaultMessage: 'Avg. page load duration distribution by country', - } - )} - - - - - ); -} diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx index 6ba080a07b9d3..30ee0ba3eaa1f 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx @@ -26,8 +26,6 @@ import { IUrlParams } from '../../../../context/UrlParamsContext/types'; import { ITransactionChartData } from '../../../../selectors/chartSelectors'; import { asDecimal, tpmUnit } from '../../../../utils/formatters'; import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue'; -import { BrowserLineChart } from './BrowserLineChart'; -import { DurationByCountryMap } from './DurationByCountryMap'; import { ErroneousTransactionsRateChart } from '../ErroneousTransactionsRateChart'; import { TransactionBreakdown } from '../../TransactionBreakdown'; import { @@ -120,24 +118,6 @@ export function TransactionCharts({ - - {transactionType === TRANSACTION_PAGE_LOAD && ( - <> - - - - - - - - - - - - - - - )} ); } diff --git a/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.test.tsx b/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.test.tsx deleted file mode 100644 index bb947e307437e..0000000000000 --- a/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.test.tsx +++ /dev/null @@ -1,41 +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 { renderHook } from '@testing-library/react-hooks'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; -import * as useFetcherModule from './useFetcher'; -import { useAvgDurationByBrowser } from './useAvgDurationByBrowser'; -import React, { ReactNode } from 'react'; -import { MemoryRouter } from 'react-router-dom'; - -function Wrapper({ children }: { children?: ReactNode }) { - return {children}; -} - -describe('useAvgDurationByBrowser', () => { - it('returns data', () => { - const data = [ - { title: 'Other', data: [{ x: 1572530100000, y: 130010.8947368421 }] }, - ]; - jest.spyOn(useFetcherModule, 'useFetcher').mockReturnValueOnce({ - data, - refetch: () => {}, - status: 'success' as useFetcherModule.FETCH_STATUS, - }); - const { result } = renderHook(() => useAvgDurationByBrowser(), { - wrapper: Wrapper, - }); - - expect(result.current.data).toEqual([ - { - color: theme.euiColorVis0, - data: [{ x: 1572530100000, y: 130010.8947368421 }], - title: 'Other', - type: 'linemark', - }, - ]); - }); -}); diff --git a/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.ts b/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.ts deleted file mode 100644 index 78dc4210711ef..0000000000000 --- a/x-pack/plugins/apm/public/hooks/useAvgDurationByBrowser.ts +++ /dev/null @@ -1,63 +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 theme from '@elastic/eui/dist/eui_theme_light.json'; -import { useParams } from 'react-router-dom'; -import { getVizColorForIndex } from '../../common/viz_colors'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AvgDurationByBrowserAPIResponse } from '../../server/lib/transactions/avg_duration_by_browser'; -import { TimeSeries } from '../../typings/timeseries'; -import { useFetcher } from './useFetcher'; -import { useUrlParams } from './useUrlParams'; - -function toTimeSeries(data?: AvgDurationByBrowserAPIResponse): TimeSeries[] { - if (!data) { - return []; - } - - return data.map((item, index) => { - return { - ...item, - color: getVizColorForIndex(index, theme), - type: 'linemark', - }; - }); -} - -export function useAvgDurationByBrowser() { - const { serviceName } = useParams<{ serviceName?: string }>(); - const { - urlParams: { start, end, transactionName }, - uiFilters, - } = useUrlParams(); - - const { data, error, status } = useFetcher( - (callApmApi) => { - if (serviceName && start && end) { - return callApmApi({ - pathname: - '/api/apm/services/{serviceName}/transaction_groups/avg_duration_by_browser', - params: { - path: { serviceName }, - query: { - start, - end, - transactionName, - uiFilters: JSON.stringify(uiFilters), - }, - }, - }); - } - }, - [serviceName, start, end, transactionName, uiFilters] - ); - - return { - data: toTimeSeries(data), - status, - error, - }; -} diff --git a/x-pack/plugins/apm/public/hooks/useAvgDurationByCountry.ts b/x-pack/plugins/apm/public/hooks/useAvgDurationByCountry.ts deleted file mode 100644 index 983f949b72961..0000000000000 --- a/x-pack/plugins/apm/public/hooks/useAvgDurationByCountry.ts +++ /dev/null @@ -1,43 +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 { useParams } from 'react-router-dom'; -import { useFetcher } from './useFetcher'; -import { useUrlParams } from './useUrlParams'; - -export function useAvgDurationByCountry() { - const { serviceName } = useParams<{ serviceName?: string }>(); - const { - urlParams: { start, end, transactionName }, - uiFilters, - } = useUrlParams(); - - const { data = [], error, status } = useFetcher( - (callApmApi) => { - if (serviceName && start && end) { - return callApmApi({ - pathname: - '/api/apm/services/{serviceName}/transaction_groups/avg_duration_by_country', - params: { - path: { serviceName }, - query: { - start, - end, - uiFilters: JSON.stringify(uiFilters), - transactionName, - }, - }, - }); - } - }, - [serviceName, start, end, uiFilters, transactionName] - ); - - return { - data, - status, - error, - }; -} diff --git a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts index cb30c6c064848..49030dc8cacc5 100644 --- a/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts +++ b/x-pack/plugins/apm/server/lib/index_pattern/get_dynamic_index_pattern.ts @@ -8,7 +8,7 @@ import LRU from 'lru-cache'; import { LegacyAPICaller } from '../../../../../../src/core/server'; import { IndexPatternsFetcher, - IIndexPattern, + FieldDescriptor, } from '../../../../../../src/plugins/data/server'; import { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; import { @@ -17,7 +17,12 @@ import { } from '../../../common/processor_event'; import { APMRequestHandlerContext } from '../../routes/typings'; -const cache = new LRU({ +interface IndexPatternTitleAndFields { + title: string; + fields: FieldDescriptor[]; +} + +const cache = new LRU({ max: 100, maxAge: 1000 * 60, }); @@ -53,7 +58,7 @@ export const getDynamicIndexPattern = async ({ pattern: patternIndices, }); - const indexPattern: IIndexPattern = { + const indexPattern: IndexPatternTitleAndFields = { fields, title: indexPatternTitle, }; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts index b3f9646f64029..cf4a5538a208d 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts @@ -19,11 +19,14 @@ import { export async function getClientMetrics({ setup, + urlQuery, }: { setup: Setup & SetupTimeRange & SetupUIFilters; + urlQuery?: string; }) { const projection = getRumPageLoadTransactionsProjection({ setup, + urlQuery, }); const params = mergeProjection(projection, { diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts b/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts index 1faee52034580..812cf9865bda8 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_long_task_metrics.ts @@ -14,12 +14,17 @@ import { SetupTimeRange, SetupUIFilters, } from '../helpers/setup_request'; -import { SPAN_DURATION } from '../../../common/elasticsearch_fieldnames'; +import { + SPAN_DURATION, + TRANSACTION_ID, +} from '../../../common/elasticsearch_fieldnames'; export async function getLongTaskMetrics({ setup, + urlQuery, }: { setup: Setup & SetupTimeRange & SetupUIFilters; + urlQuery?: string; }) { const projection = getRumLongTasksProjection({ setup, @@ -28,9 +33,6 @@ export async function getLongTaskMetrics({ const params = mergeProjection(projection, { body: { size: 0, - query: { - bool: projection.body.query.bool, - }, aggs: { transIds: { terms: { @@ -59,10 +61,13 @@ export async function getLongTaskMetrics({ const response = await apmEventClient.search(params); const { transIds } = response.aggregations ?? {}; - const validTransactions: string[] = await filterPageLoadTransactions( + const validTransactions: string[] = await filterPageLoadTransactions({ setup, - (transIds?.buckets ?? []).map((bucket) => bucket.key as string) - ); + urlQuery, + transactionIds: (transIds?.buckets ?? []).map( + (bucket) => bucket.key as string + ), + }); let noOfLongTasks = 0; let sumOfLongTasks = 0; let longestLongTask = 0; @@ -83,12 +88,18 @@ export async function getLongTaskMetrics({ }; } -async function filterPageLoadTransactions( - setup: Setup & SetupTimeRange & SetupUIFilters, - transactionIds: string[] -) { +async function filterPageLoadTransactions({ + setup, + urlQuery, + transactionIds, +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; + urlQuery?: string; + transactionIds: string[]; +}) { const projection = getRumPageLoadTransactionsProjection({ setup, + urlQuery, }); const params = mergeProjection(projection, { @@ -99,14 +110,14 @@ async function filterPageLoadTransactions( must: [ { terms: { - 'transaction.id': transactionIds, + [TRANSACTION_ID]: transactionIds, }, }, ], filter: [...projection.body.query.bool.filter], }, }, - _source: ['transaction.id'], + _source: [TRANSACTION_ID], }, }); diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts index 3d8ab7a72654d..25de9f06fefc4 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts @@ -40,13 +40,16 @@ export async function getPageLoadDistribution({ setup, minPercentile, maxPercentile, + urlQuery, }: { setup: Setup & SetupTimeRange & SetupUIFilters; minPercentile?: string; maxPercentile?: string; + urlQuery?: string; }) { const projection = getRumPageLoadTransactionsProjection({ setup, + urlQuery, }); const params = mergeProjection(projection, { diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts index f25062c67f87a..ef4f8b16e0e7b 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts @@ -18,6 +18,7 @@ export async function getPageViewTrends({ }: { setup: Setup & SetupTimeRange & SetupUIFilters; breakdowns?: string; + urlQuery?: string; }) { const projection = getRumPageLoadTransactionsProjection({ setup, @@ -51,6 +52,16 @@ export async function getPageViewTrends({ } : undefined, }, + ...(breakdownItem + ? { + topBreakdowns: { + terms: { + field: breakdownItem.fieldName, + size: 9, + }, + }, + } + : {}), }, }, }); @@ -59,25 +70,44 @@ export async function getPageViewTrends({ const response = await apmEventClient.search(params); + const { topBreakdowns } = response.aggregations ?? {}; + + // we are only displaying top 9 + const topItems: string[] = (topBreakdowns?.buckets ?? []).map( + ({ key }) => key as string + ); + const result = response.aggregations?.pageViews.buckets ?? []; - return result.map((bucket) => { - const { key: xVal, doc_count: bCount } = bucket; - const res: Record = { - x: xVal, - y: bCount, - }; - if ('breakdown' in bucket) { - const categoryBuckets = bucket.breakdown.buckets; - categoryBuckets.forEach(({ key, doc_count: docCount }) => { - if (key === 'Other') { - res[key + `(${breakdownItem?.name})`] = docCount; - } else { - res[key] = docCount; + return { + topItems, + items: result.map((bucket) => { + const { key: xVal, doc_count: bCount } = bucket; + const res: Record = { + x: xVal, + y: bCount, + }; + if ('breakdown' in bucket) { + let top9Count = 0; + const categoryBuckets = bucket.breakdown.buckets; + categoryBuckets.forEach(({ key, doc_count: docCount }) => { + if (topItems.includes(key as string)) { + if (res[key]) { + // if term is already in object, just add it to it + res[key] += docCount; + } else { + res[key] = docCount; + } + top9Count += docCount; + } + }); + // Top 9 plus others, get a diff from parent bucket total + if (bCount > top9Count) { + res.Other = bCount - top9Count; } - }); - } + } - return res; - }); + return res; + }), + }; } diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts index 1945140e35777..d59817cc682a9 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts @@ -44,11 +44,13 @@ export const getPageLoadDistBreakdown = async ({ minDuration, maxDuration, breakdown, + urlQuery, }: { setup: Setup & SetupTimeRange & SetupUIFilters; minDuration: number; maxDuration: number; breakdown: string; + urlQuery?: string; }) => { // convert secs to micros const stepValues = getPLDChartSteps({ @@ -58,6 +60,7 @@ export const getPageLoadDistBreakdown = async ({ const projection = getRumPageLoadTransactionsProjection({ setup, + urlQuery, }); const params = mergeProjection(projection, { diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_url_search.ts b/x-pack/plugins/apm/server/lib/rum_client/get_url_search.ts new file mode 100644 index 0000000000000..a7117f275c17b --- /dev/null +++ b/x-pack/plugins/apm/server/lib/rum_client/get_url_search.ts @@ -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 { mergeProjection } from '../../projections/util/merge_projection'; +import { + Setup, + SetupTimeRange, + SetupUIFilters, +} from '../helpers/setup_request'; +import { getRumPageLoadTransactionsProjection } from '../../projections/rum_page_load_transactions'; + +export async function getUrlSearch({ + setup, + urlQuery, +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; + urlQuery?: string; +}) { + const projection = getRumPageLoadTransactionsProjection({ + setup, + urlQuery, + }); + + const params = mergeProjection(projection, { + body: { + size: 0, + aggs: { + totalUrls: { + cardinality: { + field: 'url.full', + }, + }, + urls: { + terms: { + field: 'url.full', + size: 10, + }, + aggs: { + medianPLD: { + percentiles: { + field: 'transaction.duration.us', + percents: [50], + }, + }, + }, + }, + }, + }, + }); + + const { apmEventClient } = setup; + + const response = await apmEventClient.search(params); + const { urls, totalUrls } = response.aggregations ?? {}; + + return { + total: totalUrls?.value || 0, + items: (urls?.buckets ?? []).map((bucket) => ({ + url: bucket.key as string, + count: bucket.doc_count, + pld: bucket.medianPLD.values['50.0'] ?? 0, + })), + }; +} diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts index 3493307929f42..1b4388afd7c5d 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts @@ -19,11 +19,14 @@ import { export async function getVisitorBreakdown({ setup, + urlQuery, }: { setup: Setup & SetupTimeRange & SetupUIFilters; + urlQuery?: string; }) { const projection = getRumPageLoadTransactionsProjection({ setup, + urlQuery, }); const params = mergeProjection(projection, { diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts index 2ff0173b9ac12..fa34c2e25fecd 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_web_core_vitals.ts @@ -22,8 +22,10 @@ import { export async function getWebCoreVitals({ setup, + urlQuery, }: { setup: Setup & SetupTimeRange & SetupUIFilters; + urlQuery?: string; }) { const projection = getRumPageLoadTransactionsProjection({ setup, diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts index 5c183fd9150dd..7c2137ce65d83 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -96,10 +96,12 @@ async function getErrorStats({ setup, serviceName, environment, + searchAggregatedTransactions, }: { setup: Options['setup']; serviceName: string; environment?: string; + searchAggregatedTransactions: boolean; }) { const setupWithBlankUiFilters = { ...setup, @@ -108,6 +110,7 @@ async function getErrorStats({ const { noHits, average } = await getErrorRate({ setup: setupWithBlankUiFilters, serviceName, + searchAggregatedTransactions, }); return { avgErrorRate: noHits ? null : average }; } diff --git a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap index a65536df37bc8..431f11066aaff 100644 --- a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -277,6 +277,13 @@ Array [ "services": Object { "aggs": Object { "outcomes": Object { + "aggs": Object { + "count": Object { + "value_count": Object { + "field": "transaction.duration.us", + }, + }, + }, "terms": Object { "field": "event.outcome", }, @@ -284,6 +291,13 @@ Array [ "timeseries": Object { "aggs": Object { "outcomes": Object { + "aggs": Object { + "count": Object { + "value_count": Object { + "field": "transaction.duration.us", + }, + }, + }, "terms": Object { "field": "event.outcome", }, diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts index 17799203fe73b..65bc3f7e47171 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts @@ -14,7 +14,6 @@ import { EVENT_OUTCOME, } from '../../../../common/elasticsearch_fieldnames'; import { mergeProjection } from '../../../projections/util/merge_projection'; -import { ProcessorEvent } from '../../../../common/processor_event'; import { ServicesItemsSetup, ServicesItemsProjection, @@ -258,6 +257,7 @@ export const getTransactionRates = async ({ export const getTransactionErrorRates = async ({ setup, projection, + searchAggregatedTransactions, }: AggregationParams) => { const { apmEventClient, start, end } = setup; @@ -265,12 +265,25 @@ export const getTransactionErrorRates = async ({ terms: { field: EVENT_OUTCOME, }, + aggs: { + count: { + value_count: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, + }, }; const response = await apmEventClient.search( mergeProjection(projection, { apm: { - events: [ProcessorEvent.transaction], + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], }, body: { size: 0, @@ -319,11 +332,11 @@ export const getTransactionErrorRates = async ({ const successfulTransactions = outcomeResponse.buckets.find( (bucket) => bucket.key === EventOutcome.success - )?.doc_count ?? 0; + )?.count.value ?? 0; const failedTransactions = outcomeResponse.buckets.find( (bucket) => bucket.key === EventOutcome.failure - )?.doc_count ?? 0; + )?.count.value ?? 0; return failedTransactions / (successfulTransactions + failedTransactions); } diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts index 82595317342f1..3dc126c45d328 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts @@ -11,7 +11,6 @@ import { SERVICE_NAME, EVENT_OUTCOME, } from '../../../common/elasticsearch_fieldnames'; -import { ProcessorEvent } from '../../../common/processor_event'; import { rangeFilter } from '../../../common/utils/range_filter'; import { Setup, @@ -19,17 +18,23 @@ import { SetupUIFilters, } from '../helpers/setup_request'; import { getBucketSize } from '../helpers/get_bucket_size'; +import { + getProcessorEventForAggregatedTransactions, + getTransactionDurationFieldForAggregatedTransactions, +} from '../helpers/aggregated_transactions'; export async function getErrorRate({ serviceName, transactionType, transactionName, setup, + searchAggregatedTransactions, }: { serviceName: string; transactionType?: string; transactionName?: string; setup: Setup & SetupTimeRange & SetupUIFilters; + searchAggregatedTransactions: boolean; }) { const { start, end, uiFiltersES, apmEventClient } = setup; @@ -53,7 +58,11 @@ export async function getErrorRate({ const params = { apm: { - events: [ProcessorEvent.transaction], + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], }, body: { size: 0, @@ -67,8 +76,19 @@ export async function getErrorRate({ extended_bounds: { min: start, max: end }, }, aggs: { - erroneous_transactions: { - filter: { term: { [EVENT_OUTCOME]: EventOutcome.failure } }, + [EVENT_OUTCOME]: { + terms: { + field: EVENT_OUTCOME, + }, + aggs: { + count: { + value_count: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, + }, }, }, }, @@ -81,18 +101,24 @@ export async function getErrorRate({ const noHits = resp.hits.total.value === 0; const erroneousTransactionsRate = - resp.aggregations?.total_transactions.buckets.map( - ({ - key, - doc_count: totalTransactions, - erroneous_transactions: erroneousTransactions, - }) => { - return { - x: key, - y: erroneousTransactions.doc_count / totalTransactions, - }; - } - ) || []; + resp.aggregations?.total_transactions.buckets.map((bucket) => { + const successful = + bucket[EVENT_OUTCOME].buckets.find( + (eventOutcomeBucket) => + eventOutcomeBucket.key === EventOutcome.success + )?.count.value ?? 0; + + const failed = + bucket[EVENT_OUTCOME].buckets.find( + (eventOutcomeBucket) => + eventOutcomeBucket.key === EventOutcome.failure + )?.count.value ?? 0; + + return { + x: bucket.key, + y: failed / (successful + failed), + }; + }) || []; const average = mean( erroneousTransactionsRate diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/__fixtures__/responses.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/__fixtures__/responses.ts deleted file mode 100644 index 44878aa6c1f2e..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/__fixtures__/responses.ts +++ /dev/null @@ -1,76 +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 { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { APMBaseDoc } from '../../../../../typings/es_schemas/raw/apm_base_doc'; -import { - ESSearchResponse, - ESSearchRequest, -} from '../../../../../typings/elasticsearch'; - -export const response = ({ - hits: { - total: 599, - max_score: 0, - hits: [], - }, - took: 4, - timed_out: false, - _shards: { - total: 1, - successful: 1, - skipped: 0, - failed: 0, - }, - aggregations: { - user_agent_keys: { - buckets: [{ key: 'Firefox' }, { key: 'Other' }], - }, - browsers: { - buckets: [ - { - key_as_string: '2019-10-21T04:38:20.000-05:00', - key: 1571650700000, - doc_count: 0, - user_agent: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [], - }, - }, - { - key_as_string: '2019-10-21T04:40:00.000-05:00', - key: 1571650800000, - doc_count: 1, - user_agent: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { - key: 'Other', - doc_count: 1, - avg_duration: { - value: 860425.0, - }, - }, - { - key: 'Firefox', - doc_count: 10, - avg_duration: { - value: 86425.1, - }, - }, - ], - }, - }, - ], - }, - }, -} as unknown) as ESSearchResponse< - APMBaseDoc | Transaction, - ESSearchRequest, - { restTotalHitsAsInt: false } ->; diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts deleted file mode 100644 index aec124e4f4623..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.test.ts +++ /dev/null @@ -1,31 +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 { - Setup, - SetupTimeRange, - SetupUIFilters, -} from '../../helpers/setup_request'; -import { fetcher } from './fetcher'; - -describe('fetcher', () => { - it('performs a search', async () => { - const search = jest.fn(); - const setup = ({ - apmEventClient: { search }, - indices: {}, - uiFiltersES: [], - } as unknown) as Setup & SetupTimeRange & SetupUIFilters; - - await fetcher({ - serviceName: 'testServiceName', - setup, - searchAggregatedTransactions: false, - }); - - expect(search).toHaveBeenCalled(); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts deleted file mode 100644 index d40fcaaa02f60..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts +++ /dev/null @@ -1,100 +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 { ESFilter } from '../../../../typings/elasticsearch'; -import { PromiseReturnType } from '../../../../../observability/typings/common'; -import { - SERVICE_NAME, - TRANSACTION_TYPE, - USER_AGENT_NAME, - TRANSACTION_NAME, -} from '../../../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../../../../common/utils/range_filter'; -import { getBucketSize } from '../../helpers/get_bucket_size'; -import { Options } from '.'; -import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types'; -import { - getDocumentTypeFilterForAggregatedTransactions, - getTransactionDurationFieldForAggregatedTransactions, - getProcessorEventForAggregatedTransactions, -} from '../../helpers/aggregated_transactions'; - -export type ESResponse = PromiseReturnType; - -export function fetcher(options: Options) { - const { end, apmEventClient, start, uiFiltersES } = options.setup; - const { - serviceName, - searchAggregatedTransactions, - transactionName, - } = options; - const { intervalString } = getBucketSize(start, end); - - const transactionNameFilter = transactionName - ? [{ term: { [TRANSACTION_NAME]: transactionName } }] - : []; - - const filter: ESFilter[] = [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } }, - { range: rangeFilter(start, end) }, - ...getDocumentTypeFilterForAggregatedTransactions( - searchAggregatedTransactions - ), - ...uiFiltersES, - ...transactionNameFilter, - ]; - - const params = { - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - body: { - size: 0, - query: { bool: { filter } }, - aggs: { - user_agent_keys: { - terms: { - field: USER_AGENT_NAME, - }, - }, - browsers: { - date_histogram: { - extended_bounds: { - max: end, - min: start, - }, - field: '@timestamp', - fixed_interval: intervalString, - min_doc_count: 0, - }, - aggs: { - user_agent: { - terms: { - field: USER_AGENT_NAME, - }, - aggs: { - avg_duration: { - avg: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - }, - }, - }, - }, - }, - }, - }, - }, - }; - - return apmEventClient.search(params); -} diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.test.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.test.ts deleted file mode 100644 index b8cea3a032268..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.test.ts +++ /dev/null @@ -1,33 +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 { - getTransactionAvgDurationByBrowser, - Options, - AvgDurationByBrowserAPIResponse, -} from '.'; -import * as transformerModule from './transformer'; -import * as fetcherModule from './fetcher'; -import { response } from './__fixtures__/responses'; - -describe('getAvgDurationByBrowser', () => { - it('returns a transformed response', async () => { - const transformer = jest - .spyOn(transformerModule, 'transformer') - .mockReturnValueOnce(({} as unknown) as AvgDurationByBrowserAPIResponse); - const search = () => {}; - const options = ({ - setup: { client: { search }, indices: {}, uiFiltersES: [] }, - } as unknown) as Options; - jest - .spyOn<{ fetcher: any }, 'fetcher'>(fetcherModule, 'fetcher') - .mockResolvedValueOnce(response); - - await getTransactionAvgDurationByBrowser(options); - - expect(transformer).toHaveBeenCalledWith({ response }); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.ts deleted file mode 100644 index 2c259edaa26ab..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Coordinate } from '../../../../typings/timeseries'; -import { - Setup, - SetupTimeRange, - SetupUIFilters, -} from '../../helpers/setup_request'; -import { fetcher } from './fetcher'; -import { transformer } from './transformer'; - -export interface Options { - serviceName: string; - setup: Setup & SetupTimeRange & SetupUIFilters; - searchAggregatedTransactions: boolean; - transactionName?: string; -} - -export type AvgDurationByBrowserAPIResponse = Array<{ - data: Coordinate[]; - title: string; -}>; - -export async function getTransactionAvgDurationByBrowser(options: Options) { - return transformer({ response: await fetcher(options) }); -} diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.test.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.test.ts deleted file mode 100644 index 91ff2698ea554..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { transformer } from './transformer'; -import { response } from './__fixtures__/responses'; - -describe('transformer', () => { - it('transforms', () => { - expect(transformer({ response })).toEqual([ - { - data: [ - { x: 1571650700000, y: undefined }, - { x: 1571650800000, y: 86425.1 }, - ], - title: 'Firefox', - }, - { - data: [ - { x: 1571650700000, y: undefined }, - { x: 1571650800000, y: 860425.0 }, - ], - title: 'Other', - }, - ]); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.ts deleted file mode 100644 index 5234af8cede66..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/transformer.ts +++ /dev/null @@ -1,53 +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 { ESResponse } from './fetcher'; -import { AvgDurationByBrowserAPIResponse } from '.'; -import { Coordinate } from '../../../../typings/timeseries'; - -export function transformer({ - response, -}: { - response: ESResponse; -}): AvgDurationByBrowserAPIResponse { - const allUserAgentKeys = new Set( - (response.aggregations?.user_agent_keys?.buckets ?? []).map(({ key }) => - key.toString() - ) - ); - const buckets = response.aggregations?.browsers?.buckets ?? []; - - const series = buckets.reduce<{ [key: string]: Coordinate[] }>( - (acc, next) => { - const userAgentBuckets = next.user_agent?.buckets ?? []; - const x = next.key; - const seenUserAgentKeys = new Set(); - - userAgentBuckets.map((userAgentBucket) => { - const key = userAgentBucket.key; - const y = userAgentBucket.avg_duration?.value; - - seenUserAgentKeys.add(key.toString()); - acc[key] = (acc[key] || []).concat({ x, y }); - }); - - const emptyUserAgents = new Set( - [...allUserAgentKeys].filter((key) => !seenUserAgentKeys.has(key)) - ); - - // If no user agent requests exist for this bucked, fill in the data with - // undefined - [...emptyUserAgents].map((key) => { - acc[key] = (acc[key] || []).concat({ x, y: undefined }); - }); - - return acc; - }, - {} - ); - - return Object.entries(series).map(([title, data]) => ({ title, data })); -} diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts deleted file mode 100644 index bc1e0af051ace..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts +++ /dev/null @@ -1,109 +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 { - CLIENT_GEO_COUNTRY_ISO_CODE, - SERVICE_NAME, - TRANSACTION_TYPE, - TRANSACTION_NAME, -} from '../../../../common/elasticsearch_fieldnames'; -import { - Setup, - SetupTimeRange, - SetupUIFilters, -} from '../../helpers/setup_request'; -import { rangeFilter } from '../../../../common/utils/range_filter'; -import { TRANSACTION_PAGE_LOAD } from '../../../../common/transaction_types'; -import { - getProcessorEventForAggregatedTransactions, - getTransactionDurationFieldForAggregatedTransactions, - getDocumentTypeFilterForAggregatedTransactions, -} from '../../helpers/aggregated_transactions'; - -export async function getTransactionAvgDurationByCountry({ - setup, - serviceName, - transactionName, - searchAggregatedTransactions, -}: { - setup: Setup & SetupTimeRange & SetupUIFilters; - serviceName: string; - transactionName?: string; - searchAggregatedTransactions: boolean; -}) { - const { uiFiltersES, apmEventClient, start, end } = setup; - const transactionNameFilter = transactionName - ? [{ term: { [TRANSACTION_NAME]: transactionName } }] - : []; - const params = { - apm: { - events: [ - getProcessorEventForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - body: { - size: 0, - query: { - bool: { - filter: [ - { term: { [SERVICE_NAME]: serviceName } }, - ...transactionNameFilter, - { term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } }, - { exists: { field: CLIENT_GEO_COUNTRY_ISO_CODE } }, - { range: rangeFilter(start, end) }, - ...uiFiltersES, - ...getDocumentTypeFilterForAggregatedTransactions( - searchAggregatedTransactions - ), - ], - }, - }, - aggs: { - country_code: { - terms: { - field: CLIENT_GEO_COUNTRY_ISO_CODE, - size: 500, - }, - aggs: { - count: { - value_count: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - }, - }, - avg_duration: { - avg: { - field: getTransactionDurationFieldForAggregatedTransactions( - searchAggregatedTransactions - ), - }, - }, - }, - }, - }, - }, - }; - - const resp = await apmEventClient.search(params); - - if (!resp.aggregations) { - return []; - } - - const buckets = resp.aggregations.country_code.buckets; - const avgDurationsByCountry = buckets.map( - ({ key, count, avg_duration: { value } }) => ({ - key: key as string, - docCount: count.value, - value: value === null ? 0 : value, - }) - ); - - return avgDurationsByCountry; -} diff --git a/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts b/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts index 27cd9b53f8349..3c3eaaca7efdb 100644 --- a/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts +++ b/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts @@ -19,8 +19,10 @@ import { TRANSACTION_PAGE_LOAD } from '../../common/transaction_types'; export function getRumPageLoadTransactionsProjection({ setup, + urlQuery, }: { setup: Setup & SetupTimeRange & SetupUIFilters; + urlQuery?: string; }) { const { start, end, uiFiltersES } = setup; @@ -35,6 +37,17 @@ export function getRumPageLoadTransactionsProjection({ field: 'transaction.marks.navigationTiming.fetchStart', }, }, + ...(urlQuery + ? [ + { + wildcard: { + 'url.full': { + value: `*${urlQuery}*`, + }, + }, + }, + ] + : []), ...uiFiltersES, ], }; 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 1230e8aa05c9f..f975ab177f147 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -46,8 +46,6 @@ import { transactionGroupsChartsRoute, transactionGroupsDistributionRoute, transactionGroupsRoute, - transactionGroupsAvgDurationByCountry, - transactionGroupsAvgDurationByBrowser, transactionSampleForGroupRoute, transactionGroupsErrorRateRoute, } from './transaction_groups'; @@ -79,6 +77,7 @@ import { rumServicesRoute, rumVisitorsBreakdownRoute, rumWebCoreVitals, + rumUrlSearch, rumLongTaskMetrics, } from './rum_client'; import { @@ -139,8 +138,6 @@ const createApmApi = () => { .add(transactionGroupsChartsRoute) .add(transactionGroupsDistributionRoute) .add(transactionGroupsRoute) - .add(transactionGroupsAvgDurationByBrowser) - .add(transactionGroupsAvgDurationByCountry) .add(transactionSampleForGroupRoute) .add(transactionGroupsErrorRateRoute) @@ -177,6 +174,7 @@ const createApmApi = () => { .add(rumServicesRoute) .add(rumVisitorsBreakdownRoute) .add(rumWebCoreVitals) + .add(rumUrlSearch) .add(rumLongTaskMetrics) // Observability dashboard diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts index 179279b6f2d8a..e3a846f9fb5c7 100644 --- a/x-pack/plugins/apm/server/routes/rum_client.ts +++ b/x-pack/plugins/apm/server/routes/rum_client.ts @@ -16,37 +16,54 @@ import { getRumServices } from '../lib/rum_client/get_rum_services'; import { getVisitorBreakdown } from '../lib/rum_client/get_visitor_breakdown'; import { getWebCoreVitals } from '../lib/rum_client/get_web_core_vitals'; import { getLongTaskMetrics } from '../lib/rum_client/get_long_task_metrics'; +import { getUrlSearch } from '../lib/rum_client/get_url_search'; export const percentileRangeRt = t.partial({ minPercentile: t.string, maxPercentile: t.string, }); +const urlQueryRt = t.partial({ urlQuery: t.string }); + export const rumClientMetricsRoute = createRoute(() => ({ path: '/api/apm/rum/client-metrics', params: { - query: t.intersection([uiFiltersRt, rangeRt]), + query: t.intersection([uiFiltersRt, rangeRt, urlQueryRt]), }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - return getClientMetrics({ setup }); + const { + query: { urlQuery }, + } = context.params; + + return getClientMetrics({ setup, urlQuery }); }, })); export const rumPageLoadDistributionRoute = createRoute(() => ({ path: '/api/apm/rum-client/page-load-distribution', params: { - query: t.intersection([uiFiltersRt, rangeRt, percentileRangeRt]), + query: t.intersection([ + uiFiltersRt, + rangeRt, + percentileRangeRt, + urlQueryRt, + ]), }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { - query: { minPercentile, maxPercentile }, + query: { minPercentile, maxPercentile, urlQuery }, } = context.params; - return getPageLoadDistribution({ setup, minPercentile, maxPercentile }); + return getPageLoadDistribution({ + setup, + minPercentile, + maxPercentile, + urlQuery, + }); }, })); @@ -57,6 +74,7 @@ export const rumPageLoadDistBreakdownRoute = createRoute(() => ({ uiFiltersRt, rangeRt, percentileRangeRt, + urlQueryRt, t.type({ breakdown: t.string }), ]), }, @@ -64,7 +82,7 @@ export const rumPageLoadDistBreakdownRoute = createRoute(() => ({ const setup = await setupRequest(context, request); const { - query: { minPercentile, maxPercentile, breakdown }, + query: { minPercentile, maxPercentile, breakdown, urlQuery }, } = context.params; return getPageLoadDistBreakdown({ @@ -72,6 +90,7 @@ export const rumPageLoadDistBreakdownRoute = createRoute(() => ({ minDuration: Number(minPercentile), maxDuration: Number(maxPercentile), breakdown, + urlQuery, }); }, })); @@ -82,6 +101,7 @@ export const rumPageViewsTrendRoute = createRoute(() => ({ query: t.intersection([ uiFiltersRt, rangeRt, + urlQueryRt, t.partial({ breakdowns: t.string }), ]), }, @@ -89,10 +109,10 @@ export const rumPageViewsTrendRoute = createRoute(() => ({ const setup = await setupRequest(context, request); const { - query: { breakdowns }, + query: { breakdowns, urlQuery }, } = context.params; - return getPageViewTrends({ setup, breakdowns }); + return getPageViewTrends({ setup, breakdowns, urlQuery }); }, })); @@ -111,35 +131,63 @@ export const rumServicesRoute = createRoute(() => ({ export const rumVisitorsBreakdownRoute = createRoute(() => ({ path: '/api/apm/rum-client/visitor-breakdown', params: { - query: t.intersection([uiFiltersRt, rangeRt]), + query: t.intersection([uiFiltersRt, rangeRt, urlQueryRt]), }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - return getVisitorBreakdown({ setup }); + const { + query: { urlQuery }, + } = context.params; + + return getVisitorBreakdown({ setup, urlQuery }); }, })); export const rumWebCoreVitals = createRoute(() => ({ path: '/api/apm/rum-client/web-core-vitals', params: { - query: t.intersection([uiFiltersRt, rangeRt]), + query: t.intersection([uiFiltersRt, rangeRt, urlQueryRt]), }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - return getWebCoreVitals({ setup }); + const { + query: { urlQuery }, + } = context.params; + + return getWebCoreVitals({ setup, urlQuery }); }, })); export const rumLongTaskMetrics = createRoute(() => ({ path: '/api/apm/rum-client/long-task-metrics', params: { - query: t.intersection([uiFiltersRt, rangeRt]), + query: t.intersection([uiFiltersRt, rangeRt, urlQueryRt]), }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - return getLongTaskMetrics({ setup }); + const { + query: { urlQuery }, + } = context.params; + + return getLongTaskMetrics({ setup, urlQuery }); + }, +})); + +export const rumUrlSearch = createRoute(() => ({ + path: '/api/apm/rum-client/url-search', + params: { + query: t.intersection([uiFiltersRt, rangeRt, urlQueryRt]), + }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + + const { + query: { urlQuery }, + } = context.params; + + return getUrlSearch({ setup, urlQuery }); }, })); diff --git a/x-pack/plugins/apm/server/routes/transaction_groups.ts b/x-pack/plugins/apm/server/routes/transaction_groups.ts index 3c512c1fe5278..10e917f385e71 100644 --- a/x-pack/plugins/apm/server/routes/transaction_groups.ts +++ b/x-pack/plugins/apm/server/routes/transaction_groups.ts @@ -12,8 +12,6 @@ import { getTransactionBreakdown } from '../lib/transactions/breakdown'; import { getTransactionGroupList } from '../lib/transaction_groups'; import { createRoute } from './create_route'; import { uiFiltersRt, rangeRt } from './default_api_types'; -import { getTransactionAvgDurationByBrowser } from '../lib/transactions/avg_duration_by_browser'; -import { getTransactionAvgDurationByCountry } from '../lib/transactions/avg_duration_by_country'; import { getTransactionSampleForGroup } from '../lib/transaction_groups/get_transaction_sample_for_group'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { getErrorRate } from '../lib/transaction_groups/get_error_rate'; @@ -168,68 +166,6 @@ export const transactionGroupsBreakdownRoute = createRoute(() => ({ }, })); -export const transactionGroupsAvgDurationByBrowser = createRoute(() => ({ - path: `/api/apm/services/{serviceName}/transaction_groups/avg_duration_by_browser`, - params: { - path: t.type({ - serviceName: t.string, - }), - query: t.intersection([ - t.partial({ - transactionName: t.string, - }), - uiFiltersRt, - rangeRt, - ]), - }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.path; - const { transactionName } = context.params.query; - - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); - - return getTransactionAvgDurationByBrowser({ - serviceName, - setup, - searchAggregatedTransactions, - transactionName, - }); - }, -})); - -export const transactionGroupsAvgDurationByCountry = createRoute(() => ({ - path: `/api/apm/services/{serviceName}/transaction_groups/avg_duration_by_country`, - params: { - path: t.type({ - serviceName: t.string, - }), - query: t.intersection([ - uiFiltersRt, - rangeRt, - t.partial({ transactionName: t.string }), - ]), - }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.path; - const { transactionName } = context.params.query; - - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); - - return getTransactionAvgDurationByCountry({ - serviceName, - transactionName, - setup, - searchAggregatedTransactions, - }); - }, -})); - export const transactionSampleForGroupRoute = createRoute(() => ({ path: `/api/apm/transaction_sample`, params: { @@ -274,11 +210,17 @@ export const transactionGroupsErrorRateRoute = createRoute(() => ({ const { params } = context; const { serviceName } = params.path; const { transactionType, transactionName } = params.query; + + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + return getErrorRate({ serviceName, transactionType, transactionName, setup, + searchAggregatedTransactions, }); }, })); diff --git a/x-pack/plugins/apm/typings/apm_rum_react.d.ts b/x-pack/plugins/apm/typings/apm_rum_react.d.ts index 1c3e41ec12780..f9eafef59f55d 100644 --- a/x-pack/plugins/apm/typings/apm_rum_react.d.ts +++ b/x-pack/plugins/apm/typings/apm_rum_react.d.ts @@ -3,6 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + declare module '@elastic/apm-rum-react' { - export const ApmRoute: any; + import { RouteProps } from 'react-router-dom'; + + export const ApmRoute: React.ComponentClass; } diff --git a/x-pack/plugins/audit_trail/server/client/audit_trail_client.test.ts b/x-pack/plugins/audit_trail/server/client/audit_trail_client.test.ts index 45a192e40c87b..76ca3e56fe837 100644 --- a/x-pack/plugins/audit_trail/server/client/audit_trail_client.test.ts +++ b/x-pack/plugins/audit_trail/server/client/audit_trail_client.test.ts @@ -24,7 +24,9 @@ describe('AuditTrailClient', () => { beforeEach(() => { event$ = new Subject(); client = new AuditTrailClient( - httpServerMock.createKibanaRequest({ kibanaRequestState: { requestId: 'request id alpha' } }), + httpServerMock.createKibanaRequest({ + kibanaRequestState: { requestId: 'request id alpha', requestUuid: 'ignore-me' }, + }), event$, deps ); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.test.ts index 33260b5c9303f..df205dc742b07 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.test.ts @@ -49,4 +49,9 @@ describe('savedLens', () => { expect(expression.input.filters).toEqual(embeddableFilters); }); + + it('accepts an empty title when title is disabled', () => { + const expression = fn(null, { ...args, title: '' }, {} as any); + expect(expression.input.title).toEqual(''); + }); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts index 49b8c5562af65..a823d0606d46f 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts @@ -72,7 +72,7 @@ export function savedLens(): ExpressionFunctionDefinition< id: args.id, filters: getQueryFilters(filters), timeRange: args.timerange || defaultTimeRange, - title: args.title ? args.title : undefined, + title: args.title === null ? undefined : args.title, disableTriggers: true, }, embeddableType: EmbeddableTypes.lens, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts index ec640cfb5b299..a64ff7da2aa19 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts @@ -93,7 +93,7 @@ export function savedMap(): ExpressionFunctionDefinition< mapCenter: center, hideFilterActions: true, - title: args.title ? args.title : undefined, + title: args.title === null ? undefined : args.title, isLayerTOCOpen: false, hiddenLayers: args.hideLayer || [], }, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.test.ts index a64fb167dd19f..7902d09a0bdf1 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.test.ts @@ -36,6 +36,7 @@ describe('savedVisualization', () => { timerange: null, colors: null, hideLegend: null, + title: null, }; it('accepts null context', () => { @@ -50,4 +51,9 @@ describe('savedVisualization', () => { expect(expression.input.filters).toEqual(embeddableFilters); }); + + it('accepts an empty title when title is disabled', () => { + const expression = fn(null, { ...args, title: '' }, {} as any); + expect(expression.input.title).toEqual(''); + }); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts index 2782ca039d7ed..449be2db43d44 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts @@ -20,6 +20,7 @@ interface Arguments { timerange: TimeRangeArg | null; colors: SeriesStyle[] | null; hideLegend: boolean | null; + title: string | null; } type Output = EmbeddableExpression; @@ -61,9 +62,14 @@ export function savedVisualization(): ExpressionFunctionDefinition< help: argHelp.hideLegend, required: false, }, + title: { + types: ['string'], + help: argHelp.title, + required: false, + }, }, type: EmbeddableExpressionType, - fn: (input, { id, timerange, colors, hideLegend }) => { + fn: (input, { id, timerange, colors, hideLegend, title }) => { const filters = input ? input.and : []; const visOptions: VisualizeInput['vis'] = {}; @@ -90,6 +96,7 @@ export function savedVisualization(): ExpressionFunctionDefinition< timeRange: timerange || defaultTimeRange, filters: getQueryFilters(filters), vis: visOptions, + title: title === null ? undefined : title, }, embeddableType: EmbeddableTypes.visualization, generatedAt: Date.now(), diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.test.ts index 7bcfd6bef4620..0df39f281da9c 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.test.ts @@ -52,4 +52,16 @@ describe('toExpression', () => { expect(timerangeExpression.chain[0].arguments.from[0]).toEqual(input.timeRange?.from); expect(timerangeExpression.chain[0].arguments.to[0]).toEqual(input.timeRange?.to); }); + + it('includes empty panel title', () => { + const input: SavedLensInput = { + ...baseEmbeddableInput, + title: '', + }; + + const expression = toExpression(input); + const ast = fromExpression(expression); + + expect(ast.chain[0].arguments).toHaveProperty('title', [input.title]); + }); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts index 5bb45c5ca129e..a8e200dd3e4ba 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts @@ -13,7 +13,7 @@ export function toExpression(input: SavedLensInput): string { expressionParts.push(`id="${input.id}"`); - if (input.title) { + if (input.title !== undefined) { expressionParts.push(`title="${input.title}"`); } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts index d910c734a6974..d2c803a1ff208 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts @@ -69,4 +69,16 @@ describe('toExpression', () => { expect(timerangeExpression.chain[0].arguments.from[0]).toEqual(input.timeRange?.from); expect(timerangeExpression.chain[0].arguments.to[0]).toEqual(input.timeRange?.to); }); + + it('includes empty panel title', () => { + const input: MapEmbeddableInput = { + ...baseSavedMapInput, + title: '', + }; + + const expression = toExpression(input); + const ast = fromExpression(expression); + + expect(ast.chain[0].arguments).toHaveProperty('title', [input.title]); + }); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts index 111fdc71fa242..769c2c9e10e9c 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts @@ -12,7 +12,7 @@ export function toExpression(input: MapEmbeddableInput): string { expressionParts.push('savedMap'); expressionParts.push(`id="${input.id}"`); - if (input.title) { + if (input.title !== undefined) { expressionParts.push(`title="${input.title}"`); } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.test.ts index 07f828755e46f..4550a90ce98a2 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.test.ts @@ -69,4 +69,16 @@ describe('toExpression', () => { expect(aColor?.chain[0].arguments.color[0]).toBe(colorMap.a); expect(bColor?.chain[0].arguments.color[0]).toBe(colorMap.b); }); + + it('includes empty panel title', () => { + const input = { + ...baseInput, + title: '', + }; + + const expression = toExpression(input); + const ast = fromExpression(expression); + + expect(ast.chain[0].arguments).toHaveProperty('title', [input.title]); + }); }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts index f03c10e2d424e..a8adbf9d2d860 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts @@ -12,6 +12,10 @@ export function toExpression(input: VisualizeInput): string { expressionParts.push('savedVisualization'); expressionParts.push(`id="${input.id}"`); + if (input.title !== undefined) { + expressionParts.push(`title="${input.title}"`); + } + if (input.timeRange) { expressionParts.push( `timerange={timerange from="${input.timeRange.from}" to="${input.timeRange.to}"}` diff --git a/x-pack/plugins/canvas/i18n/functions/dict/saved_visualization.ts b/x-pack/plugins/canvas/i18n/functions/dict/saved_visualization.ts index e8cbddc5c1102..257e251fe2bc2 100644 --- a/x-pack/plugins/canvas/i18n/functions/dict/saved_visualization.ts +++ b/x-pack/plugins/canvas/i18n/functions/dict/saved_visualization.ts @@ -29,5 +29,8 @@ export const help: FunctionHelp> = { defaultMessage: `Specifies the option to hide the legend`, } ), + title: i18n.translate('xpack.canvas.functions.savedVisualization.args.titleHelpText', { + defaultMessage: `The title for the visualization object`, + }), }, }; diff --git a/x-pack/plugins/canvas/public/components/datatable/datatable.scss b/x-pack/plugins/canvas/public/components/datatable/datatable.scss index bd11bff18e091..8e36de3b84423 100644 --- a/x-pack/plugins/canvas/public/components/datatable/datatable.scss +++ b/x-pack/plugins/canvas/public/components/datatable/datatable.scss @@ -4,7 +4,6 @@ display: flex; flex-direction: column; justify-content: space-between; - font-size: $euiFontSizeS; .canvasDataTable__tableWrapper { @include euiScrollBar; @@ -33,7 +32,6 @@ .canvasDataTable__th, .canvasDataTable__td { - text-align: left; padding: $euiSizeS $euiSizeXS; border-bottom: $euiBorderThin; } 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 f7ae9fc6d0f91..c8fe72e6f2c1e 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts @@ -13,6 +13,7 @@ import { SearchInterceptorDeps, UI_SETTINGS, } from '../../../../../src/plugins/data/public'; +import { isErrorResponse, isCompleteResponse } from '../../../../../src/plugins/data/public'; import { AbortError, toPromise } from '../../../../../src/plugins/data/common'; import { IAsyncSearchOptions } from '.'; import { IAsyncSearchRequest, ENHANCED_ES_SEARCH_STRATEGY } from '../../common'; @@ -66,12 +67,12 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { return this.runSearch(request, combinedSignal, strategy).pipe( expand((response) => { // If the response indicates of an error, stop polling and complete the observable - if (!response || (!response.isRunning && response.isPartial)) { + if (isErrorResponse(response)) { return throwError(new AbortError()); } // If the response indicates it is complete, stop polling and complete the observable - if (!response.isRunning) { + if (isCompleteResponse(response)) { return EMPTY; } diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index 72ea1f096e8fb..f3cf67a487a68 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -19,7 +19,11 @@ import { shimHitsTotal, } from '../../../../../src/plugins/data/server'; import { IEnhancedEsSearchRequest } from '../../common'; -import { ISearchOptions, IEsSearchResponse } from '../../../../../src/plugins/data/common/search'; +import { + ISearchOptions, + IEsSearchResponse, + isCompleteResponse, +} from '../../../../../src/plugins/data/common/search'; function isEnhancedEsSearchResponse(response: any): response is IEsSearchResponse { return response.hasOwnProperty('isPartial') && response.hasOwnProperty('isRunning'); @@ -48,8 +52,7 @@ export const enhancedEsSearchStrategyProvider = ( usage && isAsync && isEnhancedEsSearchResponse(response) && - !response.isRunning && - !response.isPartial + isCompleteResponse(response) ) { usage.trackSuccess(response.rawResponse.took); } diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/README.md b/x-pack/plugins/drilldowns/url_drilldown/README.md similarity index 65% rename from x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/README.md rename to x-pack/plugins/drilldowns/url_drilldown/README.md index 996723ccb914d..8eedc44ca35ae 100644 --- a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/README.md +++ b/x-pack/plugins/drilldowns/url_drilldown/README.md @@ -1,24 +1,26 @@ -# Basic url drilldown implementation +## URL drilldown + +> NOTE: This plugin contains implementation of URL drilldown. For drilldowns infrastructure code refer to `ui_actions_enhanced` plugin. Url drilldown allows navigating to external URL or to internal kibana URL. By using variables in url template result url can be dynamic and depend on user's interaction. URL drilldown has 3 sources for variables: -- Global static variables like, for example, `kibanaUrl`. Such variables won’t change depending on a place where url drilldown is used. -- Context variables are dynamic and different depending on where drilldown is created and used. -- Event variables depend on a trigger context. These variables are dynamically extracted from the action context when drilldown is executed. +1. Global static variables like, for example, `kibanaUrl`. Such variables won’t change depending on a place where url drilldown is used. +2. Context variables are dynamic and different depending on where drilldown is created and used. +3. Event variables depend on a trigger context. These variables are dynamically extracted from the action context when drilldown is executed. Difference between `event` and `context` variables, is that real `context` variables are available during drilldown creation (e.g. embeddable panel), but `event` variables mapped from trigger context. Since there is no trigger context during drilldown creation, we have to provide some _mock_ variables for validating and previewing the URL. In current implementation url drilldown has to be used inside the embeddable and with `ValueClickTrigger` or `RangeSelectTrigger`. -- `context` variables extracted from `embeddable` -- `event` variables extracted from `trigger` context +* `context` variables extracted from `embeddable` +* `event` variables extracted from `trigger` context In future this basic url drilldown implementation would allow injecting more variables into `context` (e.g. `dashboard` app specific variables) and would allow providing support for new trigger types from outside. This extensibility improvements are tracked here: https://github.com/elastic/kibana/issues/55324 In case a solution app has a use case for url drilldown that has to be different from current basic implementation and -just extending variables list is not enough, then recommendation is to create own custom url drilldown and reuse building blocks from `ui_actions_enhanced`. +just extending variables list is not enough, then recommendation is to create own custom url drilldown and reuse building blocks from `ui_actions_enhanced`. \ No newline at end of file diff --git a/x-pack/plugins/drilldowns/url_drilldown/kibana.json b/x-pack/plugins/drilldowns/url_drilldown/kibana.json new file mode 100644 index 0000000000000..9bdd13fbfea26 --- /dev/null +++ b/x-pack/plugins/drilldowns/url_drilldown/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "urlDrilldown", + "version": "kibana", + "server": false, + "ui": true, + "requiredPlugins": ["embeddable", "uiActions", "uiActionsEnhanced"], + "requiredBundles": ["kibanaUtils", "kibanaReact"] +} diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/index.ts b/x-pack/plugins/drilldowns/url_drilldown/public/index.ts new file mode 100644 index 0000000000000..b040ef625bc1f --- /dev/null +++ b/x-pack/plugins/drilldowns/url_drilldown/public/index.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. + */ + +import { PluginInitializerContext } from 'src/core/public'; +import { UrlDrilldownPlugin } from './plugin'; + +export function plugin(context: PluginInitializerContext) { + return new UrlDrilldownPlugin(context); +} diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/i18n.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/i18n.ts similarity index 62% rename from x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/i18n.ts rename to x-pack/plugins/drilldowns/url_drilldown/public/lib/i18n.ts index 748f6f4cecedd..7e91c6b849035 100644 --- a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/i18n.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/i18n.ts @@ -6,9 +6,6 @@ import { i18n } from '@kbn/i18n'; -export const txtUrlDrilldownDisplayName = i18n.translate( - 'xpack.embeddableEnhanced.drilldowns.urlDrilldownDisplayName', - { - defaultMessage: 'Go to URL', - } -); +export const txtUrlDrilldownDisplayName = i18n.translate('xpack.urlDrilldown.DisplayName', { + defaultMessage: 'Go to URL', +}); diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/index.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/index.ts similarity index 100% rename from x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/index.ts rename to x-pack/plugins/drilldowns/url_drilldown/public/lib/index.ts diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.test.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts similarity index 100% rename from x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.test.ts rename to x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx similarity index 100% rename from x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx rename to x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.test.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts similarity index 100% rename from x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.test.ts rename to x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts similarity index 100% rename from x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts rename to x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/plugin.ts b/x-pack/plugins/drilldowns/url_drilldown/public/plugin.ts new file mode 100644 index 0000000000000..82ce7a129f497 --- /dev/null +++ b/x-pack/plugins/drilldowns/url_drilldown/public/plugin.ts @@ -0,0 +1,59 @@ +/* + * 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 { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; +import { EmbeddableSetup, EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; +import { + AdvancedUiActionsSetup, + AdvancedUiActionsStart, + urlDrilldownGlobalScopeProvider, +} from '../../../ui_actions_enhanced/public'; +import { UrlDrilldown } from './lib'; +import { createStartServicesGetter } from '../../../../../src/plugins/kibana_utils/public'; + +export interface SetupDependencies { + embeddable: EmbeddableSetup; + uiActionsEnhanced: AdvancedUiActionsSetup; +} + +export interface StartDependencies { + embeddable: EmbeddableStart; + uiActionsEnhanced: AdvancedUiActionsStart; +} + +// eslint-disable-next-line +export interface SetupContract {} + +// eslint-disable-next-line +export interface StartContract {} + +export class UrlDrilldownPlugin + implements Plugin { + constructor(protected readonly context: PluginInitializerContext) {} + + public setup(core: CoreSetup, plugins: SetupDependencies): SetupContract { + const startServices = createStartServicesGetter(core.getStartServices); + plugins.uiActionsEnhanced.registerDrilldown( + new UrlDrilldown({ + getGlobalScope: urlDrilldownGlobalScopeProvider({ core }), + navigateToUrl: (url: string) => + core.getStartServices().then(([{ application }]) => application.navigateToUrl(url)), + getSyntaxHelpDocsLink: () => + startServices().core.docLinks.links.dashboard.urlDrilldownTemplateSyntax, + getVariablesHelpDocsLink: () => + startServices().core.docLinks.links.dashboard.urlDrilldownVariables, + }) + ); + + return {}; + } + + public start(core: CoreStart, plugins: StartDependencies): StartContract { + return {}; + } + + public stop() {} +} diff --git a/x-pack/plugins/embeddable_enhanced/kibana.json b/x-pack/plugins/embeddable_enhanced/kibana.json index acada946fe0d1..8d49e3e26eb7b 100644 --- a/x-pack/plugins/embeddable_enhanced/kibana.json +++ b/x-pack/plugins/embeddable_enhanced/kibana.json @@ -3,6 +3,5 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": ["embeddable", "kibanaReact", "uiActions", "uiActionsEnhanced"], - "requiredBundles": ["kibanaUtils"] + "requiredPlugins": ["embeddable", "kibanaReact", "uiActions", "uiActionsEnhanced"] } diff --git a/x-pack/plugins/embeddable_enhanced/public/plugin.ts b/x-pack/plugins/embeddable_enhanced/public/plugin.ts index 2138a372523b7..5d5ad852839d4 100644 --- a/x-pack/plugins/embeddable_enhanced/public/plugin.ts +++ b/x-pack/plugins/embeddable_enhanced/public/plugin.ts @@ -28,11 +28,8 @@ import { UiActionsEnhancedDynamicActionManager as DynamicActionManager, AdvancedUiActionsSetup, AdvancedUiActionsStart, - urlDrilldownGlobalScopeProvider, } from '../../ui_actions_enhanced/public'; import { PanelNotificationsAction, ACTION_PANEL_NOTIFICATIONS } from './actions'; -import { UrlDrilldown } from './drilldowns'; -import { createStartServicesGetter } from '../../../../src/plugins/kibana_utils/public'; declare module '../../../../src/plugins/ui_actions/public' { export interface ActionContextMapping { @@ -64,23 +61,10 @@ export class EmbeddableEnhancedPlugin public setup(core: CoreSetup, plugins: SetupDependencies): SetupContract { this.setCustomEmbeddableFactoryProvider(plugins); - const startServices = createStartServicesGetter(core.getStartServices); const panelNotificationAction = new PanelNotificationsAction(); plugins.uiActionsEnhanced.registerAction(panelNotificationAction); plugins.uiActionsEnhanced.attachAction(PANEL_NOTIFICATION_TRIGGER, panelNotificationAction.id); - plugins.uiActionsEnhanced.registerDrilldown( - new UrlDrilldown({ - getGlobalScope: urlDrilldownGlobalScopeProvider({ core }), - navigateToUrl: (url: string) => - core.getStartServices().then(([{ application }]) => application.navigateToUrl(url)), - getSyntaxHelpDocsLink: () => - startServices().core.docLinks.links.dashboard.urlDrilldownTemplateSyntax, - getVariablesHelpDocsLink: () => - startServices().core.docLinks.links.dashboard.urlDrilldownVariables, - }) - ); - return {}; } diff --git a/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts b/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts index 4533383ebd80e..939fc2c9c2ec2 100644 --- a/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts +++ b/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts @@ -24,13 +24,12 @@ export const DEFAULT_INITIAL_APP_DATA = { }, appSearch: { accountId: 'some-id-string', - onBoardingComplete: true, + onboardingComplete: true, role: { id: 'account_id:somestring|user_oid:somestring', roleType: 'owner', ability: { accessAllEngines: true, - destroy: ['session'], manage: ['account_credentials', 'account_engines'], // etc edit: ['LocoMoco::Account'], // etc view: ['Engine'], // etc diff --git a/x-pack/plugins/enterprise_search/common/types/app_search.ts b/x-pack/plugins/enterprise_search/common/types/app_search.ts index 72259ecd2343d..203b77834bc15 100644 --- a/x-pack/plugins/enterprise_search/common/types/app_search.ts +++ b/x-pack/plugins/enterprise_search/common/types/app_search.ts @@ -6,21 +6,18 @@ export interface IAccount { accountId: string; - onBoardingComplete: boolean; - role: IRole; -} - -export interface IRole { - id: string; - roleType: string; - ability: { - accessAllEngines: boolean; - destroy: string[]; - manage: string[]; - edit: string[]; - view: string[]; - credentialTypes: string[]; - availableRoleTypes: string[]; + onboardingComplete: boolean; + role: { + id: string; + roleType: string; + ability: { + accessAllEngines: boolean; + manage: string[]; + edit: string[]; + view: string[]; + credentialTypes: string[]; + availableRoleTypes: string[]; + }; }; } diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/flash_messages_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/flash_messages_logic.mock.ts new file mode 100644 index 0000000000000..a610ea0238ac0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/flash_messages_logic.mock.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 const mockFlashMessagesValues = { + messages: [], + queuedMessages: [], +}; + +export const mockFlashMessagesActions = { + setFlashMessages: jest.fn(), + clearFlashMessages: jest.fn(), + setQueuedMessages: jest.fn(), + clearQueuedMessages: jest.fn(), +}; diff --git a/x-pack/legacy/common/poller.d.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/http_logic.mock.ts similarity index 56% rename from x-pack/legacy/common/poller.d.ts rename to x-pack/plugins/enterprise_search/public/applications/__mocks__/http_logic.mock.ts index df39d93a28a81..e77863c70c23a 100644 --- a/x-pack/legacy/common/poller.d.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/http_logic.mock.ts @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -export declare class Poller { - constructor(options: any); +import { httpServiceMock } from 'src/core/public/mocks'; - public start(): void; - public stop(): void; - public isRunning(): boolean; - public getPollFrequency(): number; -} +export const mockHttpValues = { + http: httpServiceMock.createSetupContract(), + errorConnecting: false, + readOnlyMode: false, +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts index e999d40a3f8e6..f66235ff44c6a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts @@ -7,6 +7,10 @@ export { mockHistory, mockLocation } from './react_router_history.mock'; export { mockKibanaContext } from './kibana_context.mock'; export { mockLicenseContext } from './license_context.mock'; +export { mockHttpValues } from './http_logic.mock'; +export { mockFlashMessagesValues, mockFlashMessagesActions } from './flash_messages_logic.mock'; +export { mockAllValues, mockAllActions, setMockValues } from './kea.mock'; + export { mountWithContext, mountWithKibanaContext, diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts index 5049e9da21ce9..8e6b0baa5fc00 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts @@ -4,21 +4,46 @@ * you may not use this file except in compliance with the Elastic License. */ +/** + * NOTE: These variable names MUST start with 'mock*' in order for + * Jest to accept its use within a jest.mock() + */ +import { mockHttpValues } from './http_logic.mock'; +import { mockFlashMessagesValues, mockFlashMessagesActions } from './flash_messages_logic.mock'; + +export const mockAllValues = { + ...mockHttpValues, + ...mockFlashMessagesValues, +}; +export const mockAllActions = { + ...mockFlashMessagesActions, +}; + +/** + * Import this file directly to mock useValues with a set of default values for all shared logic files. + * Example usage: + * + * import '../../../__mocks__/kea'; // Must come before kea's import, adjust relative path as needed + */ jest.mock('kea', () => ({ ...(jest.requireActual('kea') as object), - useValues: jest.fn(() => ({})), - useActions: jest.fn(() => ({})), + useValues: jest.fn(() => ({ ...mockAllValues })), + useActions: jest.fn(() => ({ ...mockAllActions })), })); /** + * Call this function to override a specific set of Kea values while retaining all other defaults * Example usage within a component test: * - * import '../../../__mocks__/kea'; // Must come before kea's import, adjust relative path as needed - * - * import { useActions, useValues } from 'kea'; + * import '../../../__mocks__/kea'; + * import { setMockValues } from ''../../../__mocks__'; * * it('some test', () => { - * (useValues as jest.Mock).mockImplementationOnce(() => ({ someValue: 'hello' })); - * (useActions as jest.Mock).mockImplementationOnce(() => ({ someAction: () => 'world' })); + * setMockValues({ someValue: 'hello' }); * }); */ +import { useValues } from 'kea'; + +export const setMockValues = (values: object) => { + (useValues as jest.Mock).mockImplementation(() => ({ ...mockAllValues, ...values })); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts index 890072ab42eb9..ea3c3923cc472 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { httpServiceMock } from 'src/core/public/mocks'; import { ExternalUrl } from '../shared/enterprise_search_url'; /** @@ -12,7 +11,6 @@ import { ExternalUrl } from '../shared/enterprise_search_url'; * @see enterprise_search/public/index.tsx for the KibanaContext definition/import */ export const mockKibanaContext = { - http: httpServiceMock.createSetupContract(), navigateToUrl: jest.fn(), setBreadcrumbs: jest.fn(), setDocTitle: jest.fn(), diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx index 826e0482acef7..5e56f17c8e7f3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx @@ -67,7 +67,7 @@ export const mountWithKibanaContext = (children: React.ReactNode, context?: obje */ export const mountWithAsyncContext = async ( children: React.ReactNode, - context: object + context?: object ): Promise => { let wrapper: ReactWrapper | undefined; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts index 842dcefd3aef8..7b3ac86ad0ab1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts @@ -14,6 +14,7 @@ export const mockHistory = { location: { pathname: '/current-path', }, + listen: jest.fn(() => jest.fn()), }; export const mockLocation = { key: 'someKey', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts index bc31b7df5d971..9410b9ef7cb03 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts @@ -17,6 +17,10 @@ describe('AppLogic', () => { const DEFAULT_VALUES = { hasInitialized: false, + account: {}, + configuredLimits: {}, + ilmEnabled: false, + myRole: {}, }; it('has expected default values', () => { @@ -29,7 +33,45 @@ describe('AppLogic', () => { expect(AppLogic.values).toEqual({ hasInitialized: true, + ilmEnabled: true, + configuredLimits: { + engine: { + maxDocumentByteSize: 102400, + maxEnginesPerMetaEngine: 15, + }, + }, + account: { + accountId: 'some-id-string', + onboardingComplete: true, + role: DEFAULT_INITIAL_APP_DATA.appSearch.role, + }, + myRole: expect.objectContaining({ + id: 'account_id:somestring|user_oid:somestring', + roleType: 'owner', + availableRoleTypes: ['owner', 'admin'], + credentialTypes: ['admin', 'private', 'search'], + canAccessAllEngines: true, + canViewAccountCredentials: true, + // Truncated for brevity - see utils/role/index.test.ts for full output + }), }); }); + + it('gracefully handles missing initial data', () => { + AppLogic.actions.initializeAppData({}); + + expect(AppLogic.values).toEqual({ + ...DEFAULT_VALUES, + hasInitialized: true, + }); + }); + }); + + describe('setOnboardingComplete()', () => { + it('sets true', () => { + expect(AppLogic.values.account.onboardingComplete).toBeFalsy(); + AppLogic.actions.setOnboardingComplete(); + expect(AppLogic.values.account.onboardingComplete).toEqual(true); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts index 9388d61041b13..932e84af45c2b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts @@ -7,18 +7,27 @@ import { kea, MakeLogicType } from 'kea'; import { IInitialAppData } from '../../../common/types'; +import { IConfiguredLimits, IAccount, IRole } from './types'; + +import { getRoleAbilities } from './utils/role'; export interface IAppValues { hasInitialized: boolean; + ilmEnabled: boolean; + configuredLimits: Partial; + account: Partial; + myRole: Partial; } export interface IAppActions { - initializeAppData(props: IInitialAppData): void; + initializeAppData(props: IInitialAppData): Required; + setOnboardingComplete(): boolean; } export const AppLogic = kea>({ path: ['enterprise_search', 'app_search', 'app_logic'], actions: { initializeAppData: (props) => props, + setOnboardingComplete: () => true, }, reducers: { hasInitialized: [ @@ -27,5 +36,33 @@ export const AppLogic = kea>({ initializeAppData: () => true, }, ], + account: [ + {}, + { + initializeAppData: (_, { appSearch: account }) => account || {}, + setOnboardingComplete: (account) => ({ + ...account, + onboardingComplete: true, + }), + }, + ], + configuredLimits: [ + {}, + { + initializeAppData: (_, { configuredLimits }) => configuredLimits?.appSearch || {}, + }, + ], + ilmEnabled: [ + false, + { + initializeAppData: (_, { ilmEnabled }) => !!ilmEnabled, + }, + ], + }, + selectors: { + myRole: [ + (selectors) => [selectors.account], + ({ role }) => (role ? getRoleAbilities(role) : {}), + ], }, }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.test.tsx index 7e6876bc9b3a4..233db7d4c5917 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.test.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import '../../../../__mocks__/kea.mock'; import '../../../../__mocks__/shallow_usecontext.mock'; import React from 'react'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.tsx index 58691cf09b4a5..5ed1f0b277306 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/empty_state.tsx @@ -5,10 +5,12 @@ */ import React, { useContext } from 'react'; +import { useValues } from 'kea'; import { EuiPageContent, EuiEmptyPrompt, EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { sendTelemetry } from '../../../../shared/telemetry'; +import { HttpLogic } from '../../../../shared/http'; import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; import { KibanaContext, IKibanaContext } from '../../../../index'; import { CREATE_ENGINES_PATH } from '../../../routes'; @@ -18,9 +20,9 @@ import { EngineOverviewHeader } from './header'; import './empty_state.scss'; export const EmptyState: React.FC = () => { + const { http } = useValues(HttpLogic); const { externalUrl: { getAppSearchUrl }, - http, } = useContext(KibanaContext) as IKibanaContext; const buttonProps = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.test.tsx index 7f22ce132d405..8c7dfa2b7c3d6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.test.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import '../../../../__mocks__/kea.mock'; import '../../../../__mocks__/shallow_usecontext.mock'; import React from 'react'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.tsx index 1a1ae295d4828..dca0d45a207b4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/header.tsx @@ -5,6 +5,7 @@ */ import React, { useContext } from 'react'; +import { useValues } from 'kea'; import { EuiPageHeader, EuiPageHeaderSection, @@ -16,12 +17,13 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { sendTelemetry } from '../../../../shared/telemetry'; +import { HttpLogic } from '../../../../shared/http'; import { KibanaContext, IKibanaContext } from '../../../../index'; export const EngineOverviewHeader: React.FC = () => { + const { http } = useValues(HttpLogic); const { externalUrl: { getAppSearchUrl }, - http, } = useContext(KibanaContext) as IKibanaContext; const buttonProps = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx index c2379fb33bd71..928d92d791094 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import '../../../__mocks__/kea.mock'; import '../../../__mocks__/react_router_history.mock'; import React from 'react'; import { act } from 'react-dom/test-utils'; import { shallow, ReactWrapper } from 'enzyme'; -import { mountWithAsyncContext, mockKibanaContext } from '../../../__mocks__'; +import { mountWithAsyncContext, mockHttpValues, setMockValues } from '../../../__mocks__'; import { LoadingState, EmptyState } from './components'; import { EngineTable } from './engine_table'; @@ -18,8 +19,6 @@ import { EngineTable } from './engine_table'; import { EngineOverview } from './'; describe('EngineOverview', () => { - const mockHttp = mockKibanaContext.http; - describe('non-happy-path states', () => { it('isLoading', () => { const wrapper = shallow(); @@ -28,15 +27,16 @@ describe('EngineOverview', () => { }); it('isEmpty', async () => { - const wrapper = await mountWithAsyncContext(, { + setMockValues({ http: { - ...mockHttp, + ...mockHttpValues.http, get: () => ({ results: [], meta: { page: { total_results: 0 } }, }), }, }); + const wrapper = await mountWithAsyncContext(); expect(wrapper.find(EmptyState)).toHaveLength(1); }); @@ -65,12 +65,11 @@ describe('EngineOverview', () => { beforeEach(() => { jest.clearAllMocks(); + setMockValues({ http: { ...mockHttpValues.http, get: mockApi } }); }); it('renders and calls the engines API', async () => { - const wrapper = await mountWithAsyncContext(, { - http: { ...mockHttp, get: mockApi }, - }); + const wrapper = await mountWithAsyncContext(); expect(wrapper.find(EngineTable)).toHaveLength(1); expect(mockApi).toHaveBeenNthCalledWith(1, '/api/app_search/engines', { @@ -84,7 +83,6 @@ describe('EngineOverview', () => { describe('when on a platinum license', () => { it('renders a 2nd meta engines table & makes a 2nd meta engines API call', async () => { const wrapper = await mountWithAsyncContext(, { - http: { ...mockHttp, get: mockApi }, license: { type: 'platinum', isActive: true }, }); @@ -103,9 +101,7 @@ describe('EngineOverview', () => { wrapper.find(EngineTable).prop('pagination'); it('passes down page data from the API', async () => { - const wrapper = await mountWithAsyncContext(, { - http: { ...mockHttp, get: mockApi }, - }); + const wrapper = await mountWithAsyncContext(); const pagination = getTablePagination(wrapper); expect(pagination.totalEngines).toEqual(100); @@ -113,9 +109,7 @@ describe('EngineOverview', () => { }); it('re-polls the API on page change', async () => { - const wrapper = await mountWithAsyncContext(, { - http: { ...mockHttp, get: mockApi }, - }); + const wrapper = await mountWithAsyncContext(); await act(async () => getTablePagination(wrapper).onPaginate(5)); wrapper.update(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx index 9703fde7e140a..c0aedbe7dc6b4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx @@ -5,6 +5,7 @@ */ import React, { useContext, useEffect, useState } from 'react'; +import { useValues } from 'kea'; import { EuiPageContent, EuiPageContentHeader, @@ -12,13 +13,13 @@ import { EuiTitle, EuiSpacer, } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { FlashMessages } from '../../../shared/flash_messages'; +import { HttpLogic } from '../../../shared/http'; import { LicenseContext, ILicenseContext, hasPlatinumLicense } from '../../../shared/licensing'; -import { KibanaContext, IKibanaContext } from '../../../index'; import { EngineIcon } from './assets/engine_icon'; import { MetaEngineIcon } from './assets/meta_engine_icon'; @@ -38,7 +39,7 @@ interface ISetEnginesCallbacks { } export const EngineOverview: React.FC = () => { - const { http } = useContext(KibanaContext) as IKibanaContext; + const { http } = useValues(HttpLogic); const { license } = useContext(LicenseContext) as ILicenseContext; const [isLoading, setIsLoading] = useState(true); @@ -94,10 +95,9 @@ export const EngineOverview: React.FC = () => {

- + {i18n.translate('xpack.enterpriseSearch.appSearch.enginesOverview.engines', { + defaultMessage: 'Engines', + })}

@@ -119,10 +119,9 @@ export const EngineOverview: React.FC = () => {

- + {i18n.translate('xpack.enterpriseSearch.appSearch.enginesOverview.metaEngines', { + defaultMessage: 'Meta Engines', + })}

diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx index 46b6e61e352de..8e92f21f8ffed 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx @@ -4,10 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import '../../../__mocks__/kea.mock'; +import '../../../__mocks__/shallow_usecontext.mock'; +import { mockHttpValues } from '../../../__mocks__/'; + import React from 'react'; +import { mount } from 'enzyme'; +import { I18nProvider } from '@kbn/i18n/react'; import { EuiBasicTable, EuiPagination, EuiButtonEmpty, EuiLink } from '@elastic/eui'; -import { mountWithContext } from '../../../__mocks__'; jest.mock('../../../shared/telemetry', () => ({ sendTelemetry: jest.fn() })); import { sendTelemetry } from '../../../shared/telemetry'; @@ -16,22 +21,24 @@ import { EngineTable } from './engine_table'; describe('EngineTable', () => { const onPaginate = jest.fn(); // onPaginate updates the engines API call upstream - const wrapper = mountWithContext( - + const wrapper = mount( + + + ); const table = wrapper.find(EuiBasicTable); @@ -56,7 +63,7 @@ describe('EngineTable', () => { link.simulate('click'); expect(sendTelemetry).toHaveBeenCalledWith({ - http: expect.any(Object), + http: mockHttpValues.http, product: 'app_search', action: 'clicked', metric: 'engine_table_link', @@ -71,10 +78,16 @@ describe('EngineTable', () => { }); it('handles empty data', () => { - const emptyWrapper = mountWithContext( - {} }} /> + const emptyWrapper = mount( + + {} }} + /> + ); const emptyTable = emptyWrapper.find(EuiBasicTable); + expect(emptyTable.prop('pagination').pageIndex).toEqual(0); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx index 9c6122c88c7d7..6888be1dc2b5b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx @@ -5,11 +5,13 @@ */ import React, { useContext } from 'react'; +import { useValues } from 'kea'; import { EuiBasicTable, EuiBasicTableColumn, EuiLink } from '@elastic/eui'; import { FormattedMessage, FormattedDate, FormattedNumber } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { sendTelemetry } from '../../../shared/telemetry'; +import { HttpLogic } from '../../../shared/http'; import { KibanaContext, IKibanaContext } from '../../../index'; import { getEngineRoute } from '../../routes'; @@ -40,9 +42,9 @@ export const EngineTable: React.FC = ({ data, pagination: { totalEngines, pageIndex, onPaginate }, }) => { + const { http } = useValues(HttpLogic); const { externalUrl: { getAppSearchUrl }, - http, } = useContext(KibanaContext) as IKibanaContext; const engineLinkProps = (name: string) => ({ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 31c7680fd2f1c..350bc97085d7b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -46,7 +46,7 @@ describe('AppSearchUnconfigured', () => { describe('AppSearchConfigured', () => { beforeEach(() => { // Mock resets - (useValues as jest.Mock).mockImplementation(() => ({})); + (useValues as jest.Mock).mockImplementation(() => ({ myRole: {} })); (useActions as jest.Mock).mockImplementation(() => ({ initializeAppData: () => {} })); }); @@ -70,7 +70,7 @@ describe('AppSearchConfigured', () => { it('does not re-initialize app data', () => { const initializeAppData = jest.fn(); (useActions as jest.Mock).mockImplementation(() => ({ initializeAppData })); - (useValues as jest.Mock).mockImplementation(() => ({ hasInitialized: true })); + (useValues as jest.Mock).mockImplementation(() => ({ myRole: {}, hasInitialized: true })); shallow(); @@ -78,7 +78,7 @@ describe('AppSearchConfigured', () => { }); it('renders ErrorConnecting', () => { - (useValues as jest.Mock).mockImplementation(() => ({ errorConnecting: true })); + (useValues as jest.Mock).mockImplementation(() => ({ myRole: {}, errorConnecting: true })); const wrapper = shallow(); @@ -86,20 +86,55 @@ describe('AppSearchConfigured', () => { }); it('passes readOnlyMode state', () => { - (useValues as jest.Mock).mockImplementation(() => ({ readOnlyMode: true })); + (useValues as jest.Mock).mockImplementation(() => ({ myRole: {}, readOnlyMode: true })); const wrapper = shallow(); expect(wrapper.find(Layout).prop('readOnlyMode')).toEqual(true); }); + + describe('ability checks', () => { + // TODO: Use this section for routes wrapped in canViewX conditionals + // e.g., it('renders settings if a user can view settings') + }); }); describe('AppSearchNav', () => { - it('renders', () => { + it('renders with the Engines link', () => { const wrapper = shallow(); expect(wrapper.find(SideNav)).toHaveLength(1); - expect(wrapper.find(SideNavLink).first().prop('to')).toEqual('/engines'); + expect(wrapper.find(SideNavLink).prop('to')).toEqual('/engines'); + }); + + it('renders the Settings link', () => { + (useValues as jest.Mock).mockImplementation(() => ({ + myRole: { canViewSettings: true }, + })); + const wrapper = shallow(); + + expect(wrapper.find(SideNavLink).last().prop('to')).toEqual( + 'http://localhost:3002/as/settings/account' + ); + }); + + it('renders the Credentials link', () => { + (useValues as jest.Mock).mockImplementation(() => ({ + myRole: { canViewAccountCredentials: true }, + })); + const wrapper = shallow(); + + expect(wrapper.find(SideNavLink).last().prop('to')).toEqual( + 'http://localhost:3002/as/credentials' + ); + }); + + it('renders the Role Mappings link', () => { + (useValues as jest.Mock).mockImplementation(() => ({ + myRole: { canViewRoleMappings: true }, + })); + const wrapper = shallow(); + expect(wrapper.find(SideNavLink).last().prop('to')).toEqual( 'http://localhost:3002/as#/role-mappings' ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index 643c4b5ccc873..c848415daf612 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -49,8 +49,8 @@ export const AppSearchUnconfigured: React.FC = () => ( ); export const AppSearchConfigured: React.FC = (props) => { - const { hasInitialized } = useValues(AppLogic); const { initializeAppData } = useActions(AppLogic); + const { hasInitialized } = useValues(AppLogic); const { errorConnecting, readOnlyMode } = useValues(HttpLogic); useEffect(() => { @@ -90,6 +90,10 @@ export const AppSearchNav: React.FC = () => { externalUrl: { getAppSearchUrl }, } = useContext(KibanaContext) as IKibanaContext; + const { + myRole: { canViewSettings, canViewAccountCredentials, canViewRoleMappings }, + } = useValues(AppLogic); + return ( @@ -97,21 +101,27 @@ export const AppSearchNav: React.FC = () => { defaultMessage: 'Engines', })} - - {i18n.translate('xpack.enterpriseSearch.appSearch.nav.settings', { - defaultMessage: 'Account Settings', - })} - - - {i18n.translate('xpack.enterpriseSearch.appSearch.nav.credentials', { - defaultMessage: 'Credentials', - })} - - - {i18n.translate('xpack.enterpriseSearch.appSearch.nav.roleMappings', { - defaultMessage: 'Role Mappings', - })} - + {canViewSettings && ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.nav.settings', { + defaultMessage: 'Account Settings', + })} + + )} + {canViewAccountCredentials && ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.nav.credentials', { + defaultMessage: 'Credentials', + })} + + )} + {canViewRoleMappings && ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.nav.roleMappings', { + defaultMessage: 'Role Mappings', + })} + + )} ); }; diff --git a/x-pack/legacy/server/lib/constants/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/types.ts similarity index 69% rename from x-pack/legacy/server/lib/constants/index.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/types.ts index 2378aca824042..3cabc1051c74a 100644 --- a/x-pack/legacy/server/lib/constants/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/types.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { XPACK_INFO_API_DEFAULT_POLL_FREQUENCY_IN_MILLIS } from './xpack_info'; +export * from '../../../common/types/app_search'; +export { IRole, TRole, TAbility } from './utils/role'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/index.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/index.test.ts new file mode 100644 index 0000000000000..a2eb3d8fbc90d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/index.test.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 { DEFAULT_INITIAL_APP_DATA } from '../../../../../common/__mocks__'; + +import { getRoleAbilities } from './'; + +describe('getRoleAbilities', () => { + const mockRole = DEFAULT_INITIAL_APP_DATA.appSearch.role; + + it('transforms server role data into a flat role obj with helper shorthands', () => { + expect(getRoleAbilities(mockRole)).toEqual({ + id: 'account_id:somestring|user_oid:somestring', + roleType: 'owner', + availableRoleTypes: ['owner', 'admin'], + credentialTypes: ['admin', 'private', 'search'], + canAccessAllEngines: true, + can: expect.any(Function), + // Has access + canViewAccountCredentials: true, + canManageEngines: true, + // Does not have access + canViewMetaEngines: false, + canViewEngineAnalytics: false, + canViewEngineApiLogs: false, + canViewEngineCrawler: false, + canViewEngineCredentials: false, + canViewEngineDocuments: false, + canViewEngineSchema: false, + canViewEngineQueryTester: false, + canViewMetaEngineSourceEngines: false, + canViewSettings: false, + canViewRoleMappings: false, + canManageMetaEngines: false, + canManageLogSettings: false, + canManageSettings: false, + canManageEngineCrawler: false, + canManageEngineDocuments: false, + canManageEngineSynonyms: false, + canManageEngineCredentials: false, + canManageEngineCurations: false, + canManageEngineRelevanceTuning: false, + canManageEngineReferenceUi: false, + canManageEngineResultSettings: false, + canManageEngineSchema: false, + canManageMetaEngineSourceEngines: false, + }); + }); + + describe('can()', () => { + it('sets view abilities to true if manage abilities are true', () => { + const role = { ...mockRole }; + role.ability.view = []; + role.ability.manage = ['account_settings']; + + const myRole = getRoleAbilities(role); + + expect(myRole.canViewSettings).toEqual(true); + expect(myRole.canManageSettings).toEqual(true); + }); + + it('returns false for invalid actions & subjects', () => { + const myRole = getRoleAbilities(mockRole); + + expect(myRole.can('hello' as any, 'world')).toEqual(false); + expect(myRole.can('edit', 'fakeSubject')).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/index.ts new file mode 100644 index 0000000000000..409aef3cd42ec --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/role/index.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IAccount } from '../../types'; + +export type TRole = 'owner' | 'admin' | 'dev' | 'editor' | 'analyst'; +export type TAbility = 'manage' | 'edit' | 'view'; + +export interface IRole { + id: string; + roleType: TRole; + availableRoleTypes: TRole[]; + credentialTypes: string[]; + canAccessAllEngines: boolean; + can(action: TAbility, subject: string): boolean; + canViewMetaEngines: boolean; + canViewAccountCredentials: boolean; + canViewEngineAnalytics: boolean; + canViewEngineApiLogs: boolean; + canViewEngineCrawler: boolean; + canViewEngineCredentials: boolean; + canViewEngineDocuments: boolean; + canViewEngineSchema: boolean; + canViewEngineQueryTester: boolean; + canViewMetaEngineSourceEngines: boolean; + canViewSettings: boolean; + canViewRoleMappings: boolean; + canManageEngines: boolean; + canManageMetaEngines: boolean; + canManageLogSettings: boolean; + canManageSettings: boolean; + canManageEngineCrawler: boolean; + canManageEngineDocuments: boolean; + canManageEngineSynonyms: boolean; + canManageEngineCredentials: boolean; + canManageEngineCurations: boolean; + canManageEngineRelevanceTuning: boolean; + canManageEngineReferenceUi: boolean; + canManageEngineResultSettings: boolean; + canManageEngineSchema: boolean; + canManageMetaEngineSourceEngines: boolean; +} + +/** + * Transforms the `role` data we receive from the Enterprise Search + * server into a more convenient format for front-end use + */ +export const getRoleAbilities = (role: IAccount['role']): IRole => { + // Role ability function helpers + const myRole = { + can: (action: TAbility, subject: string): boolean => { + return ( + role?.ability?.manage?.includes(subject) || + (Array.isArray(role.ability[action]) && role.ability[action].includes(subject)) + ); + }, + // TODO: canHaveScopedEngines fn + }; + + // Clone top-level role props, and move some props out of `ability` and into the top-level for convenience + const topLevelProps = { + id: role.id, + roleType: role.roleType as TRole, + availableRoleTypes: role.ability.availableRoleTypes as TRole[], + credentialTypes: role.ability.credentialTypes, + }; + + // Ability shorthands (also in top level of role obj for convenience) + // Example usage: `const { myRole: { canViewSettings } } = useValues(AppLogic);` + const abilities = { + canAccessAllEngines: role.ability.accessAllEngines, + canViewMetaEngines: myRole.can('view', 'account_meta_engines'), + canViewAccountCredentials: myRole.can('view', 'account_credentials'), + canViewEngineAnalytics: myRole.can('view', 'engine_analytics'), + canViewEngineApiLogs: myRole.can('view', 'engine_api_logs'), + canViewEngineCrawler: myRole.can('view', 'engine_crawler'), + canViewEngineCredentials: myRole.can('view', 'engine_credentials'), + canViewEngineDocuments: myRole.can('view', 'engine_documents'), + canViewEngineSchema: myRole.can('view', 'engine_schema'), + canViewEngineQueryTester: myRole.can('view', 'engine_query_tester'), + canViewMetaEngineSourceEngines: myRole.can('view', 'meta_engine_source_engines'), + canViewSettings: myRole.can('view', 'account_settings'), + canViewRoleMappings: myRole.can('view', 'role_mappings'), + canManageEngines: myRole.can('manage', 'account_engines'), + canManageMetaEngines: myRole.can('manage', 'account_meta_engines'), + canManageLogSettings: myRole.can('manage', 'account_log_settings'), + canManageSettings: myRole.can('manage', 'account_settings'), + canManageEngineCrawler: myRole.can('manage', 'engine_crawler'), + canManageEngineDocuments: myRole.can('manage', 'engine_documents'), + canManageEngineSynonyms: myRole.can('manage', 'engine_synonyms'), + canManageEngineCredentials: myRole.can('manage', 'engine_credentials'), + canManageEngineCurations: myRole.can('manage', 'engine_curations'), + canManageEngineRelevanceTuning: myRole.can('manage', 'engine_relevance_tuning'), + canManageEngineReferenceUi: myRole.can('manage', 'engine_reference_ui'), + canManageEngineResultSettings: myRole.can('manage', 'engine_result_settings'), + canManageEngineSchema: myRole.can('manage', 'engine_schema'), + canManageMetaEngineSourceEngines: myRole.can('manage', 'meta_engine_source_engines'), + }; + + return Object.assign(myRole, topLevelProps, abilities); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.test.tsx new file mode 100644 index 0000000000000..8d48875a8e1f5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.test.tsx @@ -0,0 +1,19 @@ +/* + * 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 { ErrorStatePrompt } from '../../../shared/error_state'; +import { ErrorConnecting } from './'; + +describe('ErrorConnecting', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(ErrorStatePrompt)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx new file mode 100644 index 0000000000000..567c77792583d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.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 React from 'react'; +import { EuiPage, EuiPageContent } from '@elastic/eui'; + +import { ErrorStatePrompt } from '../../../shared/error_state'; + +export const ErrorConnecting: React.FC = () => ( + + + + + +); diff --git a/x-pack/legacy/server/lib/check_license/index.js b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/index.ts similarity index 81% rename from x-pack/legacy/server/lib/check_license/index.js rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/index.ts index f2c070fd44b6e..c8b71e1a6e791 100644 --- a/x-pack/legacy/server/lib/check_license/index.js +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { checkLicense } from './check_license'; +export { ErrorConnecting } from './error_connecting'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx index a76b654ccddd0..f651511e61b44 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import '../../../__mocks__/kea.mock'; + import React from 'react'; import { shallow } from 'enzyme'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx index 334ca126cabb9..833a782a32f00 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext } from 'react'; +import React from 'react'; +import { useValues } from 'kea'; import upperFirst from 'lodash/upperFirst'; import snakeCase from 'lodash/snakeCase'; import { i18n } from '@kbn/i18n'; @@ -12,7 +13,7 @@ import { EuiCard, EuiTextColor } from '@elastic/eui'; import { EuiButton } from '../../../shared/react_router_helpers'; import { sendTelemetry } from '../../../shared/telemetry'; -import { KibanaContext, IKibanaContext } from '../../../index'; +import { HttpLogic } from '../../../shared/http'; import './product_card.scss'; @@ -28,7 +29,7 @@ interface IProductCard { } export const ProductCard: React.FC = ({ product, image }) => { - const { http } = useContext(KibanaContext) as IKibanaContext; + const { http } = useValues(HttpLogic); return ( { + beforeEach(() => { + (useValues as jest.Mock).mockReturnValue({ errorConnecting: false }); + }); + it('renders the overview page and product cards', () => { const wrapper = shallow( @@ -22,6 +29,14 @@ describe('EnterpriseSearch', () => { expect(wrapper.find(ProductCard)).toHaveLength(2); }); + it('renders the error connecting prompt', () => { + (useValues as jest.Mock).mockReturnValueOnce({ errorConnecting: true }); + const wrapper = shallow(); + + expect(wrapper.find(ErrorConnecting)).toHaveLength(1); + expect(wrapper.find(EuiPage)).toHaveLength(0); + }); + describe('access checks', () => { it('does not render the App Search card if the user does not have access to AS', () => { const wrapper = shallow( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx index 373f595a6a9ea..3a3ba02e07058 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx @@ -5,6 +5,7 @@ */ import React from 'react'; +import { useValues } from 'kea'; import { EuiPage, EuiPageBody, @@ -21,9 +22,11 @@ import { i18n } from '@kbn/i18n'; import { IInitialAppData } from '../../../common/types'; import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../common/constants'; +import { HttpLogic } from '../shared/http'; import { SetEnterpriseSearchChrome as SetPageChrome } from '../shared/kibana_chrome'; import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../shared/telemetry'; +import { ErrorConnecting } from './components/error_connecting'; import { ProductCard } from './components/product_card'; import AppSearchImage from './assets/app_search.png'; @@ -31,9 +34,12 @@ import WorkplaceSearchImage from './assets/workplace_search.png'; import './index.scss'; export const EnterpriseSearch: React.FC = ({ access = {} }) => { + const { errorConnecting } = useValues(HttpLogic); const { hasAppSearchAccess, hasWorkplaceSearchAccess } = access; - return ( + return errorConnecting ? ( + + ) : ( diff --git a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx index e0cf2814b46b4..053c450ab925e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx @@ -10,7 +10,7 @@ import { AppMountParameters } from 'src/core/public'; import { coreMock } from 'src/core/public/mocks'; import { licensingMock } from '../../../licensing/public/mocks'; -import { renderApp } from './'; +import { renderApp, renderHeaderActions } from './'; import { AppSearch } from './app_search'; import { WorkplaceSearch } from './workplace_search'; @@ -33,6 +33,7 @@ describe('renderApp', () => { const unmount = renderApp(MockApp, params, core, plugins, config, data); expect(params.element.querySelector('.hello-world')).not.toBeNull(); + unmount(); expect(params.element.innerHTML).toEqual(''); }); @@ -47,3 +48,16 @@ describe('renderApp', () => { expect(params.element.querySelector('.setupGuide')).not.toBeNull(); }); }); + +describe('renderHeaderActions', () => { + it('mounts and unmounts any HeaderActions component', () => { + const mockHeaderEl = document.createElement('header'); + const MockHeaderActions = () => ; + + const unmount = renderHeaderActions(MockHeaderActions, mockHeaderEl, {} as any); + expect(mockHeaderEl.querySelector('.hello-world')).not.toBeNull(); + + unmount(); + expect(mockHeaderEl.innerHTML).toEqual(''); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index 82f884644be4a..0869ef7b22729 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -13,24 +13,17 @@ import { Store } from 'redux'; import { getContext, resetContext } from 'kea'; import { I18nProvider } from '@kbn/i18n/react'; -import { - AppMountParameters, - CoreStart, - ApplicationStart, - HttpSetup, - ChromeBreadcrumb, -} from 'src/core/public'; +import { AppMountParameters, CoreStart, ApplicationStart, ChromeBreadcrumb } from 'src/core/public'; import { ClientConfigType, ClientData, PluginsSetup } from '../plugin'; import { LicenseProvider } from './shared/licensing'; -import { FlashMessagesProvider } from './shared/flash_messages'; -import { HttpProvider } from './shared/http'; +import { mountHttpLogic } from './shared/http'; +import { mountFlashMessagesLogic } from './shared/flash_messages'; import { IExternalUrl } from './shared/enterprise_search_url'; import { IInitialAppData } from '../../common/types'; export interface IKibanaContext { config: { host?: string }; externalUrl: IExternalUrl; - http: HttpSetup; navigateToUrl: ApplicationStart['navigateToUrl']; setBreadcrumbs(crumbs: ChromeBreadcrumb[]): void; setDocTitle(title: string): void; @@ -55,13 +48,20 @@ export const renderApp = ( resetContext({ createStore: true }); const store = getContext().store as Store; + const unmountHttpLogic = mountHttpLogic({ + http: core.http, + errorConnecting, + readOnlyMode: initialData.readOnlyMode, + }); + + const unmountFlashMessagesLogic = mountFlashMessagesLogic({ history: params.history }); + ReactDOM.render( - - @@ -86,5 +80,26 @@ export const renderApp = ( ); return () => { ReactDOM.unmountComponentAtNode(params.element); + unmountHttpLogic(); + unmountFlashMessagesLogic(); }; }; + +/** + * Render function for Kibana's header action menu chrome - + * reusable by any Enterprise Search plugin simply by passing in + * a custom HeaderActions component (e.g., WorkplaceSearchHeaderActions) + * @see https://github.com/elastic/kibana/blob/master/docs/development/core/public/kibana-plugin-core-public.appmountparameters.setheaderactionmenu.md + */ +interface IHeaderActionsProps { + externalUrl: IExternalUrl; +} + +export const renderHeaderActions = ( + HeaderActions: React.FC, + kibanaHeaderEl: HTMLElement, + externalUrl: IExternalUrl +) => { + ReactDOM.render(, kibanaHeaderEl); + return () => ReactDOM.unmountComponentAtNode(kibanaHeaderEl); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts index 136912847baa9..c12011b47a472 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts @@ -6,23 +6,25 @@ import { resetContext } from 'kea'; -import { FlashMessagesLogic, IFlashMessage } from './flash_messages_logic'; +import { mockHistory } from '../../__mocks__'; + +import { FlashMessagesLogic, mountFlashMessagesLogic, IFlashMessage } from './'; describe('FlashMessagesLogic', () => { - const DEFAULT_VALUES = { - messages: [], - queuedMessages: [], - historyListener: null, - }; + const mount = () => mountFlashMessagesLogic({ history: mockHistory as any }); beforeEach(() => { jest.clearAllMocks(); resetContext({}); }); - it('has expected default values', () => { - FlashMessagesLogic.mount(); - expect(FlashMessagesLogic.values).toEqual(DEFAULT_VALUES); + it('has default values', () => { + mount(); + expect(FlashMessagesLogic.values).toEqual({ + messages: [], + queuedMessages: [], + historyListener: expect.any(Function), + }); }); describe('setFlashMessages()', () => { @@ -33,7 +35,7 @@ describe('FlashMessagesLogic', () => { { type: 'info', message: 'Everything is fine, nothing is ruined' }, ]; - FlashMessagesLogic.mount(); + mount(); FlashMessagesLogic.actions.setFlashMessages(messages); expect(FlashMessagesLogic.values.messages).toEqual(messages); @@ -42,7 +44,7 @@ describe('FlashMessagesLogic', () => { it('automatically converts to an array if a single message obj is passed in', () => { const message = { type: 'success', message: 'I turn into an array!' } as IFlashMessage; - FlashMessagesLogic.mount(); + mount(); FlashMessagesLogic.actions.setFlashMessages(message); expect(FlashMessagesLogic.values.messages).toEqual([message]); @@ -51,7 +53,7 @@ describe('FlashMessagesLogic', () => { describe('clearFlashMessages()', () => { it('sets messages back to an empty array', () => { - FlashMessagesLogic.mount(); + mount(); FlashMessagesLogic.actions.setFlashMessages('test' as any); FlashMessagesLogic.actions.clearFlashMessages(); @@ -63,7 +65,7 @@ describe('FlashMessagesLogic', () => { it('sets an array of messages', () => { const queuedMessage: IFlashMessage = { type: 'error', message: 'You deleted a thing' }; - FlashMessagesLogic.mount(); + mount(); FlashMessagesLogic.actions.setQueuedMessages(queuedMessage); expect(FlashMessagesLogic.values.queuedMessages).toEqual([queuedMessage]); @@ -72,7 +74,7 @@ describe('FlashMessagesLogic', () => { describe('clearQueuedMessages()', () => { it('sets queued messages back to an empty array', () => { - FlashMessagesLogic.mount(); + mount(); FlashMessagesLogic.actions.setQueuedMessages('test' as any); FlashMessagesLogic.actions.clearQueuedMessages(); @@ -83,30 +85,25 @@ describe('FlashMessagesLogic', () => { describe('history listener logic', () => { describe('setHistoryListener()', () => { it('sets the historyListener value', () => { - FlashMessagesLogic.mount(); + mount(); FlashMessagesLogic.actions.setHistoryListener('test' as any); expect(FlashMessagesLogic.values.historyListener).toEqual('test'); }); }); - describe('listenToHistory()', () => { + describe('on mount', () => { it('listens for history changes and clears messages on change', () => { - FlashMessagesLogic.mount(); + mount(); + expect(mockHistory.listen).toHaveBeenCalled(); + FlashMessagesLogic.actions.setQueuedMessages(['queuedMessages'] as any); jest.spyOn(FlashMessagesLogic.actions, 'clearFlashMessages'); jest.spyOn(FlashMessagesLogic.actions, 'setFlashMessages'); jest.spyOn(FlashMessagesLogic.actions, 'clearQueuedMessages'); jest.spyOn(FlashMessagesLogic.actions, 'setHistoryListener'); - const mockListener = jest.fn(() => jest.fn()); - const history = { listen: mockListener } as any; - FlashMessagesLogic.actions.listenToHistory(history); - - expect(mockListener).toHaveBeenCalled(); - expect(FlashMessagesLogic.actions.setHistoryListener).toHaveBeenCalled(); - - const mockHistoryChange = (mockListener.mock.calls[0] as any)[0]; + const mockHistoryChange = (mockHistory.listen.mock.calls[0] as any)[0]; mockHistoryChange(); expect(FlashMessagesLogic.actions.clearFlashMessages).toHaveBeenCalled(); expect(FlashMessagesLogic.actions.setFlashMessages).toHaveBeenCalledWith([ @@ -116,19 +113,20 @@ describe('FlashMessagesLogic', () => { }); }); - describe('beforeUnmount', () => { - it('removes history listener on unmount', () => { + describe('on unmount', () => { + it('removes history listener', () => { const mockUnlistener = jest.fn(); - const unmount = FlashMessagesLogic.mount(); + mockHistory.listen.mockReturnValueOnce(mockUnlistener); - FlashMessagesLogic.actions.setHistoryListener(mockUnlistener); + const unmount = mount(); unmount(); expect(mockUnlistener).toHaveBeenCalled(); }); it('does not crash if no listener exists', () => { - const unmount = FlashMessagesLogic.mount(); + const unmount = mount(); + FlashMessagesLogic.actions.setHistoryListener(null as any); unmount(); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts index 37a8f16acad6d..1735cc8ac7228 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts @@ -24,7 +24,6 @@ export interface IFlashMessagesActions { clearFlashMessages(): void; setQueuedMessages(messages: IFlashMessage | IFlashMessage[]): { messages: IFlashMessage[] }; clearQueuedMessages(): void; - listenToHistory(history: History): History; setHistoryListener(historyListener: Function): { historyListener: Function }; } @@ -38,7 +37,6 @@ export const FlashMessagesLogic = kea null, setQueuedMessages: (messages) => ({ messages: convertToArray(messages) }), clearQueuedMessages: () => null, - listenToHistory: (history) => history, setHistoryListener: (historyListener) => ({ historyListener }), }, reducers: { @@ -63,21 +61,31 @@ export const FlashMessagesLogic = kea ({ - listenToHistory: (history) => { + events: ({ props, values, actions }) => ({ + afterMount: () => { // On React Router navigation, clear previous flash messages and load any queued messages - const unlisten = history.listen(() => { + const unlisten = props.history.listen(() => { actions.clearFlashMessages(); actions.setFlashMessages(values.queuedMessages); actions.clearQueuedMessages(); }); actions.setHistoryListener(unlisten); }, - }), - events: ({ values }) => ({ beforeUnmount: () => { const { historyListener: removeHistoryListener } = values; if (removeHistoryListener) removeHistoryListener(); }, }), }); + +/** + * Mount/props helper + */ +interface IFlashMessagesLogicProps { + history: History; +} +export const mountFlashMessagesLogic = (props: IFlashMessagesLogicProps) => { + FlashMessagesLogic(props); + const unmount = FlashMessagesLogic.mount(); + return unmount; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.test.tsx deleted file mode 100644 index bcd7abd6d7ce2..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.test.tsx +++ /dev/null @@ -1,46 +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 '../../__mocks__/shallow_usecontext.mock'; -import '../../__mocks__/kea.mock'; - -import React from 'react'; -import { shallow } from 'enzyme'; -import { useValues, useActions } from 'kea'; - -import { mockHistory } from '../../__mocks__'; - -import { FlashMessagesProvider } from './'; - -describe('FlashMessagesProvider', () => { - const props = { history: mockHistory as any }; - const listenToHistory = jest.fn(); - - beforeEach(() => { - jest.clearAllMocks(); - (useActions as jest.Mock).mockImplementationOnce(() => ({ listenToHistory })); - }); - - it('does not render', () => { - const wrapper = shallow(); - - expect(wrapper.isEmptyRender()).toBe(true); - }); - - it('listens to history on mount', () => { - shallow(); - - expect(listenToHistory).toHaveBeenCalledWith(mockHistory); - }); - - it('does not add another history listener if one already exists', () => { - (useValues as jest.Mock).mockImplementationOnce(() => ({ historyListener: 'exists' as any })); - - shallow(); - - expect(listenToHistory).not.toHaveBeenCalledWith(props); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.tsx deleted file mode 100644 index a3ceabcf6ac8a..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_provider.tsx +++ /dev/null @@ -1,26 +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, { useEffect } from 'react'; -import { useValues, useActions } from 'kea'; -import { History } from 'history'; - -import { FlashMessagesLogic } from './flash_messages_logic'; - -interface IFlashMessagesProviderProps { - history: History; -} - -export const FlashMessagesProvider: React.FC = ({ history }) => { - const { historyListener } = useValues(FlashMessagesLogic); - const { listenToHistory } = useActions(FlashMessagesLogic); - - useEffect(() => { - if (!historyListener) listenToHistory(history); - }, []); - - return null; -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts index c4daeb44420c8..21c1a60efa6b7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts @@ -10,7 +10,7 @@ export { IFlashMessage, IFlashMessagesValues, IFlashMessagesActions, + mountFlashMessagesLogic, } from './flash_messages_logic'; -export { FlashMessagesProvider } from './flash_messages_provider'; export { flashAPIErrors } from './handle_api_errors'; export { setSuccessMessage, setErrorMessage, setQueuedSuccessMessage } from './set_message_helpers'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.test.ts index c3c60d77f4577..f2ddd560ac9c1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/set_message_helpers.test.ts @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { mockHistory } from '../../__mocks__'; + import { FlashMessagesLogic, + mountFlashMessagesLogic, setSuccessMessage, setErrorMessage, setQueuedSuccessMessage, @@ -15,7 +18,7 @@ describe('Flash Message Helpers', () => { const message = 'I am a message'; beforeEach(() => { - FlashMessagesLogic.mount(); + mountFlashMessagesLogic({ history: mockHistory as any }); }); it('setSuccessMessage()', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts index b65499be2f7c0..df32b5496c367 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts @@ -8,31 +8,20 @@ import { resetContext } from 'kea'; import { httpServiceMock } from 'src/core/public/mocks'; -import { HttpLogic } from './http_logic'; +import { HttpLogic, mountHttpLogic } from './http_logic'; describe('HttpLogic', () => { const mockHttp = httpServiceMock.createSetupContract(); - const DEFAULT_VALUES = { - http: null, - httpInterceptors: [], - errorConnecting: false, - readOnlyMode: false, - }; + const mount = () => mountHttpLogic({ http: mockHttp }); beforeEach(() => { jest.clearAllMocks(); resetContext({}); }); - it('has expected default values', () => { - HttpLogic.mount(); - expect(HttpLogic.values).toEqual(DEFAULT_VALUES); - }); - - describe('initializeHttp()', () => { - it('sets values based on passed props', () => { - HttpLogic.mount(); - HttpLogic.actions.initializeHttp({ + describe('mounts', () => { + it('sets values from props', () => { + mountHttpLogic({ http: mockHttp, errorConnecting: true, readOnlyMode: true, @@ -40,7 +29,7 @@ describe('HttpLogic', () => { expect(HttpLogic.values).toEqual({ http: mockHttp, - httpInterceptors: [], + httpInterceptors: expect.any(Array), errorConnecting: true, readOnlyMode: true, }); @@ -49,7 +38,9 @@ describe('HttpLogic', () => { describe('setErrorConnecting()', () => { it('sets errorConnecting value', () => { - HttpLogic.mount(); + mount(); + expect(HttpLogic.values.errorConnecting).toEqual(false); + HttpLogic.actions.setErrorConnecting(true); expect(HttpLogic.values.errorConnecting).toEqual(true); @@ -60,7 +51,9 @@ describe('HttpLogic', () => { describe('setReadOnlyMode()', () => { it('sets readOnlyMode value', () => { - HttpLogic.mount(); + mount(); + expect(HttpLogic.values.readOnlyMode).toEqual(false); + HttpLogic.actions.setReadOnlyMode(true); expect(HttpLogic.values.readOnlyMode).toEqual(true); @@ -72,10 +65,8 @@ describe('HttpLogic', () => { describe('http interceptors', () => { describe('initializeHttpInterceptors()', () => { beforeEach(() => { - HttpLogic.mount(); + mount(); jest.spyOn(HttpLogic.actions, 'setHttpInterceptors'); - HttpLogic.actions.initializeHttp({ http: mockHttp }); - HttpLogic.actions.initializeHttpInterceptors(); }); it('calls http.intercept and sets an array of interceptors', () => { @@ -165,7 +156,7 @@ describe('HttpLogic', () => { }); it('sets httpInterceptors and calls all valid remove functions on unmount', () => { - const unmount = HttpLogic.mount(); + const unmount = mount(); const httpInterceptors = [jest.fn(), undefined, jest.fn()] as any; HttpLogic.actions.setHttpInterceptors(httpInterceptors); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts index 72380142fe399..d16e507bfb3bc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts @@ -7,7 +7,6 @@ import { kea, MakeLogicType } from 'kea'; import { HttpSetup, HttpInterceptorResponseError, HttpResponse } from 'src/core/public'; -import { IHttpProviderProps } from './http_provider'; import { READ_ONLY_MODE_HEADER } from '../../../../common/constants'; @@ -18,7 +17,6 @@ export interface IHttpValues { readOnlyMode: boolean; } export interface IHttpActions { - initializeHttp({ http, errorConnecting, readOnlyMode }: IHttpProviderProps): IHttpProviderProps; initializeHttpInterceptors(): void; setHttpInterceptors(httpInterceptors: Function[]): { httpInterceptors: Function[] }; setErrorConnecting(errorConnecting: boolean): { errorConnecting: boolean }; @@ -28,19 +26,13 @@ export interface IHttpActions { export const HttpLogic = kea>({ path: ['enterprise_search', 'http_logic'], actions: { - initializeHttp: (props) => props, initializeHttpInterceptors: () => null, setHttpInterceptors: (httpInterceptors) => ({ httpInterceptors }), setErrorConnecting: (errorConnecting) => ({ errorConnecting }), setReadOnlyMode: (readOnlyMode) => ({ readOnlyMode }), }, - reducers: { - http: [ - (null as unknown) as HttpSetup, - { - initializeHttp: (_, { http }) => http, - }, - ], + reducers: ({ props }) => ({ + http: [props.http, {}], httpInterceptors: [ [], { @@ -48,20 +40,18 @@ export const HttpLogic = kea>({ }, ], errorConnecting: [ - false, + props.errorConnecting || false, { - initializeHttp: (_, { errorConnecting }) => !!errorConnecting, setErrorConnecting: (_, { errorConnecting }) => errorConnecting, }, ], readOnlyMode: [ - false, + props.readOnlyMode || false, { - initializeHttp: (_, { readOnlyMode }) => !!readOnlyMode, setReadOnlyMode: (_, { readOnlyMode }) => readOnlyMode, }, ], - }, + }), listeners: ({ values, actions }) => ({ initializeHttpInterceptors: () => { const httpInterceptors = []; @@ -103,7 +93,10 @@ export const HttpLogic = kea>({ actions.setHttpInterceptors(httpInterceptors); }, }), - events: ({ values }) => ({ + events: ({ values, actions }) => ({ + afterMount: () => { + actions.initializeHttpInterceptors(); + }, beforeUnmount: () => { values.httpInterceptors.forEach((removeInterceptorFn?: Function) => { if (removeInterceptorFn) removeInterceptorFn(); @@ -112,6 +105,20 @@ export const HttpLogic = kea>({ }), }); +/** + * Mount/props helper + */ +interface IHttpLogicProps { + http: HttpSetup; + errorConnecting?: boolean; + readOnlyMode?: boolean; +} +export const mountHttpLogic = (props: IHttpLogicProps) => { + HttpLogic(props); + const unmount = HttpLogic.mount(); + return unmount; +}; + /** * Small helper that checks whether or not an http call is for an Enterprise Search API */ diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.test.tsx deleted file mode 100644 index 902c910f10d7c..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.test.tsx +++ /dev/null @@ -1,45 +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 '../../__mocks__/shallow_usecontext.mock'; -import '../../__mocks__/kea.mock'; - -import React from 'react'; -import { shallow } from 'enzyme'; -import { useActions } from 'kea'; - -import { HttpProvider } from './'; - -describe('HttpProvider', () => { - const props = { - http: {} as any, - errorConnecting: false, - readOnlyMode: false, - }; - const initializeHttp = jest.fn(); - const initializeHttpInterceptors = jest.fn(); - - beforeEach(() => { - jest.clearAllMocks(); - (useActions as jest.Mock).mockImplementationOnce(() => ({ - initializeHttp, - initializeHttpInterceptors, - })); - }); - - it('does not render', () => { - const wrapper = shallow(); - - expect(wrapper.isEmptyRender()).toBe(true); - }); - - it('calls initialization actions on mount', () => { - shallow(); - - expect(initializeHttp).toHaveBeenCalledWith(props); - expect(initializeHttpInterceptors).toHaveBeenCalled(); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx deleted file mode 100644 index db1b0d611079a..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useEffect } from 'react'; -import { useActions } from 'kea'; - -import { HttpSetup } from 'src/core/public'; - -import { HttpLogic } from './http_logic'; - -export interface IHttpProviderProps { - http: HttpSetup; - errorConnecting?: boolean; - readOnlyMode?: boolean; -} - -export const HttpProvider: React.FC = (props) => { - const { initializeHttp, initializeHttpInterceptors } = useActions(HttpLogic); - - useEffect(() => { - initializeHttp(props); - initializeHttpInterceptors(); - }, []); - - return null; -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/http/index.ts index db65e80ca25c2..46a52415f8564 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { HttpLogic, IHttpValues, IHttpActions } from './http_logic'; -export { HttpProvider } from './http_provider'; +export { HttpLogic, IHttpValues, IHttpActions, mountHttpLogic } from './http_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx index 8f7cf090e2d57..073c548ba47fa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import '../../__mocks__/kea.mock'; +import '../../__mocks__/shallow_usecontext.mock'; +import { mockHttpValues } from '../../__mocks__'; + import React from 'react'; +import { shallow } from 'enzyme'; -import { httpServiceMock } from 'src/core/public/mocks'; import { JSON_HEADER as headers } from '../../../../common/constants'; -import { mountWithKibanaContext } from '../../__mocks__'; import { sendTelemetry, @@ -18,8 +21,6 @@ import { } from './'; describe('Shared Telemetry Helpers', () => { - const httpMock = httpServiceMock.createSetupContract(); - beforeEach(() => { jest.clearAllMocks(); }); @@ -27,13 +28,13 @@ describe('Shared Telemetry Helpers', () => { describe('sendTelemetry', () => { it('successfully calls the server-side telemetry endpoint', () => { sendTelemetry({ - http: httpMock, + http: mockHttpValues.http, product: 'enterprise_search', action: 'viewed', metric: 'setup_guide', }); - expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/telemetry', { + expect(mockHttpValues.http.put).toHaveBeenCalledWith('/api/enterprise_search/stats', { headers, body: '{"product":"enterprise_search","action":"viewed","metric":"setup_guide"}', }); @@ -50,33 +51,27 @@ describe('Shared Telemetry Helpers', () => { describe('React component helpers', () => { it('SendEnterpriseSearchTelemetry component', () => { - mountWithKibanaContext(, { - http: httpMock, - }); + shallow(); - expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/telemetry', { + expect(mockHttpValues.http.put).toHaveBeenCalledWith('/api/enterprise_search/stats', { headers, body: '{"product":"enterprise_search","action":"viewed","metric":"page"}', }); }); it('SendAppSearchTelemetry component', () => { - mountWithKibanaContext(, { - http: httpMock, - }); + shallow(); - expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/telemetry', { + expect(mockHttpValues.http.put).toHaveBeenCalledWith('/api/enterprise_search/stats', { headers, body: '{"product":"app_search","action":"clicked","metric":"button"}', }); }); it('SendWorkplaceSearchTelemetry component', () => { - mountWithKibanaContext(, { - http: httpMock, - }); + shallow(); - expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/telemetry', { + expect(mockHttpValues.http.put).toHaveBeenCalledWith('/api/enterprise_search/stats', { headers, body: '{"product":"workplace_search","action":"error","metric":"not_found"}', }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx index 4df1428221de6..2f87597897b41 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useContext, useEffect } from 'react'; +import React, { useEffect } from 'react'; +import { useValues } from 'kea'; import { HttpSetup } from 'src/core/public'; import { JSON_HEADER as headers } from '../../../../common/constants'; -import { KibanaContext, IKibanaContext } from '../../index'; +import { HttpLogic } from '../http'; interface ISendTelemetryProps { action: 'viewed' | 'error' | 'clicked'; @@ -27,7 +28,7 @@ interface ISendTelemetry extends ISendTelemetryProps { export const sendTelemetry = async ({ http, product, action, metric }: ISendTelemetry) => { try { const body = JSON.stringify({ product, action, metric }); - await http.put('/api/enterprise_search/telemetry', { headers, body }); + await http.put('/api/enterprise_search/stats', { headers, body }); } catch (error) { throw new Error('Unable to send telemetry'); } @@ -41,7 +42,7 @@ export const SendEnterpriseSearchTelemetry: React.FC = ({ action, metric, }) => { - const { http } = useContext(KibanaContext) as IKibanaContext; + const { http } = useValues(HttpLogic); useEffect(() => { sendTelemetry({ http, action, metric, product: 'enterprise_search' }); @@ -51,7 +52,7 @@ export const SendEnterpriseSearchTelemetry: React.FC = ({ }; export const SendAppSearchTelemetry: React.FC = ({ action, metric }) => { - const { http } = useContext(KibanaContext) as IKibanaContext; + const { http } = useValues(HttpLogic); useEffect(() => { sendTelemetry({ http, action, metric, product: 'app_search' }); @@ -61,7 +62,7 @@ export const SendAppSearchTelemetry: React.FC = ({ action, }; export const SendWorkplaceSearchTelemetry: React.FC = ({ action, metric }) => { - const { http } = useContext(KibanaContext) as IKibanaContext; + const { http } = useValues(HttpLogic); useEffect(() => { sendTelemetry({ http, action, metric, product: 'workplace_search' }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts index c52eceb2d2fdd..974e07069ddba 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts @@ -50,5 +50,15 @@ describe('AppLogic', () => { expect(AppLogic.values).toEqual(expectedLogicValues); }); + + it('gracefully handles missing initial data', () => { + AppLogic.actions.initializeAppData({}); + + expect(AppLogic.values).toEqual({ + ...DEFAULT_VALUES, + hasInitialized: true, + isFederatedAuth: false, + }); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts index 94bd1d529b65f..629d1969a8f59 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts @@ -21,6 +21,9 @@ export interface IAppActions { initializeAppData(props: IInitialAppData): IInitialAppData; } +const emptyOrg = {} as IOrganization; +const emptyAccount = {} as IAccount; + export const AppLogic = kea>({ path: ['enterprise_search', 'workplace_search', 'app_logic'], actions: { @@ -43,15 +46,15 @@ export const AppLogic = kea>({ }, ], organization: [ - {} as IOrganization, + emptyOrg, { - initializeAppData: (_, { workplaceSearch }) => workplaceSearch!.organization, + initializeAppData: (_, { workplaceSearch }) => workplaceSearch?.organization || emptyOrg, }, ], account: [ - {} as IAccount, + emptyAccount, { - initializeAppData: (_, { workplaceSearch }) => workplaceSearch!.account, + initializeAppData: (_, { workplaceSearch }) => workplaceSearch?.account || emptyAccount, }, ], }, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts index 41861a8ee2dc5..915638246c00e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts @@ -5,3 +5,4 @@ */ export { WorkplaceSearchNav } from './nav'; +export { WorkplaceSearchHeaderActions } from './kibana_header_actions'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.test.tsx new file mode 100644 index 0000000000000..a006c5e3775d5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiButtonEmpty } from '@elastic/eui'; +import { ExternalUrl } from '../../../shared/enterprise_search_url'; + +import { WorkplaceSearchHeaderActions } from './'; + +describe('WorkplaceSearchHeaderActions', () => { + const externalUrl = new ExternalUrl('http://localhost:3002'); + + it('renders a link to the search application', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiButtonEmpty).prop('href')).toEqual('http://localhost:3002/ws/search'); + }); + + it('does not render without an Enterprise Search host URL set', () => { + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx new file mode 100644 index 0000000000000..fa32d598f848d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.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 { i18n } from '@kbn/i18n'; +import { EuiButtonEmpty } from '@elastic/eui'; + +import { IExternalUrl } from '../../../shared/enterprise_search_url'; + +interface IProps { + externalUrl: IExternalUrl; +} + +export const WorkplaceSearchHeaderActions: React.FC = ({ externalUrl }) => { + const { enterpriseSearchUrl, getWorkplaceSearchUrl } = externalUrl; + if (!enterpriseSearchUrl) return null; + + return ( + + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.headerActions.searchApplication', { + defaultMessage: 'Go to search application', + })} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.test.tsx index 429a2c509813d..c73eb05ccec16 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.test.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import '../../../../__mocks__/kea.mock'; import '../../../../__mocks__/shallow_usecontext.mock'; import React from 'react'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx index a914000654165..a80de9fd6ac82 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx @@ -5,17 +5,19 @@ */ import React, { useContext } from 'react'; +import { useValues } from 'kea'; import { EuiButton, EuiButtonProps, EuiLinkProps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { sendTelemetry } from '../../../../shared/telemetry'; +import { HttpLogic } from '../../../../shared/http'; import { KibanaContext, IKibanaContext } from '../../../../index'; export const ProductButton: React.FC = () => { + const { http } = useValues(HttpLogic); const { externalUrl: { getWorkplaceSearchUrl }, - http, } = useContext(KibanaContext) as IKibanaContext; const buttonProps = { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx index 1d7c565935e97..c890adb8ea043 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import '../../../__mocks__/kea.mock'; import '../../../__mocks__/shallow_usecontext.mock'; import React from 'react'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx index 786357358dfa6..79be7ef1cb158 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx @@ -5,6 +5,7 @@ */ import React, { useContext } from 'react'; +import { useValues } from 'kea'; import { EuiButton, @@ -17,7 +18,9 @@ import { EuiButtonEmptyProps, EuiLinkProps, } from '@elastic/eui'; + import { sendTelemetry } from '../../../shared/telemetry'; +import { HttpLogic } from '../../../shared/http'; import { KibanaContext, IKibanaContext } from '../../../index'; interface IOnboardingCardProps { @@ -39,8 +42,8 @@ export const OnboardingCard: React.FC = ({ actionPath, complete, }) => { + const { http } = useValues(HttpLogic); const { - http, externalUrl: { getWorkplaceSearchUrl }, } = useContext(KibanaContext) as IKibanaContext; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx index 0baadfc912ad5..079d981533e01 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx @@ -23,6 +23,7 @@ import { } from '@elastic/eui'; import sharedSourcesIcon from '../../components/shared/assets/share_circle.svg'; import { sendTelemetry } from '../../../shared/telemetry'; +import { HttpLogic } from '../../../shared/http'; import { KibanaContext, IKibanaContext } from '../../../index'; import { ORG_SOURCES_PATH, USERS_PATH, ORG_SETTINGS_PATH } from '../../routes'; @@ -135,8 +136,8 @@ export const OnboardingSteps: React.FC = () => { }; export const OrgNameOnboarding: React.FC = () => { + const { http } = useValues(HttpLogic); const { - http, externalUrl: { getWorkplaceSearchUrl }, } = useContext(KibanaContext) as IKibanaContext; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx index 0813999c9a078..dd62e6de7c046 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx @@ -14,6 +14,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { ContentSection } from '../../components/shared/content_section'; import { sendTelemetry } from '../../../shared/telemetry'; +import { HttpLogic } from '../../../shared/http'; import { KibanaContext, IKibanaContext } from '../../../index'; import { SOURCE_DETAILS_PATH, getContentSourcePath } from '../../routes'; @@ -93,8 +94,8 @@ export const RecentActivityItem: React.FC = ({ timestamp, sourceId, }) => { + const { http } = useValues(HttpLogic); const { - http, externalUrl: { getWorkplaceSearchUrl }, } = useContext(KibanaContext) as IKibanaContext; diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index 0ef58a7c03f10..c23bb23be3979 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -103,9 +103,16 @@ export class EnterpriseSearchPlugin implements Plugin { await this.getInitialData(coreStart.http); - const { renderApp } = await import('./applications'); + const { renderApp, renderHeaderActions } = await import('./applications'); const { WorkplaceSearch } = await import('./applications/workplace_search'); + const { WorkplaceSearchHeaderActions } = await import( + './applications/workplace_search/components/layout' + ); + params.setHeaderActionMenu((element) => + renderHeaderActions(WorkplaceSearchHeaderActions, element, this.data.externalUrl) + ); + return renderApp(WorkplaceSearch, params, coreStart, plugins, this.config, this.data); }, }); diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts index 8e3ae2cfbeb86..2bddc9f1c80bd 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts @@ -70,7 +70,6 @@ describe('callEnterpriseSearchConfigAPI', () => { role_type: 'owner', ability: { access_all_engines: true, - destroy: ['session'], manage: ['account_credentials', 'account_engines'], // etc edit: ['LocoMoco::Account'], // etc view: ['Engine'], // etc @@ -145,13 +144,12 @@ describe('callEnterpriseSearchConfigAPI', () => { }, appSearch: { accountId: undefined, - onBoardingComplete: false, + onboardingComplete: false, role: { id: undefined, roleType: undefined, ability: { accessAllEngines: false, - destroy: [], manage: [], edit: [], view: [], diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts index 10a75e59cb249..c63e3ff8ffb2b 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts @@ -89,13 +89,12 @@ export const callEnterpriseSearchConfigAPI = async ({ }, appSearch: { accountId: data?.current_user?.app_search?.account?.id, - onBoardingComplete: !!data?.current_user?.app_search?.account?.onboarding_complete, + onboardingComplete: !!data?.current_user?.app_search?.account?.onboarding_complete, role: { id: data?.current_user?.app_search?.role?.id, roleType: data?.current_user?.app_search?.role?.role_type, ability: { accessAllEngines: !!data?.current_user?.app_search?.role?.ability?.access_all_engines, - destroy: data?.current_user?.app_search?.role?.ability?.destroy || [], manage: data?.current_user?.app_search?.role?.ability?.manage || [], edit: data?.current_user?.app_search?.role?.ability?.edit || [], view: data?.current_user?.app_search?.role?.ability?.view || [], diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts index acddd3539965a..bd6f4b9da91fd 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts @@ -35,7 +35,7 @@ describe('Enterprise Search Telemetry API', () => { }); }); - describe('PUT /api/enterprise_search/telemetry', () => { + describe('PUT /api/enterprise_search/stats', () => { it('increments the saved objects counter for App Search', async () => { (incrementUICounter as jest.Mock).mockImplementation(jest.fn(() => successResponse)); diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts index bfc07c8b64ef5..8f6638ddc099e 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts @@ -25,7 +25,7 @@ export function registerTelemetryRoute({ }: IRouteDependencies) { router.put( { - path: '/api/enterprise_search/telemetry', + path: '/api/enterprise_search/stats', validate: { body: schema.object({ product: schema.oneOf([ diff --git a/x-pack/plugins/file_upload/public/util/indexing_service.js b/x-pack/plugins/file_upload/public/util/indexing_service.js index eb22b0228b48a..28cdb602455b5 100644 --- a/x-pack/plugins/file_upload/public/util/indexing_service.js +++ b/x-pack/plugins/file_upload/public/util/indexing_service.js @@ -189,19 +189,16 @@ async function chunkDataAndWriteToIndex({ id, index, data, mappings, settings }) } export async function createIndexPattern(indexPatternName) { - const indexPatterns = await indexPatternService.get(); try { - Object.assign(indexPatterns, { - id: '', - title: indexPatternName, - }); - - await indexPatterns.create(true); - const id = await getIndexPatternId(indexPatternName); - const indexPattern = await indexPatternService.get(id); + const indexPattern = await indexPatternService.createAndSave( + { + title: indexPatternName, + }, + true + ); return { success: true, - id, + id: indexPattern.id, fields: indexPattern.fields, }; } catch (error) { @@ -212,18 +209,6 @@ export async function createIndexPattern(indexPatternName) { } } -async function getIndexPatternId(name) { - const savedObjectSearch = await savedObjectsClient.find({ type: 'index-pattern', perPage: 1000 }); - const indexPatternSavedObjects = savedObjectSearch.savedObjects; - - if (indexPatternSavedObjects) { - const ip = indexPatternSavedObjects.find((i) => i.attributes.title === name); - return ip !== undefined ? ip.id : undefined; - } else { - return undefined; - } -} - export const getExistingIndexNames = async () => { const indexes = await httpService({ url: `/api/index_management/indices`, diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx index e41f9243198ad..54066cee414d8 100644 --- a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx @@ -11,6 +11,8 @@ import { EuiSelectableTemplateSitewide, EuiSelectableTemplateSitewideOption, EuiText, + EuiIcon, + EuiHeaderSectionItemButton, EuiSelectableMessage, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -138,6 +140,17 @@ export function SearchBar({ globalSearch, navigateToUrl }: Props) { + + + } searchProps={{ onKeyUpCapture: (e: React.KeyboardEvent) => setSearchValue(e.currentTarget.value), diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/point_datatype.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/point_datatype.test.tsx new file mode 100644 index 0000000000000..0ee70d63ba667 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/point_datatype.test.tsx @@ -0,0 +1,158 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { act } from 'react-dom/test-utils'; + +import { componentHelpers, MappingsEditorTestBed } from '../helpers'; + +const { setup, getMappingsEditorDataFactory } = componentHelpers.mappingsEditor; + +// Parameters automatically added to the point datatype when saved (with the default values) +export const defaultPointParameters = { + type: 'point', + ignore_malformed: false, + ignore_z_value: true, +}; + +describe('Mappings editor: point datatype', () => { + /** + * Variable to store the mappings data forwarded to the consumer component + */ + let data: any; + let onChangeHandler: jest.Mock = jest.fn(); + let getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); + let testBed: MappingsEditorTestBed; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(() => { + onChangeHandler = jest.fn(); + getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); + }); + + test('initial view and default parameters values', async () => { + const defaultMappings = { + properties: { + myField: { + type: 'point', + }, + }, + }; + + const updatedMappings = { ...defaultMappings }; + + await act(async () => { + testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + testBed.component.update(); + + const { + component, + actions: { startEditField, updateFieldAndCloseFlyout }, + } = testBed; + + // Open the flyout to edit the field + await startEditField('myField'); + + // Save the field and close the flyout + await updateFieldAndCloseFlyout(); + + // It should have the default parameters values added + updatedMappings.properties.myField = defaultPointParameters; + + ({ data } = await getMappingsEditorData(component)); + expect(data).toEqual(updatedMappings); + }); + + describe('meta parameter', () => { + const defaultMappings = { + properties: { + myField: { + type: 'point', + }, + }, + }; + + const updatedMappings = { ...defaultMappings }; + + const metaParameter = { + meta: { + my_metadata: 'foobar', + }, + }; + + beforeEach(async () => { + await act(async () => { + testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + testBed.component.update(); + }); + + test('valid meta object', async () => { + const { + component, + actions: { + startEditField, + updateFieldAndCloseFlyout, + showAdvancedSettings, + toggleFormRow, + updateJsonEditor, + }, + } = testBed; + + // Open the flyout to edit the field + await startEditField('myField'); + await showAdvancedSettings(); + + // Enable the meta parameter and add value + toggleFormRow('metaParameter'); + await act(async () => { + updateJsonEditor('metaParameterEditor', metaParameter.meta); + }); + component.update(); + + // Save the field and close the flyout + await updateFieldAndCloseFlyout(); + + // It should have the default parameters values added, plus metadata + updatedMappings.properties.myField = { + ...defaultPointParameters, + ...metaParameter, + }; + + ({ data } = await getMappingsEditorData(component)); + expect(data).toEqual(updatedMappings); + }); + + test('strip empty string', async () => { + const { + component, + actions: { startEditField, updateFieldAndCloseFlyout, showAdvancedSettings, toggleFormRow }, + } = testBed; + + // Open the flyout to edit the field + await startEditField('myField'); + await showAdvancedSettings(); + + // Enable the meta parameter + toggleFormRow('metaParameter'); + + // Save the field and close the flyout without adding any values to meta parameter + await updateFieldAndCloseFlyout(); + + // It should have the default parameters values added + updatedMappings.properties.myField = defaultPointParameters; + + ({ data } = await getMappingsEditorData(component)); + expect(data).toEqual(updatedMappings); + }); + }); +}); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx index 2a4af89c46559..e123dea6ff2ff 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx @@ -239,6 +239,10 @@ const createActions = (testBed: TestBed) => { const getCheckboxValue = (testSubject: TestSubjects): boolean => find(testSubject).props().checked; + const toggleFormRow = (formRowName: string) => { + form.toggleEuiSwitch(`${formRowName}.formRowToggle`); + }; + return { selectTab, getFieldAt, @@ -252,6 +256,7 @@ const createActions = (testBed: TestBed) => { getComboBoxValue, getToggleValue, getCheckboxValue, + toggleFormRow, }; }; @@ -365,4 +370,6 @@ export type TestSubjects = | 'searchQuoteAnalyzer-custom' | 'searchQuoteAnalyzer-toggleCustomButton' | 'searchQuoteAnalyzer-custom.input' - | 'useSameAnalyzerForSearchCheckBox.input'; + | 'useSameAnalyzerForSearchCheckBox.input' + | 'metaParameterEditor' + | string; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/ignore_z_value_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/ignore_z_value_parameter.tsx index bd118ac08964f..ce58a264db968 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/ignore_z_value_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/ignore_z_value_parameter.tsx @@ -10,15 +10,18 @@ import { i18n } from '@kbn/i18n'; import { EditFieldFormRow } from '../fields/edit_field'; -export const IgnoreZValueParameter = () => ( +export const IgnoreZValueParameter = ({ description }: { description?: string }) => ( ); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/meta_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/meta_parameter.tsx index c8af296318b61..a950ba82d0eac 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/meta_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/meta_parameter.tsx @@ -32,6 +32,7 @@ export const MetaParameter: FunctionComponent = ({ defaultToggleValue }) }), href: documentationService.getMetaLink(), }} + data-test-subj="metaParameter" > = ({ defaultToggleValue }) component={JsonEditorField} componentProps={{ euiCodeEditorProps: { + ['data-test-subj']: 'metaParameterEditor', height: '300px', 'aria-label': i18n.translate('xpack.idxMgmt.mappingsEditor.metaParameterAriaLabel', { defaultMessage: 'metadata field data editor', diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts index 8fcd02e4a362e..6b092c5561b3b 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts @@ -32,6 +32,7 @@ import { HistogramType } from './histogram_type'; import { ConstantKeywordType } from './constant_keyword_type'; import { RankFeatureType } from './rank_feature_type'; import { WildcardType } from './wildcard_type'; +import { PointType } from './point_type'; const typeToParametersFormMap: { [key in DataType]?: ComponentType } = { alias: AliasType, @@ -60,6 +61,7 @@ const typeToParametersFormMap: { [key in DataType]?: ComponentType } = { constant_keyword: ConstantKeywordType, rank_feature: RankFeatureType, wildcard: WildcardType, + point: PointType, }; export const getParametersFormForType = ( diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/point_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/point_type.tsx new file mode 100644 index 0000000000000..9108c56e4496b --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/point_type.tsx @@ -0,0 +1,76 @@ +/* + * 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, { FunctionComponent } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { NormalizedField, Field as FieldType, ParameterName } from '../../../../types'; +import { UseField, TextAreaField } from '../../../../shared_imports'; +import { getFieldConfig } from '../../../../lib'; +import { + IgnoreMalformedParameter, + IgnoreZValueParameter, + NullValueParameter, + MetaParameter, +} from '../../field_parameters'; +import { AdvancedParametersSection, BasicParametersSection } from '../edit_field'; + +interface Props { + field: NormalizedField; +} + +const getDefaultToggleValue = (param: ParameterName, field: FieldType) => { + return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; +}; + +export const PointType: FunctionComponent = ({ field }) => { + return ( + <> + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx index a4d3bf3832d5c..293ae56d57ace 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx @@ -821,6 +821,26 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = {

), }, + point: { + label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.pointDescription', { + defaultMessage: 'Point', + }), + value: 'point', + documentation: { + main: '/point.html', + }, + description: () => ( +

+ {'x,y'}, + }} + /> +

+ ), + }, wildcard: { label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.wildcardDescription', { defaultMessage: 'Wildcard', @@ -882,6 +902,7 @@ export const MAIN_TYPES: MainType[] = [ 'token_count', 'histogram', 'wildcard', + 'point', 'other', ]; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx index fd17dc1b8fd1e..4ffedc8ca114d 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx @@ -382,6 +382,50 @@ export const PARAMETERS_DEFINITION: { [key in ParameterName]: ParameterDefinitio }, schema: t.any, }, + null_value_point: { + fieldConfig: { + defaultValue: '', + label: nullValueLabel, + helpText: () => ( + + {i18n.translate( + 'xpack.idxMgmt.mappingsEditor.parameters.pointWellKnownTextDocumentationLink', + { + defaultMessage: 'Well-Known Text', + } + )} + + ), + }} + /> + ), + validations: [ + { + validator: nullValueValidateEmptyField, + }, + ], + deserializer: (value: any) => { + if (value === '') { + return value; + } + return JSON.stringify(value); + }, + serializer: (value: string) => { + try { + return JSON.parse(value); + } catch (error) { + // swallow error and return non-parsed value; + return value; + } + }, + }, + schema: t.any, + }, copy_to: { fieldConfig: { defaultValue: '', @@ -476,12 +520,22 @@ export const PARAMETERS_DEFINITION: { [key in ParameterName]: ParameterDefinitio return JSON.stringify(value, null, 2); }, serializer: (value: string) => { - const parsed = JSON.parse(value); - // If an empty object was passed, strip out this value entirely. - if (!Object.keys(parsed).length) { + // Strip out empty strings + if (value.trim() === '') { return undefined; } - return parsed; + + try { + const parsed = JSON.parse(value); + // If an empty object was passed, strip out this value entirely. + if (!Object.keys(parsed).length) { + return undefined; + } + return parsed; + } catch (error) { + // swallow error and return non-parsed value; + return value; + } }, }, schema: t.any, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts index 97dca49fc93ed..ca38a8d1e6c33 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts @@ -59,6 +59,7 @@ export type MainType = | 'geo_point' | 'geo_shape' | 'token_count' + | 'point' | 'histogram' | 'constant_keyword' | 'wildcard' @@ -109,6 +110,7 @@ export type ParameterName = | 'null_value_boolean' | 'null_value_geo_point' | 'null_value_ip' + | 'null_value_point' | 'copy_to' | 'dynamic' | 'dynamic_toggle' diff --git a/x-pack/plugins/index_management/server/plugin.ts b/x-pack/plugins/index_management/server/plugin.ts index 30aeeb6b45362..ae9633f3e22b9 100644 --- a/x-pack/plugins/index_management/server/plugin.ts +++ b/x-pack/plugins/index_management/server/plugin.ts @@ -84,7 +84,9 @@ export class IndexMgmtServerPlugin implements Plugin { return LOOKUP[field] || field; }; -export const SNAPSHOT_METRIC_TRANSLATIONS = { - cpu: i18n.translate('xpack.infra.waffle.metricOptions.cpuUsageText', { - defaultMessage: 'CPU usage', - }), - - memory: i18n.translate('xpack.infra.waffle.metricOptions.memoryUsageText', { - defaultMessage: 'Memory usage', - }), - - rx: i18n.translate('xpack.infra.waffle.metricOptions.inboundTrafficText', { - defaultMessage: 'Inbound traffic', - }), - - tx: i18n.translate('xpack.infra.waffle.metricOptions.outboundTrafficText', { - defaultMessage: 'Outbound traffic', - }), - - logRate: i18n.translate('xpack.infra.waffle.metricOptions.hostLogRateText', { - defaultMessage: 'Log rate', - }), - - load: i18n.translate('xpack.infra.waffle.metricOptions.loadText', { - defaultMessage: 'Load', - }), - - count: i18n.translate('xpack.infra.waffle.metricOptions.countText', { - defaultMessage: 'Count', - }), - diskIOReadBytes: i18n.translate('xpack.infra.waffle.metricOptions.diskIOReadBytes', { - defaultMessage: 'Disk Reads', - }), - diskIOWriteBytes: i18n.translate('xpack.infra.waffle.metricOptions.diskIOWriteBytes', { - defaultMessage: 'Disk Writes', - }), - s3BucketSize: i18n.translate('xpack.infra.waffle.metricOptions.s3BucketSize', { - defaultMessage: 'Bucket Size', - }), - s3TotalRequests: i18n.translate('xpack.infra.waffle.metricOptions.s3TotalRequests', { - defaultMessage: 'Total Requests', - }), - s3NumberOfObjects: i18n.translate('xpack.infra.waffle.metricOptions.s3NumberOfObjects', { - defaultMessage: 'Number of Objects', - }), - s3DownloadBytes: i18n.translate('xpack.infra.waffle.metricOptions.s3DownloadBytes', { - defaultMessage: 'Downloads (Bytes)', - }), - s3UploadBytes: i18n.translate('xpack.infra.waffle.metricOptions.s3UploadBytes', { - defaultMessage: 'Uploads (Bytes)', - }), - rdsConnections: i18n.translate('xpack.infra.waffle.metricOptions.rdsConnections', { - defaultMessage: 'Connections', - }), - rdsQueriesExecuted: i18n.translate('xpack.infra.waffle.metricOptions.rdsQueriesExecuted', { - defaultMessage: 'Queries Executed', - }), - rdsActiveTransactions: i18n.translate('xpack.infra.waffle.metricOptions.rdsActiveTransactions', { - defaultMessage: 'Active Transactions', - }), - rdsLatency: i18n.translate('xpack.infra.waffle.metricOptions.rdsLatency', { - defaultMessage: 'Latency', - }), - sqsMessagesVisible: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesVisible', { - defaultMessage: 'Messages Available', - }), - sqsMessagesDelayed: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesDelayed', { - defaultMessage: 'Messages Delayed', - }), - sqsMessagesSent: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesSent', { - defaultMessage: 'Messages Added', - }), - sqsMessagesEmpty: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesEmpty', { - defaultMessage: 'Messages Returned Empty', - }), - sqsOldestMessage: i18n.translate('xpack.infra.waffle.metricOptions.sqsOldestMessage', { - defaultMessage: 'Oldest Message', - }), -} as Record; +const snapshotTypeKeys = Object.keys(SnapshotMetricTypeKeys) as SnapshotMetricType[]; +export const SNAPSHOT_METRIC_TRANSLATIONS = snapshotTypeKeys.reduce((result, metric) => { + const text = toMetricOpt(metric)?.text; + if (text) return { ...result, [metric]: text }; + return result; +}, {}) as Record; diff --git a/x-pack/plugins/infra/common/inventory_models/types.ts b/x-pack/plugins/infra/common/inventory_models/types.ts index 851646ef1fa12..7eb74056dcf28 100644 --- a/x-pack/plugins/infra/common/inventory_models/types.ts +++ b/x-pack/plugins/infra/common/inventory_models/types.ts @@ -314,7 +314,7 @@ export const ESAggregationRT = rt.union([ export const MetricsUIAggregationRT = rt.record(rt.string, ESAggregationRT); export type MetricsUIAggregation = rt.TypeOf; -export const SnapshotMetricTypeRT = rt.keyof({ +export const SnapshotMetricTypeKeys = { count: null, cpu: null, load: null, @@ -339,7 +339,8 @@ export const SnapshotMetricTypeRT = rt.keyof({ sqsMessagesEmpty: null, sqsOldestMessage: null, custom: null, -}); +}; +export const SnapshotMetricTypeRT = rt.keyof(SnapshotMetricTypeKeys); export type SnapshotMetricType = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/snapshot_metric_i18n.ts b/x-pack/plugins/infra/common/snapshot_metric_i18n.ts index 412c60fd9a1a7..60454e770584e 100644 --- a/x-pack/plugins/infra/common/snapshot_metric_i18n.ts +++ b/x-pack/plugins/infra/common/snapshot_metric_i18n.ts @@ -4,204 +4,235 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; +import { mapValues } from 'lodash'; import { SnapshotMetricType } from './inventory_models/types'; -const Translations = { +// Lowercase versions of all metrics, for when they need to be used in the middle of a sentence; +// these may need to be translated differently depending on language, e.g. still capitalizing "CPU" +const TranslationsLowercase = { CPUUsage: i18n.translate('xpack.infra.waffle.metricOptions.cpuUsageText', { defaultMessage: 'CPU usage', }), MemoryUsage: i18n.translate('xpack.infra.waffle.metricOptions.memoryUsageText', { - defaultMessage: 'Memory usage', + defaultMessage: 'memory usage', }), InboundTraffic: i18n.translate('xpack.infra.waffle.metricOptions.inboundTrafficText', { - defaultMessage: 'Inbound traffic', + defaultMessage: 'inbound traffic', }), OutboundTraffic: i18n.translate('xpack.infra.waffle.metricOptions.outboundTrafficText', { - defaultMessage: 'Outbound traffic', + defaultMessage: 'outbound traffic', }), LogRate: i18n.translate('xpack.infra.waffle.metricOptions.hostLogRateText', { - defaultMessage: 'Log rate', + defaultMessage: 'log rate', }), Load: i18n.translate('xpack.infra.waffle.metricOptions.loadText', { - defaultMessage: 'Load', + defaultMessage: 'load', }), Count: i18n.translate('xpack.infra.waffle.metricOptions.countText', { - defaultMessage: 'Count', + defaultMessage: 'count', }), DiskIOReadBytes: i18n.translate('xpack.infra.waffle.metricOptions.diskIOReadBytes', { - defaultMessage: 'Disk Reads', + defaultMessage: 'disk reads', }), DiskIOWriteBytes: i18n.translate('xpack.infra.waffle.metricOptions.diskIOWriteBytes', { - defaultMessage: 'Disk Writes', + defaultMessage: 'disk writes', }), s3BucketSize: i18n.translate('xpack.infra.waffle.metricOptions.s3BucketSize', { - defaultMessage: 'Bucket Size', + defaultMessage: 'bucket size', }), s3TotalRequests: i18n.translate('xpack.infra.waffle.metricOptions.s3TotalRequests', { - defaultMessage: 'Total Requests', + defaultMessage: 'total requests', }), s3NumberOfObjects: i18n.translate('xpack.infra.waffle.metricOptions.s3NumberOfObjects', { - defaultMessage: 'Number of Objects', + defaultMessage: 'number of objects', }), s3DownloadBytes: i18n.translate('xpack.infra.waffle.metricOptions.s3DownloadBytes', { - defaultMessage: 'Downloads (Bytes)', + defaultMessage: 'downloads (bytes)', }), s3UploadBytes: i18n.translate('xpack.infra.waffle.metricOptions.s3UploadBytes', { - defaultMessage: 'Uploads (Bytes)', + defaultMessage: 'uploads (bytes)', }), rdsConnections: i18n.translate('xpack.infra.waffle.metricOptions.rdsConnections', { - defaultMessage: 'Connections', + defaultMessage: 'connections', }), rdsQueriesExecuted: i18n.translate('xpack.infra.waffle.metricOptions.rdsQueriesExecuted', { - defaultMessage: 'Queries Executed', + defaultMessage: 'queries executed', }), rdsActiveTransactions: i18n.translate('xpack.infra.waffle.metricOptions.rdsActiveTransactions', { - defaultMessage: 'Active Transactions', + defaultMessage: 'active transactions', }), rdsLatency: i18n.translate('xpack.infra.waffle.metricOptions.rdsLatency', { - defaultMessage: 'Latency', + defaultMessage: 'latency', }), sqsMessagesVisible: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesVisible', { - defaultMessage: 'Messages Available', + defaultMessage: 'messages available', }), sqsMessagesDelayed: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesDelayed', { - defaultMessage: 'Messages Delayed', + defaultMessage: 'messages delayed', }), sqsMessagesSent: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesSent', { - defaultMessage: 'Messages Added', + defaultMessage: 'messages added', }), sqsMessagesEmpty: i18n.translate('xpack.infra.waffle.metricOptions.sqsMessagesEmpty', { - defaultMessage: 'Messages Returned Empty', + defaultMessage: 'messages returned empty', }), sqsOldestMessage: i18n.translate('xpack.infra.waffle.metricOptions.sqsOldestMessage', { - defaultMessage: 'Oldest Message', + defaultMessage: 'oldest message', }), }; +const Translations = mapValues( + TranslationsLowercase, + (translation) => `${translation[0].toUpperCase()}${translation.slice(1)}` +); + export const toMetricOpt = ( metric: SnapshotMetricType -): { text: string; value: SnapshotMetricType } | undefined => { +): { text: string; textLC: string; value: SnapshotMetricType } | undefined => { switch (metric) { case 'cpu': return { text: Translations.CPUUsage, + textLC: TranslationsLowercase.CPUUsage, value: 'cpu', }; case 'memory': return { text: Translations.MemoryUsage, + textLC: TranslationsLowercase.MemoryUsage, value: 'memory', }; case 'rx': return { text: Translations.InboundTraffic, + textLC: TranslationsLowercase.InboundTraffic, value: 'rx', }; case 'tx': return { text: Translations.OutboundTraffic, + textLC: TranslationsLowercase.OutboundTraffic, value: 'tx', }; case 'logRate': return { text: Translations.LogRate, + textLC: TranslationsLowercase.LogRate, value: 'logRate', }; case 'load': return { text: Translations.Load, + textLC: TranslationsLowercase.Load, value: 'load', }; case 'count': return { text: Translations.Count, + textLC: TranslationsLowercase.Count, value: 'count', }; case 'diskIOReadBytes': return { text: Translations.DiskIOReadBytes, + textLC: TranslationsLowercase.DiskIOReadBytes, value: 'diskIOReadBytes', }; case 'diskIOWriteBytes': return { text: Translations.DiskIOWriteBytes, + textLC: TranslationsLowercase.DiskIOWriteBytes, value: 'diskIOWriteBytes', }; case 's3BucketSize': return { text: Translations.s3BucketSize, + textLC: TranslationsLowercase.s3BucketSize, value: 's3BucketSize', }; case 's3TotalRequests': return { text: Translations.s3TotalRequests, + textLC: TranslationsLowercase.s3TotalRequests, value: 's3TotalRequests', }; case 's3NumberOfObjects': return { text: Translations.s3NumberOfObjects, + textLC: TranslationsLowercase.s3NumberOfObjects, value: 's3NumberOfObjects', }; case 's3DownloadBytes': return { text: Translations.s3DownloadBytes, + textLC: TranslationsLowercase.s3DownloadBytes, value: 's3DownloadBytes', }; case 's3UploadBytes': return { text: Translations.s3UploadBytes, + textLC: TranslationsLowercase.s3UploadBytes, value: 's3UploadBytes', }; case 'rdsConnections': return { text: Translations.rdsConnections, + textLC: TranslationsLowercase.rdsConnections, value: 'rdsConnections', }; case 'rdsQueriesExecuted': return { text: Translations.rdsQueriesExecuted, + textLC: TranslationsLowercase.rdsQueriesExecuted, value: 'rdsQueriesExecuted', }; case 'rdsActiveTransactions': return { text: Translations.rdsActiveTransactions, + textLC: TranslationsLowercase.rdsActiveTransactions, value: 'rdsActiveTransactions', }; case 'rdsLatency': return { text: Translations.rdsLatency, + textLC: TranslationsLowercase.rdsLatency, value: 'rdsLatency', }; case 'sqsMessagesVisible': return { text: Translations.sqsMessagesVisible, + textLC: TranslationsLowercase.sqsMessagesVisible, value: 'sqsMessagesVisible', }; case 'sqsMessagesDelayed': return { text: Translations.sqsMessagesDelayed, + textLC: TranslationsLowercase.sqsMessagesDelayed, value: 'sqsMessagesDelayed', }; case 'sqsMessagesSent': return { text: Translations.sqsMessagesSent, + textLC: TranslationsLowercase.sqsMessagesSent, value: 'sqsMessagesSent', }; case 'sqsMessagesEmpty': return { text: Translations.sqsMessagesEmpty, + textLC: TranslationsLowercase.sqsMessagesEmpty, value: 'sqsMessagesEmpty', }; case 'sqsOldestMessage': return { text: Translations.sqsOldestMessage, + textLC: TranslationsLowercase.sqsOldestMessage, value: 'sqsOldestMessage', }; } diff --git a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx index 83fe233553351..698b0d3ad0caf 100644 --- a/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/toolbar_control.tsx @@ -13,10 +13,9 @@ import { EuiDescriptionListTitle, EuiDescriptionListDescription, } from '@elastic/eui'; -import { EuiPopover } from '@elastic/eui'; +import { EuiPopover, EuiLink } from '@elastic/eui'; import { EuiListGroup, EuiListGroupItem } from '@elastic/eui'; import { EuiFlexItem } from '@elastic/eui'; -import { EuiButtonIcon } from '@elastic/eui'; import { SavedViewCreateModal } from './create_modal'; import { SavedViewUpdateModal } from './update_modal'; import { SavedViewManageViewsFlyout } from './manage_views_flyout'; @@ -151,15 +150,6 @@ export function SavedViewsToolbarControls(props: Props) { - - - (props: Props) { id="xpack.infra.savedView.currentView" /> - - {currentView - ? currentView.name - : i18n.translate('xpack.infra.savedView.unknownView', { - defaultMessage: 'No view selected', - })} - + + + {currentView + ? currentView.name + : i18n.translate('xpack.infra.savedView.unknownView', { + defaultMessage: 'No view selected', + })} + + diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.tsx new file mode 100644 index 0000000000000..9cb84c7fff438 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/bottom_drawer.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, { useCallback, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; + +import { euiStyled, useUiTracker } from '../../../../../../observability/public'; +import { InfraFormatter } from '../../../../lib/lib'; +import { Timeline } from './timeline/timeline'; + +const showHistory = i18n.translate('xpack.infra.showHistory', { + defaultMessage: 'Show history', +}); +const hideHistory = i18n.translate('xpack.infra.hideHistory', { + defaultMessage: 'Hide history', +}); + +const TRANSITION_MS = 300; + +export const BottomDrawer: React.FC<{ + measureRef: (instance: HTMLElement | null) => void; + interval: string; + formatter: InfraFormatter; +}> = ({ measureRef, interval, formatter, children }) => { + const [isOpen, setIsOpen] = useState(false); + + const trackDrawerOpen = useUiTracker({ app: 'infra_metrics' }); + const onClick = useCallback(() => { + if (!isOpen) trackDrawerOpen({ metric: 'open_timeline_drawer__inventory' }); + setIsOpen(!isOpen); + }, [isOpen, trackDrawerOpen]); + + return ( + + + + + {isOpen ? hideHistory : showHistory} + + + + {children} + + + + + + + + + + ); +}; + +const BottomActionContainer = euiStyled.div<{ isOpen: boolean }>` + padding: ${(props) => props.theme.eui.paddingSizes.m} 0; + position: fixed; + left: 0; + bottom: 0; + right: 0; + transition: transform ${TRANSITION_MS}ms; + transform: translateY(${(props) => (props.isOpen ? 0 : '224px')}) +`; + +const BottomActionTopBar = euiStyled(EuiFlexGroup).attrs({ + justifyContent: 'spaceBetween', + alignItems: 'center', +})` + margin-bottom: 0; + height: 48px; +`; + +const ShowHideButton = euiStyled(EuiButtonEmpty).attrs({ size: 's' })` + width: 140px; +`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx index 47616c7f4f7fd..712578be7dffd 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useEffect } from 'react'; import { useInterval } from 'react-use'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { AutoSizer } from '../../../../components/auto_sizer'; import { convertIntervalToString } from '../../../../utils/convert_interval_to_string'; import { NodesOverview } from './nodes_overview'; @@ -23,12 +23,13 @@ import { euiStyled } from '../../../../../../observability/public'; import { Toolbar } from './toolbars/toolbar'; import { ViewSwitcher } from './waffle/view_switcher'; import { IntervalLabel } from './waffle/interval_label'; -import { Legend } from './waffle/legend'; import { createInventoryMetricFormatter } from '../lib/create_inventory_metric_formatter'; import { createLegend } from '../lib/create_legend'; import { useSavedViewContext } from '../../../../containers/saved_view/saved_view'; import { useWaffleViewState } from '../hooks/use_waffle_view_state'; import { SavedViewsToolbarControls } from '../../../../components/saved_views/toolbar_control'; +import { BottomDrawer } from './bottom_drawer'; +import { Legend } from './waffle/legend'; export const Layout = () => { const { sourceId, source } = useSourceContext(); @@ -104,12 +105,19 @@ export const Layout = () => { - + + + + + + + + {({ measureRef, bounds: { height = 0 } }) => ( @@ -128,24 +136,14 @@ export const Layout = () => { formatter={formatter} bottomMargin={height} /> - - - - - - - - - - - - - + + + )} @@ -164,12 +162,8 @@ const TopActionContainer = euiStyled.div` padding: ${(props) => `12px ${props.theme.eui.paddingSizes.m}`}; `; -const BottomActionContainer = euiStyled.div` - background-color: ${(props) => props.theme.eui.euiPageBackgroundColor}; - padding: ${(props) => props.theme.eui.paddingSizes.m} ${(props) => - props.theme.eui.paddingSizes.m}; - position: fixed; - left: 0; - bottom: 0; - right: 0; +const SavedViewContainer = euiStyled.div` + position: relative; + z-index: 1; + padding-left: ${(props) => props.theme.eui.paddingSizes.m}; `; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx new file mode 100644 index 0000000000000..2792b6eb18b00 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx @@ -0,0 +1,228 @@ +/* + * 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, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import moment from 'moment'; +import { first, last } from 'lodash'; +import { EuiLoadingChart, EuiText, EuiEmptyPrompt, EuiButton } from '@elastic/eui'; +import { + Axis, + Chart, + Settings, + Position, + TooltipValue, + niceTimeFormatter, + ElementClickListener, +} from '@elastic/charts'; +import { useUiSetting } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { toMetricOpt } from '../../../../../../common/snapshot_metric_i18n'; +import { MetricsExplorerAggregation } from '../../../../../../common/http_api'; +import { Color } from '../../../../../../common/color_palette'; +import { useSourceContext } from '../../../../../containers/source'; +import { useTimeline } from '../../hooks/use_timeline'; +import { useWaffleOptionsContext } from '../../hooks/use_waffle_options'; +import { useWaffleTimeContext } from '../../hooks/use_waffle_time'; +import { useWaffleFiltersContext } from '../../hooks/use_waffle_filters'; +import { MetricExplorerSeriesChart } from '../../../metrics_explorer/components/series_chart'; +import { MetricsExplorerChartType } from '../../../metrics_explorer/hooks/use_metrics_explorer_options'; +import { getTimelineChartTheme } from '../../../metrics_explorer/components/helpers/get_chart_theme'; +import { calculateDomain } from '../../../metrics_explorer/components/helpers/calculate_domain'; + +import { euiStyled } from '../../../../../../../observability/public'; +import { InfraFormatter } from '../../../../../lib/lib'; + +interface Props { + interval: string; + yAxisFormatter: InfraFormatter; + isVisible: boolean; +} + +export const Timeline: React.FC = ({ interval, yAxisFormatter, isVisible }) => { + const { sourceId } = useSourceContext(); + const { metric, nodeType, accountId, region } = useWaffleOptionsContext(); + const { currentTime, jumpToTime, stopAutoReload } = useWaffleTimeContext(); + const { filterQueryAsJson } = useWaffleFiltersContext(); + const { loading, error, timeseries, reload } = useTimeline( + filterQueryAsJson, + [metric], + nodeType, + sourceId, + currentTime, + accountId, + region, + interval, + isVisible + ); + + const metricLabel = toMetricOpt(metric.type)?.textLC; + + const chartMetric = { + color: Color.color0, + aggregation: 'avg' as MetricsExplorerAggregation, + label: metricLabel, + }; + + const dateFormatter = useMemo(() => { + if (!timeseries) return () => ''; + const firstTimestamp = first(timeseries.rows)?.timestamp; + const lastTimestamp = last(timeseries.rows)?.timestamp; + + if (firstTimestamp == null || lastTimestamp == null) { + return (value: number) => `${value}`; + } + + return niceTimeFormatter([firstTimestamp, lastTimestamp]); + }, [timeseries]); + + const isDarkMode = useUiSetting('theme:darkMode'); + const tooltipProps = { + headerFormatter: (tooltipValue: TooltipValue) => + moment(tooltipValue.value).format('Y-MM-DD HH:mm:ss.SSS'), + }; + + const dataDomain = timeseries ? calculateDomain(timeseries, [chartMetric], false) : null; + const domain = dataDomain + ? { + max: dataDomain.max * 1.1, // add 10% headroom. + min: dataDomain.min, + } + : { max: 0, min: 0 }; + + const onClickPoint: ElementClickListener = useCallback( + ([[geometryValue]]) => { + if (!Array.isArray(geometryValue)) { + const { x: timestamp } = geometryValue; + jumpToTime(timestamp); + stopAutoReload(); + } + }, + [jumpToTime, stopAutoReload] + ); + + if (loading) { + return ( + + + + + + ); + } + + if (!loading && (error || !timeseries)) { + return ( + + {error ? errorTitle : noHistoryDataTitle}} + actions={ + + {error ? retryButtonLabel : checkNewDataButtonLabel} + + } + /> + + ); + } + + return ( + + + + + + + + + + + + + + + + + + ); +}; + +const TimelineContainer = euiStyled.div` + background-color: ${(props) => props.theme.eui.euiPageBackgroundColor}; + border-top: 1px solid ${(props) => props.theme.eui.euiColorLightShade}; + height: 220px; + width: 100%; + padding: ${(props) => props.theme.eui.paddingSizes.s} ${(props) => + props.theme.eui.paddingSizes.m}; + display: flex; + flex-direction: column; +`; + +const TimelineHeader = euiStyled.div` + display: flex; + width: 100%; + padding: ${(props) => props.theme.eui.paddingSizes.s} ${(props) => + props.theme.eui.paddingSizes.m}; +`; + +const TimelineChartContainer = euiStyled.div` + padding-left: ${(props) => props.theme.eui.paddingSizes.xs}; + width: 100%; + height: 100%; +`; + +const TimelineLoadingContainer = euiStyled.div` + display: flex; + justify-content: center; + align-items: center; + height: 100%; +`; + +const noHistoryDataTitle = i18n.translate('xpack.infra.inventoryTimeline.noHistoryDataTitle', { + defaultMessage: 'There is no history data to display.', +}); + +const errorTitle = i18n.translate('xpack.infra.inventoryTimeline.errorTitle', { + defaultMessage: 'Unable to display history data.', +}); + +const checkNewDataButtonLabel = i18n.translate( + 'xpack.infra.inventoryTimeline.checkNewDataButtonLabel', + { + defaultMessage: 'Check for new data', + } +); + +const retryButtonLabel = i18n.translate('xpack.infra.inventoryTimeline.retryButtonLabel', { + defaultMessage: 'Try again', +}); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx index dbbfb0f49c0e9..6e031c8396f07 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx @@ -22,7 +22,7 @@ export const IntervalLabel = ({ intervalAsString }: Props) => {

diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.tsx index a785cb31c3cf4..262d94d8f3674 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/custom_metric_form.tsx @@ -27,10 +27,7 @@ import { SNAPSHOT_CUSTOM_AGGREGATIONS, SnapshotCustomAggregationRT, } from '../../../../../../../common/http_api/snapshot_api'; -import { - EuiTheme, - withTheme, -} from '../../../../../../../../../legacy/common/eui_styled_components'; +import { EuiTheme, withTheme } from '../../../../../../../../xpack_legacy/common'; interface SelectedOption { label: string; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_edit_mode.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_edit_mode.tsx index e75885ccbc917..831a0cde49cfb 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_edit_mode.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/metrics_edit_mode.tsx @@ -8,10 +8,7 @@ import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { getCustomMetricLabel } from '../../../../../../../common/formatters/get_custom_metric_label'; import { SnapshotCustomMetricInput } from '../../../../../../../common/http_api/snapshot_api'; -import { - EuiTheme, - withTheme, -} from '../../../../../../../../../legacy/common/eui_styled_components'; +import { EuiTheme, withTheme } from '../../../../../../../../xpack_legacy/common'; interface Props { theme: EuiTheme | undefined; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/mode_switcher.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/mode_switcher.tsx index d1abcade5d660..956241545e8be 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/mode_switcher.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/mode_switcher.tsx @@ -9,10 +9,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { CustomMetricMode } from './types'; import { SnapshotCustomMetricInput } from '../../../../../../../common/http_api/snapshot_api'; -import { - EuiTheme, - withTheme, -} from '../../../../../../../../../legacy/common/eui_styled_components'; +import { EuiTheme, withTheme } from '../../../../../../../../xpack_legacy/common'; interface Props { theme: EuiTheme | undefined; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_snaphot.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_snaphot.ts index 06b53d531f53c..702213516c123 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_snaphot.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_snaphot.ts @@ -43,7 +43,7 @@ export function useSnapshot( const timerange: InfraTimerangeInput = { interval: '1m', to: currentTime, - from: currentTime - 360 * 1000, + from: currentTime - 1200 * 1000, lookbackSize: 20, }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_timeline.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_timeline.ts new file mode 100644 index 0000000000000..650eda0362d9e --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_timeline.ts @@ -0,0 +1,121 @@ +/* + * 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 { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { first } from 'lodash'; +import { useEffect, useMemo, useCallback } from 'react'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { getIntervalInSeconds } from '../../../../../server/utils/get_interval_in_seconds'; +import { throwErrors, createPlainError } from '../../../../../common/runtime_types'; +import { useHTTPRequest } from '../../../../hooks/use_http_request'; +import { + SnapshotNodeResponseRT, + SnapshotNodeResponse, + SnapshotRequest, + InfraTimerangeInput, +} from '../../../../../common/http_api/snapshot_api'; +import { + InventoryItemType, + SnapshotMetricType, +} from '../../../../../common/inventory_models/types'; + +const ONE_MINUTE = 60; +const ONE_HOUR = ONE_MINUTE * 60; +const ONE_DAY = ONE_HOUR * 24; +const ONE_WEEK = ONE_DAY * 7; + +const getTimeLengthFromInterval = (interval: string | undefined) => { + if (interval) { + const intervalInSeconds = getIntervalInSeconds(interval); + const multiplier = + intervalInSeconds < ONE_MINUTE + ? ONE_HOUR / intervalInSeconds + : intervalInSeconds < ONE_HOUR + ? 60 + : intervalInSeconds < ONE_DAY + ? 7 + : intervalInSeconds < ONE_WEEK + ? 30 + : 1; + const timeLength = intervalInSeconds * multiplier; + return { timeLength, intervalInSeconds }; + } else { + return { timeLength: 0, intervalInSeconds: 0 }; + } +}; + +export function useTimeline( + filterQuery: string | null | undefined, + metrics: Array<{ type: SnapshotMetricType }>, + nodeType: InventoryItemType, + sourceId: string, + currentTime: number, + accountId: string, + region: string, + interval: string | undefined, + shouldReload: boolean +) { + const decodeResponse = (response: any) => { + return pipe( + SnapshotNodeResponseRT.decode(response), + fold(throwErrors(createPlainError), identity) + ); + }; + + const timeLengthResult = useMemo(() => getTimeLengthFromInterval(interval), [interval]); + const { timeLength, intervalInSeconds } = timeLengthResult; + + const timerange: InfraTimerangeInput = { + interval: interval ?? '', + to: currentTime + intervalInSeconds * 1000, + from: currentTime - timeLength * 1000, + lookbackSize: 0, + ignoreLookback: true, + }; + + const { error, loading, response, makeRequest } = useHTTPRequest( + '/api/metrics/snapshot', + 'POST', + JSON.stringify({ + metrics, + groupBy: null, + nodeType, + timerange, + filterQuery, + sourceId, + accountId, + region, + includeTimeseries: true, + } as SnapshotRequest), + decodeResponse + ); + + const loadData = useCallback(() => { + if (shouldReload) return makeRequest(); + return Promise.resolve(); + }, [makeRequest, shouldReload]); + + useEffect(() => { + (async () => { + if (timeLength) { + await loadData(); + } + })(); + }, [loadData, timeLength]); + + const timeseries = response + ? first(response.nodes.map((node) => first(node.metrics)?.timeseries)) + : null; + + return { + error: (error && error.message) || null, + loading: !interval ? true : loading, + timeseries, + reload: makeRequest, + }; +} diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/get_chart_theme.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/get_chart_theme.ts index 42469ffb5ee9a..bb6a70f65bb97 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/get_chart_theme.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/get_chart_theme.ts @@ -4,8 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Theme, LIGHT_THEME, DARK_THEME } from '@elastic/charts'; +import { + Theme, + PartialTheme, + LIGHT_THEME, + DARK_THEME, + mergeWithDefaultTheme, +} from '@elastic/charts'; export function getChartTheme(isDarkMode: boolean): Theme { return isDarkMode ? DARK_THEME : LIGHT_THEME; } + +export function getTimelineChartTheme(isDarkMode: boolean): Theme { + return isDarkMode ? DARK_THEME : mergeWithDefaultTheme(TIMELINE_LIGHT_THEME, LIGHT_THEME); +} + +const TIMELINE_LIGHT_THEME: PartialTheme = { + crosshair: { + band: { + fill: '#D3DAE6', + }, + }, + axes: { + gridLine: { + horizontal: { + stroke: '#eaeaea', + }, + }, + }, +}; diff --git a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts index f6d8a4a807e95..0ee123ed2946f 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts @@ -262,18 +262,18 @@ async function fetchLogEntryAnomalies( bucket_span: duration, timestamp: anomalyStartTime, by_field_value: categoryId, - } = result._source; + } = result.fields; return { id: result._id, - anomalyScore, - dataset, + anomalyScore: anomalyScore[0], + dataset: dataset[0], typical: typical[0], actual: actual[0], - jobId: job_id, - startTime: anomalyStartTime, - duration: duration * 1000, - categoryId, + jobId: job_id[0], + startTime: parseInt(anomalyStartTime[0], 10), + duration: duration[0] * 1000, + categoryId: categoryId?.[0], }; }); @@ -417,8 +417,8 @@ export async function fetchLogEntryExamples( return { examples: hits.map((hit) => ({ id: hit._id, - dataset: hit._source.event?.dataset ?? '', - message: hit._source.message ?? '', + dataset: hit.fields['event.dataset']?.[0] ?? '', + message: hit.fields.message?.[0] ?? '', timestamp: hit.sort[0], tiebreaker: hit.sort[1], })), diff --git a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts index ff9e3c7d2167c..1205c5ae9f61b 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts @@ -23,6 +23,7 @@ import { } from './queries/log_entry_categories'; import { createLogEntryCategoryExamplesQuery, + LogEntryCategoryExampleHit, logEntryCategoryExamplesResponseRT, } from './queries/log_entry_category_examples'; import { @@ -423,11 +424,11 @@ async function fetchLogEntryCategoryExamples( return { examples: hits.map((hit) => ({ id: hit._id, - dataset: hit._source.event?.dataset ?? '', - message: hit._source.message ?? '', + dataset: hit.fields['event.dataset']?.[0] ?? '', + message: hit.fields.message?.[0] ?? '', timestamp: hit.sort[0], tiebreaker: hit.sort[1], - context: getContextFromSource(hit._source), + context: getContextFromFields(hit.fields), })), timing: { spans: [esSearchSpan], @@ -437,10 +438,10 @@ async function fetchLogEntryCategoryExamples( const parseCategoryId = (rawCategoryId: string) => parseInt(rawCategoryId, 10); -const getContextFromSource = (source: any): LogEntryContext => { - const containerId = source.container?.id; - const hostName = source.host?.name; - const logFilePath = source.log?.file?.path; +const getContextFromFields = (fields: LogEntryCategoryExampleHit['fields']): LogEntryContext => { + const containerId = fields['container.id']?.[0]; + const hostName = fields['host.name']?.[0]; + const logFilePath = fields['log.file.path']?.[0]; if (typeof containerId === 'string') { return { 'container.id': containerId }; diff --git a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_anomalies.ts b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_anomalies.ts index c722544c509aa..e692ed019cf86 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_anomalies.ts @@ -46,13 +46,16 @@ export const createLogEntryAnomaliesQuery = ( ...createDatasetsFilters(datasets), ]; - const sourceFields = [ + const fields = [ 'job_id', 'record_score', 'typical', 'actual', 'partition_field_value', - 'timestamp', + { + field: 'timestamp', + format: 'epoch_millis', + }, 'bucket_span', 'by_field_value', ]; @@ -75,7 +78,8 @@ export const createLogEntryAnomaliesQuery = ( search_after: queryCursor, sort: sortOptions, size: pageSize, - _source: sourceFields, + _source: false, + fields, }, }; @@ -84,18 +88,18 @@ export const createLogEntryAnomaliesQuery = ( export const logEntryAnomalyHitRT = rt.type({ _id: rt.string, - _source: rt.intersection([ + fields: rt.intersection([ rt.type({ - job_id: rt.string, - record_score: rt.number, + job_id: rt.array(rt.string), + record_score: rt.array(rt.number), typical: rt.array(rt.number), actual: rt.array(rt.number), - partition_field_value: rt.string, - bucket_span: rt.number, - timestamp: rt.number, + partition_field_value: rt.array(rt.string), + bucket_span: rt.array(rt.number), + timestamp: rt.array(rt.string), }), rt.partial({ - by_field_value: rt.string, + by_field_value: rt.array(rt.string), }), ]), sort: rt.tuple([rt.union([rt.string, rt.number]), rt.union([rt.string, rt.number])]), diff --git a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_category_examples.ts b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_category_examples.ts index 6e2afa874b757..c99353d01a3e8 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_category_examples.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_category_examples.ts @@ -43,30 +43,21 @@ export const createLogEntryCategoryExamplesQuery = ( }, }, sort: [{ [timestampField]: 'asc' }, { [tiebreakerField]: 'asc' }], + _source: false, + fields: ['event.dataset', 'message', 'container.id', 'host.name', 'log.file.path'], }, - _source: ['event.dataset', 'message', 'container.id', 'host.name', 'log.file.path'], index: indices, size: exampleCount, }); export const logEntryCategoryExampleHitRT = rt.type({ _id: rt.string, - _source: rt.partial({ - event: rt.partial({ - dataset: rt.string, - }), - message: rt.string, - container: rt.partial({ - id: rt.string, - }), - host: rt.partial({ - name: rt.string, - }), - log: rt.partial({ - file: rt.partial({ - path: rt.string, - }), - }), + fields: rt.partial({ + 'event.dataset': rt.array(rt.string), + message: rt.array(rt.string), + 'container.id': rt.array(rt.string), + 'host.name': rt.array(rt.string), + 'log.file.path': rt.array(rt.string), }), sort: rt.tuple([rt.number, rt.number]), }); diff --git a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_examples.ts b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_examples.ts index 74a664e78dcd6..1b6a4c611e177 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_examples.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/queries/log_entry_examples.ts @@ -33,7 +33,7 @@ export const createLogEntryExamplesQuery = ( }, }, }, - ...(!!dataset + ...(dataset !== '' ? [ { term: { @@ -41,7 +41,19 @@ export const createLogEntryExamplesQuery = ( }, }, ] - : []), + : [ + { + bool: { + must_not: [ + { + exists: { + field: partitionField, + }, + }, + ], + }, + }, + ]), ...(categoryQuery ? [ { @@ -58,19 +70,18 @@ export const createLogEntryExamplesQuery = ( }, }, sort: [{ [timestampField]: 'asc' }, { [tiebreakerField]: 'asc' }], + _source: false, + fields: ['event.dataset', 'message'], }, - _source: ['event.dataset', 'message'], index: indices, size: exampleCount, }); export const logEntryExampleHitRT = rt.type({ _id: rt.string, - _source: rt.partial({ - event: rt.partial({ - dataset: rt.string, - }), - message: rt.string, + fields: rt.partial({ + 'event.dataset': rt.array(rt.string), + message: rt.array(rt.string), }), sort: rt.tuple([rt.number, rt.number]), }); diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts index 700f4ef39bb66..814ec5e74ff33 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts @@ -56,8 +56,10 @@ export const transformRequestToMetricsAPIRequest = async ( snapshotRequest.nodeType, source.configuration.fields ); - const groupBy = snapshotRequest.groupBy.map((g) => g.field).filter(Boolean) as string[]; - metricsApiRequest.groupBy = [...groupBy, inventoryFields.id]; + if (snapshotRequest.groupBy) { + const groupBy = snapshotRequest.groupBy.map((g) => g.field).filter(Boolean) as string[]; + metricsApiRequest.groupBy = [...groupBy, inventoryFields.id]; + } const metaAggregation = { id: META_KEY, diff --git a/x-pack/plugins/ingest_manager/common/constants/routes.ts b/x-pack/plugins/ingest_manager/common/constants/routes.ts index 3e065142ea101..d899739a74ef0 100644 --- a/x-pack/plugins/ingest_manager/common/constants/routes.ts +++ b/x-pack/plugins/ingest_manager/common/constants/routes.ts @@ -15,9 +15,11 @@ export const LIMITED_CONCURRENCY_ROUTE_TAG = 'ingest:limited-concurrency'; // EPM API routes const EPM_PACKAGES_MANY = `${EPM_API_ROOT}/packages`; +const EPM_PACKAGES_BULK = `${EPM_PACKAGES_MANY}/_bulk`; const EPM_PACKAGES_ONE = `${EPM_PACKAGES_MANY}/{pkgkey}`; const EPM_PACKAGES_FILE = `${EPM_PACKAGES_MANY}/{pkgName}/{pkgVersion}`; export const EPM_API_ROUTES = { + BULK_INSTALL_PATTERN: EPM_PACKAGES_BULK, LIST_PATTERN: EPM_PACKAGES_MANY, LIMITED_LIST_PATTERN: `${EPM_PACKAGES_MANY}/limited`, INFO_PATTERN: EPM_PACKAGES_ONE, @@ -84,7 +86,9 @@ export const AGENT_API_ROUTES = { ACTIONS_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/actions`, ENROLL_PATTERN: `${FLEET_API_ROOT}/agents/enroll`, UNENROLL_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/unenroll`, + BULK_UNENROLL_PATTERN: `${FLEET_API_ROOT}/agents/bulk_unenroll`, REASSIGN_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/reassign`, + BULK_REASSIGN_PATTERN: `${FLEET_API_ROOT}/agents/bulk_reassign`, STATUS_PATTERN: `${FLEET_API_ROOT}/agent-status`, }; diff --git a/x-pack/plugins/ingest_manager/common/services/index.ts b/x-pack/plugins/ingest_manager/common/services/index.ts index 46a1c65872d1b..4bffa01ad5ee2 100644 --- a/x-pack/plugins/ingest_manager/common/services/index.ts +++ b/x-pack/plugins/ingest_manager/common/services/index.ts @@ -12,3 +12,4 @@ export { isPackageLimited, doesAgentPolicyAlreadyIncludePackage } from './limite export { decodeCloudId } from './decode_cloud_id'; export { isValidNamespace } from './is_valid_namespace'; export { isDiffPathProtocol } from './is_diff_path_protocol'; +export { LicenseService } from './license'; diff --git a/x-pack/plugins/ingest_manager/common/services/license.ts b/x-pack/plugins/ingest_manager/common/services/license.ts new file mode 100644 index 0000000000000..6d9b20a8456c0 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/services/license.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Observable, Subscription } from 'rxjs'; +import { ILicense } from '../../../licensing/common/types'; + +// Generic license service class that works with the license observable +// Both server and client plugins instancates a singleton version of this class +export class LicenseService { + private observable: Observable | null = null; + private subscription: Subscription | null = null; + private licenseInformation: ILicense | null = null; + + private updateInformation(licenseInformation: ILicense) { + this.licenseInformation = licenseInformation; + } + + public start(license$: Observable) { + this.observable = license$; + this.subscription = this.observable.subscribe(this.updateInformation.bind(this)); + } + + public stop() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + public getLicenseInformation() { + return this.licenseInformation; + } + + public getLicenseInformation$() { + return this.observable; + } + + public isGoldPlus() { + return ( + this.licenseInformation?.isAvailable && + this.licenseInformation?.isActive && + this.licenseInformation?.hasAtLeast('gold') + ); + } +} diff --git a/x-pack/plugins/ingest_manager/common/services/routes.ts b/x-pack/plugins/ingest_manager/common/services/routes.ts index b7521f95b4f83..3c3534926908a 100644 --- a/x-pack/plugins/ingest_manager/common/services/routes.ts +++ b/x-pack/plugins/ingest_manager/common/services/routes.ts @@ -46,6 +46,10 @@ export const epmRouteService = { ); // trim trailing slash }, + getBulkInstallPath: () => { + return EPM_API_ROUTES.BULK_INSTALL_PATTERN; + }, + getRemovePath: (pkgkey: string) => { return EPM_API_ROUTES.DELETE_PATTERN.replace('{pkgkey}', pkgkey).replace(/\/$/, ''); // trim trailing slash }, @@ -127,8 +131,10 @@ export const agentRouteService = { getEventsPath: (agentId: string) => AGENT_API_ROUTES.EVENTS_PATTERN.replace('{agentId}', agentId), getUnenrollPath: (agentId: string) => AGENT_API_ROUTES.UNENROLL_PATTERN.replace('{agentId}', agentId), + getBulkUnenrollPath: () => AGENT_API_ROUTES.BULK_UNENROLL_PATTERN, getReassignPath: (agentId: string) => AGENT_API_ROUTES.REASSIGN_PATTERN.replace('{agentId}', agentId), + getBulkReassignPath: () => AGENT_API_ROUTES.BULK_REASSIGN_PATTERN, getListPath: () => AGENT_API_ROUTES.LIST_PATTERN, getStatusPath: () => AGENT_API_ROUTES.STATUS_PATTERN, }; diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts index 54cdeade3764e..1a10d4930656f 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts @@ -26,6 +26,7 @@ export interface GetAgentsRequest { export interface GetAgentsResponse { list: Agent[]; total: number; + totalInactive: number; page: number; perPage: number; } @@ -104,11 +105,24 @@ export interface PostAgentUnenrollRequest { params: { agentId: string; }; + body: { + force?: boolean; + }; } // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PostAgentUnenrollResponse {} +export interface PostBulkAgentUnenrollRequest { + body: { + agents: string[] | string; + force?: boolean; + }; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PostBulkAgentUnenrollResponse {} + export interface PutAgentReassignRequest { params: { agentId: string; @@ -119,6 +133,20 @@ export interface PutAgentReassignRequest { // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PutAgentReassignResponse {} +export interface PostBulkAgentReassignRequest { + body: { + policy_id: string; + agents: string[] | string; + }; +} + +export interface PostBulkAgentReassignResponse { + [key: string]: { + success: boolean; + error?: Error; + }; +} + export interface GetOneAgentEventsRequest { params: { agentId: string; diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts index 54e767fee4b22..7ed2fed91aa93 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts @@ -71,6 +71,30 @@ export interface InstallPackageResponse { response: AssetReference[]; } +export interface IBulkInstallPackageError { + name: string; + statusCode: number; + error: string | Error; +} + +export interface BulkInstallPackageInfo { + name: string; + newVersion: string; + // this will be null if no package was present before the upgrade (aka it was an install) + oldVersion: string | null; + assets: AssetReference[]; +} + +export interface BulkInstallPackagesResponse { + response: Array; +} + +export interface BulkInstallPackagesRequest { + body: { + packages: string[]; + }; +} + export interface MessageResponse { response: string; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts index 36b7d412bf276..64434e163f043 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/index.ts @@ -8,6 +8,7 @@ export { useCapabilities } from './use_capabilities'; export { useCore } from './use_core'; export { useConfig, ConfigContext } from './use_config'; export { useSetupDeps, useStartDeps, DepsContext } from './use_deps'; +export { licenseService, useLicense } from './use_license'; export { useBreadcrumbs } from './use_breadcrumbs'; export { useLink } from './use_link'; export { useKibanaLink } from './use_kibana_link'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_license.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_license.ts new file mode 100644 index 0000000000000..411a6d6f2168f --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_license.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. + */ +import { LicenseService } from '../services'; + +export const licenseService = new LicenseService(); + +export function useLicense() { + return licenseService; +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agents.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agents.ts index cad1791af41be..41967fd068e0b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agents.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agents.ts @@ -10,8 +10,14 @@ import { GetOneAgentResponse, GetOneAgentEventsResponse, GetOneAgentEventsRequest, + PostAgentUnenrollRequest, + PostBulkAgentUnenrollRequest, + PostBulkAgentUnenrollResponse, + PostAgentUnenrollResponse, PutAgentReassignRequest, PutAgentReassignResponse, + PostBulkAgentReassignRequest, + PostBulkAgentReassignResponse, GetAgentsRequest, GetAgentsResponse, GetAgentStatusRequest, @@ -83,3 +89,40 @@ export function sendPutAgentReassign( ...options, }); } + +export function sendPostBulkAgentReassign( + body: PostBulkAgentReassignRequest['body'], + options?: RequestOptions +) { + return sendRequest({ + method: 'post', + path: agentRouteService.getBulkReassignPath(), + body, + ...options, + }); +} + +export function sendPostAgentUnenroll( + agentId: string, + body: PostAgentUnenrollRequest['body'], + options?: RequestOptions +) { + return sendRequest({ + path: agentRouteService.getUnenrollPath(agentId), + method: 'post', + body, + ...options, + }); +} + +export function sendPostBulkAgentUnenroll( + body: PostBulkAgentUnenrollRequest['body'], + options?: RequestOptions +) { + return sendRequest({ + path: agentRouteService.getBulkUnenrollPath(), + method: 'post', + body, + ...options, + }); +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx index 5520a50463db4..0bef3c20ddd1a 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx @@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import styled from 'styled-components'; import { EuiErrorBoundary, EuiPanel, EuiEmptyPrompt, EuiCode } from '@elastic/eui'; import { CoreStart, AppMountParameters } from 'src/core/public'; -import { EuiThemeProvider } from '../../../../../legacy/common/eui_styled_components'; +import { EuiThemeProvider } from '../../../../xpack_legacy/common'; import { IngestManagerSetupDeps, IngestManagerConfigType, @@ -22,9 +22,16 @@ import { PAGE_ROUTING_PATHS } from './constants'; import { DefaultLayout, WithoutHeaderLayout } from './layouts'; import { Loading, Error } from './components'; import { IngestManagerOverview, EPMApp, AgentPolicyApp, FleetApp, DataStreamApp } from './sections'; -import { DepsContext, ConfigContext, useConfig } from './hooks'; +import { + DepsContext, + ConfigContext, + useConfig, + useCore, + sendSetup, + sendGetPermissionsCheck, + licenseService, +} from './hooks'; import { PackageInstallProvider } from './sections/epm/hooks'; -import { useCore, sendSetup, sendGetPermissionsCheck } from './hooks'; import { FleetStatusProvider } from './hooks/use_fleet_status'; import './index.scss'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; @@ -279,4 +286,5 @@ export function renderApp( export const teardownIngestManager = (coreStart: CoreStart) => { coreStart.chrome.docTitle.reset(); coreStart.chrome.setBreadcrumbs([]); + licenseService.stop(); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/layout.tsx index 3bcf0aab9a5c8..6edce74d162bb 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/layout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/layout.tsx @@ -111,7 +111,7 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{ ) : ( ); }, [from]); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/index.tsx index a02214a6fe7fa..39f35fed56ef5 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/index.tsx @@ -242,7 +242,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { notifications.toasts.addSuccess({ title: i18n.translate('xpack.ingestManager.createPackagePolicy.addedNotificationTitle', { - defaultMessage: `Successfully added '{packagePolicyName}'`, + defaultMessage: `'{packagePolicyName}' integration added.`, values: { packagePolicyName: packagePolicy.name, }, @@ -250,7 +250,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { text: agentCount && agentPolicy ? i18n.translate('xpack.ingestManager.createPackagePolicy.addedNotificationMessage', { - defaultMessage: `Fleet will deploy updates to all agents that use the '{agentPolicyName}' policy`, + defaultMessage: `Fleet will deploy updates to all agents that use the '{agentPolicyName}' policy.`, values: { agentPolicyName: agentPolicy.name, }, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/actions_menu.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/actions_menu.tsx index 636ff7a5ff989..ea5dcce8c05bb 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/actions_menu.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/actions_menu.tsx @@ -9,7 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { Agent } from '../../../../types'; import { useCapabilities } from '../../../../hooks'; import { ContextMenuActions } from '../../../../components'; -import { AgentUnenrollProvider, AgentReassignAgentPolicyFlyout } from '../../components'; +import { AgentUnenrollAgentModal, AgentReassignAgentPolicyFlyout } from '../../components'; import { useAgentRefresh } from '../hooks'; export const AgentDetailsActionMenu: React.FunctionComponent<{ @@ -20,6 +20,7 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{ const hasWriteCapabilites = useCapabilities().write; const refreshAgent = useAgentRefresh(); const [isReassignFlyoutOpen, setIsReassignFlyoutOpen] = useState(assignFlyoutOpenByDefault); + const [isUnenrollModalOpen, setIsUnenrollModalOpen] = useState(false); const isUnenrolling = agent.status === 'unenrolling'; const onClose = useMemo(() => { @@ -34,7 +35,20 @@ export const AgentDetailsActionMenu: React.FunctionComponent<{ <> {isReassignFlyoutOpen && ( - + + + )} + {isUnenrollModalOpen && ( + + { + setIsUnenrollModalOpen(false); + refreshAgent(); + }} + useForceUnenroll={isUnenrolling} + /> )} , - - {(unenrollAgentsPrompt) => ( - { - unenrollAgentsPrompt([agent.id], 1, refreshAgent); - }} - > - {isUnenrolling ? ( - - ) : ( - - )} - + { + setIsUnenrollModalOpen(true); + }} + > + {isUnenrolling ? ( + + ) : ( + )} - , + , ]} /> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/bulk_actions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/bulk_actions.tsx new file mode 100644 index 0000000000000..25684c9faf594 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/components/bulk_actions.tsx @@ -0,0 +1,225 @@ +/* + * 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 } from 'react'; +import styled from 'styled-components'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiPopover, + EuiContextMenu, + EuiButtonEmpty, + EuiIcon, + EuiPortal, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Agent } from '../../../../types'; +import { AgentReassignAgentPolicyFlyout, AgentUnenrollAgentModal } from '../../components'; + +const Divider = styled.div` + width: 0; + height: ${(props) => props.theme.eui.euiSizeL}; + border-left: ${(props) => props.theme.eui.euiBorderThin}; +`; + +const FlexItem = styled(EuiFlexItem)` + height: ${(props) => props.theme.eui.euiSizeL}; +`; + +const Button = styled(EuiButtonEmpty)` + .euiButtonEmpty__text { + font-size: ${(props) => props.theme.eui.euiFontSizeXS}; + } +`; + +export type SelectionMode = 'manual' | 'query'; + +export const AgentBulkActions: React.FunctionComponent<{ + totalAgents: number; + totalInactiveAgents: number; + selectableAgents: number; + selectionMode: SelectionMode; + setSelectionMode: (mode: SelectionMode) => void; + currentQuery: string; + selectedAgents: Agent[]; + setSelectedAgents: (agents: Agent[]) => void; + refreshAgents: () => void; +}> = ({ + totalAgents, + totalInactiveAgents, + selectableAgents, + selectionMode, + setSelectionMode, + currentQuery, + selectedAgents, + setSelectedAgents, + refreshAgents, +}) => { + // Bulk actions menu states + const [isMenuOpen, setIsMenuOpen] = useState(false); + const closeMenu = () => setIsMenuOpen(false); + const openMenu = () => setIsMenuOpen(true); + + // Actions states + const [isReassignFlyoutOpen, setIsReassignFlyoutOpen] = useState(false); + const [isUnenrollModalOpen, setIsUnenrollModalOpen] = useState(false); + + // Check if user is working with only inactive agents + const atLeastOneActiveAgentSelected = + selectionMode === 'manual' + ? !!selectedAgents.find((agent) => agent.active) + : totalAgents > totalInactiveAgents; + + const panels = [ + { + id: 0, + items: [ + { + name: ( + + ), + icon: , + disabled: !atLeastOneActiveAgentSelected, + onClick: () => { + closeMenu(); + setIsReassignFlyoutOpen(true); + }, + }, + { + name: ( + + ), + icon: , + disabled: !atLeastOneActiveAgentSelected, + onClick: () => { + closeMenu(); + setIsUnenrollModalOpen(true); + }, + }, + { + name: ( + + ), + icon: , + onClick: () => { + closeMenu(); + setSelectionMode('manual'); + setSelectedAgents([]); + }, + }, + ], + }, + ]; + + return ( + <> + {isReassignFlyoutOpen && ( + + { + setIsReassignFlyoutOpen(false); + refreshAgents(); + }} + /> + + )} + {isUnenrollModalOpen && ( + + { + setIsUnenrollModalOpen(false); + refreshAgents(); + }} + /> + + )} + + + + + + + {(selectionMode === 'manual' && selectedAgents.length) || + (selectionMode === 'query' && totalAgents > 0) ? ( + <> + + + + + + + + } + isOpen={isMenuOpen} + closePopover={closeMenu} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + + + {selectionMode === 'manual' && + selectedAgents.length === selectableAgents && + selectableAgents < totalAgents ? ( + + + + ) : null} + + ) : ( + + )} + + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx index 46f7ffb85b21f..0bc463ce98590 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useMemo, useCallback } from 'react'; +import React, { useState, useMemo, useCallback, useRef } from 'react'; import { EuiBasicTable, EuiButton, @@ -20,6 +20,7 @@ import { EuiContextMenuItem, EuiIcon, EuiPortal, + EuiHorizontalRule, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; @@ -33,11 +34,17 @@ import { useUrlParams, useLink, useBreadcrumbs, + useLicense, } from '../../../hooks'; import { SearchBar, ContextMenuActions } from '../../../components'; import { AgentStatusKueryHelper } from '../../../services'; import { AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; -import { AgentReassignAgentPolicyFlyout, AgentHealth, AgentUnenrollProvider } from '../components'; +import { + AgentReassignAgentPolicyFlyout, + AgentHealth, + AgentUnenrollAgentModal, +} from '../components'; +import { AgentBulkActions, SelectionMode } from './components/bulk_actions'; const REFRESH_INTERVAL_MS = 5000; @@ -63,72 +70,68 @@ const statusFilters = [ }, ] as Array<{ label: string; status: string }>; -const RowActions = React.memo<{ agent: Agent; onReassignClick: () => void; refresh: () => void }>( - ({ agent, refresh, onReassignClick }) => { - const { getHref } = useLink(); - const hasWriteCapabilites = useCapabilities().write; +const RowActions = React.memo<{ + agent: Agent; + refresh: () => void; + onReassignClick: () => void; + onUnenrollClick: () => void; +}>(({ agent, refresh, onReassignClick, onUnenrollClick }) => { + const { getHref } = useLink(); + const hasWriteCapabilites = useCapabilities().write; - const isUnenrolling = agent.status === 'unenrolling'; - const [isMenuOpen, setIsMenuOpen] = useState(false); - return ( - setIsMenuOpen(isOpen)} - items={[ - + const isUnenrolling = agent.status === 'unenrolling'; + const [isMenuOpen, setIsMenuOpen] = useState(false); + return ( + setIsMenuOpen(isOpen)} + items={[ + + + , + { + onReassignClick(); + }} + disabled={!agent.active} + key="reassignPolicy" + > + + , + { + onUnenrollClick(); + }} + > + {isUnenrolling ? ( - , - { - onReassignClick(); - }} - disabled={!agent.active} - key="reassignPolicy" - > + ) : ( - , - - {(unenrollAgentsPrompt) => ( - { - unenrollAgentsPrompt([agent.id], 1, () => { - refresh(); - setIsMenuOpen(false); - }); - }} - > - {isUnenrolling ? ( - - ) : ( - - )} - - )} - , - ]} - /> - ); - } -); + )} + , + ]} + /> + ); +}); function safeMetadata(val: any) { if (typeof val !== 'string') { @@ -142,12 +145,16 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { const { getHref } = useLink(); const defaultKuery: string = (useUrlParams().urlParams.kuery as string) || ''; const hasWriteCapabilites = useCapabilities().write; + const isGoldPlus = useLicense().isGoldPlus(); // Agent data states const [showInactive, setShowInactive] = useState(false); // Table and search states - const [search, setSearch] = useState(defaultKuery); + const [search, setSearch] = useState(defaultKuery); + const [selectionMode, setSelectionMode] = useState('manual'); + const [selectedAgents, setSelectedAgents] = useState([]); + const tableRef = useRef>(null); const { pagination, pageSizeOptions, setPagination } = usePagination(); // Policies state for filtering @@ -179,8 +186,9 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { // Agent enrollment flyout state const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false); - // Agent reassignment flyout state - const [agentToReassignId, setAgentToReassignId] = useState(undefined); + // Agent actions states + const [agentToReassign, setAgentToReassign] = useState(undefined); + const [agentToUnenroll, setAgentToUnenroll] = useState(undefined); let kuery = search.trim(); if (selectedAgentPolicies.length) { @@ -229,6 +237,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { const agents = agentsRequest.data ? agentsRequest.data.list : []; const totalAgents = agentsRequest.data ? agentsRequest.data.total : 0; + const totalInactiveAgents = agentsRequest.data ? agentsRequest.data.totalInactive : 0; const { isLoading } = agentsRequest; const agentPoliciesRequest = useGetAgentPolicies({ @@ -345,7 +354,8 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { agentsRequest.resendRequest()} - onReassignClick={() => setAgentToReassignId(agent.id)} + onReassignClick={() => setAgentToReassign(agent)} + onUnenrollClick={() => setAgentToUnenroll(agent)} /> ); }, @@ -378,8 +388,6 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { /> ); - const agentToReassign = agentToReassignId && agents.find((a) => a.id === agentToReassignId); - return ( <> {isEnrollmentFlyoutOpen ? ( @@ -391,15 +399,30 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { {agentToReassign && ( { - setAgentToReassignId(undefined); + setAgentToReassign(undefined); agentsRequest.resendRequest(); }} /> )} - + {agentToUnenroll && ( + + { + setAgentToUnenroll(undefined); + agentsRequest.resendRequest(); + }} + useForceUnenroll={agentToUnenroll.status === 'unenrolling'} + /> + + )} + + {/* Search and filter bar */} + @@ -510,9 +533,31 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { - + + {/* Agent total and bulk actions */} + agent.active).length || 0} + selectionMode={selectionMode} + setSelectionMode={setSelectionMode} + currentQuery={kuery} + selectedAgents={selectedAgents} + setSelectedAgents={(newAgents: Agent[]) => { + if (tableRef?.current) { + tableRef.current.setSelection(newAgents); + setSelectionMode('manual'); + } + }} + refreshAgents={() => agentsRequest.resendRequest()} + /> + + + + {/* Agent list table */} + ref={tableRef} className="fleet__agentList__table" data-test-subj="fleetAgentListTable" loading={isLoading} @@ -551,6 +596,18 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { totalItemCount: totalAgents, pageSizeOptions, }} + isSelectable={true} + selection={ + isGoldPlus + ? { + onSelectionChange: (newAgents: Agent[]) => { + setSelectedAgents(newAgents); + setSelectionMode('manual'); + }, + selectable: (agent: Agent) => agent.active, + } + : undefined + } onChange={({ page }: { page: { index: number; size: number } }) => { const newPagination = { ...pagination, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_policy_flyout/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_policy_flyout/index.tsx index 0c154bf1074c0..d3af1287c4025 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_policy_flyout/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_policy_flyout/index.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlyout, @@ -22,40 +22,55 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { Agent } from '../../../../types'; -import { sendPutAgentReassign, useCore, useGetAgentPolicies } from '../../../../hooks'; +import { + sendPutAgentReassign, + sendPostBulkAgentReassign, + useCore, + useGetAgentPolicies, +} from '../../../../hooks'; import { AgentPolicyPackageBadges } from '../agent_policy_package_badges'; interface Props { onClose: () => void; - agent: Agent; + agents: Agent[] | string; } export const AgentReassignAgentPolicyFlyout: React.FunctionComponent = ({ onClose, - agent, + agents, }) => { const { notifications } = useCore(); + const isSingleAgent = Array.isArray(agents) && agents.length === 1; + const [selectedAgentPolicyId, setSelectedAgentPolicyId] = useState( - agent.policy_id + isSingleAgent ? (agents[0] as Agent).policy_id : undefined ); - const agentPoliciesRequest = useGetAgentPolicies({ page: 1, perPage: 1000, }); const agentPolicies = agentPoliciesRequest.data ? agentPoliciesRequest.data.items : []; + useEffect(() => { + if (!selectedAgentPolicyId && agentPolicies[0]) { + setSelectedAgentPolicyId(agentPolicies[0].id); + } + }, [agentPolicies, selectedAgentPolicyId]); const [isSubmitting, setIsSubmitting] = useState(false); - async function onSubmit() { try { setIsSubmitting(true); if (!selectedAgentPolicyId) { throw new Error('No selected agent policy id'); } - const res = await sendPutAgentReassign(agent.id, { - policy_id: selectedAgentPolicyId, - }); + const res = isSingleAgent + ? await sendPutAgentReassign((agents[0] as Agent).id, { + policy_id: selectedAgentPolicyId, + }) + : await sendPostBulkAgentReassign({ + policy_id: selectedAgentPolicyId, + agents: Array.isArray(agents) ? agents.map((agent) => agent.id) : agents, + }); if (res.error) { throw res.error; } @@ -91,7 +106,10 @@ export const AgentReassignAgentPolicyFlyout: React.FunctionComponent = ({ @@ -106,6 +124,7 @@ export const AgentReassignAgentPolicyFlyout: React.FunctionComponent = ({ > ({ value: agentPolicy.id, text: agentPolicy.name, @@ -134,7 +153,7 @@ export const AgentReassignAgentPolicyFlyout: React.FunctionComponent = ({ void; + agents: Agent[] | string; + agentCount: number; + useForceUnenroll?: boolean; +} + +export const AgentUnenrollAgentModal: React.FunctionComponent = ({ + onClose, + agents, + agentCount, + useForceUnenroll, +}) => { + const { notifications } = useCore(); + const [forceUnenroll, setForceUnenroll] = useState(useForceUnenroll || false); + const [isSubmitting, setIsSubmitting] = useState(false); + const isSingleAgent = Array.isArray(agents) && agents.length === 1; + + async function onSubmit() { + try { + setIsSubmitting(true); + const { error } = isSingleAgent + ? await sendPostAgentUnenroll((agents[0] as Agent).id, { + force: forceUnenroll, + }) + : await sendPostBulkAgentUnenroll({ + agents: Array.isArray(agents) ? agents.map((agent) => agent.id) : agents, + force: forceUnenroll, + }); + if (error) { + throw error; + } + setIsSubmitting(false); + if (forceUnenroll) { + const successMessage = isSingleAgent + ? i18n.translate( + 'xpack.ingestManager.unenrollAgents.successForceSingleNotificationTitle', + { defaultMessage: 'Agent unenrolled' } + ) + : i18n.translate( + 'xpack.ingestManager.unenrollAgents.successForceMultiNotificationTitle', + { defaultMessage: 'Agents unenrolled' } + ); + notifications.toasts.addSuccess(successMessage); + } else { + const successMessage = isSingleAgent + ? i18n.translate('xpack.ingestManager.unenrollAgents.successSingleNotificationTitle', { + defaultMessage: 'Unenrolling agent', + }) + : i18n.translate('xpack.ingestManager.unenrollAgents.successMultiNotificationTitle', { + defaultMessage: 'Unenrolling agents', + }); + notifications.toasts.addSuccess(successMessage); + } + onClose(); + } catch (error) { + setIsSubmitting(false); + notifications.toasts.addError(error, { + title: i18n.translate('xpack.ingestManager.unenrollAgents.fatalErrorNotificationTitle', { + defaultMessage: 'Error unenrolling {count, plural, one {agent} other {agents}}', + values: { count: agentCount }, + }), + }); + } + } + + return ( + + + ) : ( + + ) + } + onCancel={onClose} + onConfirm={onSubmit} + cancelButtonText={ + + } + confirmButtonDisabled={isSubmitting} + confirmButtonText={ + isSingleAgent ? ( + + ) : ( + + ) + } + buttonColor="danger" + > +

+ {isSingleAgent ? ( + + ) : ( + + )} +

+ + ), + }} + > + + } + checked={forceUnenroll} + onChange={(e) => setForceUnenroll(e.target.checked)} + disabled={useForceUnenroll} + /> + +
+
+ ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_provider.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_provider.tsx deleted file mode 100644 index 6f1cba70bbcee..0000000000000 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_provider.tsx +++ /dev/null @@ -1,174 +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, { Fragment, useRef, useState } from 'react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { useCore, sendRequest } from '../../../hooks'; -import { PostAgentUnenrollResponse } from '../../../types'; -import { agentRouteService } from '../../../services'; - -interface Props { - children: (unenrollAgents: UnenrollAgents) => React.ReactElement; - forceUnenroll?: boolean; -} - -export type UnenrollAgents = ( - agents: string[] | string, - agentsCount: number, - onSuccess?: OnSuccessCallback -) => void; - -type OnSuccessCallback = (agentsUnenrolled: string[]) => void; - -export const AgentUnenrollProvider: React.FunctionComponent = ({ - children, - forceUnenroll = false, -}) => { - const core = useCore(); - const [agents, setAgents] = useState([]); - const [agentsCount, setAgentsCount] = useState(0); - const [isModalOpen, setIsModalOpen] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const onSuccessCallback = useRef(null); - - const unenrollAgentsPrompt: UnenrollAgents = ( - agentsToUnenroll, - agentsToUnenrollCount, - onSuccess = () => undefined - ) => { - if ( - agentsToUnenroll === undefined || - // !Only supports unenrolling one agent - (Array.isArray(agentsToUnenroll) && agentsToUnenroll.length !== 1) - ) { - throw new Error('No agents specified for unenrollment'); - } - setIsModalOpen(true); - setAgents(agentsToUnenroll); - setAgentsCount(agentsToUnenrollCount); - onSuccessCallback.current = onSuccess; - }; - - const closeModal = () => { - setAgents([]); - setAgentsCount(0); - setIsLoading(false); - setIsModalOpen(false); - }; - - const unenrollAgents = async () => { - setIsLoading(true); - - try { - const agentId = agents[0]; - const { error } = await sendRequest({ - path: agentRouteService.getUnenrollPath(agentId), - method: 'post', - body: { - force: forceUnenroll, - }, - }); - - if (error) { - throw new Error(error.message); - } - - const successMessage = forceUnenroll - ? i18n.translate('xpack.ingestManager.unenrollAgents.successForceSingleNotificationTitle', { - defaultMessage: "Agent '{id}' unenrolled", - values: { id: agentId }, - }) - : i18n.translate('xpack.ingestManager.unenrollAgents.successSingleNotificationTitle', { - defaultMessage: "Unenrolling agent '{id}'", - values: { id: agentId }, - }); - core.notifications.toasts.addSuccess(successMessage); - - if (onSuccessCallback.current) { - onSuccessCallback.current([agentId]); - } - } catch (e) { - core.notifications.toasts.addDanger( - i18n.translate('xpack.ingestManager.unenrollAgents.fatalErrorNotificationTitle', { - defaultMessage: 'Error unenrolling agents', - }) - ); - } - - closeModal(); - }; - - const renderModal = () => { - if (!isModalOpen) { - return null; - } - - const unenrollByKuery = typeof agents === 'string'; - const isSingle = agentsCount === 1; - - return ( - - - ) : ( - - ) - ) : ( - - ) - } - onCancel={closeModal} - onConfirm={unenrollAgents} - cancelButtonText={ - - } - confirmButtonText={ - isLoading ? ( - - ) : ( - - ) - } - buttonColor="danger" - confirmButtonDisabled={isLoading} - /> - - ); - }; - - return ( - - {children(unenrollAgentsPrompt)} - {renderModal()} - - ); -}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/index.tsx index 527f920f24365..eea4ed3b712b1 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/index.tsx @@ -8,4 +8,4 @@ export * from './loading'; export * from './agent_reassign_policy_flyout'; export * from './agent_enrollment_flyout'; export * from './agent_health'; -export * from './agent_unenroll_provider'; +export * from './agent_unenroll_modal'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/components/confirm_delete_modal.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/components/confirm_delete_modal.tsx index d2092f070a22a..a115e03a369a2 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/components/confirm_delete_modal.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/components/confirm_delete_modal.tsx @@ -21,7 +21,7 @@ export const ConfirmEnrollmentTokenDelete = (props: Props) => { { confirmButtonText={i18n.translate( 'xpack.ingestManager.enrollmentTokenDeleteModal.deleteButton', { - defaultMessage: 'Delete', + defaultMessage: 'Revoke enrollment token', } )} defaultFocusedButton="confirm" @@ -42,7 +42,8 @@ export const ConfirmEnrollmentTokenDelete = (props: Props) => { > = ({

diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx index d85a6e8b5b833..f447469a02df2 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx @@ -268,7 +268,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { @@ -290,7 +290,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { setFlyoutOpen(true)}>
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/setup_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/setup_page/index.tsx index eeade9036df00..fbd74f8b03e72 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/setup_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/setup_page/index.tsx @@ -95,8 +95,7 @@ export const SetupPage: React.FunctionComponent<{ diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts index 2c9e8b84d4069..ed6ba5c891a0b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/services/index.ts @@ -25,4 +25,5 @@ export { isPackageLimited, doesAgentPolicyAlreadyIncludePackage, isValidNamespace, + LicenseService, } from '../../../../common'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts index 30a6742af6ea6..71a44089b8bf7 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts @@ -49,13 +49,18 @@ export { GetAgentsResponse, GetAgentsRequest, GetOneAgentResponse, + PostAgentUnenrollRequest, PostAgentUnenrollResponse, + PostBulkAgentUnenrollRequest, + PostBulkAgentUnenrollResponse, GetOneAgentEventsRequest, GetOneAgentEventsResponse, GetAgentStatusRequest, GetAgentStatusResponse, PutAgentReassignRequest, PutAgentReassignResponse, + PostBulkAgentReassignRequest, + PostBulkAgentReassignResponse, // API schemas - Enrollment API Keys GetEnrollmentAPIKeysResponse, GetEnrollmentAPIKeysRequest, diff --git a/x-pack/plugins/ingest_manager/public/plugin.ts b/x-pack/plugins/ingest_manager/public/plugin.ts index 536832cdaed64..5f7bfe865e892 100644 --- a/x-pack/plugins/ingest_manager/public/plugin.ts +++ b/x-pack/plugins/ingest_manager/public/plugin.ts @@ -23,7 +23,7 @@ import { BASE_PATH } from './applications/ingest_manager/constants'; import { IngestManagerConfigType } from '../common/types'; import { setupRouteService, appRoutesService } from '../common'; -import { setHttpClient } from './applications/ingest_manager/hooks'; +import { setHttpClient, licenseService } from './applications/ingest_manager/hooks'; import { TutorialDirectoryNotice, TutorialDirectoryHeaderLink, @@ -71,6 +71,9 @@ export class IngestManagerPlugin // Set up http client setHttpClient(core.http); + // Set up license service + licenseService.start(deps.licensing.license$); + // Register main Ingest Manager app core.application.register({ id: PLUGIN_ID, diff --git a/x-pack/plugins/ingest_manager/server/errors/handlers.ts b/x-pack/plugins/ingest_manager/server/errors/handlers.ts index 9f776565cf262..b621f2dd29331 100644 --- a/x-pack/plugins/ingest_manager/server/errors/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/errors/handlers.ts @@ -56,10 +56,7 @@ const getHTTPResponseCode = (error: IngestManagerError): number => { return 400; // Bad Request }; -export const defaultIngestErrorHandler: IngestErrorHandler = async ({ - error, - response, -}: IngestErrorHandlerParams): Promise => { +export function ingestErrorToResponseOptions(error: IngestErrorHandlerParams['error']) { const logger = appContextService.getLogger(); if (isLegacyESClientError(error)) { // there was a problem communicating with ES (e.g. via `callCluster`) @@ -72,36 +69,44 @@ export const defaultIngestErrorHandler: IngestErrorHandler = async ({ logger.error(message); - return response.customError({ + return { statusCode: error?.statusCode || error.status, body: { message }, - }); + }; } // our "expected" errors if (error instanceof IngestManagerError) { // only log the message logger.error(error.message); - return response.customError({ + return { statusCode: getHTTPResponseCode(error), body: { message: error.message }, - }); + }; } // handle any older Boom-based errors or the few places our app uses them if (isBoom(error)) { // only log the message logger.error(error.output.payload.message); - return response.customError({ + return { statusCode: error.output.statusCode, body: { message: error.output.payload.message }, - }); + }; } // not sure what type of error this is. log as much as possible logger.error(error); - return response.customError({ + return { statusCode: 500, body: { message: error.message }, - }); + }; +} + +export const defaultIngestErrorHandler: IngestErrorHandler = async ({ + error, + response, +}: IngestErrorHandlerParams): Promise => { + const options = ingestErrorToResponseOptions(error); + return response.customError(options); }; diff --git a/x-pack/plugins/ingest_manager/server/errors/index.ts b/x-pack/plugins/ingest_manager/server/errors/index.ts index 5e36a2ec9a884..f495bf551dcff 100644 --- a/x-pack/plugins/ingest_manager/server/errors/index.ts +++ b/x-pack/plugins/ingest_manager/server/errors/index.ts @@ -5,7 +5,7 @@ */ /* eslint-disable max-classes-per-file */ -export { defaultIngestErrorHandler } from './handlers'; +export { defaultIngestErrorHandler, ingestErrorToResponseOptions } from './handlers'; export class IngestManagerError extends Error { constructor(message?: string) { diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts index b0439b30e8973..fb320b01dea97 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/acks_handlers.ts @@ -10,6 +10,7 @@ import { RequestHandler } from 'kibana/server'; import { AcksService } from '../../services/agents'; import { AgentEvent } from '../../../common/types/models'; import { PostAgentAcksRequest, PostAgentAcksResponse } from '../../../common/types/rest_spec'; +import { defaultIngestErrorHandler } from '../../errors'; export const postAgentAcksHandlerBuilder = function ( ackService: AcksService @@ -43,18 +44,8 @@ export const postAgentAcksHandlerBuilder = function ( }; return response.ok({ body }); - } catch (e) { - if (e.isBoom) { - return response.customError({ - statusCode: e.output.statusCode, - body: { message: e.message }, - }); - } - - return response.customError({ - statusCode: 500, - body: { message: e.message }, - }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); } }; }; diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.ts index 12a0956b79155..64a7795cc9dac 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.ts @@ -11,6 +11,7 @@ import { TypeOf } from '@kbn/config-schema'; import { PostNewAgentActionRequestSchema } from '../../types/rest_spec'; import { ActionsService } from '../../services/agents'; import { PostNewAgentActionResponse } from '../../../common/types/rest_spec'; +import { defaultIngestErrorHandler } from '../../errors'; export const postNewAgentActionHandlerBuilder = function ( actionsService: ActionsService @@ -38,18 +39,8 @@ export const postNewAgentActionHandlerBuilder = function ( }; return response.ok({ body }); - } catch (e) { - if (e.isBoom) { - return response.customError({ - statusCode: e.output.statusCode, - body: { message: e.message }, - }); - } - - return response.customError({ - statusCode: 500, - body: { message: e.message }, - }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); } }; }; diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts index b9789b770eb2e..fb867af513fdc 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts @@ -16,6 +16,7 @@ import { GetAgentStatusResponse, PutAgentReassignResponse, PostAgentEnrollRequest, + PostBulkAgentReassignResponse, } from '../../../common/types'; import { GetAgentsRequestSchema, @@ -26,11 +27,13 @@ import { PostAgentCheckinRequest, GetAgentStatusRequestSchema, PutAgentReassignRequestSchema, + PostBulkAgentReassignRequestSchema, } from '../../types'; +import { defaultIngestErrorHandler } from '../../errors'; +import { licenseService } from '../../services'; import * as AgentService from '../../services/agents'; import * as APIKeyService from '../../services/api_keys'; import { appContextService } from '../../services/app_context'; -import { defaultIngestErrorHandler } from '../../errors'; export const getAgentHandler: RequestHandler= 500) { - logger.error(err); - } - - return response.customError({ - statusCode: err.output.statusCode, - body: { message: err.output.payload.message }, - }); - } - - logger.error(err); - - return response.customError({ - statusCode: 500, - body: { message: err.message }, - }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); } }; @@ -247,18 +224,8 @@ export const postAgentEnrollHandler: RequestHandler< }; return response.ok({ body }); - } catch (e) { - if (e.isBoom) { - return response.customError({ - statusCode: e.output.statusCode, - body: { message: e.message }, - }); - } - - return response.customError({ - statusCode: 500, - body: { message: e.message }, - }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); } }; @@ -274,6 +241,11 @@ export const getAgentsHandler: RequestHandler< showInactive: request.query.showInactive, kuery: request.query.kuery, }); + const totalInactive = request.query.showInactive + ? await AgentService.countInactiveAgents(soClient, { + kuery: request.query.kuery, + }) + : 0; const body: GetAgentsResponse = { list: agents.map((agent) => ({ @@ -281,6 +253,7 @@ export const getAgentsHandler: RequestHandler< status: AgentService.getAgentStatus(agent), })), total, + totalInactive, page, perPage, }; @@ -301,12 +274,50 @@ export const putAgentsReassignHandler: RequestHandler< const body: PutAgentReassignResponse = {}; return response.ok({ body }); - } catch (e) { + } catch (error) { + return defaultIngestErrorHandler({ error, response }); + } +}; + +export const postBulkAgentsReassignHandler: RequestHandler< + undefined, + undefined, + TypeOf +> = async (context, request, response) => { + if (!licenseService.isGoldPlus()) { return response.customError({ - statusCode: 500, - body: { message: e.message }, + statusCode: 403, + body: { message: 'Requires Gold license' }, }); } + + const soClient = context.core.savedObjects.client; + try { + // Reassign by array of IDs + const result = Array.isArray(request.body.agents) + ? await AgentService.reassignAgents( + soClient, + { agentIds: request.body.agents }, + request.body.policy_id + ) + : await AgentService.reassignAgents( + soClient, + { kuery: request.body.agents }, + request.body.policy_id + ); + const body: PostBulkAgentReassignResponse = result.saved_objects.reduce((acc, so) => { + return { + ...acc, + [so.id]: { + success: !so.error, + error: so.error || undefined, + }, + }; + }, {}); + return response.ok({ body }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); + } }; export const getAgentStatusForAgentPolicyHandler: RequestHandler< @@ -324,10 +335,7 @@ export const getAgentStatusForAgentPolicyHandler: RequestHandler< const body: GetAgentStatusResponse = { results }; return response.ok({ body }); - } catch (e) { - return response.customError({ - statusCode: 500, - body: { message: e.message }, - }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); } }; diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts index a2e5c742ad6b5..eafc726ea166d 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts @@ -23,9 +23,11 @@ import { PostAgentAcksRequestParamsJSONSchema, PostAgentAcksRequestBodyJSONSchema, PostAgentUnenrollRequestSchema, + PostBulkAgentUnenrollRequestSchema, GetAgentStatusRequestSchema, PostNewAgentActionRequestSchema, PutAgentReassignRequestSchema, + PostBulkAgentReassignRequestSchema, PostAgentEnrollRequestBodyJSONSchema, } from '../../types'; import { @@ -38,12 +40,13 @@ import { postAgentEnrollHandler, getAgentStatusForAgentPolicyHandler, putAgentsReassignHandler, + postBulkAgentsReassignHandler, } from './handlers'; import { postAgentAcksHandlerBuilder } from './acks_handlers'; import * as AgentService from '../../services/agents'; import { postNewAgentActionHandlerBuilder } from './actions_handlers'; import { appContextService } from '../../services'; -import { postAgentsUnenrollHandler } from './unenroll_handler'; +import { postAgentUnenrollHandler, postBulkAgentsUnenrollHandler } from './unenroll_handler'; import { IngestManagerConfigType } from '../..'; const ajv = new Ajv({ @@ -181,7 +184,7 @@ export const registerRoutes = (router: IRouter, config: IngestManagerConfigType) validate: PostAgentUnenrollRequestSchema, options: { tags: [`access:${PLUGIN_ID}-all`] }, }, - postAgentsUnenrollHandler + postAgentUnenrollHandler ); router.put( @@ -212,4 +215,24 @@ export const registerRoutes = (router: IRouter, config: IngestManagerConfigType) }, getAgentStatusForAgentPolicyHandler ); + + // Bulk reassign + router.post( + { + path: AGENT_API_ROUTES.BULK_REASSIGN_PATTERN, + validate: PostBulkAgentReassignRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + postBulkAgentsReassignHandler + ); + + // Bulk unenroll + router.post( + { + path: AGENT_API_ROUTES.BULK_UNENROLL_PATTERN, + validate: PostBulkAgentUnenrollRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + postBulkAgentsUnenrollHandler + ); }; diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/unenroll_handler.ts b/x-pack/plugins/ingest_manager/server/routes/agent/unenroll_handler.ts index 5df695d248f5b..861d7c45c6f0a 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/unenroll_handler.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/unenroll_handler.ts @@ -6,11 +6,13 @@ import { RequestHandler } from 'src/core/server'; import { TypeOf } from '@kbn/config-schema'; -import { PostAgentUnenrollResponse } from '../../../common/types'; -import { PostAgentUnenrollRequestSchema } from '../../types'; +import { PostAgentUnenrollResponse, PostBulkAgentUnenrollResponse } from '../../../common/types'; +import { PostAgentUnenrollRequestSchema, PostBulkAgentUnenrollRequestSchema } from '../../types'; +import { licenseService } from '../../services'; import * as AgentService from '../../services/agents'; +import { defaultIngestErrorHandler } from '../../errors'; -export const postAgentsUnenrollHandler: RequestHandler< +export const postAgentUnenrollHandler: RequestHandler< TypeOf, undefined, TypeOf @@ -25,10 +27,36 @@ export const postAgentsUnenrollHandler: RequestHandler< const body: PostAgentUnenrollResponse = {}; return response.ok({ body }); - } catch (e) { + } catch (error) { + return defaultIngestErrorHandler({ error, response }); + } +}; + +export const postBulkAgentsUnenrollHandler: RequestHandler< + undefined, + undefined, + TypeOf +> = async (context, request, response) => { + if (!licenseService.isGoldPlus()) { return response.customError({ - statusCode: 500, - body: { message: e.message }, + statusCode: 403, + body: { message: 'Requires Gold license' }, }); } + const soClient = context.core.savedObjects.client; + const unenrollAgents = + request.body?.force === true ? AgentService.forceUnenrollAgents : AgentService.unenrollAgents; + + try { + if (Array.isArray(request.body.agents)) { + await unenrollAgents(soClient, { agentIds: request.body.agents }); + } else { + await unenrollAgents(soClient, { kuery: request.body.agents }); + } + + const body: PostBulkAgentUnenrollResponse = {}; + return response.ok({ body }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); + } }; diff --git a/x-pack/plugins/ingest_manager/server/routes/agent_policy/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent_policy/handlers.ts index 7eb3df2346106..311b3bbf7f13b 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent_policy/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent_policy/handlers.ts @@ -32,6 +32,7 @@ import { DeleteAgentPolicyResponse, GetFullAgentPolicyResponse, } from '../../../common'; +import { defaultIngestErrorHandler } from '../../errors'; export const getAgentPoliciesHandler: RequestHandler< undefined, @@ -64,11 +65,8 @@ export const getAgentPoliciesHandler: RequestHandler< ); return response.ok({ body }); - } catch (e) { - return response.customError({ - statusCode: 500, - body: { message: e.message }, - }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); } }; @@ -91,11 +89,8 @@ export const getOneAgentPolicyHandler: RequestHandler > = async (context, request, response) => { - const logger = appContextService.getLogger(); const savedObjectsClient = context.core.savedObjects.client; const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; const { pkgkey } = request.params; const { pkgName, pkgVersion } = splitPkgKey(pkgkey); const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); - const installType = getInstallType({ pkgVersion, installedPkg }); try { const res = await installPackage({ savedObjectsClient, @@ -155,36 +157,38 @@ export const installPackageFromRegistryHandler: RequestHandler< }; return response.ok({ body }); } catch (e) { - // could have also done `return defaultIngestErrorHandler({ error: e, response })` at each of the returns, - // but doing it this way will log the outer/install errors before any inner/rollback errors const defaultResult = await defaultIngestErrorHandler({ error: e, response }); - if (e instanceof IngestManagerError) { - return defaultResult; - } + await handleInstallPackageFailure({ + savedObjectsClient, + error: e, + pkgName, + pkgVersion, + installedPkg, + callCluster, + }); - // if there is an unknown server error, uninstall any package assets or reinstall the previous version if update - try { - if (installType === 'install' || installType === 'reinstall') { - logger.error(`uninstalling ${pkgkey} after error installing`); - await removeInstallation({ savedObjectsClient, pkgkey, callCluster }); - } - if (installType === 'update') { - // @ts-ignore getInstallType ensures we have installedPkg - const prevVersion = `${pkgName}-${installedPkg.attributes.version}`; - logger.error(`rolling back to ${prevVersion} after error installing ${pkgkey}`); - await installPackage({ - savedObjectsClient, - pkgkey: prevVersion, - callCluster, - }); - } - } catch (error) { - logger.error(`failed to uninstall or rollback package after installation error ${error}`); - } return defaultResult; } }; +export const bulkInstallPackagesFromRegistryHandler: RequestHandler< + undefined, + undefined, + TypeOf +> = async (context, request, response) => { + const savedObjectsClient = context.core.savedObjects.client; + const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; + const res = await bulkInstallPackages({ + savedObjectsClient, + callCluster, + packagesToUpgrade: request.body.packages, + }); + const body: BulkInstallPackagesResponse = { + response: res, + }; + return response.ok({ body }); +}; + export const installPackageByUploadHandler: RequestHandler< undefined, undefined, diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/index.ts b/x-pack/plugins/ingest_manager/server/routes/epm/index.ts index 9048652f0e8a9..eaf61335b5e06 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/index.ts @@ -14,6 +14,7 @@ import { installPackageFromRegistryHandler, installPackageByUploadHandler, deletePackageHandler, + bulkInstallPackagesFromRegistryHandler, } from './handlers'; import { GetCategoriesRequestSchema, @@ -23,6 +24,7 @@ import { InstallPackageFromRegistryRequestSchema, InstallPackageByUploadRequestSchema, DeletePackageRequestSchema, + BulkUpgradePackagesFromRegistryRequestSchema, } from '../../types'; const MAX_FILE_SIZE_BYTES = 104857600; // 100MB @@ -82,6 +84,15 @@ export const registerRoutes = (router: IRouter) => { installPackageFromRegistryHandler ); + router.post( + { + path: EPM_API_ROUTES.BULK_INSTALL_PATTERN, + validate: BulkUpgradePackagesFromRegistryRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + bulkInstallPackagesFromRegistryHandler + ); + router.post( { path: EPM_API_ROUTES.INSTALL_BY_UPLOAD_PATTERN, diff --git a/x-pack/plugins/ingest_manager/server/routes/output/handler.ts b/x-pack/plugins/ingest_manager/server/routes/output/handler.ts index d920ad4e6cb71..9cdab757acc87 100644 --- a/x-pack/plugins/ingest_manager/server/routes/output/handler.ts +++ b/x-pack/plugins/ingest_manager/server/routes/output/handler.ts @@ -9,6 +9,7 @@ import { TypeOf } from '@kbn/config-schema'; import { GetOneOutputRequestSchema, PutOutputRequestSchema } from '../../types'; import { GetOneOutputResponse, GetOutputsResponse } from '../../../common'; import { outputService } from '../../services/output'; +import { defaultIngestErrorHandler } from '../../errors'; export const getOutputsHandler: RequestHandler = async (context, request, response) => { const soClient = context.core.savedObjects.client; @@ -23,11 +24,8 @@ export const getOutputsHandler: RequestHandler = async (context, request, respon }; return response.ok({ body }); - } catch (e) { - return response.customError({ - statusCode: 500, - body: { message: e.message }, - }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); } }; @@ -43,17 +41,14 @@ export const getOneOuputHandler: RequestHandler { @@ -19,17 +19,14 @@ export const getSettingsHandler: RequestHandler = async (context, request, respo item: settings, }; return response.ok({ body }); - } catch (e) { - if (e.isBoom && e.output.statusCode === 404) { + } catch (error) { + if (error.isBoom && error.output.statusCode === 404) { return response.notFound({ body: { message: `Setings not found` }, }); } - return response.customError({ - statusCode: 500, - body: { message: e.message }, - }); + return defaultIngestErrorHandler({ error, response }); } }; @@ -49,17 +46,14 @@ export const putSettingsHandler: RequestHandler< item: settings, }; return response.ok({ body }); - } catch (e) { - if (e.isBoom && e.output.statusCode === 404) { + } catch (error) { + if (error.isBoom && error.output.statusCode === 404) { return response.notFound({ body: { message: `Setings not found` }, }); } - return response.customError({ - statusCode: 500, - body: { message: e.message }, - }); + return defaultIngestErrorHandler({ error, response }); } }; diff --git a/x-pack/plugins/ingest_manager/server/services/agent_policy.ts b/x-pack/plugins/ingest_manager/server/services/agent_policy.ts index 938cfb4351630..64b11512fae10 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_policy.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_policy.ts @@ -26,6 +26,7 @@ import { packagePolicyService } from './package_policy'; import { outputService } from './output'; import { agentPolicyUpdateEventHandler } from './agent_policy_update'; import { getSettings } from './settings'; +import { normalizeKuery } from './saved_object'; const SAVED_OBJECT_TYPE = AGENT_POLICY_SAVED_OBJECT_TYPE; @@ -166,13 +167,7 @@ class AgentPolicyService { sortOrder, page, perPage, - // To ensure users don't need to know about SO data structure... - filter: kuery - ? kuery.replace( - new RegExp(`${SAVED_OBJECT_TYPE}\.`, 'g'), - `${SAVED_OBJECT_TYPE}.attributes.` - ) - : undefined, + filter: kuery ? normalizeKuery(SAVED_OBJECT_TYPE, kuery) : undefined, }); const agentPolicies = await Promise.all( diff --git a/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts index 866aa587b8a56..c7b4098803827 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts @@ -57,7 +57,7 @@ describe('test agent acks services', () => { ); }); - it('should update config field on the agent if a policy change is acknowledged', async () => { + it('should update config field on the agent if a policy change is acknowledged with an agent without policy', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); const actionAttributes = { @@ -116,6 +116,114 @@ describe('test agent acks services', () => { `); }); + it('should update config field on the agent if a policy change is acknowledged with a higher revision than the agent one', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + + const actionAttributes = { + type: 'CONFIG_CHANGE', + policy_id: 'policy1', + policy_revision: 4, + sent_at: '2020-03-14T19:45:02.620Z', + timestamp: '2019-01-04T14:32:03.36764-05:00', + created_at: '2020-03-14T19:45:02.620Z', + ack_data: JSON.stringify({ packages: ['system'] }), + }; + + mockSavedObjectsClient.bulkGet.mockReturnValue( + Promise.resolve({ + saved_objects: [ + { + id: 'action2', + references: [], + type: AGENT_ACTION_SAVED_OBJECT_TYPE, + attributes: actionAttributes, + }, + ], + } as SavedObjectsBulkResponse) + ); + + await acknowledgeAgentActions( + mockSavedObjectsClient, + ({ + id: 'id', + type: AGENT_TYPE_PERMANENT, + policy_id: 'policy1', + policy_revision: 3, + } as unknown) as Agent, + [ + { + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: 'action2', + agent_id: 'id', + } as AgentEvent, + ] + ); + expect(mockSavedObjectsClient.bulkUpdate).toBeCalled(); + expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0]).toHaveLength(1); + expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0][0]).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "packages": Array [ + "system", + ], + "policy_revision": 4, + }, + "id": "id", + "type": "fleet-agents", + } + `); + }); + + it('should not update config field on the agent if a policy change is acknowledged with a lower revision than the agent one', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + + const actionAttributes = { + type: 'CONFIG_CHANGE', + policy_id: 'policy1', + policy_revision: 4, + sent_at: '2020-03-14T19:45:02.620Z', + timestamp: '2019-01-04T14:32:03.36764-05:00', + created_at: '2020-03-14T19:45:02.620Z', + ack_data: JSON.stringify({ packages: ['system'] }), + }; + + mockSavedObjectsClient.bulkGet.mockReturnValue( + Promise.resolve({ + saved_objects: [ + { + id: 'action2', + references: [], + type: AGENT_ACTION_SAVED_OBJECT_TYPE, + attributes: actionAttributes, + }, + ], + } as SavedObjectsBulkResponse) + ); + + await acknowledgeAgentActions( + mockSavedObjectsClient, + ({ + id: 'id', + type: AGENT_TYPE_PERMANENT, + policy_id: 'policy1', + policy_revision: 5, + } as unknown) as Agent, + [ + { + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: 'action2', + agent_id: 'id', + } as AgentEvent, + ] + ); + expect(mockSavedObjectsClient.bulkUpdate).toBeCalled(); + expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0]).toHaveLength(0); + }); + it('should not update config field on the agent if a policy change for an old revision is acknowledged', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts index d29dfcec7ef30..1392710eb0eff 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts @@ -139,16 +139,12 @@ function getLatestConfigChangePolicyActionIfUpdated( !isAgentPolicyAction(action) || action.type !== 'CONFIG_CHANGE' || action.policy_id !== agent.policy_id || - (acc?.policy_revision ?? 0) < (agent.policy_revision || 0) + (action?.policy_revision ?? 0) < (agent.policy_revision || 0) ) { return acc; } - if (action.policy_revision > (acc?.policy_revision ?? 0)) { - return action; - } - - return acc; + return action; }, null); } diff --git a/x-pack/plugins/ingest_manager/server/services/agents/actions.ts b/x-pack/plugins/ingest_manager/server/services/agents/actions.ts index 254c2c8b21e32..1d4db44edf88a 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/actions.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/actions.ts @@ -29,12 +29,20 @@ export async function createAgentAction( return createAction(soClient, newAgentAction); } +export async function bulkCreateAgentActions( + soClient: SavedObjectsClientContract, + newAgentActions: Array> +): Promise { + return bulkCreateActions(soClient, newAgentActions); +} + export function createAgentPolicyAction( soClient: SavedObjectsClientContract, newAgentAction: Omit ): Promise { return createAction(soClient, newAgentAction); } + async function createAction( soClient: SavedObjectsClientContract, newAgentAction: Omit @@ -47,19 +55,25 @@ async function createAction( soClient: SavedObjectsClientContract, newAgentAction: Omit | Omit ): Promise { - const so = await soClient.create(AGENT_ACTION_SAVED_OBJECT_TYPE, { - ...newAgentAction, - data: newAgentAction.data ? JSON.stringify(newAgentAction.data) : undefined, - ack_data: newAgentAction.ack_data ? JSON.stringify(newAgentAction.ack_data) : undefined, - }); + const actionSO = await soClient.create( + AGENT_ACTION_SAVED_OBJECT_TYPE, + { + ...newAgentAction, + data: newAgentAction.data ? JSON.stringify(newAgentAction.data) : undefined, + ack_data: newAgentAction.ack_data ? JSON.stringify(newAgentAction.ack_data) : undefined, + } + ); - if (isAgentActionSavedObject(so)) { - const agentAction = savedObjectToAgentAction(so); + if (isAgentActionSavedObject(actionSO)) { + const agentAction = savedObjectToAgentAction(actionSO); + // Action `data` is encrypted, so is not returned from the saved object + // so we add back the original value from the request to form the expected + // response shape for POST create agent action endpoint agentAction.data = newAgentAction.data; return agentAction; - } else if (isPolicyActionSavedObject(so)) { - const agentAction = savedObjectToAgentAction(so); + } else if (isPolicyActionSavedObject(actionSO)) { + const agentAction = savedObjectToAgentAction(actionSO); agentAction.data = newAgentAction.data; return agentAction; @@ -67,6 +81,44 @@ async function createAction( throw new Error('Invalid action'); } +async function bulkCreateActions( + soClient: SavedObjectsClientContract, + newAgentActions: Array> +): Promise; +async function bulkCreateActions( + soClient: SavedObjectsClientContract, + newAgentActions: Array> +): Promise; +async function bulkCreateActions( + soClient: SavedObjectsClientContract, + newAgentActions: Array | Omit> +): Promise> { + const { saved_objects: actionSOs } = await soClient.bulkCreate( + newAgentActions.map((newAgentAction) => ({ + type: AGENT_ACTION_SAVED_OBJECT_TYPE, + attributes: { + ...newAgentAction, + data: newAgentAction.data ? JSON.stringify(newAgentAction.data) : undefined, + ack_data: newAgentAction.ack_data ? JSON.stringify(newAgentAction.ack_data) : undefined, + }, + })) + ); + + return actionSOs.map((actionSO) => { + if (isAgentActionSavedObject(actionSO)) { + const agentAction = savedObjectToAgentAction(actionSO); + // Compared to single create (createAction()), we don't add back the + // original value of `agentAction.data` as this method isn't exposed + // via an HTTP endpoint + return agentAction; + } else if (isPolicyActionSavedObject(actionSO)) { + const agentAction = savedObjectToAgentAction(actionSO); + return agentAction; + } + throw new Error('Invalid action'); + }); +} + export async function getAgentActionsForCheckin( soClient: SavedObjectsClientContract, agentId: string diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.test.ts new file mode 100644 index 0000000000000..5e84e3a50bb44 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.test.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 { TestScheduler } from 'rxjs/testing'; +import { createRateLimiter } from './rxjs_utils'; + +describe('createRateLimiter', () => { + it('should rate limit correctly with 1 request per 10ms', async () => { + const scheduler = new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected); + }); + + scheduler.run(({ expectObservable, cold }) => { + const source = cold('a-b-c-d-e-f|'); + const rateLimiter = createRateLimiter(10, 1, 2, scheduler); + const obs = source.pipe(rateLimiter()); + const results = 'a 9ms b 9ms c 9ms d 9ms e 9ms (f|)'; + expectObservable(obs).toBe(results); + }); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.ts index dddade6841460..3bbfbbd4ec1bf 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/rxjs_utils.ts @@ -5,6 +5,7 @@ */ import * as Rx from 'rxjs'; +import { concatMap, delay } from 'rxjs/operators'; export class AbortError extends Error {} @@ -45,63 +46,35 @@ export const toPromiseAbortable = ( export function createRateLimiter( ratelimitIntervalMs: number, - ratelimitRequestPerInterval: number + ratelimitRequestPerInterval: number, + maxDelay: number, + scheduler = Rx.asyncScheduler ) { - function createCurrentInterval() { - return { - startedAt: Rx.asyncScheduler.now(), - numRequests: 0, - }; - } + let intervalEnd = 0; + let countInCurrentInterval = 0; - let currentInterval: { startedAt: number; numRequests: number } = createCurrentInterval(); - let observers: Array<[Rx.Subscriber, any]> = []; - let timerSubscription: Rx.Subscription | undefined; + function createRateLimitOperator(): Rx.OperatorFunction { + return Rx.pipe( + concatMap(function rateLimit(value: T) { + const now = scheduler.now(); + if (intervalEnd <= now) { + countInCurrentInterval = 1; + intervalEnd = now + ratelimitIntervalMs; + return Rx.of(value); + } else if (intervalEnd >= now + maxDelay) { + // re-rate limit in the future to avoid to schedule too far in the future as some observer can unsubscribe + return Rx.of(value).pipe(delay(maxDelay, scheduler), createRateLimitOperator()); + } else { + if (++countInCurrentInterval > ratelimitRequestPerInterval) { + countInCurrentInterval = 1; + intervalEnd += ratelimitIntervalMs; + } - function createTimeout() { - if (timerSubscription) { - return; - } - timerSubscription = Rx.asyncScheduler.schedule(() => { - timerSubscription = undefined; - currentInterval = createCurrentInterval(); - for (const [waitingObserver, value] of observers) { - if (currentInterval.numRequests >= ratelimitRequestPerInterval) { - createTimeout(); - continue; + const wait = intervalEnd - ratelimitIntervalMs - now; + return wait > 0 ? Rx.of(value).pipe(delay(wait, scheduler)) : Rx.of(value); } - currentInterval.numRequests++; - waitingObserver.next(value); - } - }, ratelimitIntervalMs); + }) + ); } - - return function limit(): Rx.MonoTypeOperatorFunction { - return (observable) => - new Rx.Observable((observer) => { - const subscription = observable.subscribe({ - next(value) { - if (currentInterval.numRequests < ratelimitRequestPerInterval) { - currentInterval.numRequests++; - observer.next(value); - return; - } - - observers = [...observers, [observer, value]]; - createTimeout(); - }, - error(err) { - observer.error(err); - }, - complete() { - observer.complete(); - }, - }); - - return () => { - observers = observers.filter((o) => o[0] !== observer); - subscription.unsubscribe(); - }; - }); - }; + return createRateLimitOperator; } diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts index 4122677a615ca..fbbed87b031e2 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts @@ -10,7 +10,6 @@ import { shareReplay, distinctUntilKeyChanged, switchMap, - mergeMap, merge, filter, timeout, @@ -33,6 +32,8 @@ import { import { appContextService } from '../../app_context'; import { toPromiseAbortable, AbortError, createRateLimiter } from './rxjs_utils'; +const RATE_LIMIT_MAX_DELAY_MS = 5 * 60 * 1000; // 5 minutes + function getInternalUserSOClient() { const fakeRequest = ({ headers: {}, @@ -135,11 +136,19 @@ export function agentCheckinStateNewActionsFactory() { const agentPolicies$ = new Map>(); const newActions$ = createNewActionsSharedObservable(); // Rx operators - const rateLimiter = createRateLimiter( + const pollingTimeoutMs = appContextService.getConfig()?.fleet.pollingRequestTimeout ?? 0; + const rateLimiterIntervalMs = appContextService.getConfig()?.fleet.agentPolicyRolloutRateLimitIntervalMs ?? - AGENT_POLICY_ROLLOUT_RATE_LIMIT_INTERVAL_MS, + AGENT_POLICY_ROLLOUT_RATE_LIMIT_INTERVAL_MS; + const rateLimiterRequestPerInterval = appContextService.getConfig()?.fleet.agentPolicyRolloutRateLimitRequestPerInterval ?? - AGENT_POLICY_ROLLOUT_RATE_LIMIT_REQUEST_PER_INTERVAL + AGENT_POLICY_ROLLOUT_RATE_LIMIT_REQUEST_PER_INTERVAL; + const rateLimiterMaxDelay = Math.min(RATE_LIMIT_MAX_DELAY_MS, pollingTimeoutMs); + + const rateLimiter = createRateLimiter( + rateLimiterIntervalMs, + rateLimiterRequestPerInterval, + rateLimiterMaxDelay ); async function subscribeToNewActions( @@ -162,7 +171,7 @@ export function agentCheckinStateNewActionsFactory() { const stream$ = agentPolicy$.pipe( timeout( // Set a timeout 3s before the real timeout to have a chance to respond an empty response before socket timeout - Math.max((appContextService.getConfig()?.fleet.pollingRequestTimeout ?? 0) - 3000, 3000) + Math.max(pollingTimeoutMs - 3000, 3000) ), filter( (action) => @@ -173,9 +182,9 @@ export function agentCheckinStateNewActionsFactory() { (!agent.policy_revision || action.policy_revision > agent.policy_revision) ), rateLimiter(), - mergeMap((policyAction) => createAgentActionFromPolicyAction(soClient, agent, policyAction)), + switchMap((policyAction) => createAgentActionFromPolicyAction(soClient, agent, policyAction)), merge(newActions$), - mergeMap(async (data) => { + switchMap(async (data) => { if (!data) { return; } diff --git a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts index a57735e25ff7b..c941b0512e597 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts @@ -3,25 +3,37 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - import Boom from 'boom'; import { SavedObjectsClientContract } from 'src/core/server'; -import { - AGENT_SAVED_OBJECT_TYPE, - AGENT_EVENT_SAVED_OBJECT_TYPE, - AGENT_TYPE_EPHEMERAL, - AGENT_POLLING_THRESHOLD_MS, -} from '../../constants'; +import { AGENT_SAVED_OBJECT_TYPE, AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../constants'; import { AgentSOAttributes, Agent, AgentEventSOAttributes, ListWithKuery } from '../../types'; +import { escapeSearchQueryPhrase, normalizeKuery, findAllSOs } from '../saved_object'; import { savedObjectToAgent } from './saved_objects'; -import { escapeSearchQueryPhrase } from '../saved_object'; + +const ACTIVE_AGENT_CONDITION = `${AGENT_SAVED_OBJECT_TYPE}.attributes.active:true`; +const INACTIVE_AGENT_CONDITION = `NOT (${ACTIVE_AGENT_CONDITION})`; + +function _joinFilters(filters: string[], operator = 'AND') { + return filters.reduce((acc: string | undefined, filter) => { + if (acc) { + return `${acc} ${operator} (${filter})`; + } + + return `(${filter})`; + }, undefined); +} export async function listAgents( soClient: SavedObjectsClientContract, options: ListWithKuery & { showInactive: boolean; } -) { +): Promise<{ + agents: Agent[]; + total: number; + page: number; + perPage: number; +}> { const { page = 1, perPage = 20, @@ -30,47 +42,86 @@ export async function listAgents( kuery, showInactive = false, } = options; - const filters = []; if (kuery && kuery !== '') { - // To ensure users dont need to know about SO data structure... - filters.push( - kuery.replace( - new RegExp(`${AGENT_SAVED_OBJECT_TYPE}\.`, 'g'), - `${AGENT_SAVED_OBJECT_TYPE}.attributes.` - ) - ); + filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)); } if (showInactive === false) { - const agentActiveCondition = `${AGENT_SAVED_OBJECT_TYPE}.attributes.active:true AND not ${AGENT_SAVED_OBJECT_TYPE}.attributes.type:${AGENT_TYPE_EPHEMERAL}`; - const recentlySeenEphemeralAgent = `${AGENT_SAVED_OBJECT_TYPE}.attributes.active:true AND ${AGENT_SAVED_OBJECT_TYPE}.attributes.type:${AGENT_TYPE_EPHEMERAL} AND ${AGENT_SAVED_OBJECT_TYPE}.attributes.last_checkin > ${ - Date.now() - 3 * AGENT_POLLING_THRESHOLD_MS - }`; - filters.push(`(${agentActiveCondition}) OR (${recentlySeenEphemeralAgent})`); + filters.push(ACTIVE_AGENT_CONDITION); } - // eslint-disable-next-line @typescript-eslint/naming-convention - const { saved_objects, total } = await soClient.find({ + const { saved_objects: agentSOs, total } = await soClient.find({ type: AGENT_SAVED_OBJECT_TYPE, + filter: _joinFilters(filters), sortField, sortOrder, page, perPage, - filter: _joinFilters(filters), }); - const agents: Agent[] = saved_objects.map(savedObjectToAgent); - return { - agents, + agents: agentSOs.map(savedObjectToAgent), total, page, perPage, }; } +export async function listAllAgents( + soClient: SavedObjectsClientContract, + options: Omit & { + showInactive: boolean; + } +): Promise<{ + agents: Agent[]; + total: number; +}> { + const { sortField = 'enrolled_at', sortOrder = 'desc', kuery, showInactive = false } = options; + const filters = []; + + if (kuery && kuery !== '') { + filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)); + } + + if (showInactive === false) { + filters.push(ACTIVE_AGENT_CONDITION); + } + + const { saved_objects: agentSOs, total } = await findAllSOs(soClient, { + type: AGENT_SAVED_OBJECT_TYPE, + kuery: _joinFilters(filters), + sortField, + sortOrder, + }); + + return { + agents: agentSOs.map(savedObjectToAgent), + total, + }; +} + +export async function countInactiveAgents( + soClient: SavedObjectsClientContract, + options: Pick +): Promise { + const { kuery } = options; + const filters = [INACTIVE_AGENT_CONDITION]; + + if (kuery && kuery !== '') { + filters.push(normalizeKuery(AGENT_SAVED_OBJECT_TYPE, kuery)); + } + + const { total } = await soClient.find({ + type: AGENT_SAVED_OBJECT_TYPE, + filter: _joinFilters(filters), + perPage: 0, + }); + + return total; +} + export async function getAgent(soClient: SavedObjectsClientContract, agentId: string) { const agent = savedObjectToAgent( await soClient.get(AGENT_SAVED_OBJECT_TYPE, agentId) @@ -78,6 +129,17 @@ export async function getAgent(soClient: SavedObjectsClientContract, agentId: st return agent; } +export async function getAgents(soClient: SavedObjectsClientContract, agentIds: string[]) { + const agentSOs = await soClient.bulkGet( + agentIds.map((agentId) => ({ + id: agentId, + type: AGENT_SAVED_OBJECT_TYPE, + })) + ); + const agents = agentSOs.saved_objects.map(savedObjectToAgent); + return agents; +} + export async function getAgentByAccessAPIKeyId( soClient: SavedObjectsClientContract, accessAPIKeyId: string @@ -142,13 +204,3 @@ export async function deleteAgent(soClient: SavedObjectsClientContract, agentId: active: false, }); } - -function _joinFilters(filters: string[], operator = 'AND') { - return filters.reduce((acc: string | undefined, filter) => { - if (acc) { - return `${acc} ${operator} (${filter})`; - } - - return `(${filter})`; - }, undefined); -} diff --git a/x-pack/plugins/ingest_manager/server/services/agents/events.ts b/x-pack/plugins/ingest_manager/server/services/agents/events.ts index dfa599e4ffdfd..627fe4f231d3d 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/events.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/events.ts @@ -7,6 +7,7 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../constants'; import { AgentEventSOAttributes, AgentEvent } from '../../types'; +import { normalizeKuery } from '../saved_object'; export async function getAgentEvents( soClient: SavedObjectsClientContract, @@ -23,12 +24,7 @@ export async function getAgentEvents( const { total, saved_objects } = await soClient.find({ type: AGENT_EVENT_SAVED_OBJECT_TYPE, filter: - kuery && kuery !== '' - ? kuery.replace( - new RegExp(`${AGENT_EVENT_SAVED_OBJECT_TYPE}\.`, 'g'), - `${AGENT_EVENT_SAVED_OBJECT_TYPE}.attributes.` - ) - : undefined, + kuery && kuery !== '' ? normalizeKuery(AGENT_EVENT_SAVED_OBJECT_TYPE, kuery) : undefined, perPage, page, sortField: 'timestamp', diff --git a/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts b/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts index 3075e146093e3..345c07511f032 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts @@ -9,6 +9,7 @@ import Boom from 'boom'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { AgentSOAttributes } from '../../types'; import { agentPolicyService } from '../agent_policy'; +import { getAgents, listAllAgents } from './crud'; export async function reassignAgent( soClient: SavedObjectsClientContract, @@ -25,3 +26,44 @@ export async function reassignAgent( policy_revision: null, }); } + +export async function reassignAgents( + soClient: SavedObjectsClientContract, + options: + | { + agentIds: string[]; + } + | { + kuery: string; + }, + newAgentPolicyId: string +) { + const agentPolicy = await agentPolicyService.get(soClient, newAgentPolicyId); + if (!agentPolicy) { + throw Boom.notFound(`Agent policy not found: ${newAgentPolicyId}`); + } + + // Filter to agents that do not already use the new agent policy ID + const agents = + 'agentIds' in options + ? await getAgents(soClient, options.agentIds) + : ( + await listAllAgents(soClient, { + kuery: options.kuery, + showInactive: false, + }) + ).agents; + const agentsToUpdate = agents.filter((agent) => agent.policy_id !== newAgentPolicyId); + + // Update the necessary agents + return await soClient.bulkUpdate( + agentsToUpdate.map((agent) => ({ + type: AGENT_SAVED_OBJECT_TYPE, + id: agent.id, + attributes: { + policy_id: newAgentPolicyId, + policy_revision: null, + }, + })) + ); +} diff --git a/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts b/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts index e0ac2620cafd3..60533e1285141 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts @@ -3,13 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { chunk } from 'lodash'; import { SavedObjectsClientContract } from 'src/core/server'; import { AgentSOAttributes } from '../../types'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { getAgent } from './crud'; import * as APIKeyService from '../api_keys'; -import { createAgentAction } from './actions'; +import { createAgentAction, bulkCreateAgentActions } from './actions'; +import { getAgents, listAllAgents } from './crud'; export async function unenrollAgent(soClient: SavedObjectsClientContract, agentId: string) { const now = new Date().toISOString(); @@ -23,6 +24,53 @@ export async function unenrollAgent(soClient: SavedObjectsClientContract, agentI }); } +export async function unenrollAgents( + soClient: SavedObjectsClientContract, + options: + | { + agentIds: string[]; + } + | { + kuery: string; + } +) { + // Filter to agents that do not already unenrolled, or unenrolling + const agents = + 'agentIds' in options + ? await getAgents(soClient, options.agentIds) + : ( + await listAllAgents(soClient, { + kuery: options.kuery, + showInactive: false, + }) + ).agents; + const agentsToUpdate = agents.filter( + (agent) => !agent.unenrollment_started_at && !agent.unenrolled_at + ); + const now = new Date().toISOString(); + + // Create unenroll action for each agent + await bulkCreateAgentActions( + soClient, + agentsToUpdate.map((agent) => ({ + agent_id: agent.id, + created_at: now, + type: 'UNENROLL', + })) + ); + + // Update the necessary agents + return await soClient.bulkUpdate( + agentsToUpdate.map((agent) => ({ + type: AGENT_SAVED_OBJECT_TYPE, + id: agent.id, + attributes: { + unenrollment_started_at: now, + }, + })) + ); +} + export async function forceUnenrollAgent(soClient: SavedObjectsClientContract, agentId: string) { const agent = await getAgent(soClient, agentId); @@ -40,3 +88,63 @@ export async function forceUnenrollAgent(soClient: SavedObjectsClientContract, a unenrolled_at: new Date().toISOString(), }); } + +export async function forceUnenrollAgents( + soClient: SavedObjectsClientContract, + options: + | { + agentIds: string[]; + } + | { + kuery: string; + } +) { + // Filter to agents that are not already unenrolled + const agents = + 'agentIds' in options + ? await getAgents(soClient, options.agentIds) + : ( + await listAllAgents(soClient, { + kuery: options.kuery, + showInactive: false, + }) + ).agents; + const agentsToUpdate = agents.filter((agent) => !agent.unenrolled_at); + const now = new Date().toISOString(); + const apiKeys: string[] = []; + + // Get all API keys that need to be invalidated + agentsToUpdate.forEach((agent) => { + if (agent.access_api_key_id) { + apiKeys.push(agent.access_api_key_id); + } + if (agent.default_api_key_id) { + apiKeys.push(agent.default_api_key_id); + } + }); + + // Invalidate all API keys + // ES doesn't provide a bulk invalidate API, so this could take a long time depending on + // number of keys to invalidate. We run these in batches to avoid overloading ES. + if (apiKeys.length) { + const BATCH_SIZE = 500; + const batches = chunk(apiKeys, BATCH_SIZE); + for (const apiKeysBatch of batches) { + await Promise.all( + apiKeysBatch.map((apiKey) => APIKeyService.invalidateAPIKey(soClient, apiKey)) + ); + } + } + + // Update the necessary agents + return await soClient.bulkUpdate( + agentsToUpdate.map((agent) => ({ + type: AGENT_SAVED_OBJECT_TYPE, + id: agent.id, + attributes: { + active: false, + unenrolled_at: now, + }, + })) + ); +} diff --git a/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts b/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts index f058166fc2a4f..ea5d25dc9884f 100644 --- a/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts +++ b/x-pack/plugins/ingest_manager/server/services/api_keys/enrollment_api_key.ts @@ -12,6 +12,7 @@ import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; import { createAPIKey, invalidateAPIKey } from './security'; import { agentPolicyService } from '../agent_policy'; import { appContextService } from '../app_context'; +import { normalizeKuery } from '../saved_object'; export async function listEnrollmentApiKeys( soClient: SavedObjectsClientContract, @@ -33,10 +34,7 @@ export async function listEnrollmentApiKeys( sortOrder: 'desc', filter: kuery && kuery !== '' - ? kuery.replace( - new RegExp(`${ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE}\.`, 'g'), - `${ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE}.attributes.` - ) + ? normalizeKuery(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, kuery) : undefined, }); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 54b9c4d3fbb17..800151a41a429 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -6,6 +6,9 @@ import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; import semver from 'semver'; +import Boom from 'boom'; +import { UnwrapPromise } from '@kbn/utility-types'; +import { BulkInstallPackageInfo, IBulkInstallPackageError } from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE, MAX_TIME_COMPLETE_INSTALL } from '../../../constants'; import { AssetReference, @@ -32,10 +35,15 @@ import { ArchiveAsset, } from '../kibana/assets/install'; import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; -import { deleteKibanaSavedObjectsAssets } from './remove'; -import { PackageOutdatedError } from '../../../errors'; +import { deleteKibanaSavedObjectsAssets, removeInstallation } from './remove'; +import { + IngestManagerError, + PackageOutdatedError, + ingestErrorToResponseOptions, +} from '../../../errors'; import { getPackageSavedObjects } from './get'; import { installTransformForDataset } from '../elasticsearch/transform/install'; +import { appContextService } from '../../app_context'; export async function installLatestPackage(options: { savedObjectsClient: SavedObjectsClientContract; @@ -94,17 +102,185 @@ export async function ensureInstalledPackage(options: { return installation; } -export async function installPackage({ +export async function handleInstallPackageFailure({ savedObjectsClient, - pkgkey, + error, + pkgName, + pkgVersion, + installedPkg, callCluster, - force = false, }: { + savedObjectsClient: SavedObjectsClientContract; + error: IngestManagerError | Boom | Error; + pkgName: string; + pkgVersion: string; + installedPkg: SavedObject | undefined; + callCluster: CallESAsCurrentUser; +}) { + if (error instanceof IngestManagerError) { + return; + } + const logger = appContextService.getLogger(); + const pkgkey = Registry.pkgToPkgKey({ + name: pkgName, + version: pkgVersion, + }); + + // if there is an unknown server error, uninstall any package assets or reinstall the previous version if update + try { + const installType = getInstallType({ pkgVersion, installedPkg }); + if (installType === 'install' || installType === 'reinstall') { + logger.error(`uninstalling ${pkgkey} after error installing`); + await removeInstallation({ savedObjectsClient, pkgkey, callCluster }); + } + + if (installType === 'update') { + if (!installedPkg) { + logger.error( + `failed to rollback package after installation error ${error} because saved object was undefined` + ); + return; + } + const prevVersion = `${pkgName}-${installedPkg.attributes.version}`; + logger.error(`rolling back to ${prevVersion} after error installing ${pkgkey}`); + await installPackage({ + savedObjectsClient, + pkgkey: prevVersion, + callCluster, + }); + } + } catch (e) { + logger.error(`failed to uninstall or rollback package after installation error ${e}`); + } +} + +type BulkInstallResponse = BulkInstallPackageInfo | IBulkInstallPackageError; +function bulkInstallErrorToOptions({ + pkgToUpgrade, + error, +}: { + pkgToUpgrade: string; + error: Error; +}): IBulkInstallPackageError { + const { statusCode, body } = ingestErrorToResponseOptions(error); + return { + name: pkgToUpgrade, + statusCode, + error: body.message, + }; +} + +interface UpgradePackageParams { + savedObjectsClient: SavedObjectsClientContract; + callCluster: CallESAsCurrentUser; + installedPkg: UnwrapPromise>; + latestPkg: UnwrapPromise>; + pkgToUpgrade: string; +} +async function upgradePackage({ + savedObjectsClient, + callCluster, + installedPkg, + latestPkg, + pkgToUpgrade, +}: UpgradePackageParams): Promise { + if (!installedPkg || semver.gt(latestPkg.version, installedPkg.attributes.version)) { + const pkgkey = Registry.pkgToPkgKey({ + name: latestPkg.name, + version: latestPkg.version, + }); + + try { + const assets = await installPackage({ savedObjectsClient, pkgkey, callCluster }); + return { + name: pkgToUpgrade, + newVersion: latestPkg.version, + oldVersion: installedPkg?.attributes.version ?? null, + assets, + }; + } catch (installFailed) { + await handleInstallPackageFailure({ + savedObjectsClient, + error: installFailed, + pkgName: latestPkg.name, + pkgVersion: latestPkg.version, + installedPkg, + callCluster, + }); + return bulkInstallErrorToOptions({ pkgToUpgrade, error: installFailed }); + } + } else { + // package was already at the latest version + return { + name: pkgToUpgrade, + newVersion: latestPkg.version, + oldVersion: latestPkg.version, + assets: [ + ...installedPkg.attributes.installed_es, + ...installedPkg.attributes.installed_kibana, + ], + }; + } +} + +interface BulkInstallPackagesParams { + savedObjectsClient: SavedObjectsClientContract; + packagesToUpgrade: string[]; + callCluster: CallESAsCurrentUser; +} +export async function bulkInstallPackages({ + savedObjectsClient, + packagesToUpgrade, + callCluster, +}: BulkInstallPackagesParams): Promise { + const installedAndLatestPromises = packagesToUpgrade.map((pkgToUpgrade) => + Promise.all([ + getInstallationObject({ savedObjectsClient, pkgName: pkgToUpgrade }), + Registry.fetchFindLatestPackage(pkgToUpgrade), + ]) + ); + const installedAndLatestResults = await Promise.allSettled(installedAndLatestPromises); + const installResponsePromises = installedAndLatestResults.map(async (result, index) => { + const pkgToUpgrade = packagesToUpgrade[index]; + if (result.status === 'fulfilled') { + const [installedPkg, latestPkg] = result.value; + return upgradePackage({ + savedObjectsClient, + callCluster, + installedPkg, + latestPkg, + pkgToUpgrade, + }); + } else { + return bulkInstallErrorToOptions({ pkgToUpgrade, error: result.reason }); + } + }); + const installResults = await Promise.allSettled(installResponsePromises); + const installResponses = installResults.map((result, index) => { + const pkgToUpgrade = packagesToUpgrade[index]; + if (result.status === 'fulfilled') { + return result.value; + } else { + return bulkInstallErrorToOptions({ pkgToUpgrade, error: result.reason }); + } + }); + + return installResponses; +} + +interface InstallPackageParams { savedObjectsClient: SavedObjectsClientContract; pkgkey: string; callCluster: CallESAsCurrentUser; force?: boolean; -}): Promise { +} + +export async function installPackage({ + savedObjectsClient, + pkgkey, + callCluster, + force = false, +}: InstallPackageParams): Promise { // TODO: change epm API to /packageName/version so we don't need to do this const { pkgName, pkgVersion } = Registry.splitPkgKey(pkgkey); // TODO: calls to getInstallationObject, Registry.fetchInfo, and Registry.fetchFindLatestPackge diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts index b788d1bcbb4a9..6618220a27085 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.ts @@ -29,9 +29,8 @@ const getDefaultRegistryUrl = (): string => { }; export const getRegistryUrl = (): string => { - const license = licenseService.getLicenseInformation(); const customUrl = appContextService.getConfig()?.registryUrl; - const isGoldPlus = license?.isAvailable && license?.isActive && license?.hasAtLeast('gold'); + const isGoldPlus = licenseService.isGoldPlus(); if (customUrl && isGoldPlus) { return customUrl; diff --git a/x-pack/plugins/ingest_manager/server/services/index.ts b/x-pack/plugins/ingest_manager/server/services/index.ts index 5942277e90824..7a62c307973c2 100644 --- a/x-pack/plugins/ingest_manager/server/services/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/index.ts @@ -7,6 +7,7 @@ import { SavedObjectsClientContract, KibanaRequest } from 'kibana/server'; import { AgentStatus, Agent, EsAssetReference } from '../types'; import * as settingsService from './settings'; +import { getAgent, listAgents } from './agents'; export { ESIndexPatternSavedObjectService } from './es_index_pattern'; export { getRegistryUrl } from './epm/registry/registry_url'; @@ -40,7 +41,7 @@ export interface AgentService { /** * Get an Agent by id */ - getAgent(soClient: SavedObjectsClientContract, agentId: string): Promise; + getAgent: typeof getAgent; /** * Authenticate an agent with access toekn */ @@ -55,20 +56,7 @@ export interface AgentService { /** * List agents */ - listAgents( - soClient: SavedObjectsClientContract, - options: { - page: number; - perPage: number; - kuery?: string; - showInactive: boolean; - } - ): Promise<{ - agents: Agent[]; - total: number; - page: number; - perPage: number; - }>; + listAgents: typeof listAgents; } // Saved object services diff --git a/x-pack/plugins/ingest_manager/server/services/license.ts b/x-pack/plugins/ingest_manager/server/services/license.ts index bd96dbc7e3aff..a67ec9880ec09 100644 --- a/x-pack/plugins/ingest_manager/server/services/license.ts +++ b/x-pack/plugins/ingest_manager/server/services/license.ts @@ -3,36 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Observable, Subscription } from 'rxjs'; -import { ILicense } from '../../../licensing/server'; - -class LicenseService { - private observable: Observable | null = null; - private subscription: Subscription | null = null; - private licenseInformation: ILicense | null = null; - - private updateInformation(licenseInformation: ILicense) { - this.licenseInformation = licenseInformation; - } - - public start(license$: Observable) { - this.observable = license$; - this.subscription = this.observable.subscribe(this.updateInformation.bind(this)); - } - - public stop() { - if (this.subscription) { - this.subscription.unsubscribe(); - } - } - - public getLicenseInformation() { - return this.licenseInformation; - } - - public getLicenseInformation$() { - return this.observable; - } -} +import { LicenseService } from '../../common'; export const licenseService = new LicenseService(); diff --git a/x-pack/plugins/ingest_manager/server/services/package_policy.ts b/x-pack/plugins/ingest_manager/server/services/package_policy.ts index b7e1806979db8..3a02544250ff0 100644 --- a/x-pack/plugins/ingest_manager/server/services/package_policy.ts +++ b/x-pack/plugins/ingest_manager/server/services/package_policy.ts @@ -30,6 +30,7 @@ import * as Registry from './epm/registry'; import { getPackageInfo, getInstallation, ensureInstalledPackage } from './epm/packages'; import { getAssetsData } from './epm/packages/assets'; import { createStream } from './epm/agent/agent'; +import { normalizeKuery } from './saved_object'; const SAVED_OBJECT_TYPE = PACKAGE_POLICY_SAVED_OBJECT_TYPE; @@ -211,13 +212,7 @@ class PackagePolicyService { sortOrder, page, perPage, - // To ensure users don't need to know about SO data structure... - filter: kuery - ? kuery.replace( - new RegExp(`${SAVED_OBJECT_TYPE}\.`, 'g'), - `${SAVED_OBJECT_TYPE}.attributes.` - ) - : undefined, + filter: kuery ? normalizeKuery(SAVED_OBJECT_TYPE, kuery) : undefined, }); return { diff --git a/x-pack/plugins/ingest_manager/server/services/saved_object.ts b/x-pack/plugins/ingest_manager/server/services/saved_object.ts index 8fe7ffcdfc896..06772206d5198 100644 --- a/x-pack/plugins/ingest_manager/server/services/saved_object.ts +++ b/x-pack/plugins/ingest_manager/server/services/saved_object.ts @@ -3,6 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { SavedObjectsClientContract, SavedObjectsFindResponse } from 'src/core/server'; +import { ListWithKuery } from '../types'; /** * Escape a value with double quote to use with saved object search @@ -12,3 +14,70 @@ export function escapeSearchQueryPhrase(val: string): string { return `"${val.replace(/["]/g, '"')}"`; } + +// Adds `.attribute` to any kuery strings that are missing it, this comes from +// internal SO structure. Kuery strings that come from UI will typicall have +// `.attribute` hidden to simplify UX, so this normalizes any kuery string for +// filtering SOs +export const normalizeKuery = (savedObjectType: string, kuery: string): string => { + return kuery.replace( + new RegExp(`${savedObjectType}\.(?!attributes\.)`, 'g'), + `${savedObjectType}.attributes.` + ); +}; + +// Like saved object client `.find()`, but ignores `page` and `perPage` parameters and +// returns *all* matching saved objects by collocating results from all `.find` pages. +// This function actually doesn't offer any additional benefits over `.find()` for now +// due to SO client limitations (see comments below), so is a placeholder for when SO +// client is improved. +export const findAllSOs = async ( + soClient: SavedObjectsClientContract, + options: Omit & { + type: string; + } +): Promise, 'saved_objects' | 'total'>> => { + const { type, sortField, sortOrder, kuery } = options; + let savedObjectResults: SavedObjectsFindResponse['saved_objects'] = []; + + // TODO: This is the default `index.max_result_window` ES setting, which dictates + // the maximum amount of results allowed to be returned from a search. It's possible + // for the actual setting to differ from the default. Can we retrieve the real + // setting in the future? + const searchLimit = 10000; + + const query = { + type, + sortField, + sortOrder, + filter: kuery, + page: 1, + perPage: searchLimit, + }; + + const { saved_objects: initialSOs, total } = await soClient.find(query); + + savedObjectResults = initialSOs; + + // The saved object client can't actually page through more than the first 10,000 + // results, due to the same `index.max_result_window` constraint. The commented out + // code below is an example of paging through rest of results when the SO client + // offers that kind of support. + // if (total > searchLimit) { + // const remainingPages = Math.ceil((total - searchLimit) / searchLimit); + // for (let currentPage = 2; currentPage <= remainingPages + 1; currentPage++) { + // const { saved_objects: currentPageSavedObjects } = await soClient.find({ + // ...query, + // page: currentPage, + // }); + // if (currentPageSavedObjects.length) { + // savedObjectResults = savedObjectResults.concat(currentPageSavedObjects); + // } + // } + // } + + return { + saved_objects: savedObjectResults, + total, + }; +}; diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts index 43ee0c89126e9..4aefa56e0ca0a 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts @@ -172,6 +172,13 @@ export const PostAgentUnenrollRequestSchema = { ), }; +export const PostBulkAgentUnenrollRequestSchema = { + body: schema.object({ + agents: schema.oneOf([schema.arrayOf(schema.string()), schema.string()]), + force: schema.maybe(schema.boolean()), + }), +}; + export const PutAgentReassignRequestSchema = { params: schema.object({ agentId: schema.string(), @@ -181,6 +188,13 @@ export const PutAgentReassignRequestSchema = { }), }; +export const PostBulkAgentReassignRequestSchema = { + body: schema.object({ + policy_id: schema.string(), + agents: schema.oneOf([schema.arrayOf(schema.string()), schema.string()]), + }), +}; + export const GetOneAgentEventsRequestSchema = { params: schema.object({ agentId: schema.string(), diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts index d7a801feec34f..5d2a078374854 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts @@ -43,6 +43,12 @@ export const InstallPackageFromRegistryRequestSchema = { ), }; +export const BulkUpgradePackagesFromRegistryRequestSchema = { + body: schema.object({ + packages: schema.arrayOf(schema.string(), { minSize: 1 }), + }), +}; + export const InstallPackageByUploadRequestSchema = { body: schema.buffer(), }; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_clone.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_clone.helpers.ts index f369bfe66f642..2559b93bd606d 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_clone.helpers.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_clone.helpers.ts @@ -5,10 +5,10 @@ */ import { registerTestBed, TestBedConfig, TestBed } from '../../../../../test_utils'; -import { BASE_PATH } from '../../../common/constants'; import { PipelinesClone } from '../../../public/application/sections/pipelines_clone'; import { getFormActions, PipelineFormTestSubjects } from './pipeline_form.helpers'; import { WithAppDependencies } from './setup_environment'; +import { getClonePath, ROUTES } from '../../../public/application/services/navigation'; export type PipelinesCloneTestBed = TestBed & { actions: ReturnType; @@ -29,8 +29,8 @@ export const PIPELINE_TO_CLONE = { const testBedConfig: TestBedConfig = { memoryRouter: { - initialEntries: [`${BASE_PATH}create/${PIPELINE_TO_CLONE.name}`], - componentRoutePath: `${BASE_PATH}create/:name`, + initialEntries: [getClonePath({ clonedPipelineName: PIPELINE_TO_CLONE.name })], + componentRoutePath: ROUTES.clone, }, doMountAsync: true, }; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create.helpers.ts index ce5ab1faa01be..22f68f12804d6 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create.helpers.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_create.helpers.ts @@ -5,10 +5,10 @@ */ import { registerTestBed, TestBedConfig, TestBed } from '../../../../../test_utils'; -import { BASE_PATH } from '../../../common/constants'; import { PipelinesCreate } from '../../../public/application/sections/pipelines_create'; import { getFormActions, PipelineFormTestSubjects } from './pipeline_form.helpers'; import { WithAppDependencies } from './setup_environment'; +import { getCreatePath, ROUTES } from '../../../public/application/services/navigation'; export type PipelinesCreateTestBed = TestBed & { actions: ReturnType; @@ -16,8 +16,8 @@ export type PipelinesCreateTestBed = TestBed & { const testBedConfig: TestBedConfig = { memoryRouter: { - initialEntries: [`${BASE_PATH}/create`], - componentRoutePath: `${BASE_PATH}/create`, + initialEntries: [getCreatePath()], + componentRoutePath: ROUTES.create, }, doMountAsync: true, }; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_edit.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_edit.helpers.ts index 31c9630086178..5e0739f78eecd 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_edit.helpers.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_edit.helpers.ts @@ -5,10 +5,10 @@ */ import { registerTestBed, TestBedConfig, TestBed } from '../../../../../test_utils'; -import { BASE_PATH } from '../../../common/constants'; import { PipelinesEdit } from '../../../public/application/sections/pipelines_edit'; import { getFormActions, PipelineFormTestSubjects } from './pipeline_form.helpers'; import { WithAppDependencies } from './setup_environment'; +import { getEditPath, ROUTES } from '../../../public/application/services/navigation'; export type PipelinesEditTestBed = TestBed & { actions: ReturnType; @@ -29,8 +29,8 @@ export const PIPELINE_TO_EDIT = { const testBedConfig: TestBedConfig = { memoryRouter: { - initialEntries: [`${BASE_PATH}edit/${PIPELINE_TO_EDIT.name}`], - componentRoutePath: `${BASE_PATH}edit/:name`, + initialEntries: [getEditPath({ pipelineName: PIPELINE_TO_EDIT.name })], + componentRoutePath: ROUTES.edit, }, doMountAsync: true, }; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts index 03ffe361bb5a6..43ca849e61aee 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipelines_list.helpers.ts @@ -6,7 +6,6 @@ import { act } from 'react-dom/test-utils'; -import { BASE_PATH } from '../../../common/constants'; import { registerTestBed, TestBed, @@ -16,11 +15,12 @@ import { } from '../../../../../test_utils'; import { PipelinesList } from '../../../public/application/sections/pipelines_list'; import { WithAppDependencies } from './setup_environment'; +import { getListPath, ROUTES } from '../../../public/application/services/navigation'; const testBedConfig: TestBedConfig = { memoryRouter: { - initialEntries: [BASE_PATH], - componentRoutePath: BASE_PATH, + initialEntries: [getListPath()], + componentRoutePath: ROUTES.list, }, doMountAsync: true, }; diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx index 6074c64d2bdb0..18ca71f2bb73a 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx @@ -185,30 +185,5 @@ describe('', () => { expect(find('savePipelineError').find('li').length).toBe(8); }); }); - - describe('test pipeline', () => { - beforeEach(async () => { - await act(async () => { - testBed = await setup(); - - const { waitFor } = testBed; - - await waitFor('pipelineForm'); - }); - }); - - test('should open the test pipeline flyout', async () => { - const { actions, exists, find, waitFor } = testBed; - - await act(async () => { - actions.clickAddDocumentsButton(); - await waitFor('testPipelineFlyout'); - }); - - // Verify test pipeline flyout opens - expect(exists('testPipelineFlyout')).toBe(true); - expect(find('testPipelineFlyout.title').text()).toBe('Test pipeline'); - }); - }); }); }); diff --git a/x-pack/plugins/ingest_pipelines/common/constants.ts b/x-pack/plugins/ingest_pipelines/common/constants.ts index 4c6c6fefaad83..0d6f977bfbfed 100644 --- a/x-pack/plugins/ingest_pipelines/common/constants.ts +++ b/x-pack/plugins/ingest_pipelines/common/constants.ts @@ -9,9 +9,9 @@ const basicLicense: LicenseType = 'basic'; export const PLUGIN_ID = 'ingest_pipelines'; -export const PLUGIN_MIN_LICENSE_TYPE = basicLicense; +export const MANAGEMENT_APP_ID = 'management'; -export const BASE_PATH = '/'; +export const PLUGIN_MIN_LICENSE_TYPE = basicLicense; export const API_BASE_PATH = '/api/ingest_pipelines'; diff --git a/x-pack/plugins/ingest_pipelines/kibana.json b/x-pack/plugins/ingest_pipelines/kibana.json index 38d28fbba20b4..2fe87c5e7a162 100644 --- a/x-pack/plugins/ingest_pipelines/kibana.json +++ b/x-pack/plugins/ingest_pipelines/kibana.json @@ -3,7 +3,7 @@ "version": "8.0.0", "server": true, "ui": true, - "requiredPlugins": ["licensing", "management", "features"], + "requiredPlugins": ["licensing", "management", "features", "share"], "optionalPlugins": ["security", "usageCollection"], "configPath": ["xpack", "ingest_pipelines"], "requiredBundles": ["esUiShared", "kibanaReact"] diff --git a/x-pack/plugins/ingest_pipelines/public/application/app.tsx b/x-pack/plugins/ingest_pipelines/public/application/app.tsx index 55b59caab8d60..e78c4d3983183 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/app.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/app.tsx @@ -21,13 +21,14 @@ import { } from '../shared_imports'; import { PipelinesList, PipelinesCreate, PipelinesEdit, PipelinesClone } from './sections'; +import { ROUTES } from './services/navigation'; export const AppWithoutRouter = () => ( - - - - + + + + {/* Catch all */} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx index 5279bd718c16e..ffd82b0bbaf35 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx @@ -11,8 +11,6 @@ import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from import { useForm, Form, FormConfig } from '../../../shared_imports'; import { Pipeline, Processor } from '../../../../common/types'; -import './pipeline_form.scss'; - import { OnUpdateHandlerArg, OnUpdateHandler } from '../pipeline_processors_editor'; import { PipelineRequestFlyout } from './pipeline_request_flyout'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx index 6033f34af6825..a7ffe7ba02caa 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx @@ -6,7 +6,7 @@ import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiSpacer, EuiSwitch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiSpacer, EuiSwitch } from '@elastic/eui'; import { Processor } from '../../../../common/types'; @@ -14,15 +14,11 @@ import { getUseField, getFormRow, Field } from '../../../shared_imports'; import { ProcessorsEditorContextProvider, - GlobalOnFailureProcessorsEditor, - ProcessorsEditor, OnUpdateHandler, OnDoneLoadJsonHandler, + PipelineProcessorsEditor, } from '../pipeline_processors_editor'; -import { ProcessorsHeader } from './processors_header'; -import { OnFailureProcessorsTitle } from './on_failure_processors_title'; - interface Props { processors: Processor[]; onFailure?: Processor[]; @@ -118,28 +114,12 @@ export const PipelineFormFields: React.FunctionComponent = ({ {/* Pipeline Processors Editor */} - -
- - - - - - - - - - - - - - -
+
); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx index e46e5156e30f3..10fb73df1ce1c 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx @@ -11,12 +11,7 @@ import { notificationServiceMock, scopedHistoryMock } from 'src/core/public/mock import { LocationDescriptorObject } from 'history'; import { KibanaContextProvider } from 'src/plugins/kibana_react/public'; import { registerTestBed, TestBed } from '../../../../../../../test_utils'; -import { - ProcessorsEditorContextProvider, - Props, - ProcessorsEditor, - GlobalOnFailureProcessorsEditor, -} from '../'; +import { ProcessorsEditorContextProvider, Props, PipelineProcessorsEditor } from '../'; import { breadcrumbService, @@ -90,7 +85,7 @@ const testBedSetup = registerTestBed( (props: Props) => ( - + ), @@ -210,4 +205,5 @@ type TestSubject = | 'processorSettingsFormFlyout' | 'processorTypeSelector' | 'pipelineEditorOnFailureTree' + | 'processorsEmptyPrompt' | string; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx index 74ae8b8894b9f..b80d238362118 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx @@ -55,6 +55,23 @@ describe('Pipeline Editor', () => { expect(arg.getData()).toEqual(testProcessors); }); + describe('no processors', () => { + beforeEach(async () => { + testBed = await setup({ + value: { + processors: [], + }, + onFlyoutOpen: jest.fn(), + onUpdate, + }); + }); + + it('displays an empty prompt if no processors are defined', () => { + const { exists } = testBed; + expect(exists('processorsEmptyPrompt')).toBe(true); + }); + }); + describe('processors', () => { it('adds a new processor', async () => { const { actions } = testBed; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/add_processor_button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/add_processor_button.tsx index 4aabcc1d59d73..03b497320dfbc 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/add_processor_button.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/add_processor_button.tsx @@ -6,30 +6,49 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiButtonEmpty } from '@elastic/eui'; +import { EuiButtonEmpty, EuiButton } from '@elastic/eui'; import { usePipelineProcessorsContext } from '../context'; export interface Props { onClick: () => void; + renderButtonAsLink?: boolean; } +const addProcessorButtonLabel = i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.addProcessorButtonLabel', + { + defaultMessage: 'Add a processor', + } +); + export const AddProcessorButton: FunctionComponent = (props) => { - const { onClick } = props; + const { onClick, renderButtonAsLink } = props; const { state: { editor }, } = usePipelineProcessorsContext(); + + if (renderButtonAsLink) { + return ( + + {addProcessorButtonLabel} + + ); + } + return ( - - {i18n.translate('xpack.ingestPipelines.pipelineEditor.addProcessorButtonLabel', { - defaultMessage: 'Add a processor', - })} - + {addProcessorButtonLabel} + ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts index d476202aa43bb..2e62a81ffa153 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts @@ -19,3 +19,9 @@ export { OnDoneLoadJsonHandler, LoadFromJsonButton } from './load_from_json'; export { TestPipelineActions } from './test_pipeline'; export { PipelineProcessorsItemTooltip, Position } from './pipeline_processors_editor_item_tooltip'; + +export { ProcessorsEmptyPrompt } from './processors_empty_prompt'; + +export { ProcessorsHeader } from './processors_header'; + +export { OnFailureProcessorsTitle } from './on_failure_processors_title'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/button.tsx index 21d15fc86a0ce..38700d6a7a87c 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/button.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/button.tsx @@ -15,7 +15,7 @@ interface Props { const i18nTexts = { buttonLabel: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.buttonLabel', { - defaultMessage: 'Import', + defaultMessage: 'Import processors', }), }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/on_failure_processors_title.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx similarity index 96% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/on_failure_processors_title.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx index 0beb5657b54cb..7adc37d1897d1 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/on_failure_processors_title.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx @@ -8,7 +8,7 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useKibana } from '../../../shared_imports'; +import { useKibana } from '../../../../shared_imports'; export const OnFailureProcessorsTitle: FunctionComponent = () => { const { services } = useKibana(); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_form.container.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_form.container.tsx index 332908d0756f2..c3b1799ac2a28 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_form.container.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_form.container.tsx @@ -113,15 +113,15 @@ export const ProcessorFormContainer: FunctionComponent = ({ handleSubmit={handleSubmit} /> ); - } else { - return ( - - ); } + + return ( + + ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_empty_prompt.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_empty_prompt.tsx new file mode 100644 index 0000000000000..3750ddda25d10 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_empty_prompt.tsx @@ -0,0 +1,73 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiEmptyPrompt, EuiSpacer, EuiLink } from '@elastic/eui'; +import { useKibana } from '../../../../shared_imports'; +import { usePipelineProcessorsContext } from '../context'; +import { AddProcessorButton } from './add_processor_button'; +import { OnDoneLoadJsonHandler, LoadFromJsonButton } from './load_from_json'; + +const i18nTexts = { + emptyPromptTitle: i18n.translate('xpack.ingestPipelines.pipelineEditor.emptyPrompt.title', { + defaultMessage: 'Add your first processor', + }), +}; + +export interface Props { + onLoadJson: OnDoneLoadJsonHandler; +} + +export const ProcessorsEmptyPrompt: FunctionComponent = ({ onLoadJson }) => { + const { onTreeAction } = usePipelineProcessorsContext(); + const { services } = useKibana(); + + return ( + {i18nTexts.emptyPromptTitle}} + data-test-subj="processorsEmptyPrompt" + body={ +

+ + {i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.processorsDocumentationLink', + { + defaultMessage: 'Learn more.', + } + )} + + ), + }} + /> +

+ } + actions={ + <> + { + onTreeAction({ type: 'addProcessor', payload: { target: ['processors'] } }); + }} + /> + + + + + + } + /> + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/processors_header.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_header.tsx similarity index 78% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/processors_header.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_header.tsx index 43477affa8d94..24f3207d6bea4 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/processors_header.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_header.tsx @@ -9,21 +9,32 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, EuiTitle } from '@elastic/ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useKibana } from '../../../shared_imports'; +import { useKibana } from '../../../../shared_imports'; -import { - LoadFromJsonButton, - OnDoneLoadJsonHandler, - TestPipelineActions, -} from '../pipeline_processors_editor'; +import { LoadFromJsonButton, OnDoneLoadJsonHandler, TestPipelineActions } from './'; export interface Props { onLoadJson: OnDoneLoadJsonHandler; + hasProcessors: boolean; } -export const ProcessorsHeader: FunctionComponent = ({ onLoadJson }) => { +export const ProcessorsHeader: FunctionComponent = ({ onLoadJson, hasProcessors }) => { const { services } = useKibana(); + const ProcessorTitle: FunctionComponent = () => ( + +

+ {i18n.translate('xpack.ingestPipelines.pipelineEditor.processorsTreeTitle', { + defaultMessage: 'Processors', + })} +

+
+ ); + + if (!hasProcessors) { + return ; + } + return ( = ({ onLoadJson }) => { - -

- {i18n.translate('xpack.ingestPipelines.pipelineEditor.processorsTreeTitle', { - defaultMessage: 'Processors', - })} -

-
+
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/tree_node.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/tree_node.tsx index e9008e6f5b693..3a8299c017d8d 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/tree_node.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/tree_node.tsx @@ -70,6 +70,7 @@ export const TreeNode: FunctionComponent = ({ /> onAction({ type: 'addProcessor', diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.tsx index 8b344a137f3a8..ffc0a1459b791 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.tsx @@ -99,7 +99,7 @@ export const ProcessorsTree: FunctionComponent = memo((props) => { - + {!processors.length && ( = memo((props) => { onClick={() => { onAction({ type: 'addProcessor', payload: { target: baseSelector } }); }} + renderButtonAsLink /> diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/index.ts index c462b19c79327..ca5184da25a07 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/index.ts @@ -15,3 +15,5 @@ export { OnUpdateHandlerArg, OnUpdateHandler } from './types'; export { SerializeResult } from './serialize'; export { LoadFromJsonButton, OnDoneLoadJsonHandler, TestPipelineActions } from './components'; + +export { PipelineProcessorsEditor } from './pipeline_processors_editor'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.scss similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.scss rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.scss diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx new file mode 100644 index 0000000000000..beb165973d3cd --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.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 { EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { usePipelineProcessorsContext } from './context'; +import { + ProcessorsEmptyPrompt, + OnFailureProcessorsTitle, + ProcessorsHeader, + OnDoneLoadJsonHandler, +} from './components'; +import { ProcessorsEditor, GlobalOnFailureProcessorsEditor } from './editors'; + +import './pipeline_processors_editor.scss'; + +interface Props { + onLoadJson: OnDoneLoadJsonHandler; +} + +export const PipelineProcessorsEditor: React.FunctionComponent = ({ onLoadJson }) => { + const { + state: { processors: allProcessors }, + } = usePipelineProcessorsContext(); + + const { + state: { processors, onFailure }, + } = allProcessors; + + const showEmptyPrompt = processors.length === 0 && onFailure.length === 0; + + let content: React.ReactNode; + + if (showEmptyPrompt) { + content = ; + } else { + content = ( + <> + + + + + + + + + ); + } + + return ( +
+ + + 0} /> + + + {content} + + +
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx index acca1c4e03f40..d4aa11715248e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_create/pipelines_create.tsx @@ -16,7 +16,7 @@ import { EuiSpacer, } from '@elastic/eui'; -import { BASE_PATH } from '../../../../common/constants'; +import { getListPath } from '../../services/navigation'; import { Pipeline } from '../../../../common/types'; import { useKibana } from '../../../shared_imports'; import { PipelineForm } from '../../components'; @@ -50,11 +50,11 @@ export const PipelinesCreate: React.FunctionComponent { - history.push(BASE_PATH); + history.push(getListPath()); }; useEffect(() => { diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx index e09cf4820771f..35ca1635ab9c3 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_edit/pipelines_edit.tsx @@ -17,11 +17,11 @@ import { } from '@elastic/eui'; import { EuiCallOut } from '@elastic/eui'; -import { BASE_PATH } from '../../../../common/constants'; import { Pipeline } from '../../../../common/types'; import { useKibana, SectionLoading } from '../../../shared_imports'; -import { PipelineForm } from '../../components'; +import { getListPath } from '../../services/navigation'; +import { PipelineForm } from '../../components'; import { attemptToURIDecode } from '../shared'; interface MatchParams { @@ -56,11 +56,11 @@ export const PipelinesEdit: React.FunctionComponent { - history.push(BASE_PATH); + history.push(getListPath()); }; useEffect(() => { diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx index eba69ff454911..7f4caa09b6df0 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/empty_list.tsx @@ -11,6 +11,7 @@ import { useHistory } from 'react-router-dom'; import { ScopedHistory } from 'kibana/public'; import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; import { useKibana } from '../../../shared_imports'; +import { getCreatePath } from '../../services/navigation'; export const EmptyList: FunctionComponent = () => { const { services } = useKibana(); @@ -44,7 +45,11 @@ export const EmptyList: FunctionComponent = () => {

} actions={ - + {i18n.translate('xpack.ingestPipelines.list.table.emptyPrompt.createButtonLabel', { defaultMessage: 'Create a pipeline', })} diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx index 88148f1bc5746..be31f86e30c27 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx @@ -24,9 +24,9 @@ import { } from '@elastic/eui'; import { Pipeline } from '../../../../common/types'; -import { BASE_PATH } from '../../../../common/constants'; import { useKibana, SectionLoading } from '../../../shared_imports'; import { UIM_PIPELINES_LIST_LOAD } from '../../constants'; +import { getEditPath, getClonePath, getListPath } from '../../services/navigation'; import { EmptyList } from './empty_list'; import { PipelineTable } from './table'; @@ -67,17 +67,17 @@ export const PipelinesList: React.FunctionComponent = ({ } }, [pipelineNameFromLocation, data]); - const goToEditPipeline = (name: string) => { - history.push(`${BASE_PATH}/edit/${encodeURIComponent(name)}`); + const goToEditPipeline = (pipelineName: string) => { + history.push(getEditPath({ pipelineName })); }; - const goToClonePipeline = (name: string) => { - history.push(`${BASE_PATH}/create/${encodeURIComponent(name)}`); + const goToClonePipeline = (clonedPipelineName: string) => { + history.push(getClonePath({ clonedPipelineName })); }; const goHome = () => { setShowFlyout(false); - history.push(BASE_PATH); + history.push(getListPath()); }; if (data && data.length === 0) { diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/navigation.ts b/x-pack/plugins/ingest_pipelines/public/application/services/navigation.ts new file mode 100644 index 0000000000000..3ac3de6eac710 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/services/navigation.ts @@ -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. + */ + +const BASE_PATH = '/'; + +const EDIT_PATH = 'edit'; + +const CREATE_PATH = 'create'; + +const _getEditPath = (name: string, encode = true): string => { + return `${BASE_PATH}${EDIT_PATH}/${encode ? encodeURIComponent(name) : name}`; +}; + +const _getCreatePath = (): string => { + return `${BASE_PATH}${CREATE_PATH}`; +}; + +const _getClonePath = (name: string, encode = true): string => { + return `${BASE_PATH}${CREATE_PATH}/${encode ? encodeURIComponent(name) : name}`; +}; +const _getListPath = (name?: string): string => { + return `${BASE_PATH}${name ? `?pipeline=${encodeURIComponent(name)}` : ''}`; +}; + +export const ROUTES = { + list: _getListPath(), + edit: _getEditPath(':name', false), + create: _getCreatePath(), + clone: _getClonePath(':sourceName', false), +}; + +export const getListPath = ({ + inspectedPipelineName, +}: { + inspectedPipelineName?: string; +} = {}): string => _getListPath(inspectedPipelineName); +export const getEditPath = ({ pipelineName }: { pipelineName: string }): string => + _getEditPath(pipelineName, true); +export const getCreatePath = (): string => _getCreatePath(); +export const getClonePath = ({ clonedPipelineName }: { clonedPipelineName: string }): string => + _getClonePath(clonedPipelineName, true); diff --git a/x-pack/plugins/ingest_pipelines/public/index.ts b/x-pack/plugins/ingest_pipelines/public/index.ts index 7247973703804..637d4aad7264a 100644 --- a/x-pack/plugins/ingest_pipelines/public/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/index.ts @@ -9,3 +9,10 @@ import { IngestPipelinesPlugin } from './plugin'; export function plugin() { return new IngestPipelinesPlugin(); } + +export { + INGEST_PIPELINES_APP_ULR_GENERATOR, + IngestPipelinesUrlGenerator, + IngestPipelinesUrlGeneratorState, + INGEST_PIPELINES_PAGES, +} from './url_generator'; diff --git a/x-pack/plugins/ingest_pipelines/public/plugin.ts b/x-pack/plugins/ingest_pipelines/public/plugin.ts index 339068f185d1d..6c2f4a0898327 100644 --- a/x-pack/plugins/ingest_pipelines/public/plugin.ts +++ b/x-pack/plugins/ingest_pipelines/public/plugin.ts @@ -10,10 +10,11 @@ import { CoreSetup, Plugin } from 'src/core/public'; import { PLUGIN_ID } from '../common/constants'; import { uiMetricService, apiService } from './application/services'; import { Dependencies } from './types'; +import { registerUrlGenerator } from './url_generator'; export class IngestPipelinesPlugin implements Plugin { public setup(coreSetup: CoreSetup, plugins: Dependencies): void { - const { management, usageCollection } = plugins; + const { management, usageCollection, share } = plugins; const { http, getStartServices } = coreSetup; // Initialize services @@ -46,6 +47,8 @@ export class IngestPipelinesPlugin implements Plugin { }; }, }); + + registerUrlGenerator(coreSetup, management, share); } public start() {} diff --git a/x-pack/plugins/ingest_pipelines/public/types.ts b/x-pack/plugins/ingest_pipelines/public/types.ts index 91783ea04fa9a..e968c87226d07 100644 --- a/x-pack/plugins/ingest_pipelines/public/types.ts +++ b/x-pack/plugins/ingest_pipelines/public/types.ts @@ -6,8 +6,10 @@ import { ManagementSetup } from 'src/plugins/management/public'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; +import { SharePluginSetup } from '../../../../src/plugins/share/public'; export interface Dependencies { management: ManagementSetup; usageCollection: UsageCollectionSetup; + share: SharePluginSetup; } diff --git a/x-pack/plugins/ingest_pipelines/public/url_generator.test.ts b/x-pack/plugins/ingest_pipelines/public/url_generator.test.ts new file mode 100644 index 0000000000000..1267d526fb7d4 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/url_generator.test.ts @@ -0,0 +1,107 @@ +/* + * 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 { IngestPipelinesUrlGenerator, INGEST_PIPELINES_PAGES } from './url_generator'; + +describe('IngestPipelinesUrlGenerator', () => { + const getAppBasePath = (absolute: boolean = false) => { + if (absolute) { + return Promise.resolve('http://localhost/app/test_app'); + } + return Promise.resolve('/app/test_app'); + }; + const urlGenerator = new IngestPipelinesUrlGenerator(getAppBasePath); + + describe('Pipelines List', () => { + it('generates relative url for list without pipelineId', async () => { + const url = await urlGenerator.createUrl({ + page: INGEST_PIPELINES_PAGES.LIST, + }); + expect(url).toBe('/app/test_app/'); + }); + + it('generates absolute url for list without pipelineId', async () => { + const url = await urlGenerator.createUrl({ + page: INGEST_PIPELINES_PAGES.LIST, + absolute: true, + }); + expect(url).toBe('http://localhost/app/test_app/'); + }); + it('generates relative url for list with a pipelineId', async () => { + const url = await urlGenerator.createUrl({ + page: INGEST_PIPELINES_PAGES.LIST, + pipelineId: 'pipeline_name', + }); + expect(url).toBe('/app/test_app/?pipeline=pipeline_name'); + }); + + it('generates absolute url for list with a pipelineId', async () => { + const url = await urlGenerator.createUrl({ + page: INGEST_PIPELINES_PAGES.LIST, + pipelineId: 'pipeline_name', + absolute: true, + }); + expect(url).toBe('http://localhost/app/test_app/?pipeline=pipeline_name'); + }); + }); + + describe('Pipeline Edit', () => { + it('generates relative url for pipeline edit', async () => { + const url = await urlGenerator.createUrl({ + page: INGEST_PIPELINES_PAGES.EDIT, + pipelineId: 'pipeline_name', + }); + expect(url).toBe('/app/test_app/edit/pipeline_name'); + }); + + it('generates absolute url for pipeline edit', async () => { + const url = await urlGenerator.createUrl({ + page: INGEST_PIPELINES_PAGES.EDIT, + pipelineId: 'pipeline_name', + absolute: true, + }); + expect(url).toBe('http://localhost/app/test_app/edit/pipeline_name'); + }); + }); + + describe('Pipeline Clone', () => { + it('generates relative url for pipeline clone', async () => { + const url = await urlGenerator.createUrl({ + page: INGEST_PIPELINES_PAGES.CLONE, + pipelineId: 'pipeline_name', + }); + expect(url).toBe('/app/test_app/create/pipeline_name'); + }); + + it('generates absolute url for pipeline clone', async () => { + const url = await urlGenerator.createUrl({ + page: INGEST_PIPELINES_PAGES.CLONE, + pipelineId: 'pipeline_name', + absolute: true, + }); + expect(url).toBe('http://localhost/app/test_app/create/pipeline_name'); + }); + }); + + describe('Pipeline Create', () => { + it('generates relative url for pipeline create', async () => { + const url = await urlGenerator.createUrl({ + page: INGEST_PIPELINES_PAGES.CREATE, + pipelineId: 'pipeline_name', + }); + expect(url).toBe('/app/test_app/create'); + }); + + it('generates absolute url for pipeline create', async () => { + const url = await urlGenerator.createUrl({ + page: INGEST_PIPELINES_PAGES.CREATE, + pipelineId: 'pipeline_name', + absolute: true, + }); + expect(url).toBe('http://localhost/app/test_app/create'); + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/url_generator.ts b/x-pack/plugins/ingest_pipelines/public/url_generator.ts new file mode 100644 index 0000000000000..043d449a0440a --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/url_generator.ts @@ -0,0 +1,98 @@ +/* + * 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 { CoreSetup } from 'src/core/public'; +import { MANAGEMENT_APP_ID } from '../../../../src/plugins/management/public'; +import { UrlGeneratorsDefinition } from '../../../../src/plugins/share/public'; +import { + getClonePath, + getCreatePath, + getEditPath, + getListPath, +} from './application/services/navigation'; +import { Dependencies } from './types'; +import { PLUGIN_ID } from '../common/constants'; + +export const INGEST_PIPELINES_APP_ULR_GENERATOR = 'INGEST_PIPELINES_APP_URL_GENERATOR'; + +export enum INGEST_PIPELINES_PAGES { + LIST = 'pipelines_list', + EDIT = 'pipeline_edit', + CREATE = 'pipeline_create', + CLONE = 'pipeline_clone', +} + +interface UrlGeneratorState { + pipelineId: string; + absolute?: boolean; +} +export interface PipelinesListUrlGeneratorState extends Partial { + page: INGEST_PIPELINES_PAGES.LIST; +} + +export interface PipelineEditUrlGeneratorState extends UrlGeneratorState { + page: INGEST_PIPELINES_PAGES.EDIT; +} + +export interface PipelineCloneUrlGeneratorState extends UrlGeneratorState { + page: INGEST_PIPELINES_PAGES.CLONE; +} + +export interface PipelineCreateUrlGeneratorState extends UrlGeneratorState { + page: INGEST_PIPELINES_PAGES.CREATE; +} + +export type IngestPipelinesUrlGeneratorState = + | PipelinesListUrlGeneratorState + | PipelineEditUrlGeneratorState + | PipelineCloneUrlGeneratorState + | PipelineCreateUrlGeneratorState; + +export class IngestPipelinesUrlGenerator + implements UrlGeneratorsDefinition { + constructor(private readonly getAppBasePath: (absolute: boolean) => Promise) {} + + public readonly id = INGEST_PIPELINES_APP_ULR_GENERATOR; + + public readonly createUrl = async (state: IngestPipelinesUrlGeneratorState): Promise => { + switch (state.page) { + case INGEST_PIPELINES_PAGES.EDIT: { + return `${await this.getAppBasePath(!!state.absolute)}${getEditPath({ + pipelineName: state.pipelineId, + })}`; + } + case INGEST_PIPELINES_PAGES.CREATE: { + return `${await this.getAppBasePath(!!state.absolute)}${getCreatePath()}`; + } + case INGEST_PIPELINES_PAGES.LIST: { + return `${await this.getAppBasePath(!!state.absolute)}${getListPath({ + inspectedPipelineName: state.pipelineId, + })}`; + } + case INGEST_PIPELINES_PAGES.CLONE: { + return `${await this.getAppBasePath(!!state.absolute)}${getClonePath({ + clonedPipelineName: state.pipelineId, + })}`; + } + } + }; +} + +export const registerUrlGenerator = ( + coreSetup: CoreSetup, + management: Dependencies['management'], + share: Dependencies['share'] +) => { + const getAppBasePath = async (absolute = false) => { + const [coreStart] = await coreSetup.getStartServices(); + return coreStart.application.getUrlForApp(MANAGEMENT_APP_ID, { + path: management.sections.section.ingest.getApp(PLUGIN_ID)!.basePath, + absolute: !!absolute, + }); + }; + + share.urlGenerators.registerUrlGenerator(new IngestPipelinesUrlGenerator(getAppBasePath)); +}; diff --git a/x-pack/plugins/lens/common/constants.ts b/x-pack/plugins/lens/common/constants.ts index ea2331a577743..9099554cb8c7e 100644 --- a/x-pack/plugins/lens/common/constants.ts +++ b/x-pack/plugins/lens/common/constants.ts @@ -17,6 +17,6 @@ export function getEditPath(id: string) { return `#/edit/${encodeURIComponent(id)}`; } -export function getFullPath(id: string) { - return `/app/${PLUGIN_ID}${getEditPath(id)}`; +export function getFullPath(id?: string) { + return `/app/${PLUGIN_ID}${id ? getEditPath(id) : getBasePath()}`; } diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index 67d9d5ef64483..f5fba766e60ee 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -13,7 +13,7 @@ "dashboard", "charts" ], - "optionalPlugins": ["embeddable", "usageCollection", "taskManager", "uiActions"], + "optionalPlugins": ["embeddable", "usageCollection", "taskManager", "uiActions", "globalSearch"], "configPath": ["xpack", "lens"], "extraPublicDirs": ["common/constants"], "requiredBundles": ["savedObjects", "kibanaUtils", "kibanaReact", "embeddable"] diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx index 325f18ee9833a..3d692b1f7f5a8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/bucket_nesting_editor.tsx @@ -75,6 +75,10 @@ export function BucketNestingEditor({ defaultMessage: 'Top values for each {field}', values: { field: fieldName }, }), + range: i18n.translate('xpack.lens.indexPattern.groupingOverallRanges', { + defaultMessage: 'Top values for each {field}', + values: { field: fieldName }, + }), }; const bottomLevelCopy: Record = { @@ -90,6 +94,10 @@ export function BucketNestingEditor({ defaultMessage: 'Overall top {target}', values: { target: target.fieldName }, }), + range: i18n.translate('xpack.lens.indexPattern.groupingSecondRanges', { + defaultMessage: 'Overall top {target}', + values: { target: target.fieldName }, + }), }; return ( 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 153757ac37da1..2f64a36e0462e 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 @@ -332,7 +332,6 @@ export function DimensionEditor(props: DimensionEditorProps) { {!incompatibleSelectedOperationType && ParamEditor && ( <> - { if ( type === 'date' && @@ -180,7 +179,7 @@ export const dateHistogramOperation: OperationDefinition + <> {!intervalIsRestricted && ( )} - + ); }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx index cc1e23cb82a49..9985ad7229ecc 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filters.tsx @@ -226,6 +226,7 @@ export const FilterList = ({ removeTitle={i18n.translate('xpack.lens.indexPattern.filters.removeFilter', { defaultMessage: 'Remove a filter', })} + isNotRemovable={localFilters.length === 1} > + range.label || + formatter.convert({ + gte: isValidNumber(range.from) ? range.from : FROM_PLACEHOLDER, + lt: isValidNumber(range.to) ? range.to : TO_PLACEHOLDER, + }); + +export const RangePopover = ({ + range, + setRange, + Button, + isOpenByCreation, + setIsOpenByCreation, +}: { + range: LocalRangeType; + setRange: (newRange: LocalRangeType) => void; + Button: React.FunctionComponent<{ onClick: MouseEventHandler }>; + isOpenByCreation: boolean; + setIsOpenByCreation: (open: boolean) => void; + formatter: IFieldFormat; +}) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [tempRange, setTempRange] = useState(range); + + const saveRangeAndReset = (newRange: LocalRangeType, resetRange = false) => { + if (resetRange) { + // reset the temporary range for later use + setTempRange(range); + } + // send the range back to the main state + setRange(newRange); + }; + const { from, to } = tempRange; + + const lteAppendLabel = i18n.translate('xpack.lens.indexPattern.ranges.lessThanOrEqualAppend', { + defaultMessage: '\u2264', + }); + const lteTooltipContent = i18n.translate( + 'xpack.lens.indexPattern.ranges.lessThanOrEqualTooltip', + { + defaultMessage: 'Less than or equal to', + } + ); + const ltPrependLabel = i18n.translate('xpack.lens.indexPattern.ranges.lessThanPrepend', { + defaultMessage: '\u003c', + }); + const ltTooltipContent = i18n.translate('xpack.lens.indexPattern.ranges.lessThanTooltip', { + defaultMessage: 'Less than', + }); + + const onSubmit = () => { + setIsPopoverOpen(false); + setIsOpenByCreation(false); + saveRangeAndReset(tempRange, true); + }; + + return ( + { + setIsPopoverOpen((isOpen) => !isOpen); + setIsOpenByCreation(false); + }} + /> + } + data-test-subj="indexPattern-ranges-popover" + > + + + + { + const newRange = { + ...tempRange, + from: target.value !== '' ? Number(target.value) : -Infinity, + }; + setTempRange(newRange); + saveRangeAndReset(newRange); + }} + append={ + + {lteAppendLabel} + + } + fullWidth + compressed + placeholder={FROM_PLACEHOLDER} + isInvalid={!isValidRange(tempRange)} + /> + + + + + + { + const newRange = { + ...tempRange, + to: target.value !== '' ? Number(target.value) : -Infinity, + }; + setTempRange(newRange); + saveRangeAndReset(newRange); + }} + prepend={ + + {ltPrependLabel} + + } + fullWidth + compressed + placeholder={TO_PLACEHOLDER} + isInvalid={!isValidRange(tempRange)} + onKeyDown={({ key }: React.KeyboardEvent) => { + if (keys.ENTER === key && onSubmit) { + onSubmit(); + } + }} + /> + + + + + ); +}; + +export const AdvancedRangeEditor = ({ + ranges, + setRanges, + onToggleEditor, + formatter, +}: { + ranges: RangeTypeLens[]; + setRanges: (newRanges: RangeTypeLens[]) => void; + onToggleEditor: () => void; + formatter: IFieldFormat; +}) => { + // use a local state to store ids with range objects + const [localRanges, setLocalRanges] = useState(() => + ranges.map((range) => ({ ...range, id: generateId() })) + ); + // we need to force the open state of the popover from the outside in some scenarios + // so we need an extra state here + const [isOpenByCreation, setIsOpenByCreation] = useState(false); + + const lastIndex = localRanges.length - 1; + + // Update locally all the time, but bounce the parents prop function + // to aviod too many requests + useDebounce( + () => { + setRanges(localRanges.map(({ id, ...rest }) => ({ ...rest }))); + }, + TYPING_DEBOUNCE_TIME, + [localRanges] + ); + + const addNewRange = () => { + setLocalRanges([ + ...localRanges, + { + id: generateId(), + from: localRanges[localRanges.length - 1].to, + to: Infinity, + label: '', + }, + ]); + }; + + return ( + + + {' '} + {i18n.translate('xpack.lens.indexPattern.ranges.customIntervalsRemoval', { + defaultMessage: 'Remove custom intervals', + })} + + + } + > + <> + setIsOpenByCreation(false)} + droppableId="RANGES_DROPPABLE_AREA" + items={localRanges} + > + {localRanges.map((range: LocalRangeType, idx: number) => ( + { + const newRanges = localRanges.filter((_, i) => i !== idx); + setLocalRanges(newRanges); + }} + removeTitle={i18n.translate('xpack.lens.indexPattern.ranges.deleteRange', { + defaultMessage: 'Delete range', + })} + isNotRemovable={localRanges.length === 1} + > + { + const newRanges = [...localRanges]; + if (newRange.id === newRanges[idx].id) { + newRanges[idx] = newRange; + } else { + newRanges.push(newRange); + } + setLocalRanges(newRanges); + }} + formatter={formatter} + Button={({ onClick }: { onClick: MouseEventHandler }) => ( + + + {getBetterLabel(range, formatter)} + + + )} + /> + + ))} + + { + addNewRange(); + setIsOpenByCreation(true); + }} + label={i18n.translate('xpack.lens.indexPattern.ranges.addInterval', { + defaultMessage: 'Add interval', + })} + /> + + + ); +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/constants.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/constants.ts new file mode 100644 index 0000000000000..5c3c3c19a2b0f --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/constants.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const TYPING_DEBOUNCE_TIME = 256; +// Taken from the Visualize editor +export const FROM_PLACEHOLDER = '\u2212\u221E'; +export const TO_PLACEHOLDER = '+\u221E'; + +export const DEFAULT_INTERVAL = 1000; +export const AUTO_BARS = 'auto'; +export const MIN_HISTOGRAM_BARS = 1; +export const SLICES = 6; + +export const MODES = { + Range: 'range', + Histogram: 'histogram', +} as const; diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/index.ts similarity index 88% rename from x-pack/plugins/embeddable_enhanced/public/drilldowns/index.ts rename to x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/index.ts index a8d5a179dbac1..ccae0c949af0d 100644 --- a/x-pack/plugins/embeddable_enhanced/public/drilldowns/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './url_drilldown'; +export * from './ranges'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx new file mode 100644 index 0000000000000..5d5acf7778973 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/range_editor.tsx @@ -0,0 +1,175 @@ +/* + * 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 } from 'react'; +import { i18n } from '@kbn/i18n'; +import { useDebounce } from 'react-use'; +import { + EuiButtonEmpty, + EuiFormRow, + EuiRange, + EuiFlexItem, + EuiFlexGroup, + EuiButtonIcon, + EuiToolTip, +} from '@elastic/eui'; +import { IFieldFormat } from 'src/plugins/data/public'; +import { RangeColumnParams, UpdateParamsFnType, MODES_TYPES } from './ranges'; +import { AdvancedRangeEditor } from './advanced_editor'; +import { TYPING_DEBOUNCE_TIME, MODES, MIN_HISTOGRAM_BARS } from './constants'; + +const BaseRangeEditor = ({ + maxBars, + step, + maxHistogramBars, + onToggleEditor, + onMaxBarsChange, +}: { + maxBars: number; + step: number; + maxHistogramBars: number; + onToggleEditor: () => void; + onMaxBarsChange: (newMaxBars: number) => void; +}) => { + const [maxBarsValue, setMaxBarsValue] = useState(String(maxBars)); + + useDebounce( + () => { + onMaxBarsChange(Number(maxBarsValue)); + }, + TYPING_DEBOUNCE_TIME, + [maxBarsValue] + ); + + const granularityLabel = i18n.translate('xpack.lens.indexPattern.ranges.granularity', { + defaultMessage: 'Granularity', + }); + const decreaseButtonLabel = i18n.translate('xpack.lens.indexPattern.ranges.decreaseButtonLabel', { + defaultMessage: 'Decrease granularity', + }); + const increaseButtonLabel = i18n.translate('xpack.lens.indexPattern.ranges.increaseButtonLabel', { + defaultMessage: 'Increase granularity', + }); + + return ( + <> + + + + + + setMaxBarsValue('' + Math.max(Number(maxBarsValue) - step, MIN_HISTOGRAM_BARS)) + } + aria-label={decreaseButtonLabel} + /> + + + + setMaxBarsValue(currentTarget.value)} + /> + + + + + setMaxBarsValue('' + Math.min(Number(maxBarsValue) + step, maxHistogramBars)) + } + aria-label={increaseButtonLabel} + /> + + + + + + onToggleEditor()}> + {i18n.translate('xpack.lens.indexPattern.ranges.customIntervalsToggle', { + defaultMessage: 'Create custom intervals', + })} + + + ); +}; + +export const RangeEditor = ({ + setParam, + params, + maxHistogramBars, + maxBars, + granularityStep, + onChangeMode, + rangeFormatter, +}: { + params: RangeColumnParams; + maxHistogramBars: number; + maxBars: number; + granularityStep: number; + setParam: UpdateParamsFnType; + onChangeMode: (mode: MODES_TYPES) => void; + rangeFormatter: IFieldFormat; +}) => { + const [isAdvancedEditor, toggleAdvancedEditor] = useState(params.type === MODES.Range); + + // if the maxBars in the params is set to auto refresh it with the default value + // only on bootstrap + useEffect(() => { + if (params.maxBars !== maxBars) { + setParam('maxBars', maxBars); + } + }, [maxBars, params.maxBars, setParam]); + + if (isAdvancedEditor) { + return ( + { + setParam('ranges', ranges); + }} + onToggleEditor={() => { + onChangeMode(MODES.Histogram); + toggleAdvancedEditor(false); + }} + formatter={rangeFormatter} + /> + ); + } + + return ( + { + setParam('maxBars', newMaxBars); + }} + onToggleEditor={() => { + onChangeMode(MODES.Range); + toggleAdvancedEditor(true); + }} + /> + ); +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx new file mode 100644 index 0000000000000..2409406afcdbc --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx @@ -0,0 +1,555 @@ +/* + * 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 'react-dom/test-utils'; +import { EuiFieldNumber, EuiRange, EuiButtonEmpty, EuiLink } from '@elastic/eui'; +import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public'; +import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; +import { IndexPatternPrivateState, IndexPattern } from '../../../types'; +import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks'; +import { rangeOperation } from '../index'; +import { RangeIndexPatternColumn } from './ranges'; +import { + MODES, + DEFAULT_INTERVAL, + TYPING_DEBOUNCE_TIME, + MIN_HISTOGRAM_BARS, + SLICES, +} from './constants'; +import { RangePopover } from './advanced_editor'; +import { DragDropBuckets } from '../shared_components'; + +const dataPluginMockValue = dataPluginMock.createStartContract(); +// need to overwrite the formatter field first +dataPluginMockValue.fieldFormats.deserialize = jest.fn().mockImplementation(() => { + return { convert: ({ gte, lt }: { gte: string; lt: string }) => `${gte} - ${lt}` }; +}); + +type ReactMouseEvent = React.MouseEvent & + React.MouseEvent; + +const defaultOptions = { + storage: {} as IStorageWrapper, + // need this for MAX_HISTOGRAM value + uiSettings: ({ + get: () => 100, + } as unknown) as IUiSettingsClient, + savedObjectsClient: {} as SavedObjectsClientContract, + dateRange: { + fromDate: 'now-1y', + toDate: 'now', + }, + data: dataPluginMockValue, + http: {} as HttpSetup, +}; + +describe('ranges', () => { + let state: IndexPatternPrivateState; + const InlineOptions = rangeOperation.paramEditor!; + const sourceField = 'MyField'; + const MAX_HISTOGRAM_VALUE = 100; + const GRANULARITY_DEFAULT_VALUE = (MAX_HISTOGRAM_VALUE - MIN_HISTOGRAM_BARS) / 2; + const GRANULARITY_STEP = (MAX_HISTOGRAM_VALUE - MIN_HISTOGRAM_BARS) / SLICES; + + function setToHistogramMode() { + const column = state.layers.first.columns.col1 as RangeIndexPatternColumn; + column.dataType = 'number'; + column.scale = 'interval'; + column.params.type = MODES.Histogram; + } + + function setToRangeMode() { + const column = state.layers.first.columns.col1 as RangeIndexPatternColumn; + column.dataType = 'string'; + column.scale = 'ordinal'; + column.params.type = MODES.Range; + } + + function getDefaultState(): IndexPatternPrivateState { + return { + indexPatternRefs: [], + indexPatterns: {}, + existingFields: {}, + currentIndexPatternId: '1', + isFirstExistenceFetch: false, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + // Start with the histogram type + col1: { + label: sourceField, + dataType: 'number', + operationType: 'range', + scale: 'interval', + isBucketed: true, + sourceField, + params: { + type: MODES.Histogram, + ranges: [{ from: 0, to: DEFAULT_INTERVAL, label: '' }], + maxBars: 'auto', + }, + }, + col2: { + label: 'Count', + dataType: 'number', + isBucketed: false, + sourceField: 'Records', + operationType: 'count', + }, + }, + }, + }, + }; + } + + beforeAll(() => { + jest.useFakeTimers(); + }); + + beforeEach(() => { + state = getDefaultState(); + }); + + describe('toEsAggConfig', () => { + afterAll(() => setToHistogramMode()); + + it('should reflect params correctly', () => { + const esAggsConfig = rangeOperation.toEsAggsConfig( + state.layers.first.columns.col1 as RangeIndexPatternColumn, + 'col1', + {} as IndexPattern + ); + expect(esAggsConfig).toEqual( + expect.objectContaining({ + type: MODES.Histogram, + params: expect.objectContaining({ + field: sourceField, + maxBars: null, + }), + }) + ); + }); + + it('should reflect the type correctly', () => { + setToRangeMode(); + + const esAggsConfig = rangeOperation.toEsAggsConfig( + state.layers.first.columns.col1 as RangeIndexPatternColumn, + 'col1', + {} as IndexPattern + ); + + expect(esAggsConfig).toEqual( + expect.objectContaining({ + type: MODES.Range, + }) + ); + }); + }); + + describe('getPossibleOperationForField', () => { + it('should return operation with the right type for number', () => { + expect( + rangeOperation.getPossibleOperationForField({ + aggregatable: true, + searchable: true, + name: 'test', + displayName: 'test', + type: 'number', + }) + ).toEqual({ + dataType: 'number', + isBucketed: true, + scale: 'interval', + }); + }); + + it('should not return operation if field type is not number', () => { + expect( + rangeOperation.getPossibleOperationForField({ + aggregatable: false, + searchable: true, + name: 'test', + displayName: 'test', + type: 'string', + }) + ).toEqual(undefined); + }); + }); + + describe('paramEditor', () => { + describe('Modify intervals in basic mode', () => { + beforeEach(() => { + state = getDefaultState(); + }); + + it('should start update the state with the default maxBars value', () => { + const setStateSpy = jest.fn(); + mount( + + ); + + expect(setStateSpy).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + ...state.layers.first.columns.col1, + params: { + ...state.layers.first.columns.col1.params, + maxBars: GRANULARITY_DEFAULT_VALUE, + }, + }, + }, + }, + }, + }); + }); + + it('should update state when changing Max bars number', () => { + const setStateSpy = jest.fn(); + + const instance = mount( + + ); + + act(() => { + instance.find(EuiRange).prop('onChange')!( + { + currentTarget: { + value: '' + MAX_HISTOGRAM_VALUE, + }, + } as React.ChangeEvent, + true + ); + jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4); + + expect(setStateSpy).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + ...state.layers.first.columns.col1, + params: { + ...state.layers.first.columns.col1.params, + maxBars: MAX_HISTOGRAM_VALUE, + }, + }, + }, + }, + }, + }); + }); + }); + + it('should update the state using the plus or minus buttons by the step amount', () => { + const setStateSpy = jest.fn(); + + const instance = mount( + + ); + + act(() => { + // minus button + instance + .find('[data-test-subj="lns-indexPattern-range-maxBars-minus"]') + .find('button') + .prop('onClick')!({} as ReactMouseEvent); + jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4); + + expect(setStateSpy).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + ...state.layers.first.columns.col1, + params: { + ...state.layers.first.columns.col1.params, + maxBars: GRANULARITY_DEFAULT_VALUE - GRANULARITY_STEP, + }, + }, + }, + }, + }, + }); + + // plus button + instance + .find('[data-test-subj="lns-indexPattern-range-maxBars-plus"]') + .find('button') + .prop('onClick')!({} as ReactMouseEvent); + jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4); + + expect(setStateSpy).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + ...state.layers.first.columns.col1, + params: { + ...state.layers.first.columns.col1.params, + maxBars: GRANULARITY_DEFAULT_VALUE, + }, + }, + }, + }, + }, + }); + }); + }); + }); + + describe('Specify range intervals manually', () => { + // @ts-expect-error + window['__react-beautiful-dnd-disable-dev-warnings'] = true; // issue with enzyme & react-beautiful-dnd throwing errors: https://github.com/atlassian/react-beautiful-dnd/issues/1593 + + beforeEach(() => setToRangeMode()); + + it('should show one range interval to start with', () => { + const setStateSpy = jest.fn(); + + const instance = mount( + + ); + + expect(instance.find(DragDropBuckets).children).toHaveLength(1); + }); + + it('should add a new range', () => { + const setStateSpy = jest.fn(); + + const instance = mount( + + ); + + // This series of act clojures are made to make it work properly the update flush + act(() => { + instance.find(EuiButtonEmpty).prop('onClick')!({} as ReactMouseEvent); + }); + + act(() => { + // need another wrapping for this in order to work + instance.update(); + + expect(instance.find(RangePopover)).toHaveLength(2); + + // edit the range and check + instance.find(RangePopover).find(EuiFieldNumber).first().prop('onChange')!({ + target: { + value: '50', + }, + } as React.ChangeEvent); + jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4); + + expect(setStateSpy).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + ...state.layers.first.columns.col1, + params: { + ...state.layers.first.columns.col1.params, + ranges: [ + { from: 0, to: DEFAULT_INTERVAL, label: '' }, + { from: 50, to: Infinity, label: '' }, + ], + }, + }, + }, + }, + }, + }); + }); + }); + + it('should open a popover to edit an existing range', () => { + const setStateSpy = jest.fn(); + + const instance = mount( + + ); + + // This series of act clojures are made to make it work properly the update flush + act(() => { + instance.find(RangePopover).find(EuiLink).prop('onClick')!({} as ReactMouseEvent); + }); + + act(() => { + // need another wrapping for this in order to work + instance.update(); + + // edit the range "to" field + instance.find(RangePopover).find(EuiFieldNumber).last().prop('onChange')!({ + target: { + value: '50', + }, + } as React.ChangeEvent); + jest.advanceTimersByTime(TYPING_DEBOUNCE_TIME * 4); + + expect(setStateSpy).toHaveBeenCalledWith({ + ...state, + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + ...state.layers.first.columns.col1, + params: { + ...state.layers.first.columns.col1.params, + ranges: [{ from: 0, to: 50, label: '' }], + }, + }, + }, + }, + }, + }); + }); + }); + + it('should not accept invalid ranges', () => { + const setStateSpy = jest.fn(); + + const instance = mount( + + ); + + // This series of act clojures are made to make it work properly the update flush + act(() => { + instance.find(RangePopover).find(EuiLink).prop('onClick')!({} as ReactMouseEvent); + }); + + act(() => { + // need another wrapping for this in order to work + instance.update(); + + // edit the range "to" field + instance.find(RangePopover).find(EuiFieldNumber).last().prop('onChange')!({ + target: { + value: '-1', + }, + } as React.ChangeEvent); + }); + + act(() => { + instance.update(); + + // and check + expect(instance.find(RangePopover).find(EuiFieldNumber).last().prop('isInvalid')).toBe( + true + ); + }); + }); + + it('should be possible to remove a range if multiple', () => { + const setStateSpy = jest.fn(); + + // Add an extra range + (state.layers.first.columns.col1 as RangeIndexPatternColumn).params.ranges.push({ + from: DEFAULT_INTERVAL, + to: 2 * DEFAULT_INTERVAL, + label: '', + }); + + const instance = mount( + + ); + + expect(instance.find(RangePopover)).toHaveLength(2); + + // This series of act closures are made to make it work properly the update flush + act(() => { + instance + .find('[data-test-subj="lns-customBucketContainer-remove"]') + .last() + .prop('onClick')!({} as ReactMouseEvent); + }); + + act(() => { + // need another wrapping for this in order to work + instance.update(); + + expect(instance.find(RangePopover)).toHaveLength(1); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx new file mode 100644 index 0000000000000..530c2e962759b --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.tsx @@ -0,0 +1,199 @@ +/* + * 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 { UI_SETTINGS } from '../../../../../../../../src/plugins/data/common'; +import { Range } from '../../../../../../../../src/plugins/expressions/common/expression_types/index'; +import { RangeEditor } from './range_editor'; +import { OperationDefinition } from '../index'; +import { FieldBasedIndexPatternColumn } from '../column_types'; +import { updateColumnParam, changeColumn } from '../../../state_helpers'; +import { MODES, AUTO_BARS, DEFAULT_INTERVAL, MIN_HISTOGRAM_BARS, SLICES } from './constants'; + +type RangeType = Omit; +export type RangeTypeLens = RangeType & { label: string }; + +export type MODES_TYPES = typeof MODES[keyof typeof MODES]; + +export interface RangeIndexPatternColumn extends FieldBasedIndexPatternColumn { + operationType: 'range'; + params: { + type: MODES_TYPES; + maxBars: typeof AUTO_BARS | number; + ranges: RangeTypeLens[]; + }; +} + +export type RangeColumnParams = RangeIndexPatternColumn['params']; +export type UpdateParamsFnType = ( + paramName: K, + value: RangeColumnParams[K] +) => void; + +export const isValidNumber = (value: number | '') => + value !== '' && !isNaN(value) && isFinite(value); +export const isRangeWithin = (range: RangeTypeLens): boolean => range.from <= range.to; +const isFullRange = ({ from, to }: RangeType) => isValidNumber(from) && isValidNumber(to); +export const isValidRange = (range: RangeTypeLens): boolean => { + if (isFullRange(range)) { + return isRangeWithin(range); + } + return true; +}; + +function getEsAggsParams({ sourceField, params }: RangeIndexPatternColumn) { + if (params.type === MODES.Range) { + return { + field: sourceField, + ranges: params.ranges.filter(isValidRange).map>((range) => { + if (isFullRange(range)) { + return { from: range.from, to: range.to }; + } + const partialRange: Partial = {}; + // be careful with the fields to set on partial ranges + if (isValidNumber(range.from)) { + partialRange.from = range.from; + } + if (isValidNumber(range.to)) { + partialRange.to = range.to; + } + return partialRange; + }), + }; + } + return { + field: sourceField, + // fallback to 0 in case of empty string + maxBars: params.maxBars === AUTO_BARS ? null : params.maxBars, + has_extended_bounds: false, + min_doc_count: 0, + extended_bounds: { min: '', max: '' }, + }; +} + +export const rangeOperation: OperationDefinition = { + type: 'range', + displayName: i18n.translate('xpack.lens.indexPattern.ranges', { + defaultMessage: 'Ranges', + }), + priority: 4, // Higher than terms, so numbers get histogram + getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type }) => { + if ( + type === 'number' && + aggregatable && + (!aggregationRestrictions || aggregationRestrictions.range) + ) { + return { + dataType: 'number', + isBucketed: true, + scale: 'interval', + }; + } + }, + buildColumn({ suggestedPriority, field }) { + return { + label: field.name, + dataType: 'number', // string for Range + operationType: 'range', + suggestedPriority, + sourceField: field.name, + isBucketed: true, + scale: 'interval', // ordinal for Range + params: { + type: MODES.Histogram, + ranges: [{ from: 0, to: DEFAULT_INTERVAL, label: '' }], + maxBars: AUTO_BARS, + }, + }; + }, + isTransferable: (column, newIndexPattern) => { + const newField = newIndexPattern.fields.find((field) => field.name === column.sourceField); + + return Boolean( + newField && + newField.type === 'number' && + newField.aggregatable && + (!newField.aggregationRestrictions || newField.aggregationRestrictions.range) + ); + }, + onFieldChange: (oldColumn, indexPattern, field) => { + return { + ...oldColumn, + label: field.name, + sourceField: field.name, + }; + }, + toEsAggsConfig: (column, columnId) => { + const params = getEsAggsParams(column); + return { + id: columnId, + enabled: true, + type: column.params.type, + schema: 'segment', + params, + }; + }, + paramEditor: ({ state, setState, currentColumn, layerId, columnId, uiSettings, data }) => { + const rangeFormatter = data.fieldFormats.deserialize({ id: 'range' }); + const MAX_HISTOGRAM_BARS = uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS); + const granularityStep = (MAX_HISTOGRAM_BARS - MIN_HISTOGRAM_BARS) / SLICES; + const maxBarsDefaultValue = (MAX_HISTOGRAM_BARS - MIN_HISTOGRAM_BARS) / 2; + + // Used to change one param at the time + const setParam: UpdateParamsFnType = (paramName, value) => { + setState( + updateColumnParam({ + state, + layerId, + currentColumn, + paramName, + value, + }) + ); + }; + + // Useful to change more params at once + const onChangeMode = (newMode: MODES_TYPES) => { + const scale = newMode === MODES.Range ? 'ordinal' : 'interval'; + const dataType = newMode === MODES.Range ? 'string' : 'number'; + setState( + changeColumn({ + state, + layerId, + columnId, + newColumn: { + ...currentColumn, + scale, + dataType, + params: { + type: newMode, + ranges: [{ from: 0, to: DEFAULT_INTERVAL, label: '' }], + maxBars: maxBarsDefaultValue, + }, + }, + keepParams: false, + }) + ); + }; + return ( + + ); + }, +}; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/buckets.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/buckets.tsx index 73378cea919a6..47380f7865578 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/buckets.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/buckets.tsx @@ -35,6 +35,7 @@ interface BucketContainerProps { invalidMessage: string; onRemoveClick: () => void; removeTitle: string; + isNotRemovable?: boolean; children: React.ReactNode; dataTestSubj?: string; } @@ -46,6 +47,7 @@ const BucketContainer = ({ removeTitle, children, dataTestSubj, + isNotRemovable, }: BucketContainerProps) => { return ( @@ -75,6 +77,7 @@ const BucketContainer = ({ onClick={onRemoveClick} aria-label={removeTitle} title={removeTitle} + disabled={isNotRemovable} />
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx index 20c421008a746..c1a87a2013747 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiForm, EuiFormRow, EuiRange, EuiSelect } from '@elastic/eui'; +import { EuiFormRow, EuiRange, EuiSelect } from '@elastic/eui'; import { IndexPatternColumn } from '../../indexpattern'; import { updateColumnParam } from '../../state_helpers'; import { DataType } from '../../../types'; @@ -171,7 +171,7 @@ export const termsOperation: OperationDefinition = { }), }); return ( - + <> = { })} /> - + ); }, }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts index 4ac3fc89500f9..703431f724c5d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/operations.test.ts @@ -225,29 +225,43 @@ describe('getOperationTypesForField', () => { it('should list out all field-operation tuples for different operation meta data', () => { expect(getAvailableOperationsByMetadata(expectedIndexPatterns[1])).toMatchInlineSnapshot(` Array [ + Object { + "operationMetaData": Object { + "dataType": "date", + "isBucketed": true, + "scale": "interval", + }, + "operations": Array [ + Object { + "field": "timestamp", + "operationType": "date_histogram", + "type": "field", + }, + ], + }, Object { "operationMetaData": Object { "dataType": "number", "isBucketed": true, - "scale": "ordinal", + "scale": "interval", }, "operations": Array [ Object { "field": "bytes", - "operationType": "terms", + "operationType": "range", "type": "field", }, ], }, Object { "operationMetaData": Object { - "dataType": "string", + "dataType": "number", "isBucketed": true, "scale": "ordinal", }, "operations": Array [ Object { - "field": "source", + "field": "bytes", "operationType": "terms", "type": "field", }, @@ -255,14 +269,14 @@ describe('getOperationTypesForField', () => { }, Object { "operationMetaData": Object { - "dataType": "date", + "dataType": "string", "isBucketed": true, - "scale": "interval", + "scale": "ordinal", }, "operations": Array [ Object { - "field": "timestamp", - "operationType": "date_histogram", + "field": "source", + "operationType": "terms", "type": "field", }, ], diff --git a/x-pack/plugins/lens/public/lens_ui_telemetry/factory.test.ts b/x-pack/plugins/lens/public/lens_ui_telemetry/factory.test.ts index 8bb1e086a37c2..fa7747dd18e42 100644 --- a/x-pack/plugins/lens/public/lens_ui_telemetry/factory.test.ts +++ b/x-pack/plugins/lens/public/lens_ui_telemetry/factory.test.ts @@ -83,7 +83,7 @@ describe('Lens UI telemetry', () => { jest.runOnlyPendingTimers(); - expect(http.post).toHaveBeenCalledWith(`/api/lens/telemetry`, { + expect(http.post).toHaveBeenCalledWith(`/api/lens/stats`, { body: JSON.stringify({ events: { '2019-10-23': { diff --git a/x-pack/plugins/lens/public/lens_ui_telemetry/factory.ts b/x-pack/plugins/lens/public/lens_ui_telemetry/factory.ts index cb517acff4f7a..8f9ce7f2ceab8 100644 --- a/x-pack/plugins/lens/public/lens_ui_telemetry/factory.ts +++ b/x-pack/plugins/lens/public/lens_ui_telemetry/factory.ts @@ -86,7 +86,7 @@ export class LensReportManager { this.readFromStorage(); if (Object.keys(this.events).length || Object.keys(this.suggestionEvents).length) { try { - await this.http.post(`${BASE_API_URL}/telemetry`, { + await this.http.post(`${BASE_API_URL}/stats`, { body: JSON.stringify({ events: this.events, suggestionEvents: this.suggestionEvents, diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index f9c63f54d6713..60c7011d55300 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -11,6 +11,7 @@ import { ExpressionsSetup, ExpressionsStart } from 'src/plugins/expressions/publ import { VisualizationsSetup } from 'src/plugins/visualizations/public'; import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; import { UrlForwardingSetup } from 'src/plugins/url_forwarding/public'; +import { GlobalSearchPluginSetup } from '../../global_search/public'; import { ChartsPluginSetup } from '../../../../src/plugins/charts/public'; import { EditorFrameService } from './editor_frame_service'; import { @@ -31,6 +32,7 @@ import { UiActionsStart } from '../../../../src/plugins/ui_actions/public'; import { NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common'; import { EditorFrameStart } from './types'; import { getLensAliasConfig } from './vis_type_alias'; +import { getSearchProvider } from './search_provider'; import './index.scss'; @@ -41,6 +43,7 @@ export interface LensPluginSetupDependencies { embeddable?: EmbeddableSetup; visualizations: VisualizationsSetup; charts: ChartsPluginSetup; + globalSearch?: GlobalSearchPluginSetup; } export interface LensPluginStartDependencies { @@ -78,6 +81,7 @@ export class LensPlugin { embeddable, visualizations, charts, + globalSearch, }: LensPluginSetupDependencies ) { const editorFrameSetupInterface = this.editorFrameService.setup(core, { @@ -116,6 +120,20 @@ export class LensPlugin { }, }); + if (globalSearch) { + globalSearch.registerResultProvider( + getSearchProvider( + core.getStartServices().then( + ([ + { + application: { capabilities }, + }, + ]) => capabilities + ) + ) + ); + } + urlForwarding.forwardApp('lens', 'lens'); } diff --git a/x-pack/plugins/lens/public/search_provider.ts b/x-pack/plugins/lens/public/search_provider.ts new file mode 100644 index 0000000000000..c19e7970b45ae --- /dev/null +++ b/x-pack/plugins/lens/public/search_provider.ts @@ -0,0 +1,79 @@ +/* + * 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 levenshtein from 'js-levenshtein'; +import { ApplicationStart } from 'kibana/public'; +import { from } from 'rxjs'; +import { i18n } from '@kbn/i18n'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; +import { GlobalSearchResultProvider } from '../../global_search/public'; +import { getFullPath } from '../common'; + +/** + * Global search provider adding a Lens entry. + * This is necessary because Lens does not show up in the nav bar and is filtered out by the + * default app provider. + * + * It is inlining the same search term matching logic as the application search provider. + * + * TODO: This is a workaround and can be removed once there is a generic way to register sub features + * of apps. In this case, Lens should be considered a feature of Visualize. + */ +export const getSearchProvider: ( + uiCapabilities: Promise +) => GlobalSearchResultProvider = (uiCapabilities) => ({ + id: 'lens', + find: (term) => { + return from( + uiCapabilities.then(({ navLinks: { visualize: visualizeNavLink } }) => { + if (!visualizeNavLink) { + return []; + } + const title = i18n.translate('xpack.lens.searchTitle', { + defaultMessage: 'Lens: create visualizations', + description: 'Lens is a product name and should not be translated', + }); + const searchableTitle = title.toLowerCase(); + + term = term.toLowerCase(); + let score = 0; + + // shortcuts to avoid calculating the distance when there is an exact match somewhere. + if (searchableTitle === term) { + score = 100; + } else if (searchableTitle.startsWith(term)) { + score = 90; + } else if (searchableTitle.includes(term)) { + score = 75; + } else { + const length = Math.max(term.length, searchableTitle.length); + const distance = levenshtein(term, searchableTitle); + + // maximum lev distance is length, we compute the match ratio (lower distance is better) + const ratio = Math.floor((1 - distance / length) * 100); + if (ratio >= 60) { + score = ratio; + } + } + if (score === 0) return []; + return [ + { + id: 'lens', + title, + type: 'application', + icon: 'logoKibana', + meta: { + categoryId: DEFAULT_APP_CATEGORIES.kibana.id, + categoryLabel: DEFAULT_APP_CATEGORIES.kibana.label, + }, + score, + url: getFullPath(), + }, + ]; + }) + ); + }, +}); 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 c7781c2e1d50c..ee22ee51301df 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 @@ -17,7 +17,6 @@ import { EuiFormRow, EuiText, htmlIdGenerator, - EuiForm, EuiColorPicker, EuiColorPickerProps, EuiToolTip, @@ -366,7 +365,7 @@ export function DimensionEditor(props: VisualizationDimensionEditorProps) 'auto'; return ( - + <> ) }} /> - + ); } diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx index 9379c8a612eb2..24bf78dba2121 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -24,6 +24,7 @@ import { ExpressionFunctionDefinition, ExpressionRenderDefinition, ExpressionValueSearchContext, + KibanaDatatable, } from 'src/plugins/expressions/public'; import { IconType } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -251,6 +252,12 @@ export function XYChart({ ({ id }) => id === filteredLayers[0].xAccessor ); const xAxisFormatter = formatFactory(xAxisColumn && xAxisColumn.formatHint); + const layersAlreadyFormatted: Record = {}; + // This is a safe formatter for the xAccessor that abstracts the knowledge of already formatted layers + const safeXAccessorLabelRenderer = (value: unknown): string => + xAxisColumn && layersAlreadyFormatted[xAxisColumn.id] + ? (value as string) + : xAxisFormatter.convert(value); const chartHasMoreThanOneSeries = filteredLayers.length > 1 || @@ -364,7 +371,7 @@ export function XYChart({ theme={chartTheme} baseTheme={chartBaseTheme} tooltip={{ - headerFormatter: (d) => xAxisFormatter.convert(d.value), + headerFormatter: (d) => safeXAccessorLabelRenderer(d.value), }} rotation={shouldRotate ? 90 : 0} xDomain={xDomain} @@ -409,9 +416,15 @@ export function XYChart({ const points = [ { - row: table.rows.findIndex( - (row) => layer.xAccessor && row[layer.xAccessor] === xyGeometry.x - ), + row: table.rows.findIndex((row) => { + if (layer.xAccessor) { + if (layersAlreadyFormatted[layer.xAccessor]) { + // stringify the value to compare with the chart value + return xAxisFormatter.convert(row[layer.xAccessor]) === xyGeometry.x; + } + return row[layer.xAccessor] === xyGeometry.x; + } + }), column: table.columns.findIndex((col) => col.id === layer.xAccessor), value: xyGeometry.x, }, @@ -455,7 +468,7 @@ export function XYChart({ strokeWidth: 2, }} hide={filteredLayers[0].hide || !filteredLayers[0].xAccessor} - tickFormat={(d) => xAxisFormatter.convert(d)} + tickFormat={(d) => safeXAccessorLabelRenderer(d)} style={{ tickLabel: { visible: tickLabelsVisibilitySettings?.x, @@ -504,9 +517,43 @@ export function XYChart({ const table = data.tables[layerId]; + const isPrimitive = (value: unknown): boolean => + value != null && typeof value !== 'object'; + + // what if row values are not primitive? That is the case of, for instance, Ranges + // remaps them to their serialized version with the formatHint metadata + // In order to do it we need to make a copy of the table as the raw one is required for more features (filters, etc...) later on + const tableConverted: KibanaDatatable = { + ...table, + rows: table.rows.map((row) => { + const newRow = { ...row }; + for (const column of table.columns) { + const record = newRow[column.id]; + if (record && !isPrimitive(record)) { + newRow[column.id] = formatFactory(column.formatHint).convert(record); + } + } + return newRow; + }), + }; + + // save the id of the layer with the custom table + table.columns.reduce>( + (alreadyFormatted: Record, { id }) => { + if (alreadyFormatted[id]) { + return alreadyFormatted; + } + alreadyFormatted[id] = table.rows.some( + (row, i) => row[id] !== tableConverted.rows[i][id] + ); + return alreadyFormatted; + }, + layersAlreadyFormatted + ); + // For date histogram chart type, we're getting the rows that represent intervals without data. // To not display them in the legend, they need to be filtered out. - const rows = table.rows.filter( + const rows = tableConverted.rows.filter( (row) => !(xAccessor && typeof row[xAccessor] === 'undefined') && !( @@ -559,19 +606,28 @@ export function XYChart({ // * Key - Y name // * Formatted value - Y name if (accessors.length > 1) { - return d.seriesKeys + const result = d.seriesKeys .map((key: string | number, i) => { - if (i === 0 && splitHint) { + if ( + i === 0 && + splitHint && + splitAccessor && + !layersAlreadyFormatted[splitAccessor] + ) { return formatFactory(splitHint).convert(key); } return splitAccessor && i === 0 ? key : columnToLabelMap[key] ?? ''; }) .join(' - '); + return result; } // For formatted split series, format the key // This handles splitting by dates, for example if (splitHint) { + if (splitAccessor && layersAlreadyFormatted[splitAccessor]) { + return d.seriesKeys[0]; + } return formatFactory(splitHint).convert(d.seriesKeys[0]); } // This handles both split and single-y cases: diff --git a/x-pack/plugins/lens/server/routes/telemetry.ts b/x-pack/plugins/lens/server/routes/telemetry.ts index 7925416ff5df2..06a7091104871 100644 --- a/x-pack/plugins/lens/server/routes/telemetry.ts +++ b/x-pack/plugins/lens/server/routes/telemetry.ts @@ -15,7 +15,7 @@ export async function initLensUsageRoute(setup: CoreSetup) { const router = setup.http.createRouter(); router.post( { - path: `${BASE_API_URL}/telemetry`, + path: `${BASE_API_URL}/stats`, validate: { body: schema.object({ events: schema.mapOf(schema.string(), schema.mapOf(schema.string(), schema.number())), diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index d72d04d2a1843..be891b6e59608 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -37,6 +37,7 @@ export const FONTS_API_PATH = `${GIS_API_PATH}/fonts`; export const API_ROOT_PATH = `/${GIS_API_PATH}`; export const MVT_GETTILE_API_PATH = 'mvt/getTile'; +export const MVT_GETGRIDTILE_API_PATH = 'mvt/getGridTile'; export const MVT_SOURCE_LAYER_NAME = 'source_layer'; export const KBN_TOO_MANY_FEATURES_PROPERTY = '__kbn_too_many_features__'; export const KBN_TOO_MANY_FEATURES_IMAGE_ID = '__kbn_too_many_features_image_id__'; @@ -165,8 +166,13 @@ export enum GRID_RESOLUTION { COARSE = 'COARSE', FINE = 'FINE', MOST_FINE = 'MOST_FINE', + SUPER_FINE = 'SUPER_FINE', } +export const SUPER_FINE_ZOOM_DELTA = 7; // (2 ^ SUPER_FINE_ZOOM_DELTA) ^ 2 = number of cells in a given tile +export const GEOTILE_GRID_AGG_NAME = 'gridSplit'; +export const GEOCENTROID_AGG_NAME = 'gridCentroid'; + export const TOP_TERM_PERCENTAGE_SUFFIX = '__percentage'; export const COUNT_PROP_LABEL = i18n.translate('xpack.maps.aggs.defaultCountLabel', { @@ -230,8 +236,6 @@ export enum SCALING_TYPES { MVT = 'MVT', } -export const RGBA_0000 = 'rgba(0,0,0,0)'; - export enum MVT_FIELD_TYPE { STRING = 'String', NUMBER = 'Number', diff --git a/x-pack/plugins/maps/common/elasticsearch_util/convert_to_geojson.d.ts b/x-pack/plugins/maps/common/elasticsearch_util/convert_to_geojson.d.ts new file mode 100644 index 0000000000000..b1c1b181d8130 --- /dev/null +++ b/x-pack/plugins/maps/common/elasticsearch_util/convert_to_geojson.d.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. + */ + +import { Feature } from 'geojson'; +import { RENDER_AS } from '../constants'; + +export function convertCompositeRespToGeoJson(esResponse: any, renderAs: RENDER_AS): Feature[]; +export function convertRegularRespToGeoJson(esResponse: any, renderAs: RENDER_AS): Feature[]; diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/convert_to_geojson.js b/x-pack/plugins/maps/common/elasticsearch_util/convert_to_geojson.js similarity index 79% rename from x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/convert_to_geojson.js rename to x-pack/plugins/maps/common/elasticsearch_util/convert_to_geojson.js index 35dbebdfd3c8a..a8f32bb4e7f5d 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/convert_to_geojson.js +++ b/x-pack/plugins/maps/common/elasticsearch_util/convert_to_geojson.js @@ -5,12 +5,12 @@ */ import _ from 'lodash'; -import { RENDER_AS } from '../../../../common/constants'; -import { getTileBoundingBox } from './geo_tile_utils'; -import { extractPropertiesFromBucket } from '../../util/es_agg_utils'; -import { clamp } from '../../../../common/elasticsearch_geo_utils'; +import { RENDER_AS, GEOTILE_GRID_AGG_NAME, GEOCENTROID_AGG_NAME } from '../constants'; +import { getTileBoundingBox } from '../geo_tile_utils'; +import { extractPropertiesFromBucket } from './es_agg_utils'; +import { clamp } from './elasticsearch_geo_utils'; -const GRID_BUCKET_KEYS_TO_IGNORE = ['key', 'gridCentroid']; +const GRID_BUCKET_KEYS_TO_IGNORE = ['key', GEOCENTROID_AGG_NAME]; export function convertCompositeRespToGeoJson(esResponse, renderAs) { return convertToGeoJson( @@ -20,7 +20,7 @@ export function convertCompositeRespToGeoJson(esResponse, renderAs) { return _.get(esResponse, 'aggregations.compositeSplit.buckets', []); }, (gridBucket) => { - return gridBucket.key.gridSplit; + return gridBucket.key[GEOTILE_GRID_AGG_NAME]; } ); } @@ -30,7 +30,7 @@ export function convertRegularRespToGeoJson(esResponse, renderAs) { esResponse, renderAs, (esResponse) => { - return _.get(esResponse, 'aggregations.gridSplit.buckets', []); + return _.get(esResponse, `aggregations.${GEOTILE_GRID_AGG_NAME}.buckets`, []); }, (gridBucket) => { return gridBucket.key; @@ -49,7 +49,7 @@ function convertToGeoJson(esResponse, renderAs, pluckGridBuckets, pluckGridKey) type: 'Feature', geometry: rowToGeometry({ gridKey, - gridCentroid: gridBucket.gridCentroid, + [GEOCENTROID_AGG_NAME]: gridBucket[GEOCENTROID_AGG_NAME], renderAs, }), id: gridKey, diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/convert_to_geojson.test.ts b/x-pack/plugins/maps/common/elasticsearch_util/convert_to_geojson.test.ts similarity index 97% rename from x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/convert_to_geojson.test.ts rename to x-pack/plugins/maps/common/elasticsearch_util/convert_to_geojson.test.ts index 523cc86915010..ee40a1f2fc751 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/convert_to_geojson.test.ts +++ b/x-pack/plugins/maps/common/elasticsearch_util/convert_to_geojson.test.ts @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../../../kibana_services', () => {}); - // @ts-ignore import { convertCompositeRespToGeoJson, convertRegularRespToGeoJson } from './convert_to_geojson'; -import { RENDER_AS } from '../../../../common/constants'; +import { RENDER_AS } from '../constants'; describe('convertCompositeRespToGeoJson', () => { const esResponse = { diff --git a/x-pack/plugins/maps/common/elasticsearch_geo_utils.d.ts b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.d.ts similarity index 89% rename from x-pack/plugins/maps/common/elasticsearch_geo_utils.d.ts rename to x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.d.ts index e57efca94d95e..cff8ba119e1de 100644 --- a/x-pack/plugins/maps/common/elasticsearch_geo_utils.d.ts +++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.d.ts @@ -5,8 +5,8 @@ */ import { FeatureCollection, GeoJsonProperties } from 'geojson'; -import { MapExtent } from './descriptor_types'; -import { ES_GEO_FIELD_TYPE } from './constants'; +import { MapExtent } from '../descriptor_types'; +import { ES_GEO_FIELD_TYPE } from '../constants'; export function scaleBounds(bounds: MapExtent, scaleFactor: number): MapExtent; diff --git a/x-pack/plugins/maps/common/elasticsearch_geo_utils.js b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.js similarity index 98% rename from x-pack/plugins/maps/common/elasticsearch_geo_utils.js rename to x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.js index f2bf83ae18bb0..be214e3b01e67 100644 --- a/x-pack/plugins/maps/common/elasticsearch_geo_utils.js +++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.js @@ -15,9 +15,9 @@ import { POLYGON_COORDINATES_EXTERIOR_INDEX, LON_INDEX, LAT_INDEX, -} from '../common/constants'; -import { getEsSpatialRelationLabel } from './i18n_getters'; -import { FILTERS } from '../../../../src/plugins/data/common'; +} from '../constants'; +import { getEsSpatialRelationLabel } from '../i18n_getters'; +import { FILTERS } from '../../../../../src/plugins/data/common'; import turfCircle from '@turf/circle'; const SPATIAL_FILTER_TYPE = FILTERS.SPATIAL_FILTER; diff --git a/x-pack/plugins/maps/common/elasticsearch_geo_utils.test.js b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js similarity index 100% rename from x-pack/plugins/maps/common/elasticsearch_geo_utils.test.js rename to x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js diff --git a/x-pack/plugins/maps/public/classes/util/es_agg_utils.test.ts b/x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.test.ts similarity index 100% rename from x-pack/plugins/maps/public/classes/util/es_agg_utils.test.ts rename to x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.test.ts diff --git a/x-pack/plugins/maps/public/classes/util/es_agg_utils.ts b/x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts similarity index 92% rename from x-pack/plugins/maps/public/classes/util/es_agg_utils.ts rename to x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts index 329a2a6fc64fb..7828c3cc6410b 100644 --- a/x-pack/plugins/maps/public/classes/util/es_agg_utils.ts +++ b/x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts @@ -5,8 +5,8 @@ */ import { i18n } from '@kbn/i18n'; import _ from 'lodash'; -import { IndexPattern, IFieldType } from '../../../../../../src/plugins/data/public'; -import { TOP_TERM_PERCENTAGE_SUFFIX } from '../../../common/constants'; +import { IndexPattern, IFieldType } from '../../../../../src/plugins/data/common'; +import { TOP_TERM_PERCENTAGE_SUFFIX } from '../constants'; export function getField(indexPattern: IndexPattern, fieldName: string) { const field = indexPattern.fields.getByName(fieldName); diff --git a/x-pack/plugins/security_solution/server/graphql/tls/index.ts b/x-pack/plugins/maps/common/elasticsearch_util/index.ts similarity index 68% rename from x-pack/plugins/security_solution/server/graphql/tls/index.ts rename to x-pack/plugins/maps/common/elasticsearch_util/index.ts index 7d745742090a6..ffb4a542374fa 100644 --- a/x-pack/plugins/security_solution/server/graphql/tls/index.ts +++ b/x-pack/plugins/maps/common/elasticsearch_util/index.ts @@ -4,5 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export { createTlsResolvers } from './resolvers'; -export { tlsSchema } from './schema.gql'; +export * from './es_agg_utils'; +export * from './convert_to_geojson'; +export * from './elasticsearch_geo_utils'; diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/geo_tile_utils.test.js b/x-pack/plugins/maps/common/geo_tile_utils.test.js similarity index 96% rename from x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/geo_tile_utils.test.js rename to x-pack/plugins/maps/common/geo_tile_utils.test.js index 88a6ce048a178..ae2623e168766 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/geo_tile_utils.test.js +++ b/x-pack/plugins/maps/common/geo_tile_utils.test.js @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../../../kibana_services', () => {}); - import { parseTileKey, getTileBoundingBox, expandToTileBoundaries } from './geo_tile_utils'; it('Should parse tile key', () => { diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/geo_tile_utils.js b/x-pack/plugins/maps/common/geo_tile_utils.ts similarity index 65% rename from x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/geo_tile_utils.js rename to x-pack/plugins/maps/common/geo_tile_utils.ts index 89b24522e4275..c6e35224458c5 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/geo_tile_utils.js +++ b/x-pack/plugins/maps/common/geo_tile_utils.ts @@ -5,18 +5,32 @@ */ import _ from 'lodash'; -import { DECIMAL_DEGREES_PRECISION } from '../../../../common/constants'; -import { clampToLatBounds } from '../../../../common/elasticsearch_geo_utils'; +import { DECIMAL_DEGREES_PRECISION } from './constants'; +import { clampToLatBounds } from './elasticsearch_util'; +import { MapExtent } from './descriptor_types'; const ZOOM_TILE_KEY_INDEX = 0; const X_TILE_KEY_INDEX = 1; const Y_TILE_KEY_INDEX = 2; -function getTileCount(zoom) { +function getTileCount(zoom: number): number { return Math.pow(2, zoom); } -export function parseTileKey(tileKey) { +export interface ESBounds { + top_left: { + lon: number; + lat: number; + }; + bottom_right: { + lon: number; + lat: number; + }; +} + +export function parseTileKey( + tileKey: string +): { x: number; y: number; zoom: number; tileCount: number } { const tileKeyParts = tileKey.split('/'); if (tileKeyParts.length !== 3) { @@ -42,7 +56,7 @@ export function parseTileKey(tileKey) { return { x, y, zoom, tileCount }; } -function sinh(x) { +function sinh(x: number): number { return (Math.exp(x) - Math.exp(-x)) / 2; } @@ -55,24 +69,52 @@ function sinh(x) { // We add one extra decimal level of precision because, at high zoom // levels rounding exactly can cause the boxes to render as uneven sizes // (some will be slightly larger and some slightly smaller) -function precisionRounding(v, minPrecision, binSize) { +function precisionRounding(v: number, minPrecision: number, binSize: number): number { let precision = Math.ceil(Math.abs(Math.log10(binSize))) + 1; precision = Math.max(precision, minPrecision); return _.round(v, precision); } -function tileToLatitude(y, tileCount) { +export function tile2long(x: number, z: number): number { + const tileCount = getTileCount(z); + return tileToLongitude(x, tileCount); +} + +export function tile2lat(y: number, z: number): number { + const tileCount = getTileCount(z); + return tileToLatitude(y, tileCount); +} + +export function tileToESBbox(x: number, y: number, z: number): ESBounds { + const wLon = tile2long(x, z); + const sLat = tile2lat(y + 1, z); + const eLon = tile2long(x + 1, z); + const nLat = tile2lat(y, z); + + return { + top_left: { + lon: wLon, + lat: nLat, + }, + bottom_right: { + lon: eLon, + lat: sLat, + }, + }; +} + +export function tileToLatitude(y: number, tileCount: number) { const radians = Math.atan(sinh(Math.PI - (2 * Math.PI * y) / tileCount)); const lat = (180 / Math.PI) * radians; return precisionRounding(lat, DECIMAL_DEGREES_PRECISION, 180 / tileCount); } -function tileToLongitude(x, tileCount) { +export function tileToLongitude(x: number, tileCount: number) { const lon = (x / tileCount) * 360 - 180; return precisionRounding(lon, DECIMAL_DEGREES_PRECISION, 360 / tileCount); } -export function getTileBoundingBox(tileKey) { +export function getTileBoundingBox(tileKey: string) { const { x, y, tileCount } = parseTileKey(tileKey); return { @@ -83,22 +125,22 @@ export function getTileBoundingBox(tileKey) { }; } -function sec(value) { +function sec(value: number): number { return 1 / Math.cos(value); } -function latitudeToTile(lat, tileCount) { +function latitudeToTile(lat: number, tileCount: number) { const radians = (clampToLatBounds(lat) * Math.PI) / 180; const y = ((1 - Math.log(Math.tan(radians) + sec(radians)) / Math.PI) / 2) * tileCount; return Math.floor(y); } -function longitudeToTile(lon, tileCount) { +function longitudeToTile(lon: number, tileCount: number) { const x = ((lon + 180) / 360) * tileCount; return Math.floor(x); } -export function expandToTileBoundaries(extent, zoom) { +export function expandToTileBoundaries(extent: MapExtent, zoom: number): MapExtent { const tileCount = getTileCount(zoom); const upperLeftX = longitudeToTile(extent.minLon, tileCount); diff --git a/x-pack/plugins/maps/public/actions/data_request_actions.ts b/x-pack/plugins/maps/public/actions/data_request_actions.ts index 2876f3d668a69..14d8196900506 100644 --- a/x-pack/plugins/maps/public/actions/data_request_actions.ts +++ b/x-pack/plugins/maps/public/actions/data_request_actions.ts @@ -40,7 +40,7 @@ import { ILayer } from '../classes/layers/layer'; import { IVectorLayer } from '../classes/layers/vector_layer/vector_layer'; import { DataMeta, MapExtent, MapFilters } from '../../common/descriptor_types'; import { DataRequestAbortError } from '../classes/util/data_request'; -import { scaleBounds, turfBboxToBounds } from '../../common/elasticsearch_geo_utils'; +import { scaleBounds, turfBboxToBounds } from '../../common/elasticsearch_util'; import { IVectorStyle } from '../classes/styles/vector/vector_style'; const FIT_TO_BOUNDS_SCALE_FACTOR = 0.1; diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index b00594cb7fb23..09491e5c3a7b3 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -54,7 +54,7 @@ import { MapRefreshConfig, } from '../../common/descriptor_types'; import { INITIAL_LOCATION } from '../../common/constants'; -import { scaleBounds } from '../../common/elasticsearch_geo_utils'; +import { scaleBounds } from '../../common/elasticsearch_util'; export function setMapInitError(errorMessage: string) { return { diff --git a/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts b/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts index 7b184819b839b..8cff98205186f 100644 --- a/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts @@ -12,7 +12,7 @@ import { IVectorSource } from '../sources/vector_source'; import { ESDocField } from './es_doc_field'; import { AGG_TYPE, FIELD_ORIGIN } from '../../../common/constants'; import { isMetricCountable } from '../util/is_metric_countable'; -import { getField, addFieldToDSL } from '../util/es_agg_utils'; +import { getField, addFieldToDSL } from '../../../common/elasticsearch_util'; import { TopTermPercentageField } from './top_term_percentage_field'; import { ITooltipProperty, TooltipProperty } from '../tooltips/tooltip_property'; import { ESAggTooltipProperty } from '../tooltips/es_agg_tooltip_property'; @@ -30,6 +30,7 @@ export class ESAggField implements IESAggField { private readonly _label?: string; private readonly _aggType: AGG_TYPE; private readonly _esDocField?: IField | undefined; + private readonly _canReadFromGeoJson: boolean; constructor({ label, @@ -37,18 +38,21 @@ export class ESAggField implements IESAggField { aggType, esDocField, origin, + canReadFromGeoJson = true, }: { label?: string; source: IESAggSource; aggType: AGG_TYPE; esDocField?: IField; origin: FIELD_ORIGIN; + canReadFromGeoJson?: boolean; }) { this._source = source; this._origin = origin; this._label = label; this._aggType = aggType; this._esDocField = esDocField; + this._canReadFromGeoJson = canReadFromGeoJson; } getSource(): IVectorSource { @@ -132,18 +136,19 @@ export class ESAggField implements IESAggField { } supportsAutoDomain(): boolean { - return true; + return this._canReadFromGeoJson ? true : this.supportsFieldMeta(); } canReadFromGeoJson(): boolean { - return true; + return this._canReadFromGeoJson; } } export function esAggFieldsFactory( aggDescriptor: AggDescriptor, source: IESAggSource, - origin: FIELD_ORIGIN + origin: FIELD_ORIGIN, + canReadFromGeoJson: boolean = true ): IESAggField[] { const aggField = new ESAggField({ label: aggDescriptor.label, @@ -153,12 +158,13 @@ export function esAggFieldsFactory( aggType: aggDescriptor.type, source, origin, + canReadFromGeoJson, }); const aggFields: IESAggField[] = [aggField]; if (aggDescriptor.field && aggDescriptor.type === AGG_TYPE.TERMS) { - aggFields.push(new TopTermPercentageField(aggField)); + aggFields.push(new TopTermPercentageField(aggField, canReadFromGeoJson)); } return aggFields; diff --git a/x-pack/plugins/maps/public/classes/fields/field.ts b/x-pack/plugins/maps/public/classes/fields/field.ts index 2c190d54f0265..658c2bba87847 100644 --- a/x-pack/plugins/maps/public/classes/fields/field.ts +++ b/x-pack/plugins/maps/public/classes/fields/field.ts @@ -21,12 +21,12 @@ export interface IField { getOrdinalFieldMetaRequest(): Promise; getCategoricalFieldMetaRequest(size: number): Promise; - // Determines whether Maps-app can automatically determine the domain of the field-values + // Whether Maps-app can automatically determine the domain of the field-values // if this is not the case (e.g. for .mvt tiled data), // then styling properties that require the domain to be known cannot use this property. supportsAutoDomain(): boolean; - // Determinse wheter Maps-app can automatically deterime the domain of the field-values + // Whether Maps-app can automatically determine the domain of the field-values // _without_ having to retrieve the data as GeoJson // e.g. for ES-sources, this would use the extended_stats API supportsFieldMeta(): boolean; diff --git a/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts b/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts index fc931b13619ef..50db04d08b2aa 100644 --- a/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts @@ -6,16 +6,17 @@ import { IESAggField } from './es_agg_field'; import { IVectorSource } from '../sources/vector_source'; -// @ts-ignore import { ITooltipProperty, TooltipProperty } from '../tooltips/tooltip_property'; import { TOP_TERM_PERCENTAGE_SUFFIX } from '../../../common/constants'; import { FIELD_ORIGIN } from '../../../common/constants'; export class TopTermPercentageField implements IESAggField { private readonly _topTermAggField: IESAggField; + private readonly _canReadFromGeoJson: boolean; - constructor(topTermAggField: IESAggField) { + constructor(topTermAggField: IESAggField, canReadFromGeoJson: boolean = true) { this._topTermAggField = topTermAggField; + this._canReadFromGeoJson = canReadFromGeoJson; } getSource(): IVectorSource { @@ -61,7 +62,7 @@ export class TopTermPercentageField implements IESAggField { } supportsAutoDomain(): boolean { - return true; + return this._canReadFromGeoJson; } supportsFieldMeta(): boolean { @@ -81,6 +82,6 @@ export class TopTermPercentageField implements IESAggField { } canReadFromGeoJson(): boolean { - return true; + return this._canReadFromGeoJson; } } diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index 8026f48fe6093..cd720063c6703 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -423,7 +423,7 @@ export class AbstractLayer implements ILayer { renderSourceSettingsEditor({ onChange }: SourceEditorArgs) { const source = this.getSourceForEditing(); - return source.renderSourceSettingsEditor({ onChange }); + return source.renderSourceSettingsEditor({ onChange, currentLayerType: this._descriptor.type }); } getPrevRequestToken(dataId: string): symbol | undefined { diff --git a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.ts b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.ts index a9c886617d3af..be947d79f4e39 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.ts @@ -31,14 +31,25 @@ export interface IESAggSource extends IESSource { export class AbstractESAggSource extends AbstractESSource { private readonly _metricFields: IESAggField[]; + private readonly _canReadFromGeoJson: boolean; - constructor(descriptor: AbstractESAggSourceDescriptor, inspectorAdapters: Adapters) { + constructor( + descriptor: AbstractESAggSourceDescriptor, + inspectorAdapters: Adapters, + canReadFromGeoJson = true + ) { super(descriptor, inspectorAdapters); this._metricFields = []; + this._canReadFromGeoJson = canReadFromGeoJson; if (descriptor.metrics) { descriptor.metrics.forEach((aggDescriptor: AggDescriptor) => { this._metricFields.push( - ...esAggFieldsFactory(aggDescriptor, this, this.getOriginForField()) + ...esAggFieldsFactory( + aggDescriptor, + this, + this.getOriginForField(), + this._canReadFromGeoJson + ) ); }); } @@ -72,7 +83,12 @@ export class AbstractESAggSource extends AbstractESSource { const metrics = this._metricFields.filter((esAggField) => esAggField.isValid()); // Handle case where metrics is empty because older saved object state is empty array or there are no valid aggs. return metrics.length === 0 - ? esAggFieldsFactory({ type: AGG_TYPE.COUNT }, this, this.getOriginForField()) + ? esAggFieldsFactory( + { type: AGG_TYPE.COUNT }, + this, + this.getOriginForField(), + this._canReadFromGeoJson + ) : metrics; } diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/resolution_editor.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/resolution_editor.test.tsx.snap new file mode 100644 index 0000000000000..ca9775594a9d7 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/resolution_editor.test.tsx.snap @@ -0,0 +1,73 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`resolution editor should add super-fine option 1`] = ` + + + +`; + +exports[`resolution editor should omit super-fine option 1`] = ` + + + +`; diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/update_source_editor.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/update_source_editor.test.tsx.snap new file mode 100644 index 0000000000000..dfce6b36396a7 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/update_source_editor.test.tsx.snap @@ -0,0 +1,121 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`source editor geo_grid_source default vector layer config should allow super-fine option 1`] = ` + + + +
+ +
+
+ + +
+ + + +
+ +
+
+ + + +
+ +
+`; + +exports[`source editor geo_grid_source should put limitations based on heatmap-rendering selection should not allow super-fine option for heatmaps and should not allow multiple metrics 1`] = ` + + + +
+ +
+
+ + +
+ + + +
+ +
+
+ + + +
+ +
+`; diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.d.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.d.ts index 2ce4353fca13c..ada76b8e4e674 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.d.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.d.ts @@ -5,11 +5,17 @@ */ import { AbstractESAggSource } from '../es_agg_source'; -import { ESGeoGridSourceDescriptor } from '../../../../common/descriptor_types'; +import { + ESGeoGridSourceDescriptor, + MapFilters, + MapQuery, + VectorSourceSyncMeta, +} from '../../../../common/descriptor_types'; import { GRID_RESOLUTION } from '../../../../common/constants'; import { IField } from '../../fields/field'; +import { ITiledSingleLayerVectorSource } from '../vector_source'; -export class ESGeoGridSource extends AbstractESAggSource { +export class ESGeoGridSource extends AbstractESAggSource implements ITiledSingleLayerVectorSource { static createDescriptor({ indexPatternId, geoField, @@ -19,8 +25,27 @@ export class ESGeoGridSource extends AbstractESAggSource { constructor(sourceDescriptor: ESGeoGridSourceDescriptor, inspectorAdapters: unknown); + readonly _descriptor: ESGeoGridSourceDescriptor; + getFieldNames(): string[]; getGridResolution(): GRID_RESOLUTION; getGeoGridPrecision(zoom: number): number; createField({ fieldName }: { fieldName: string }): IField; + + getLayerName(): string; + + getUrlTemplateWithMeta( + searchFilters: MapFilters & { + applyGlobalQuery: boolean; + fieldNames: string[]; + geogridPrecision?: number; + sourceQuery: MapQuery; + sourceMeta: VectorSourceSyncMeta; + } + ): Promise<{ + layerName: string; + urlTemplate: string; + minSourceZoom: number; + maxSourceZoom: number; + }>; } diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js index aa167cb577672..89258f04612fd 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js @@ -7,7 +7,11 @@ import React from 'react'; import uuid from 'uuid/v4'; -import { convertCompositeRespToGeoJson, convertRegularRespToGeoJson } from './convert_to_geojson'; +import { + convertCompositeRespToGeoJson, + convertRegularRespToGeoJson, + makeESBbox, +} from '../../../../common/elasticsearch_util'; import { UpdateSourceEditor } from './update_source_editor'; import { SOURCE_TYPES, @@ -15,13 +19,20 @@ import { RENDER_AS, GRID_RESOLUTION, VECTOR_SHAPE_TYPE, + MVT_SOURCE_LAYER_NAME, + GIS_API_PATH, + MVT_GETGRIDTILE_API_PATH, + GEOTILE_GRID_AGG_NAME, + GEOCENTROID_AGG_NAME, } from '../../../../common/constants'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { AbstractESAggSource, DEFAULT_METRIC } from '../es_agg_source'; import { DataRequestAbortError } from '../../util/data_request'; import { registerSource } from '../source_registry'; -import { makeESBbox } from '../../../../common/elasticsearch_geo_utils'; + +import rison from 'rison-node'; +import { getHttp } from '../../../kibana_services'; export const MAX_GEOTILE_LEVEL = 29; @@ -48,9 +59,14 @@ export class ESGeoGridSource extends AbstractESAggSource { }; } - renderSourceSettingsEditor({ onChange }) { + constructor(descriptor, inspectorAdapters) { + super(descriptor, inspectorAdapters, descriptor.resolution !== GRID_RESOLUTION.SUPER_FINE); + } + + renderSourceSettingsEditor({ onChange, currentLayerType }) { return ( { @@ -96,59 +99,67 @@ describe('ESGeoGridSource', () => { }; }; - describe('getGeoJsonWithMeta', () => { - let mockSearchSource: unknown; - beforeEach(async () => { - mockSearchSource = new MockSearchSource(); - const mockSearchService = { - searchSource: { - async create() { - return mockSearchSource as SearchSource; - }, - createEmpty() { - return mockSearchSource as SearchSource; - }, + let mockSearchSource: unknown; + beforeEach(async () => { + mockSearchSource = new MockSearchSource(); + const mockSearchService = { + searchSource: { + async create() { + return mockSearchSource as SearchSource; }, - }; + createEmpty() { + return mockSearchSource as SearchSource; + }, + }, + }; - // @ts-expect-error - getIndexPatternService.mockReturnValue(mockIndexPatternService); - // @ts-expect-error - getSearchService.mockReturnValue(mockSearchService); + // @ts-expect-error + getIndexPatternService.mockReturnValue(mockIndexPatternService); + // @ts-expect-error + getSearchService.mockReturnValue(mockSearchService); + // @ts-expect-error + getHttp.mockReturnValue({ + basePath: { + prepend(path: string) { + return `rootdir${path};`; + }, + }, }); + }); - const extent: MapExtent = { - minLon: -160, - minLat: -80, - maxLon: 160, - maxLat: 80, - }; + const extent: MapExtent = { + minLon: -160, + minLat: -80, + maxLon: 160, + maxLat: 80, + }; - const mapFilters: VectorSourceRequestMeta = { - geogridPrecision: 4, - filters: [], - timeFilters: { - from: 'now', - to: '15m', - mode: 'relative', - }, - extent, - applyGlobalQuery: true, - fieldNames: [], - buffer: extent, - sourceQuery: { - query: '', - language: 'KQL', - queryLastTriggeredAt: '2019-04-25T20:53:22.331Z', - }, - sourceMeta: null, - zoom: 0, - }; + const vectorSourceRequestMeta: VectorSourceRequestMeta = { + geogridPrecision: 4, + filters: [], + timeFilters: { + from: 'now', + to: '15m', + mode: 'relative', + }, + extent, + applyGlobalQuery: true, + fieldNames: [], + buffer: extent, + sourceQuery: { + query: '', + language: 'KQL', + queryLastTriggeredAt: '2019-04-25T20:53:22.331Z', + }, + sourceMeta: null, + zoom: 0, + }; + describe('getGeoJsonWithMeta', () => { it('Should configure the SearchSource correctly', async () => { const { data, meta } = await geogridSource.getGeoJsonWithMeta( 'foobarLayer', - mapFilters, + vectorSourceRequestMeta, () => {} ); @@ -215,5 +226,48 @@ describe('ESGeoGridSource', () => { it('should use heuristic to derive precision', () => { expect(geogridSource.getGeoGridPrecision(10)).toBe(12); }); + + it('Should not return valid precision for super-fine resolution', () => { + const superFineSource = new ESGeoGridSource( + { + id: 'foobar', + indexPatternId: 'fooIp', + geoField: geoFieldName, + metrics: [], + resolution: GRID_RESOLUTION.SUPER_FINE, + type: SOURCE_TYPES.ES_GEO_GRID, + requestType: RENDER_AS.HEATMAP, + }, + {} + ); + expect(superFineSource.getGeoGridPrecision(10)).toBe(NaN); + }); + }); + + describe('ITiledSingleLayerVectorSource', () => { + it('getLayerName', () => { + expect(geogridSource.getLayerName()).toBe('source_layer'); + }); + + it('getMinZoom', () => { + expect(geogridSource.getMinZoom()).toBe(0); + }); + + it('getMaxZoom', () => { + expect(geogridSource.getMaxZoom()).toBe(24); + }); + + it('getUrlTemplateWithMeta', async () => { + const urlTemplateWithMeta = await geogridSource.getUrlTemplateWithMeta( + vectorSourceRequestMeta + ); + + expect(urlTemplateWithMeta.layerName).toBe('source_layer'); + expect(urlTemplateWithMeta.minSourceZoom).toBe(0); + expect(urlTemplateWithMeta.maxSourceZoom).toBe(24); + expect(urlTemplateWithMeta.urlTemplate).toBe( + "rootdir/api/maps/mvt/getGridTile;?x={x}&y={y}&z={z}&geometryFieldName=bar&index=undefined&requestBody=(foobar:ES_DSL_PLACEHOLDER,params:('0':('0':index,'1':(fields:())),'1':('0':size,'1':0),'2':('0':filter,'1':!((geo_bounding_box:(bar:(bottom_right:!(180,-82.67628),top_left:!(-180,82.67628)))))),'3':('0':query),'4':('0':index,'1':(fields:())),'5':('0':query,'1':(language:KQL,query:'',queryLastTriggeredAt:'2019-04-25T20:53:22.331Z')),'6':('0':aggs,'1':(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:bar))),geotile_grid:(bounds:!n,field:bar,precision:!n,shard_size:65535,size:65535))))))&requestType=heatmap&geoFieldType=geo_point" + ); + }); }); }); diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.js b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.js index 28c24f58a0efc..71133cb25280c 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.js +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.js @@ -9,7 +9,7 @@ import { GRID_RESOLUTION } from '../../../../common/constants'; import { EuiSelect, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -const OPTIONS = [ +const BASE_OPTIONS = [ { value: GRID_RESOLUTION.COARSE, text: i18n.translate('xpack.maps.source.esGrid.coarseDropdownOption', { @@ -30,7 +30,18 @@ const OPTIONS = [ }, ]; -export function ResolutionEditor({ resolution, onChange }) { +export function ResolutionEditor({ resolution, onChange, includeSuperFine }) { + const options = [...BASE_OPTIONS]; + + if (includeSuperFine) { + options.push({ + value: GRID_RESOLUTION.SUPER_FINE, + text: i18n.translate('xpack.maps.source.esGrid.superFineDropDownOption', { + defaultMessage: 'super fine (beta)', + }), + }); + } + return ( onChange(e.target.value)} compressed diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.test.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.test.tsx new file mode 100644 index 0000000000000..369203dbe16c0 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +// @ts-expect-error +import { ResolutionEditor } from './resolution_editor'; +import { GRID_RESOLUTION } from '../../../../common/constants'; + +const defaultProps = { + resolution: GRID_RESOLUTION.COARSE, + onChange: () => {}, + includeSuperFine: false, +}; + +describe('resolution editor', () => { + test('should omit super-fine option', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + test('should add super-fine option', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.js b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.js index ac7d809c40f61..7e885c291b952 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.js +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.js @@ -6,7 +6,7 @@ import React, { Fragment, Component } from 'react'; -import { RENDER_AS } from '../../../../common/constants'; +import { GRID_RESOLUTION, LAYER_TYPE } from '../../../../common/constants'; import { MetricsEditor } from '../../../components/metrics_editor'; import { getIndexPatternService } from '../../../kibana_services'; import { ResolutionEditor } from './resolution_editor'; @@ -62,8 +62,25 @@ export class UpdateSourceEditor extends Component { this.props.onChange({ propName: 'metrics', value: metrics }); }; - _onResolutionChange = (e) => { - this.props.onChange({ propName: 'resolution', value: e }); + _onResolutionChange = (resolution) => { + let newLayerType; + if ( + this.props.currentLayerType === LAYER_TYPE.VECTOR || + this.props.currentLayerType === LAYER_TYPE.TILED_VECTOR + ) { + newLayerType = + resolution === GRID_RESOLUTION.SUPER_FINE ? LAYER_TYPE.TILED_VECTOR : LAYER_TYPE.VECTOR; + } else if (this.props.currentLayerType === LAYER_TYPE.HEATMAP) { + if (resolution === GRID_RESOLUTION.SUPER_FINE) { + throw new Error('Heatmap does not support SUPER_FINE resolution'); + } else { + newLayerType = LAYER_TYPE.HEATMAP; + } + } else { + throw new Error('Unexpected layer-type'); + } + + this.props.onChange({ propName: 'resolution', value: resolution, newLayerType }); }; _onRequestTypeSelect = (requestType) => { @@ -72,13 +89,13 @@ export class UpdateSourceEditor extends Component { _renderMetricsPanel() { const metricsFilter = - this.props.renderAs === RENDER_AS.HEATMAP + this.props.currentLayerType === LAYER_TYPE.HEATMAP ? (metric) => { //these are countable metrics, where blending heatmap color blobs make sense return isMetricCountable(metric.value); } : null; - const allowMultipleMetrics = this.props.renderAs !== RENDER_AS.HEATMAP; + const allowMultipleMetrics = this.props.currentLayerType !== LAYER_TYPE.HEATMAP; return ( @@ -115,6 +132,7 @@ export class UpdateSourceEditor extends Component { diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.test.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.test.tsx new file mode 100644 index 0000000000000..ceb79230bc832 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.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 { shallow } from 'enzyme'; + +// @ts-expect-error +import { UpdateSourceEditor } from './update_source_editor'; +import { GRID_RESOLUTION, LAYER_TYPE, RENDER_AS } from '../../../../common/constants'; + +const defaultProps = { + currentLayerType: LAYER_TYPE.VECTOR, + indexPatternId: 'foobar', + onChange: () => {}, + metrics: [], + renderAs: RENDER_AS.POINT, + resolution: GRID_RESOLUTION.COARSE, +}; + +describe('source editor geo_grid_source', () => { + describe('default vector layer config', () => { + test('should allow super-fine option', async () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); + }); + + describe('should put limitations based on heatmap-rendering selection', () => { + test('should not allow super-fine option for heatmaps and should not allow multiple metrics', async () => { + const component = shallow( + + ); + expect(component).toMatchSnapshot(); + }); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/convert_to_lines.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/convert_to_lines.js index 96a7f50cdf523..24ac6d31bc645 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/convert_to_lines.js +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/convert_to_lines.js @@ -5,7 +5,7 @@ */ import _ from 'lodash'; -import { extractPropertiesFromBucket } from '../../util/es_agg_utils'; +import { extractPropertiesFromBucket } from '../../../../common/elasticsearch_util'; const LAT_INDEX = 0; const LON_INDEX = 1; diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js index 9ec54335d4e78..0360208ef8370 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js @@ -16,7 +16,7 @@ import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { convertToLines } from './convert_to_lines'; import { AbstractESAggSource, DEFAULT_METRIC } from '../es_agg_source'; import { registerSource } from '../source_registry'; -import { turfBboxToBounds } from '../../../../common/elasticsearch_geo_utils'; +import { turfBboxToBounds } from '../../../../common/elasticsearch_util'; import { DataRequestAbortError } from '../../util/data_request'; const MAX_GEOTILE_LEVEL = 29; diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js index df83bd1cf5e60..edcafae54d54c 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js @@ -10,7 +10,7 @@ import rison from 'rison-node'; import { AbstractESSource } from '../es_source'; import { getSearchService, getHttp } from '../../../kibana_services'; -import { hitsToGeoJson } from '../../../../common/elasticsearch_geo_utils'; +import { hitsToGeoJson, getField, addFieldToDSL } from '../../../../common/elasticsearch_util'; import { UpdateSourceEditor } from './update_source_editor'; import { SOURCE_TYPES, @@ -31,7 +31,7 @@ import uuid from 'uuid/v4'; import { DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants'; import { ESDocField } from '../../fields/es_doc_field'; -import { getField, addFieldToDSL } from '../../util/es_agg_utils'; + import { registerSource } from '../source_registry'; export const sourceTitle = i18n.translate('xpack.maps.source.esSearchTitle', { diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js index d51ca46fd98ff..ab56ceeab4e77 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js @@ -11,14 +11,14 @@ import { getTimeFilter, getSearchService, } from '../../../kibana_services'; -import { createExtentFilter } from '../../../../common/elasticsearch_geo_utils'; +import { createExtentFilter } from '../../../../common/elasticsearch_util'; import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; import { copyPersistentState } from '../../../reducers/util'; import { DataRequestAbortError } from '../../util/data_request'; -import { expandToTileBoundaries } from '../es_geo_grid_source/geo_tile_utils'; +import { expandToTileBoundaries } from '../../../../common/geo_tile_utils'; import { search } from '../../../../../../../src/plugins/data/public'; export class AbstractESSource extends AbstractVectorSource { diff --git a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.js b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.js index b4ad256c1598a..359d22d2c44ce 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.js +++ b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.js @@ -16,7 +16,11 @@ import { import { getJoinAggKey } from '../../../../common/get_agg_key'; import { ESDocField } from '../../fields/es_doc_field'; import { AbstractESAggSource } from '../es_agg_source'; -import { getField, addFieldToDSL, extractPropertiesFromBucket } from '../../util/es_agg_utils'; +import { + getField, + addFieldToDSL, + extractPropertiesFromBucket, +} from '../../../../common/elasticsearch_util'; const TERMS_AGG_NAME = 'join'; diff --git a/x-pack/plugins/maps/public/classes/sources/source.ts b/x-pack/plugins/maps/public/classes/sources/source.ts index 7e7a7bd8f049d..946381817b8fc 100644 --- a/x-pack/plugins/maps/public/classes/sources/source.ts +++ b/x-pack/plugins/maps/public/classes/sources/source.ts @@ -18,6 +18,7 @@ import { OnSourceChangeArgs } from '../../connected_components/layer_panel/view' export type SourceEditorArgs = { onChange: (...args: OnSourceChangeArgs[]) => void; + currentLayerType?: string; }; export type ImmutableSourceProperty = { @@ -50,7 +51,7 @@ export interface ISource { getImmutableProperties(): Promise; getAttributions(): Promise; isESSource(): boolean; - renderSourceSettingsEditor({ onChange }: SourceEditorArgs): ReactElement | null; + renderSourceSettingsEditor(sourceEditorArgs: SourceEditorArgs): ReactElement | null; supportsFitToBounds(): Promise; showJoinEditor(): boolean; getJoinsDisabledReason(): string | null; @@ -126,7 +127,7 @@ export class AbstractSource implements ISource { return []; } - renderSourceSettingsEditor({ onChange }: SourceEditorArgs): ReactElement | null { + renderSourceSettingsEditor(sourceEditorArgs: SourceEditorArgs): ReactElement | null { return null; } diff --git a/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.tsx b/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.tsx index a300225178526..c75698805225f 100644 --- a/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.tsx +++ b/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.tsx @@ -86,6 +86,7 @@ export class HeatmapStyle implements IStyle { } else if (resolution === GRID_RESOLUTION.MOST_FINE) { radius = 32; } else { + // SUPER_FINE or any other is not supported. const errorMessage = i18n.translate('xpack.maps.style.heatmap.resolutionStyleErrorMessage', { defaultMessage: `Resolution param not recognized: {resolution}`, values: { resolution }, diff --git a/x-pack/plugins/maps/public/classes/styles/vector/style_util.ts b/x-pack/plugins/maps/public/classes/styles/vector/style_util.ts index d190a62e6f300..49d6ccdeb9316 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/style_util.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/style_util.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { MB_LOOKUP_FUNCTION, VECTOR_SHAPE_TYPE } from '../../../../common/constants'; +import { MB_LOOKUP_FUNCTION, VECTOR_SHAPE_TYPE, VECTOR_STYLES } from '../../../../common/constants'; import { Category } from '../../../../common/descriptor_types'; export function getOtherCategoryLabel() { @@ -14,8 +14,8 @@ export function getOtherCategoryLabel() { }); } -export function getComputedFieldName(styleName: string, fieldName: string) { - return `${getComputedFieldNamePrefix(fieldName)}__${styleName}`; +export function getComputedFieldName(styleName: VECTOR_STYLES, fieldName: string) { + return `${getComputedFieldNamePrefix(fieldName)}__${styleName as string}`; } export function getComputedFieldNamePrefix(fieldName: string) { diff --git a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js index 98267965fd30f..33a0f1c5bf088 100644 --- a/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js +++ b/x-pack/plugins/maps/public/connected_components/map/features_tooltip/feature_geometry_filter_form.js @@ -8,7 +8,7 @@ import React, { Component } from 'react'; import { i18n } from '@kbn/i18n'; import { URL_MAX_LENGTH } from '../../../../../../../src/core/public'; -import { createSpatialFilterWithGeometry } from '../../../../common/elasticsearch_geo_utils'; +import { createSpatialFilterWithGeometry } from '../../../../common/elasticsearch_util'; import { GEO_JSON_TYPE } from '../../../../common/constants'; import { GeometryFilterForm } from '../../../components/geometry_filter_form'; diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js index 49675ac6a3924..0356a8267c18a 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js @@ -15,7 +15,7 @@ import { createSpatialFilterWithGeometry, getBoundingBoxGeometry, roundCoordinates, -} from '../../../../../common/elasticsearch_geo_utils'; +} from '../../../../../common/elasticsearch_util'; import { DrawTooltip } from './draw_tooltip'; const DRAW_RECTANGLE = 'draw_rectangle'; diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/plugins/maps/public/connected_components/map/mb/view.js index eede1edf40cc4..ddc48cfc9c329 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/view.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/view.js @@ -23,7 +23,7 @@ import sprites1 from '@elastic/maki/dist/sprite@1.png'; import sprites2 from '@elastic/maki/dist/sprite@2.png'; import { DrawControl } from './draw_control'; import { TooltipControl } from './tooltip_control'; -import { clampToLatBounds, clampToLonBounds } from '../../../../common/elasticsearch_geo_utils'; +import { clampToLatBounds, clampToLonBounds } from '../../../../common/elasticsearch_util'; import { getInitialView } from './get_initial_view'; import { getPreserveDrawingBuffer } from '../../../kibana_services'; diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts index 03e0f753812c9..db4371e9cd590 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts @@ -31,7 +31,7 @@ import { SPATIAL_FILTERS_LAYER_ID, } from '../../common/constants'; // @ts-ignore -import { extractFeaturesFromFilters } from '../../common/elasticsearch_geo_utils'; +import { extractFeaturesFromFilters } from '../../common/elasticsearch_util'; import { MapStoreState } from '../reducers/store'; import { DataRequestDescriptor, diff --git a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts index 8688bbe549f51..2af6413da039b 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts +++ b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts @@ -6,11 +6,12 @@ import _ from 'lodash'; import { + SavedObject, SavedObjectAttribute, SavedObjectAttributes, SavedObjectsClientContract, } from 'kibana/server'; -import { IFieldType, IIndexPattern } from 'src/plugins/data/public'; +import { IFieldType, IndexPatternAttributes } from 'src/plugins/data/public'; import { ES_GEO_FIELD_TYPE, LAYER_TYPE, @@ -64,7 +65,9 @@ function getUniqueLayerCounts(layerCountsList: ILayerTypeCount[], mapsCount: num }, {}); } -function getIndexPatternsWithGeoFieldCount(indexPatterns: IIndexPattern[]) { +function getIndexPatternsWithGeoFieldCount( + indexPatterns: Array> +) { const fieldLists = indexPatterns.map((indexPattern) => indexPattern.attributes && indexPattern.attributes.fields ? JSON.parse(indexPattern.attributes.fields) @@ -112,7 +115,7 @@ function getEMSLayerCount(layerLists: LayerDescriptor[][]): ILayerTypeCount[] { } function isFieldGeoShape( - indexPatterns: IIndexPattern[], + indexPatterns: Array>, indexPatternId: string, geoField: string | undefined ): boolean { @@ -120,9 +123,11 @@ function isFieldGeoShape( return false; } - const matchIndexPattern = indexPatterns.find((indexPattern: IIndexPattern) => { - return indexPattern.id === indexPatternId; - }); + const matchIndexPattern = indexPatterns.find( + (indexPattern: SavedObject) => { + return indexPattern.id === indexPatternId; + } + ); if (!matchIndexPattern) { return false; @@ -140,7 +145,10 @@ function isFieldGeoShape( return !!matchField && matchField.type === ES_GEO_FIELD_TYPE.GEO_SHAPE; } -function isGeoShapeAggLayer(indexPatterns: IIndexPattern[], layer: LayerDescriptor): boolean { +function isGeoShapeAggLayer( + indexPatterns: Array>, + layer: LayerDescriptor +): boolean { if (layer.sourceDescriptor === null) { return false; } @@ -176,7 +184,7 @@ function isGeoShapeAggLayer(indexPatterns: IIndexPattern[], layer: LayerDescript function getGeoShapeAggCount( layerLists: LayerDescriptor[][], - indexPatterns: IIndexPattern[] + indexPatterns: Array> ): number { const countsPerMap: number[] = layerLists.map((layerList: LayerDescriptor[]) => { const geoShapeAggLayers = layerList.filter((layerDescriptor) => { @@ -204,7 +212,7 @@ export function buildMapsTelemetry({ settings, }: { mapSavedObjects: MapSavedObject[]; - indexPatternSavedObjects: IIndexPattern[]; + indexPatternSavedObjects: Array>; settings: SavedObjectAttribute; }): SavedObjectAttributes { const layerLists: LayerDescriptor[][] = getLayerLists(mapSavedObjects); @@ -283,10 +291,12 @@ export async function getMapsTelemetry(config: MapsConfigType) { const savedObjectsClient = getInternalRepository(); // @ts-ignore const mapSavedObjects: MapSavedObject[] = await getMapSavedObjects(savedObjectsClient); - const indexPatternSavedObjects: IIndexPattern[] = (await getIndexPatternSavedObjects( + const indexPatternSavedObjects: Array> = (await getIndexPatternSavedObjects( // @ts-ignore savedObjectsClient - )) as IIndexPattern[]; + )) as Array>; const settings: SavedObjectAttribute = { showMapVisualizationTypes: config.showMapVisualizationTypes, }; diff --git a/x-pack/plugins/maps/server/mvt/__tests__/json/0_0_0_gridagg.json b/x-pack/plugins/maps/server/mvt/__tests__/json/0_0_0_gridagg.json new file mode 100644 index 0000000000000..0945dc57fa512 --- /dev/null +++ b/x-pack/plugins/maps/server/mvt/__tests__/json/0_0_0_gridagg.json @@ -0,0 +1 @@ +{"took":2,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":10000,"relation":"gte"},"max_score":null,"hits":[]},"aggregations":{"gridSplit":{"buckets":[{"key":"7/37/48","doc_count":42637,"avg_of_TOTAL_AV":{"value":5398920.390458991},"gridCentroid":{"location":{"lat":40.77936432658204,"lon":-73.96795676049909},"count":42637}}]}}} diff --git a/x-pack/plugins/maps/server/mvt/__tests__/pbf/0_0_0.pbf b/x-pack/plugins/maps/server/mvt/__tests__/pbf/0_0_0_docs.pbf similarity index 100% rename from x-pack/plugins/maps/server/mvt/__tests__/pbf/0_0_0.pbf rename to x-pack/plugins/maps/server/mvt/__tests__/pbf/0_0_0_docs.pbf diff --git a/x-pack/plugins/maps/server/mvt/__tests__/pbf/0_0_0_grid_asgrid.pbf b/x-pack/plugins/maps/server/mvt/__tests__/pbf/0_0_0_grid_asgrid.pbf new file mode 100644 index 0000000000000..f2289865b8022 Binary files /dev/null and b/x-pack/plugins/maps/server/mvt/__tests__/pbf/0_0_0_grid_asgrid.pbf differ diff --git a/x-pack/plugins/maps/server/mvt/__tests__/pbf/0_0_0_grid_aspoint.pbf b/x-pack/plugins/maps/server/mvt/__tests__/pbf/0_0_0_grid_aspoint.pbf new file mode 100644 index 0000000000000..54b0791ccd136 Binary files /dev/null and b/x-pack/plugins/maps/server/mvt/__tests__/pbf/0_0_0_grid_aspoint.pbf differ diff --git a/x-pack/plugins/maps/server/mvt/__tests__/tile_searches.ts b/x-pack/plugins/maps/server/mvt/__tests__/tile_es_responses.ts similarity index 57% rename from x-pack/plugins/maps/server/mvt/__tests__/tile_searches.ts rename to x-pack/plugins/maps/server/mvt/__tests__/tile_es_responses.ts index 317d6434cf81e..9fbaba21e71d5 100644 --- a/x-pack/plugins/maps/server/mvt/__tests__/tile_searches.ts +++ b/x-pack/plugins/maps/server/mvt/__tests__/tile_es_responses.ts @@ -7,10 +7,6 @@ import * as path from 'path'; import * as fs from 'fs'; -const search000path = path.resolve(__dirname, './json/0_0_0_search.json'); -const search000raw = fs.readFileSync(search000path); -const search000json = JSON.parse((search000raw as unknown) as string); - export const TILE_SEARCHES = { '0.0.0': { countResponse: { @@ -22,7 +18,18 @@ export const TILE_SEARCHES = { failed: 0, }, }, - searchResponse: search000json, + searchResponse: loadJson('./json/0_0_0_search.json'), + }, +}; + +export const TILE_GRIDAGGS = { + '0.0.0': { + gridAggResponse: loadJson('./json/0_0_0_gridagg.json'), }, - '1.1.0': {}, }; + +function loadJson(filePath: string) { + const absolutePath = path.resolve(__dirname, filePath); + const rawContents = fs.readFileSync(absolutePath); + return JSON.parse((rawContents as unknown) as string); +} diff --git a/x-pack/plugins/maps/server/mvt/get_tile.test.ts b/x-pack/plugins/maps/server/mvt/get_tile.test.ts index b9c928d594539..76c1741ab2ad0 100644 --- a/x-pack/plugins/maps/server/mvt/get_tile.test.ts +++ b/x-pack/plugins/maps/server/mvt/get_tile.test.ts @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getTile } from './get_tile'; -import { TILE_SEARCHES } from './__tests__/tile_searches'; +import { getGridTile, getTile } from './get_tile'; +import { TILE_GRIDAGGS, TILE_SEARCHES } from './__tests__/tile_es_responses'; import { Logger } from 'src/core/server'; import * as path from 'path'; import * as fs from 'fs'; +import { ES_GEO_FIELD_TYPE, RENDER_AS } from '../../common/constants'; describe('getTile', () => { const mockCallElasticsearch = jest.fn(); @@ -51,13 +52,84 @@ describe('getTile', () => { callElasticsearch: mockCallElasticsearch, }); - if (tile === null) { - throw new Error('Tile should be created'); - } + compareTiles('./__tests__/pbf/0_0_0_docs.pbf', tile); + }); +}); + +describe('getGridTile', () => { + const mockCallElasticsearch = jest.fn(); + + const geometryFieldName = 'geometry'; + + // For mock-purposes only. The ES-call response is mocked in 0_0_0_gridagg.json file + const requestBody = { + _source: { excludes: [] }, + aggs: { + gridSplit: { + aggs: { + // eslint-disable-next-line @typescript-eslint/naming-convention + avg_of_TOTAL_AV: { avg: { field: 'TOTAL_AV' } }, + gridCentroid: { geo_centroid: { field: geometryFieldName } }, + }, + geotile_grid: { + bounds: null, + field: geometryFieldName, + precision: null, + shard_size: 65535, + size: 65535, + }, + }, + }, + docvalue_fields: [], + query: { + bool: { + filter: [], + }, + }, + script_fields: {}, + size: 0, + stored_fields: ['*'], + }; - const expectedPath = path.resolve(__dirname, './__tests__/pbf/0_0_0.pbf'); - const expectedBin = fs.readFileSync(expectedPath, 'binary'); - const expectedTile = Buffer.from(expectedBin, 'binary'); - expect(expectedTile.equals(tile)).toBe(true); + beforeEach(() => { + mockCallElasticsearch.mockReset(); + mockCallElasticsearch.mockImplementation((type) => { + return TILE_GRIDAGGS['0.0.0'].gridAggResponse; + }); + }); + + const defaultParams = { + x: 0, + y: 0, + z: 0, + index: 'manhattan', + requestBody, + geometryFieldName, + logger: ({ + info: () => {}, + } as unknown) as Logger, + callElasticsearch: mockCallElasticsearch, + requestType: RENDER_AS.POINT, + geoFieldType: ES_GEO_FIELD_TYPE.GEO_POINT, + }; + + test('0.0.0 tile (clusters)', async () => { + const tile = await getGridTile(defaultParams); + compareTiles('./__tests__/pbf/0_0_0_grid_aspoint.pbf', tile); + }); + + test('0.0.0 tile (grids)', async () => { + const tile = await getGridTile({ ...defaultParams, requestType: RENDER_AS.GRID }); + compareTiles('./__tests__/pbf/0_0_0_grid_asgrid.pbf', tile); }); }); + +function compareTiles(expectedRelativePath: string, actualTile: Buffer | null) { + if (actualTile === null) { + throw new Error('Tile should be created'); + } + const expectedPath = path.resolve(__dirname, expectedRelativePath); + const expectedBin = fs.readFileSync(expectedPath, 'binary'); + const expectedTile = Buffer.from(expectedBin, 'binary'); + expect(expectedTile.equals(actualTile)).toBe(true); +} diff --git a/x-pack/plugins/maps/server/mvt/get_tile.ts b/x-pack/plugins/maps/server/mvt/get_tile.ts index 9621f7f174a30..dd88be7f80c2e 100644 --- a/x-pack/plugins/maps/server/mvt/get_tile.ts +++ b/x-pack/plugins/maps/server/mvt/get_tile.ts @@ -13,22 +13,89 @@ import { Feature, FeatureCollection, Polygon } from 'geojson'; import { ES_GEO_FIELD_TYPE, FEATURE_ID_PROPERTY_NAME, + GEOTILE_GRID_AGG_NAME, KBN_TOO_MANY_FEATURES_PROPERTY, + MAX_ZOOM, MVT_SOURCE_LAYER_NAME, + RENDER_AS, + SUPER_FINE_ZOOM_DELTA, } from '../../common/constants'; -import { hitsToGeoJson } from '../../common/elasticsearch_geo_utils'; +import { hitsToGeoJson } from '../../common/elasticsearch_util'; import { flattenHit } from './util'; +import { convertRegularRespToGeoJson } from '../../common/elasticsearch_util'; +import { ESBounds, tile2lat, tile2long, tileToESBbox } from '../../common/geo_tile_utils'; -interface ESBounds { - top_left: { - lon: number; - lat: number; - }; - bottom_right: { - lon: number; - lat: number; - }; +export async function getGridTile({ + logger, + callElasticsearch, + index, + geometryFieldName, + x, + y, + z, + requestBody = {}, + requestType = RENDER_AS.POINT, + geoFieldType = ES_GEO_FIELD_TYPE.GEO_POINT, +}: { + x: number; + y: number; + z: number; + geometryFieldName: string; + index: string; + callElasticsearch: (type: string, ...args: any[]) => Promise; + logger: Logger; + requestBody: any; + requestType: RENDER_AS; + geoFieldType: ES_GEO_FIELD_TYPE; +}): Promise { + const esBbox: ESBounds = tileToESBbox(x, y, z); + try { + let bboxFilter; + if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT) { + bboxFilter = { + geo_bounding_box: { + [geometryFieldName]: esBbox, + }, + }; + } else if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_SHAPE) { + const geojsonPolygon = tileToGeoJsonPolygon(x, y, z); + bboxFilter = { + geo_shape: { + [geometryFieldName]: { + shape: geojsonPolygon, + relation: 'INTERSECTS', + }, + }, + }; + } else { + throw new Error(`${geoFieldType} is not valid geo field-type`); + } + requestBody.query.bool.filter.push(bboxFilter); + + requestBody.aggs[GEOTILE_GRID_AGG_NAME].geotile_grid.precision = Math.min( + z + SUPER_FINE_ZOOM_DELTA, + MAX_ZOOM + ); + requestBody.aggs[GEOTILE_GRID_AGG_NAME].geotile_grid.bounds = esBbox; + + const esGeotileGridQuery = { + index, + body: requestBody, + }; + + const gridAggResult = await callElasticsearch('search', esGeotileGridQuery); + const features: Feature[] = convertRegularRespToGeoJson(gridAggResult, requestType); + const featureCollection: FeatureCollection = { + features, + type: 'FeatureCollection', + }; + + return createMvtTile(featureCollection, z, x, y); + } catch (e) { + logger.warn(`Cannot generate grid-tile for ${z}/${x}/${y}: ${e.message}`); + return null; + } } export async function getTile({ @@ -149,26 +216,7 @@ export async function getTile({ type: 'FeatureCollection', }; - const tileIndex = geojsonvt(featureCollection, { - maxZoom: 24, // max zoom to preserve detail on; can't be higher than 24 - tolerance: 3, // simplification tolerance (higher means simpler) - extent: 4096, // tile extent (both width and height) - buffer: 64, // tile buffer on each side - debug: 0, // logging level (0 to disable, 1 or 2) - lineMetrics: false, // whether to enable line metrics tracking for LineString/MultiLineString features - promoteId: null, // name of a feature property to promote to feature.id. Cannot be used with `generateId` - generateId: false, // whether to generate feature ids. Cannot be used with `promoteId` - indexMaxZoom: 5, // max zoom in the initial tile index - indexMaxPoints: 100000, // max number of points per tile in the index - }); - const tile = tileIndex.getTile(z, x, y); - - if (tile) { - const pbf = vtpbf.fromGeojsonVt({ [MVT_SOURCE_LAYER_NAME]: tile }, { version: 2 }); - return Buffer.from(pbf); - } else { - return null; - } + return createMvtTile(featureCollection, z, x, y); } catch (e) { logger.warn(`Cannot generate tile for ${z}/${x}/${y}: ${e.message}`); return null; @@ -195,15 +243,6 @@ function tileToGeoJsonPolygon(x: number, y: number, z: number): Polygon { }; } -function tile2long(x: number, z: number): number { - return (x / Math.pow(2, z)) * 360 - 180; -} - -function tile2lat(y: number, z: number): number { - const n = Math.PI - (2 * Math.PI * y) / Math.pow(2, z); - return (180 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))); -} - function esBboxToGeoJsonPolygon(esBounds: ESBounds): Polygon { let minLon = esBounds.top_left.lon; const maxLon = esBounds.bottom_right.lon; @@ -224,3 +263,31 @@ function esBboxToGeoJsonPolygon(esBounds: ESBounds): Polygon { ], }; } + +function createMvtTile( + featureCollection: FeatureCollection, + z: number, + x: number, + y: number +): Buffer | null { + const tileIndex = geojsonvt(featureCollection, { + maxZoom: 24, // max zoom to preserve detail on; can't be higher than 24 + tolerance: 3, // simplification tolerance (higher means simpler) + extent: 4096, // tile extent (both width and height) + buffer: 64, // tile buffer on each side + debug: 0, // logging level (0 to disable, 1 or 2) + lineMetrics: false, // whether to enable line metrics tracking for LineString/MultiLineString features + promoteId: null, // name of a feature property to promote to feature.id. Cannot be used with `generateId` + generateId: false, // whether to generate feature ids. Cannot be used with `promoteId` + indexMaxZoom: 5, // max zoom in the initial tile index + indexMaxPoints: 100000, // max number of points per tile in the index + }); + const tile = tileIndex.getTile(z, x, y); + + if (tile) { + const pbf = vtpbf.fromGeojsonVt({ [MVT_SOURCE_LAYER_NAME]: tile }, { version: 2 }); + return Buffer.from(pbf); + } else { + return null; + } +} diff --git a/x-pack/plugins/maps/server/mvt/mvt_routes.ts b/x-pack/plugins/maps/server/mvt/mvt_routes.ts index 32c14a355ba2a..266a240b53017 100644 --- a/x-pack/plugins/maps/server/mvt/mvt_routes.ts +++ b/x-pack/plugins/maps/server/mvt/mvt_routes.ts @@ -6,10 +6,21 @@ import rison from 'rison-node'; import { schema } from '@kbn/config-schema'; -import { Logger } from 'src/core/server'; +import { + KibanaRequest, + KibanaResponseFactory, + Logger, + RequestHandlerContext, +} from 'src/core/server'; import { IRouter } from 'src/core/server'; -import { MVT_GETTILE_API_PATH, API_ROOT_PATH } from '../../common/constants'; -import { getTile } from './get_tile'; +import { + MVT_GETTILE_API_PATH, + API_ROOT_PATH, + MVT_GETGRIDTILE_API_PATH, + ES_GEO_FIELD_TYPE, + RENDER_AS, +} from '../../common/constants'; +import { getGridTile, getTile } from './get_tile'; const CACHE_TIMEOUT = 0; // Todo. determine good value. Unsure about full-implications (e.g. wrt. time-based data). @@ -28,46 +39,93 @@ export function initMVTRoutes({ router, logger }: { logger: Logger; router: IRou }), }, }, - async (context, request, response) => { + async ( + context: RequestHandlerContext, + request: KibanaRequest, unknown>, + response: KibanaResponseFactory + ) => { const { query } = request; + const requestBodyDSL = rison.decode(query.requestBody as string); - const callElasticsearch = async (type: string, ...args: any[]): Promise => { - return await context.core.elasticsearch.legacy.client.callAsCurrentUser(type, ...args); - }; + const tile = await getTile({ + logger, + callElasticsearch: makeCallElasticsearch(context), + geometryFieldName: query.geometryFieldName as string, + x: query.x as number, + y: query.y as number, + z: query.z as number, + index: query.index as string, + requestBody: requestBodyDSL as any, + }); - const requestBodyDSL = rison.decode(query.requestBody); + return sendResponse(response, tile); + } + ); - const tile = await getTile({ + router.get( + { + path: `${API_ROOT_PATH}/${MVT_GETGRIDTILE_API_PATH}`, + validate: { + query: schema.object({ + x: schema.number(), + y: schema.number(), + z: schema.number(), + geometryFieldName: schema.string(), + requestBody: schema.string(), + index: schema.string(), + requestType: schema.string(), + geoFieldType: schema.string(), + }), + }, + }, + async ( + context: RequestHandlerContext, + request: KibanaRequest, unknown>, + response: KibanaResponseFactory + ) => { + const { query } = request; + const requestBodyDSL = rison.decode(query.requestBody as string); + + const tile = await getGridTile({ logger, - callElasticsearch, - geometryFieldName: query.geometryFieldName, - x: query.x, - y: query.y, - z: query.z, - index: query.index, - requestBody: requestBodyDSL, + callElasticsearch: makeCallElasticsearch(context), + geometryFieldName: query.geometryFieldName as string, + x: query.x as number, + y: query.y as number, + z: query.z as number, + index: query.index as string, + requestBody: requestBodyDSL as any, + requestType: query.requestType as RENDER_AS, + geoFieldType: query.geoFieldType as ES_GEO_FIELD_TYPE, }); - if (tile) { - return response.ok({ - body: tile, - headers: { - 'content-disposition': 'inline', - 'content-length': `${tile.length}`, - 'Content-Type': 'application/x-protobuf', - 'Cache-Control': `max-age=${CACHE_TIMEOUT}`, - }, - }); - } else { - return response.ok({ - headers: { - 'content-disposition': 'inline', - 'content-length': '0', - 'Content-Type': 'application/x-protobuf', - 'Cache-Control': `max-age=${CACHE_TIMEOUT}`, - }, - }); - } + return sendResponse(response, tile); } ); } + +function sendResponse(response: KibanaResponseFactory, tile: any) { + const headers = { + 'content-disposition': 'inline', + 'content-length': tile ? `${tile.length}` : `0`, + 'Content-Type': 'application/x-protobuf', + 'Cache-Control': `max-age=${CACHE_TIMEOUT}`, + }; + + if (tile) { + return response.ok({ + body: tile, + headers, + }); + } else { + return response.ok({ + headers, + }); + } +} + +function makeCallElasticsearch(context: RequestHandlerContext) { + return async (type: string, ...args: any[]): Promise => { + return context.core.elasticsearch.legacy.client.callAsCurrentUser(type, ...args); + }; +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index 161dde51df43e..1c8bfafeb10ff 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -12,6 +12,7 @@ import { extractErrorMessage } from '../../../../../../../common/util/errors'; import { DeepReadonly } from '../../../../../../../common/types/common'; import { ml } from '../../../../../services/ml_api_service'; import { useMlContext } from '../../../../../contexts/ml'; +import { DuplicateIndexPatternError } from '../../../../../../../../../../src/plugins/data/public'; import { useRefreshAnalyticsList, @@ -130,19 +131,25 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { const indexPatternName = destinationIndex; try { - const newIndexPattern = await mlContext.indexPatterns.make(); + await mlContext.indexPatterns.createAndSave( + { + title: indexPatternName, + }, + false, + true + ); - Object.assign(newIndexPattern, { - id: '', - title: indexPatternName, + addRequestMessage({ + message: i18n.translate( + 'xpack.ml.dataframe.analytics.create.createIndexPatternSuccessMessage', + { + defaultMessage: 'Kibana index pattern {indexPatternName} created.', + values: { indexPatternName }, + } + ), }); - - const id = await newIndexPattern.create(); - - await mlContext.indexPatterns.clearCache(); - - // id returns false if there's a duplicate index pattern. - if (id === false) { + } catch (e) { + if (e instanceof DuplicateIndexPatternError) { addRequestMessage({ error: i18n.translate( 'xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessageError', @@ -158,34 +165,17 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { } ), }); - return; - } - - // check if there's a default index pattern, if not, - // set the newly created one as the default index pattern. - if (!mlContext.kibanaConfig.get('defaultIndex')) { - await mlContext.kibanaConfig.set('defaultIndex', id); + } else { + addRequestMessage({ + error: extractErrorMessage(e), + message: i18n.translate( + 'xpack.ml.dataframe.analytics.create.createIndexPatternErrorMessage', + { + defaultMessage: 'An error occurred creating the Kibana index pattern:', + } + ), + }); } - - addRequestMessage({ - message: i18n.translate( - 'xpack.ml.dataframe.analytics.create.createIndexPatternSuccessMessage', - { - defaultMessage: 'Kibana index pattern {indexPatternName} created.', - values: { indexPatternName }, - } - ), - }); - } catch (e) { - addRequestMessage({ - error: extractErrorMessage(e), - message: i18n.translate( - 'xpack.ml.dataframe.analytics.create.createIndexPatternErrorMessage', - { - defaultMessage: 'An error occurred creating the Kibana index pattern:', - } - ), - }); } }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js index 08b61a5fa4eed..2ad0c9b1ac263 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/import_view/import_view.js @@ -215,7 +215,7 @@ export class ImportView extends Component { // mappings, use this field as the time field. // This relies on the field being populated by // the ingest pipeline on ingest - if (mappings[DEFAULT_TIME_FIELD] !== undefined) { + if (mappings.properties[DEFAULT_TIME_FIELD] !== undefined) { timeFieldName = DEFAULT_TIME_FIELD; this.setState({ timeFieldName }); } @@ -615,34 +615,16 @@ export class ImportView extends Component { } } -async function createKibanaIndexPattern( - indexPatternName, - indexPatterns, - timeFieldName, - kibanaConfig -) { +async function createKibanaIndexPattern(indexPatternName, indexPatterns, timeFieldName) { try { - const emptyPattern = await indexPatterns.make(); - - Object.assign(emptyPattern, { - id: '', + const emptyPattern = await indexPatterns.createAndSave({ title: indexPatternName, timeFieldName, }); - const id = await emptyPattern.create(); - - await indexPatterns.clearCache(); - - // check if there's a default index pattern, if not, - // set the newly created one as the default index pattern. - if (!kibanaConfig.get('defaultIndex')) { - await kibanaConfig.set('defaultIndex', id); - } - return { success: true, - id, + id: emptyPattern.id, }; } catch (error) { return { diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap b/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap index fed435d47dfc6..ad76bb9115617 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap @@ -56,6 +56,7 @@ exports[`CalendarForm Renders calendar form 1`] = ` labelType="label" > - +

{description}

@@ -116,6 +116,7 @@ export const CalendarForm = ({ value={calendarId} onChange={onCalendarIdChange} disabled={isEdit === true || saving === true} + data-test-subj="mlCalendarIdInput" />
@@ -132,6 +133,7 @@ export const CalendarForm = ({ value={description} onChange={onDescriptionChange} disabled={isEdit === true || saving === true} + data-test-subj="mlCalendarDescriptionInput" /> diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js index d80e248674a8f..0b5d2b7b5a3ea 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js @@ -257,7 +257,12 @@ export class NewEventModal extends Component { return ( - + @@ -293,13 +299,18 @@ export class NewEventModal extends Component { - + - + c.calendar_id).join(', '), + }} /> } onCancel={this.closeDestroyModal} @@ -130,18 +135,7 @@ export class CalendarsListUI extends Component { } buttonColor="danger" defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - > -

- c.calendar_id).join(', '), - }} - /> -

-
+ /> ); } diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap b/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap index 6e9cd17deabee..969406724537d 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap @@ -7,7 +7,7 @@ exports[`AddItemPopover calls addItems with multiple items on clicking Add butto button={ @@ -71,6 +72,7 @@ exports[`AddItemPopover calls addItems with multiple items on clicking Add butto grow={false} > @@ -93,7 +95,7 @@ exports[`AddItemPopover opens the popover onButtonClick 1`] = ` button={ @@ -157,6 +160,7 @@ exports[`AddItemPopover opens the popover onButtonClick 1`] = ` grow={false} > @@ -179,7 +183,7 @@ exports[`AddItemPopover renders the popover 1`] = ` button={ @@ -243,6 +248,7 @@ exports[`AddItemPopover renders the popover 1`] = ` grow={false} > diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/add_item_popover.js b/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/add_item_popover.js index 07e060d87b36a..53a3877e2f1bd 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/add_item_popover.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/add_item_popover.js @@ -84,7 +84,7 @@ export class AddItemPopover extends Component { iconSide="right" onClick={this.onButtonClick} isDisabled={this.props.canCreateFilter === false} - data-test-subj="mlFilterListAddItemButton" + data-test-subj="mlFilterListOpenNewItemsPopoverButton" > } > - + @@ -127,6 +131,7 @@ export class AddItemPopover extends Component { } + data-test-subj="mlFilterListDeleteConfirmation" defaultFocusedButton="confirm" onCancel={[Function]} onConfirm={[Function]} diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js b/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js index 75fdce8e2bac8..5aafe79645f6a 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js @@ -86,6 +86,7 @@ export class DeleteFilterListModal extends Component { } buttonColor="danger" defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + data-test-subj={'mlFilterListDeleteConfirmation'} /> ); diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap b/x-pack/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap index 9904e90a5afae..268b93923a432 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap @@ -47,6 +47,7 @@ exports[`FilterListUsagePopover opens the popover onButtonClick 1`] = ` labelType="label" > diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.js b/x-pack/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.js index 06ace034ca819..b7bcb201f2438 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.js @@ -102,6 +102,7 @@ export class EditDescriptionPopover extends Component { name="filter_list_description" value={value} onChange={this.onChange} + data-test-subj={'mlFilterListDescriptionInput'} /> diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap index c2fab64473228..f6a4f76975553 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap @@ -80,6 +80,7 @@ exports[`EditFilterList adds new items to filter list 1`] = ` grow={false} > - +

A test filter list

@@ -180,6 +183,7 @@ exports[`EditFilterListHeader renders the header when creating a new filter list labelType="label" > - +

A test filter list

diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js index 681c54ca9eee0..9ea470a388f02 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js @@ -362,7 +362,10 @@ export class EditFilterListUI extends Component { /> - this.returnToFiltersList()}> + this.returnToFiltersList()} + > updateNewFilterId(e.target.value)} + data-test-subj={'mlNewFilterListIdInput'} /> ); @@ -96,7 +97,7 @@ export const EditFilterListHeader = ({ if (description !== undefined && description.length > 0) { descriptionField = ( - +

{description}

); diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.js b/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.js index ed992b4e866ff..9e1457483cb2c 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.js @@ -214,7 +214,7 @@ export function FilterListsTable({ isSelectable={true} data-test-subj="mlFilterListsTable" rowProps={(item) => ({ - 'data-test-subj': `mlFilterListsRow row-${item.filter_id}`, + 'data-test-subj': `mlFilterListRow row-${item.filter_id}`, })} />
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 192552b25d15a..42be3dd8252f9 100644 --- a/x-pack/plugins/ml/public/application/util/index_utils.ts +++ b/x-pack/plugins/ml/public/application/util/index_utils.ts @@ -104,7 +104,11 @@ export function getQueryFromSavedSearch(savedSearch: SavedSearchSavedObject) { export function getIndexPatternById(id: string): Promise { if (indexPatternsContract !== null) { - return indexPatternsContract.get(id); + if (id) { + return indexPatternsContract.get(id); + } else { + return indexPatternsContract.create({}); + } } else { throw new Error('Index patterns are not initialized!'); } diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/index_patterns.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/index_patterns.ts index d1a4df768a6ae..394dff1408134 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/index_patterns.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/index_patterns.ts @@ -5,13 +5,13 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; -import { IIndexPattern } from 'src/plugins/data/server'; +import { IndexPatternAttributes } from 'src/plugins/data/server'; export class IndexPatternHandler { constructor(private savedObjectsClient: SavedObjectsClientContract) {} // returns a id based on an index pattern name async getIndexPatternId(indexName: string) { - const response = await this.savedObjectsClient.find({ + const response = await this.savedObjectsClient.find({ type: 'index-pattern', perPage: 10, search: `"${indexName}"`, diff --git a/x-pack/plugins/monitoring/public/angular/app_modules.ts b/x-pack/plugins/monitoring/public/angular/app_modules.ts index 499610045d771..4ef905fd35fc4 100644 --- a/x-pack/plugins/monitoring/public/angular/app_modules.ts +++ b/x-pack/plugins/monitoring/public/angular/app_modules.ts @@ -41,10 +41,6 @@ import { licenseProvider } from '../services/license'; // @ts-ignore import { titleProvider } from '../services/title'; // @ts-ignore -import { monitoringBeatsBeatProvider } from '../directives/beats/beat'; -// @ts-ignore -import { monitoringBeatsOverviewProvider } from '../directives/beats/overview'; -// @ts-ignore import { monitoringMlListingProvider } from '../directives/elasticsearch/ml_job_listing'; // @ts-ignore import { monitoringMainProvider } from '../directives/main'; @@ -153,8 +149,6 @@ function createMonitoringAppServices() { function createMonitoringAppDirectives() { angular .module('monitoring/directives', []) - .directive('monitoringBeatsBeat', monitoringBeatsBeatProvider) - .directive('monitoringBeatsOverview', monitoringBeatsOverviewProvider) .directive('monitoringMlListing', monitoringMlListingProvider) .directive('monitoringMain', monitoringMainProvider); } diff --git a/x-pack/plugins/monitoring/public/components/apm/instance/instance.js b/x-pack/plugins/monitoring/public/components/apm/instance/instance.js index 396d2258edd0c..eec24e741ac41 100644 --- a/x-pack/plugins/monitoring/public/components/apm/instance/instance.js +++ b/x-pack/plugins/monitoring/public/components/apm/instance/instance.js @@ -42,9 +42,7 @@ export function ApmServerInstance({ summary, metrics, ...props }) { const charts = seriesToShow.map((data, index) => ( - - - + )); @@ -55,15 +53,15 @@ export function ApmServerInstance({ summary, metrics, ...props }) {

+ + + + - - - - {charts} diff --git a/x-pack/plugins/monitoring/public/components/apm/instances/instances.js b/x-pack/plugins/monitoring/public/components/apm/instances/instances.js index 6dcfa6dd043aa..e05ba1878caed 100644 --- a/x-pack/plugins/monitoring/public/components/apm/instances/instances.js +++ b/x-pack/plugins/monitoring/public/components/apm/instances/instances.js @@ -156,11 +156,11 @@ export function ApmServerInstances({ apms, setupMode }) { /> + + + + - - - - {setupModeCallout} ( - - - + )); @@ -51,15 +49,15 @@ export function ApmOverview({ stats, metrics, ...props }) {

+ + + + - - - - {charts} diff --git a/x-pack/plugins/monitoring/public/components/beats/beat/beat.js b/x-pack/plugins/monitoring/public/components/beats/beat/beat.js index 3fe211c0f2edc..f489271659bfe 100644 --- a/x-pack/plugins/monitoring/public/components/beats/beat/beat.js +++ b/x-pack/plugins/monitoring/public/components/beats/beat/beat.js @@ -135,6 +135,9 @@ export function Beat({ summary, metrics, ...props }) { + + + diff --git a/x-pack/plugins/monitoring/public/components/beats/listing/listing.js b/x-pack/plugins/monitoring/public/components/beats/listing/listing.js index be8595e8e6bbe..60a35e00a4c63 100644 --- a/x-pack/plugins/monitoring/public/components/beats/listing/listing.js +++ b/x-pack/plugins/monitoring/public/components/beats/listing/listing.js @@ -13,6 +13,7 @@ import { EuiSpacer, EuiLink, EuiScreenReaderOnly, + EuiPanel, } from '@elastic/eui'; import { Stats } from '../../beats'; import { formatMetric } from '../../../lib/format_number'; @@ -153,9 +154,11 @@ export class Listing extends PureComponent { /> - + - + + + {setupModeCallOut} - + + + + - - -

- -

-
- - -
+ +

+ +

+
+ +
- - -

- -

-
- - -
+ +

+ +

+
+ +
- - -

- -

-
- - -
+ +

+ +

+
+ +
- +
+ + @@ -212,18 +213,25 @@ exports[`Overview that overview page shows a message if there is no beats data 1 /> - + + + + - + + + diff --git a/x-pack/plugins/monitoring/public/components/beats/overview/overview.js b/x-pack/plugins/monitoring/public/components/beats/overview/overview.js index 83f92ea1b481c..897f017f44f41 100644 --- a/x-pack/plugins/monitoring/public/components/beats/overview/overview.js +++ b/x-pack/plugins/monitoring/public/components/beats/overview/overview.js @@ -30,46 +30,40 @@ function renderLatestActive(latestActive, latestTypes, latestVersions) { return ( - - -

- -

-
- - -
+ +

+ +

+
+ +
- - -

- -

-
- - -
+ +

+ +

+
+ +
- - -

- -

-
- - -
+ +

+ +

+
+ +
); @@ -118,10 +112,13 @@ export function BeatsOverview({ /> - + - {renderLatestActive(latestActive, latestTypes, latestVersions)} - + + + {renderLatestActive(latestActive, latestTypes, latestVersions)} + + {charts}
diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js index ccbf0b0ec711d..4bf07710393ea 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/apm_panel.js @@ -55,7 +55,7 @@ export function ApmPanel(props) { {...props} url="apm" title={i18n.translate('xpack.monitoring.cluster.overview.apmPanel.apmTitle', { - defaultMessage: 'APM', + defaultMessage: 'APM server', })} > @@ -70,21 +70,21 @@ export function ApmPanel(props) { aria-label={i18n.translate( 'xpack.monitoring.cluster.overview.apmPanel.overviewLinkAriaLabel', { - defaultMessage: 'APM Overview', + defaultMessage: 'APM server overview', } )} data-test-subj="apmOverview" > - + {formatMetric(props.totalEvents, '0.[0]a')} - + {apmsTotal} }} /> @@ -144,7 +144,7 @@ export function ApmPanel(props) { - + - + {formatMetric(props.totalEvents, '0.[0]a')} - + {props.logs.types.map((log, index) => ( - + - + @@ -276,7 +277,7 @@ export function ElasticsearchPanel(props) { - + - + - + - + {showMlJobs()} - + - + - + - + - + {formatNumber(get(indices, 'docs.count'), 'int_commas')} - + - + - + - + diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js index 6fa533302db48..7df0a3ca7138e 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js @@ -111,7 +111,7 @@ export function KibanaPanel(props) { data-test-subj="kibana_overview" data-overview-status={props.status} > - + {props.requests_total} - + - + {formatNumber(props.concurrent_connections, 'int_commas')} - + - + {formatNumber(props.events_in_total, '0.[0]a')} - + - + {props.max_uptime ? formatNumber(props.max_uptime, 'time_since') : 0} - + - + {queueTypes[LOGSTASH.QUEUE_TYPES.MEMORY] || 0} - + } checked={showSystemIndices} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/__snapshots__/cells.test.js.snap b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/__snapshots__/cells.test.js.snap index c7081dc439085..b0b5ceb46d16c 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/__snapshots__/cells.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/__snapshots__/cells.test.js.snap @@ -10,30 +10,46 @@ exports[`Node Listing Metric Cell should format N/A as the metric for an offline exports[`Node Listing Metric Cell should format a non-percentage metric 1`] = `
+
- - 206.3 GB  - - -
- 206.5 GB max -
- 206.3 GB min +
+
+
+
+
+
+
+
+
+ 206.3 GB +
+
@@ -41,30 +57,46 @@ exports[`Node Listing Metric Cell should format a non-percentage metric 1`] = ` exports[`Node Listing Metric Cell should format a percentage metric 1`] = `
+
- - 0%  - - -
- 2% max -
- 0% min +
+
+
+
+
+
+
+
+
+ 0% +
+
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js index 0c4b4b2b3c3f4..f0b131b65433c 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/__tests__/cells.test.js @@ -27,6 +27,7 @@ describe('Node Listing Metric Cell', () => { }, summary: { minVal: 0, maxVal: 2, lastVal: 0, slope: -1 }, }, + 'data-test-subj': 'testCell', }; expect(renderWithIntl()).toMatchSnapshot(); }); @@ -54,6 +55,7 @@ describe('Node Listing Metric Cell', () => { slope: -1, }, }, + 'data-test-subj': 'testCell2', }; expect(renderWithIntl()).toMatchSnapshot(); }); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/cells.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/cells.js index 4c3b642213d99..9956dd4da7d8a 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/cells.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/cells.js @@ -4,19 +4,42 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useState } from 'react'; import { get } from 'lodash'; import { formatMetric } from '../../../lib/format_number'; -import { EuiText, EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { + EuiText, + EuiPopover, + EuiIcon, + EuiDescriptionList, + EuiSpacer, + EuiKeyboardAccessible, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +const TRENDING_DOWN = i18n.translate('xpack.monitoring.elasticsearch.node.cells.trendingDownText', { + defaultMessage: 'down', +}); +const TRENDING_UP = i18n.translate('xpack.monitoring.elasticsearch.node.cells.trendingUpText', { + defaultMessage: 'up', +}); + function OfflineCell() { return
N/A
; } -const getSlopeArrow = (slope) => { +const getDirection = (slope) => { + if (slope || slope === 0) { + return slope > 0 ? TRENDING_UP : TRENDING_DOWN; + } + return null; +}; + +const getIcon = (slope) => { if (slope || slope === 0) { - return slope > 0 ? 'up' : 'down'; + return slope > 0 ? 'arrowUp' : 'arrowDown'; } return null; }; @@ -28,40 +51,82 @@ const metricVal = (metric, format, isPercent, units) => { return formatMetric(metric, format, units); }; -const noWrapStyle = { overflowX: 'hidden', whiteSpace: 'nowrap' }; - function MetricCell({ isOnline, metric = {}, isPercent, ...props }) { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const onButtonClick = () => setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen); + const closePopover = () => setIsPopoverOpen(false); + if (isOnline) { const { lastVal, maxVal, minVal, slope } = get(metric, 'summary', {}); const format = get(metric, 'metric.format'); const units = get(metric, 'metric.units'); + const tooltipItems = [ + { + title: i18n.translate('xpack.monitoring.elasticsearch.node.cells.tooltip.trending', { + defaultMessage: 'Trending', + }), + description: getDirection(slope), + }, + { + title: i18n.translate('xpack.monitoring.elasticsearch.node.cells.tooltip.max', { + defaultMessage: 'Max value', + }), + description: metricVal(maxVal, format, isPercent, units), + }, + { + title: i18n.translate('xpack.monitoring.elasticsearch.node.cells.tooltip.min', { + defaultMessage: 'Min value', + }), + description: metricVal(minVal, format, isPercent, units), + }, + ]; + + const button = ( + + + + ); + return ( - + + - - - {metricVal(lastVal, format, isPercent)} -   - - - - - {i18n.translate('xpack.monitoring.elasticsearch.nodes.cells.maxText', { - defaultMessage: '{metric} max', - values: { - metric: metricVal(maxVal, format, isPercent, units), - }, - })} - - - {i18n.translate('xpack.monitoring.elasticsearch.nodes.cells.minText', { - defaultMessage: '{metric} min', - values: { - metric: metricVal(minVal, format, isPercent, units), - }, - })} - + + + +
+ + + + {i18n.translate('xpack.monitoring.elasticsearch.node.cells.tooltip.preface', { + defaultMessage: 'Applies to current time period', + })} + +
+
+
+ + {metricVal(lastVal, format, isPercent)} + +
); diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js index 43512f8e528f6..f088f7c0d348a 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js @@ -73,7 +73,6 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler name: i18n.translate('xpack.monitoring.elasticsearch.nodes.nameColumnTitle', { defaultMessage: 'Name', }), - width: '20%', field: 'name', sortable: true, render: (value, node) => { @@ -131,7 +130,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler defaultMessage: 'Alerts', }), field: 'alerts', - width: '175px', + // width: '175px', sortable: true, render: (_field, node) => { return ( @@ -148,6 +147,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler name: i18n.translate('xpack.monitoring.elasticsearch.nodes.statusColumnTitle', { defaultMessage: 'Status', }), + dataType: 'boolean', field: 'isOnline', sortable: true, render: (value) => { @@ -181,22 +181,18 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler name: i18n.translate('xpack.monitoring.elasticsearch.nodes.shardsColumnTitle', { defaultMessage: 'Shards', }), + dataType: 'number', field: 'shardCount', sortable: true, render: (value, node) => { - return node.isOnline ? ( -
- {value} -
- ) : ( - - ); + return node.isOnline ? {value} : ; }, }); if (showCgroupMetricsElasticsearch) { cols.push({ name: cpuUsageColumnTitle, + dataType: 'number', field: 'node_cgroup_quota', sortable: getSortHandler('node_cgroup_quota'), render: (value, node) => ( @@ -213,6 +209,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler name: i18n.translate('xpack.monitoring.elasticsearch.nodes.cpuThrottlingColumnTitle', { defaultMessage: 'CPU Throttling', }), + dataType: 'number', field: 'node_cgroup_throttled', sortable: getSortHandler('node_cgroup_throttled'), render: (value, node) => ( @@ -227,6 +224,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler } else { cols.push({ name: cpuUsageColumnTitle, + dataType: 'number', field: 'node_cpu_utilization', sortable: getSortHandler('node_cpu_utilization'), render: (value, node) => { @@ -245,6 +243,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler name: i18n.translate('xpack.monitoring.elasticsearch.nodes.loadAverageColumnTitle', { defaultMessage: 'Load Average', }), + dataType: 'number', field: 'node_load_average', sortable: getSortHandler('node_load_average'), render: (value, node) => ( @@ -265,6 +264,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler javaVirtualMachine: 'JVM', }, }), + dataType: 'number', field: 'node_jvm_mem_percent', sortable: getSortHandler('node_jvm_mem_percent'), render: (value, node) => ( @@ -281,6 +281,7 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler name: i18n.translate('xpack.monitoring.elasticsearch.nodes.diskFreeSpaceColumnTitle', { defaultMessage: 'Disk Free Space', }), + dataType: 'number', field: 'node_free_space', sortable: getSortHandler('node_free_space'), render: (value, node) => ( diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_head.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_head.js index fd5f28ea02039..3c875667fe04c 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_head.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_head.js @@ -5,6 +5,7 @@ */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiSwitch } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -37,7 +38,12 @@ class IndexLabel extends React.Component { $el && $el[0] && unmountComponentAtNode($el[0])); - - scope.$watch('data', (data = {}) => { - render( - , - $el[0] - ); - }); - }, - }; -} diff --git a/x-pack/plugins/monitoring/public/directives/beats/overview/index.js b/x-pack/plugins/monitoring/public/directives/beats/overview/index.js deleted file mode 100644 index 4faf69e13d02c..0000000000000 --- a/x-pack/plugins/monitoring/public/directives/beats/overview/index.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { BeatsOverview } from '../../../components/beats/overview'; - -export function monitoringBeatsOverviewProvider() { - return { - restrict: 'E', - scope: { - data: '=', - onBrush: '<', - zoomInfo: '<', - }, - link(scope, $el) { - scope.$on('$destroy', () => $el && $el[0] && unmountComponentAtNode($el[0])); - - scope.$watch('data', (data = {}) => { - render( - , - $el[0] - ); - }); - }, - }; -} diff --git a/x-pack/plugins/monitoring/public/directives/main/index.html b/x-pack/plugins/monitoring/public/directives/main/index.html index fabd207d72b1f..fb24d9e678d56 100644 --- a/x-pack/plugins/monitoring/public/directives/main/index.html +++ b/x-pack/plugins/monitoring/public/directives/main/index.html @@ -1,19 +1,32 @@
-
- - +
+
+
+
+
+
+

{{pageTitle || monitoringMain.instance}}

+
+
+
+
+
+ + +
+
+ +
+ + + + Delete + + +
+
+ +
+ + + + Delete + + +
+
+ +
+ + + + Delete + + +
+
+ +
+ + + + Delete + + +
+
+ +
+ + + + Delete + + +
+
+ +
+ + + + Delete + + +
+
+ +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ - -
- Name + + + + Delete + + +
+ + + + +
+ Name
+ +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ @@ -2174,6 +2838,22 @@ exports[`TrustedAppsList renders correctly when loading data for the first time + +
+ + Actions + +
+ @@ -2182,7 +2862,7 @@ exports[`TrustedAppsList renders correctly when loading data for the first time >
+ +
+ + Actions + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ - + +
+ + + + Delete + + +
+ + + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ @@ -3890,7 +5186,7 @@ exports[`TrustedAppsList renders correctly when loading data for the second time `; -exports[`TrustedAppsList renders correctly when new page and page sie set (not loading yet) 1`] = ` +exports[`TrustedAppsList renders correctly when new page and page size set (not loading yet) 1`] = `
+ +
+ + Actions + +
+
+ +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ + +
+ + + + Delete + + +
+ diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap index c8d9b46d5a0d2..9e0a6b16f8b8a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap @@ -263,6 +263,22 @@ Object { + +
+ + Actions + +
+ @@ -271,7 +287,7 @@ Object { >
+ +
+ + Actions + +
+ @@ -496,7 +528,7 @@ Object { >
( return { ...prevState, entries: [ - ...prevState.entries.filter((entry) => !isWindowsTrustedAppCondition(entry)), + ...prevState.entries.filter((entry) => isMacosLinuxTrustedAppCondition(entry)), generateNewEntry(), ] as MacosLinuxConditionEntry[], }; @@ -154,10 +155,15 @@ export const CreateTrustedAppForm = memo( }; if (!isWindowsTrustedApp(updatedState)) { updatedState.entries.push( - ...(prevState.entries.filter( - (entry) => !isWindowsTrustedAppCondition(entry) + ...(prevState.entries.filter((entry) => + isMacosLinuxTrustedAppCondition(entry) ) as MacosLinuxConditionEntry[]) ); + if (updatedState.entries.length === 0) { + updatedState.entries.push(generateNewEntry() as MacosLinuxConditionEntry); + } + } else { + updatedState.entries.push(...prevState.entries); } return updatedState; } diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/components/condition_entry.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/components/condition_entry.tsx index 23bced0c048b1..7eeadeb02a385 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/components/condition_entry.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/components/condition_entry.tsx @@ -76,7 +76,7 @@ export const ConditionEntry = memo( 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.path', { defaultMessage: 'Path' } ), - value: 'process.path', + value: 'process.path.text', }, ]; }, []); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/logical_condition_builder.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/logical_condition_builder.tsx index e5e84acd0c4ad..ef799343d96d6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/logical_condition_builder.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/logical_condition/logical_condition_builder.tsx @@ -5,7 +5,7 @@ */ import React, { memo, useCallback } from 'react'; -import { EuiButton, CommonProps, EuiText, EuiSpacer } from '@elastic/eui'; +import { EuiButton, CommonProps, EuiText, EuiSpacer, EuiPanel } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { ConditionGroup, ConditionGroupProps } from './components/condition_group'; @@ -75,14 +75,14 @@ LogicalConditionBuilder.displayName = 'LogicalConditionBuilder'; // FIXME:PT need to style this better. const NoEntries = memo(() => { return ( -
- + + -
+ ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.test.tsx new file mode 100644 index 0000000000000..a82d7a9cdba61 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.test.tsx @@ -0,0 +1,126 @@ +/* + * 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 { Provider } from 'react-redux'; +import { render } from '@testing-library/react'; +import { I18nProvider } from '@kbn/i18n/react'; + +import { + createGlobalNoMiddlewareStore, + createSampleTrustedApp, + createServerApiError, +} from '../test_utils'; + +import { + TrustedAppDeletionDialogStarted, + TrustedAppDeletionSubmissionResourceStateChanged, +} from '../store/action'; + +import { TrustedAppDeletionDialog } from './trusted_app_deletion_dialog'; + +const renderDeletionDialog = (store: ReturnType) => { + const Wrapper: React.FC = ({ children }) => ( + + {children} + + ); + + return render(, { wrapper: Wrapper }); +}; + +const createDialogStartAction = (): TrustedAppDeletionDialogStarted => ({ + type: 'trustedAppDeletionDialogStarted', + payload: { entry: createSampleTrustedApp(3) }, +}); + +const createDialogLoadingAction = (): TrustedAppDeletionSubmissionResourceStateChanged => ({ + type: 'trustedAppDeletionSubmissionResourceStateChanged', + payload: { + newState: { + type: 'LoadingResourceState', + previousState: { type: 'UninitialisedResourceState' }, + }, + }, +}); + +const createDialogFailedAction = (): TrustedAppDeletionSubmissionResourceStateChanged => ({ + type: 'trustedAppDeletionSubmissionResourceStateChanged', + payload: { + newState: { type: 'FailedResourceState', error: createServerApiError('Not Found') }, + }, +}); + +describe('TrustedAppDeletionDialog', () => { + it('renders correctly initially', () => { + expect(renderDeletionDialog(createGlobalNoMiddlewareStore()).baseElement).toMatchSnapshot(); + }); + + it('renders correctly when dialog started', () => { + const store = createGlobalNoMiddlewareStore(); + + store.dispatch(createDialogStartAction()); + + expect(renderDeletionDialog(store).baseElement).toMatchSnapshot(); + }); + + it('renders correctly when deletion is in progress', () => { + const store = createGlobalNoMiddlewareStore(); + + store.dispatch(createDialogStartAction()); + store.dispatch(createDialogLoadingAction()); + + expect(renderDeletionDialog(store).baseElement).toMatchSnapshot(); + }); + + it('renders correctly when deletion failed', () => { + const store = createGlobalNoMiddlewareStore(); + + store.dispatch(createDialogStartAction()); + store.dispatch(createDialogFailedAction()); + + expect(renderDeletionDialog(store).baseElement).toMatchSnapshot(); + }); + + it('triggers confirmation action when confirm button clicked', async () => { + const store = createGlobalNoMiddlewareStore(); + + store.dispatch(createDialogStartAction()); + store.dispatch = jest.fn(); + + (await renderDeletionDialog(store).findByTestId('trustedAppDeletionConfirm')).click(); + + expect(store.dispatch).toBeCalledWith({ + type: 'trustedAppDeletionDialogConfirmed', + }); + }); + + it('triggers closing action when cancel button clicked', async () => { + const store = createGlobalNoMiddlewareStore(); + + store.dispatch(createDialogStartAction()); + store.dispatch = jest.fn(); + + (await renderDeletionDialog(store).findByTestId('trustedAppDeletionCancel')).click(); + + expect(store.dispatch).toBeCalledWith({ + type: 'trustedAppDeletionDialogClosed', + }); + }); + + it('does not trigger closing action when deletion in progress and cancel button clicked', async () => { + const store = createGlobalNoMiddlewareStore(); + + store.dispatch(createDialogStartAction()); + store.dispatch(createDialogLoadingAction()); + + store.dispatch = jest.fn(); + + (await renderDeletionDialog(store).findByTestId('trustedAppDeletionCancel')).click(); + + expect(store.dispatch).not.toBeCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx new file mode 100644 index 0000000000000..846fa794ceefd --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx @@ -0,0 +1,115 @@ +/* + * 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, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import { Dispatch } from 'redux'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiButton, + EuiButtonEmpty, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiOverlayMask, + EuiText, +} from '@elastic/eui'; + +import { Immutable, TrustedApp } from '../../../../../common/endpoint/types'; +import { AppAction } from '../../../../common/store/actions'; +import { useTrustedAppsSelector } from './hooks'; +import { + getDeletionDialogEntry, + isDeletionDialogOpen, + isDeletionInProgress, +} from '../store/selectors'; + +const CANCEL_SUBJ = 'trustedAppDeletionCancel'; +const CONFIRM_SUBJ = 'trustedAppDeletionConfirm'; + +const getTranslations = (entry: Immutable | undefined) => ({ + title: ( + + ), + mainMessage: ( + {entry?.name} }} + /> + ), + subMessage: ( + + ), + cancelButton: ( + + ), + confirmButton: ( + + ), +}); + +export const TrustedAppDeletionDialog = memo(() => { + const dispatch = useDispatch>(); + const isBusy = useTrustedAppsSelector(isDeletionInProgress); + const entry = useTrustedAppsSelector(getDeletionDialogEntry); + const translations = useMemo(() => getTranslations(entry), [entry]); + const onConfirm = useCallback(() => { + dispatch({ type: 'trustedAppDeletionDialogConfirmed' }); + }, [dispatch]); + const onCancel = useCallback(() => { + if (!isBusy) { + dispatch({ type: 'trustedAppDeletionDialogClosed' }); + } + }, [dispatch, isBusy]); + + if (useTrustedAppsSelector(isDeletionDialogOpen)) { + return ( + + + + {translations.title} + + + + +

{translations.mainMessage}

+

{translations.subMessage}

+
+
+ + + + {translations.cancelButton} + + + + {translations.confirmButton} + + +
+
+ ); + } else { + return <>; + } +}); + +TrustedAppDeletionDialog.displayName = 'TrustedAppDeletionDialog'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx index 0362f5c7a9de6..a457ecd0d088f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx @@ -3,40 +3,28 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { combineReducers, createStore } from 'redux'; import { render } from '@testing-library/react'; import React from 'react'; import { Provider } from 'react-redux'; -import { - MANAGEMENT_STORE_GLOBAL_NAMESPACE, - MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE, -} from '../../../common/constants'; -import { trustedAppsPageReducer } from '../store/reducer'; import { TrustedAppsList } from './trusted_apps_list'; import { + createSampleTrustedApp, createListFailedResourceState, createListLoadedResourceState, createListLoadingResourceState, createTrustedAppsListResourceStateChangedAction, createUserChangedUrlAction, + createGlobalNoMiddlewareStore, } from '../test_utils'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ htmlIdGenerator: () => () => 'mockId', })); -const createStoreSetup = () => { - return createStore( - combineReducers({ - [MANAGEMENT_STORE_GLOBAL_NAMESPACE]: combineReducers({ - [MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: trustedAppsPageReducer, - }), - }) - ); -}; +const now = 111111; -const renderList = (store: ReturnType) => { +const renderList = (store: ReturnType) => { const Wrapper: React.FC = ({ children }) => {children}; return render(, { wrapper: Wrapper }); @@ -44,11 +32,11 @@ const renderList = (store: ReturnType) => { describe('TrustedAppsList', () => { it('renders correctly initially', () => { - expect(renderList(createStoreSetup()).container).toMatchSnapshot(); + expect(renderList(createGlobalNoMiddlewareStore()).container).toMatchSnapshot(); }); it('renders correctly when loading data for the first time', () => { - const store = createStoreSetup(); + const store = createGlobalNoMiddlewareStore(); store.dispatch( createTrustedAppsListResourceStateChangedAction(createListLoadingResourceState()) @@ -58,7 +46,7 @@ describe('TrustedAppsList', () => { }); it('renders correctly when failed loading data for the first time', () => { - const store = createStoreSetup(); + const store = createGlobalNoMiddlewareStore(); store.dispatch( createTrustedAppsListResourceStateChangedAction( @@ -70,23 +58,23 @@ describe('TrustedAppsList', () => { }); it('renders correctly when loaded data', () => { - const store = createStoreSetup(); + const store = createGlobalNoMiddlewareStore(); store.dispatch( createTrustedAppsListResourceStateChangedAction( - createListLoadedResourceState({ index: 0, size: 20 }, 200) + createListLoadedResourceState({ index: 0, size: 20 }, 200, now) ) ); expect(renderList(store).container).toMatchSnapshot(); }); - it('renders correctly when new page and page sie set (not loading yet)', () => { - const store = createStoreSetup(); + it('renders correctly when new page and page size set (not loading yet)', () => { + const store = createGlobalNoMiddlewareStore(); store.dispatch( createTrustedAppsListResourceStateChangedAction( - createListLoadedResourceState({ index: 0, size: 20 }, 200) + createListLoadedResourceState({ index: 0, size: 20 }, 200, now) ) ); store.dispatch(createUserChangedUrlAction('/trusted_apps', '?page_index=2&page_size=50')); @@ -95,11 +83,13 @@ describe('TrustedAppsList', () => { }); it('renders correctly when loading data for the second time', () => { - const store = createStoreSetup(); + const store = createGlobalNoMiddlewareStore(); store.dispatch( createTrustedAppsListResourceStateChangedAction( - createListLoadingResourceState(createListLoadedResourceState({ index: 0, size: 20 }, 200)) + createListLoadingResourceState( + createListLoadedResourceState({ index: 0, size: 20 }, 200, now) + ) ) ); @@ -107,17 +97,37 @@ describe('TrustedAppsList', () => { }); it('renders correctly when failed loading data for the second time', () => { - const store = createStoreSetup(); + const store = createGlobalNoMiddlewareStore(); store.dispatch( createTrustedAppsListResourceStateChangedAction( createListFailedResourceState( 'Intenal Server Error', - createListLoadedResourceState({ index: 0, size: 20 }, 200) + createListLoadedResourceState({ index: 0, size: 20 }, 200, now) ) ) ); expect(renderList(store).container).toMatchSnapshot(); }); + + it('triggers deletion dialog when delete action clicked', async () => { + const store = createGlobalNoMiddlewareStore(); + + store.dispatch( + createTrustedAppsListResourceStateChangedAction( + createListLoadedResourceState({ index: 0, size: 20 }, 200, now) + ) + ); + store.dispatch = jest.fn(); + + (await renderList(store).findAllByTestId('trustedAppDeleteAction'))[0].click(); + + expect(store.dispatch).toBeCalledWith({ + type: 'trustedAppDeletionDialogStarted', + payload: { + entry: createSampleTrustedApp(0), + }, + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx index ea834060d5223..c91512d477510 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx @@ -4,12 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Dispatch } from 'redux'; import React, { memo, useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; +import { EuiBasicTable, EuiBasicTableColumn, EuiTableActionsColumnType } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Immutable } from '../../../../../common/endpoint/types'; +import { AppAction } from '../../../../common/store/actions'; import { TrustedApp } from '../../../../../common/endpoint/types/trusted_apps'; import { MANAGEMENT_PAGE_SIZE_OPTIONS } from '../../../common/constants'; import { getTrustedAppsListPath } from '../../../common/routing'; @@ -28,7 +31,9 @@ import { useTrustedAppsSelector } from './hooks'; import { FormattedDate } from '../../../../common/components/formatted_date'; import { OS_TITLES } from './constants'; -const COLUMN_TITLES: Readonly<{ [K in keyof Omit]: string }> = { +const COLUMN_TITLES: Readonly< + { [K in keyof Omit | 'actions']: string } +> = { name: i18n.translate('xpack.securitySolution.trustedapps.list.columns.name', { defaultMessage: 'Name', }), @@ -41,9 +46,41 @@ const COLUMN_TITLES: Readonly<{ [K in keyof Omit]: created_by: i18n.translate('xpack.securitySolution.trustedapps.list.columns.createdBy', { defaultMessage: 'Created By', }), + actions: i18n.translate('xpack.securitySolution.trustedapps.list.columns.actions', { + defaultMessage: 'Actions', + }), }; -const getColumnDefinitions = (): Array>> => [ +type ActionsList = EuiTableActionsColumnType>['actions']; + +const getActionDefinitions = (dispatch: Dispatch>): ActionsList => [ + { + name: i18n.translate('xpack.securitySolution.trustedapps.list.actions.delete', { + defaultMessage: 'Delete', + }), + description: i18n.translate( + 'xpack.securitySolution.trustedapps.list.actions.delete.description', + { + defaultMessage: 'Delete this entry', + } + ), + 'data-test-subj': 'trustedAppDeleteAction', + isPrimary: true, + icon: 'trash', + color: 'danger', + type: 'icon', + onClick: (item: Immutable) => { + dispatch({ + type: 'trustedAppDeletionDialogStarted', + payload: { entry: item }, + }); + }, + }, +]; + +type ColumnsList = Array>>; + +const getColumnDefinitions = (dispatch: Dispatch>): ColumnsList => [ { field: 'name', name: COLUMN_TITLES.name, @@ -72,6 +109,10 @@ const getColumnDefinitions = (): Array field: 'created_by', name: COLUMN_TITLES.created_by, }, + { + name: COLUMN_TITLES.actions, + actions: getActionDefinitions(dispatch), + }, ]; export const TrustedAppsList = memo(() => { @@ -79,11 +120,12 @@ export const TrustedAppsList = memo(() => { const pageSize = useTrustedAppsSelector(getListCurrentPageSize); const totalItemCount = useTrustedAppsSelector(getListTotalItemsCount); const listItems = useTrustedAppsSelector(getListItems); + const dispatch = useDispatch(); const history = useHistory(); return ( getColumnDefinitions(dispatch), [dispatch])} items={useMemo(() => [...listItems], [listItems])} error={useTrustedAppsSelector(getListErrorMessage)} loading={useTrustedAppsSelector(isListLoading)} diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.test.tsx new file mode 100644 index 0000000000000..cc45abf493582 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.test.tsx @@ -0,0 +1,98 @@ +/* + * 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 { Provider } from 'react-redux'; +import { render } from '@testing-library/react'; + +import { NotificationsStart } from 'kibana/public'; + +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public/context'; + +import { + createGlobalNoMiddlewareStore, + createSampleTrustedApp, + createServerApiError, +} from '../test_utils'; + +import { TrustedAppsNotifications } from './trusted_apps_notifications'; + +const mockNotifications = () => coreMock.createStart({ basePath: '/mock' }).notifications; + +const renderNotifications = ( + store: ReturnType, + notifications: NotificationsStart +) => { + const Wrapper: React.FC = ({ children }) => ( + + {children} + + ); + + return render(, { wrapper: Wrapper }); +}; + +describe('TrustedAppsNotifications', () => { + it('renders correctly initially', () => { + const notifications = mockNotifications(); + + renderNotifications(createGlobalNoMiddlewareStore(), notifications); + + expect(notifications.toasts.addSuccess).not.toBeCalled(); + expect(notifications.toasts.addDanger).not.toBeCalled(); + }); + + it('shows success notification when deletion successful', () => { + const store = createGlobalNoMiddlewareStore(); + const notifications = mockNotifications(); + + renderNotifications(store, notifications); + + store.dispatch({ + type: 'trustedAppDeletionDialogStarted', + payload: { entry: createSampleTrustedApp(3) }, + }); + store.dispatch({ + type: 'trustedAppDeletionSubmissionResourceStateChanged', + payload: { newState: { type: 'LoadedResourceState', data: null } }, + }); + store.dispatch({ + type: 'trustedAppDeletionDialogClosed', + }); + + expect(notifications.toasts.addSuccess).toBeCalledWith({ + text: '"trusted app 3" has been removed from the Trusted Applications list.', + title: 'Successfully removed', + }); + expect(notifications.toasts.addDanger).not.toBeCalled(); + }); + + it('shows error notification when deletion fails', () => { + const store = createGlobalNoMiddlewareStore(); + const notifications = mockNotifications(); + + renderNotifications(store, notifications); + + store.dispatch({ + type: 'trustedAppDeletionDialogStarted', + payload: { entry: createSampleTrustedApp(3) }, + }); + store.dispatch({ + type: 'trustedAppDeletionSubmissionResourceStateChanged', + payload: { + newState: { type: 'FailedResourceState', error: createServerApiError('Not Found') }, + }, + }); + + expect(notifications.toasts.addSuccess).not.toBeCalled(); + expect(notifications.toasts.addDanger).toBeCalledWith({ + text: + 'Unable to remove "trusted app 3" from the Trusted Applications list. Reason: Not Found', + title: 'Removal failure', + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.tsx new file mode 100644 index 0000000000000..9c0fe8eb6f0cb --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_notifications.tsx @@ -0,0 +1,58 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { ServerApiError } from '../../../../common/types'; +import { Immutable, TrustedApp } from '../../../../../common/endpoint/types'; +import { getDeletionDialogEntry, getDeletionError, isDeletionSuccessful } from '../store/selectors'; + +import { useToasts } from '../../../../common/lib/kibana'; +import { useTrustedAppsSelector } from './hooks'; + +const getDeletionErrorMessage = (error: ServerApiError, entry: Immutable) => { + return { + title: i18n.translate('xpack.securitySolution.trustedapps.deletionError.title', { + defaultMessage: 'Removal failure', + }), + text: i18n.translate('xpack.securitySolution.trustedapps.deletionError.text', { + defaultMessage: + 'Unable to remove "{name}" from the Trusted Applications list. Reason: {message}', + values: { name: entry.name, message: error.message }, + }), + }; +}; + +const getDeletionSuccessMessage = (entry: Immutable) => { + return { + title: i18n.translate('xpack.securitySolution.trustedapps.deletionSuccess.title', { + defaultMessage: 'Successfully removed', + }), + text: i18n.translate('xpack.securitySolution.trustedapps.deletionSuccess.text', { + defaultMessage: '"{name}" has been removed from the Trusted Applications list.', + values: { name: entry?.name }, + }), + }; +}; + +export const TrustedAppsNotifications = memo(() => { + const deletionError = useTrustedAppsSelector(getDeletionError); + const deletionDialogEntry = useTrustedAppsSelector(getDeletionDialogEntry); + const deletionSuccessful = useTrustedAppsSelector(isDeletionSuccessful); + const toasts = useToasts(); + + if (deletionError && deletionDialogEntry) { + toasts.addDanger(getDeletionErrorMessage(deletionError, deletionDialogEntry)); + } + + if (deletionSuccessful && deletionDialogEntry) { + toasts.addSuccess(getDeletionSuccessMessage(deletionDialogEntry)); + } + + return <>; +}); + +TrustedAppsNotifications.displayName = 'TrustedAppsNotifications'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx index 218cef36ed50a..457f96dbff768 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx @@ -21,6 +21,15 @@ describe('TrustedAppsPage', () => { let coreStart: AppContextTestRender['coreStart']; let waitForAction: MiddlewareActionSpyHelper['waitForAction']; let render: () => ReturnType; + const originalScrollTo = window.scrollTo; + + beforeAll(() => { + window.scrollTo = () => {}; + }); + + afterAll(() => { + window.scrollTo = originalScrollTo; + }); beforeEach(() => { const mockedContext = createAppRootMockRenderer(); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx index a0dae900eb30e..c1c23a3960962 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx @@ -9,6 +9,8 @@ import { EuiButton } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { AdministrationListPage } from '../../../components/administration_list_page'; import { TrustedAppsList } from './trusted_apps_list'; +import { TrustedAppDeletionDialog } from './trusted_app_deletion_dialog'; +import { TrustedAppsNotifications } from './trusted_apps_notifications'; import { CreateTrustedAppFlyout } from './components/create_trusted_app_flyout'; import { getTrustedAppsListPath } from '../../../common/routing'; import { useTrustedAppsSelector } from './hooks'; @@ -63,6 +65,8 @@ export const TrustedAppsPage = memo(() => { } actions={addButton} > + + {showAddFlout && ( = { [MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: initialPolicyListState(), [MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: initialPolicyDetailsState(), [MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: initialEndpointListState, - [MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: initialTrustedAppsPageState, + [MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: initialTrustedAppsPageState(), }; /** diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/kpi_network/__snapshots__/index.test.tsx.snap index 49562162e94a8..a03d7c2317517 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/__snapshots__/index.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`KpiNetwork Component rendering it renders the default widget 1`] = ` -; data: NetworkKpiStrategyResponse; loading?: boolean; @@ -64,6 +64,6 @@ export const KpiNetworkBaseComponent = React.memo<{ ); }); -KpiNetworkBaseComponent.displayName = 'KpiNetworkBaseComponent'; +NetworkKpiBaseComponent.displayName = 'NetworkKpiBaseComponent'; -export const KpiNetworkBaseComponentManage = manageQuery(KpiNetworkBaseComponent); +export const NetworkKpiBaseComponentManage = manageQuery(NetworkKpiBaseComponent); diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.tsx index 889f3dacc2d98..0f13b0e8f874e 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { StatItems } from '../../../../common/components/stat_items'; import { useNetworkKpiDns } from '../../../containers/kpi_network/dns'; -import { KpiNetworkBaseComponentManage } from '../common'; +import { NetworkKpiBaseComponentManage } from '../common'; import { NetworkKpiProps } from '../types'; import * as i18n from './translations'; @@ -41,7 +41,7 @@ const NetworkKpiDnsComponent: React.FC = ({ }); return ( - { +describe('NetworkKpiComponent', () => { const state: State = mockGlobalState; const props = { from: '2019-06-15T06:00:00.000Z', @@ -53,11 +53,11 @@ describe('KpiNetwork Component', () => { test('it renders the default widget', () => { const wrapper = shallow( - + ); - expect(wrapper.find('KpiNetworkComponent')).toMatchSnapshot(); + expect(wrapper.find('NetworkKpiComponent')).toMatchSnapshot(); }); }); }); diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/index.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/index.tsx index 674e592940fa6..95534e1a61988 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/index.tsx @@ -14,7 +14,7 @@ import { NetworkKpiUniqueFlows } from './unique_flows'; import { NetworkKpiUniquePrivateIps } from './unique_private_ips'; import { NetworkKpiProps } from './types'; -export const KpiNetworkComponent = React.memo( +export const NetworkKpiComponent = React.memo( ({ filterQuery, from, to, setQuery, skip, narrowDateRange }) => ( @@ -78,4 +78,4 @@ export const KpiNetworkComponent = React.memo( ) ); -KpiNetworkComponent.displayName = 'KpiNetworkComponent'; +NetworkKpiComponent.displayName = 'NetworkKpiComponent'; diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/network_events/index.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/network_events/index.tsx index 3ee2acf1a115c..18217e41f2a27 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/network_events/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/network_events/index.tsx @@ -9,7 +9,7 @@ import { euiPaletteColorBlind } from '@elastic/eui'; import { StatItems } from '../../../../common/components/stat_items'; import { useNetworkKpiNetworkEvents } from '../../../containers/kpi_network/network_events'; -import { KpiNetworkBaseComponentManage } from '../common'; +import { NetworkKpiBaseComponentManage } from '../common'; import { NetworkKpiProps } from '../types'; import * as i18n from './translations'; @@ -46,7 +46,7 @@ const NetworkKpiNetworkEventsComponent: React.FC = ({ }); return ( - = ({ }); return ( - = ({ }); return ( - = ({ }); return ( - , - Columns, - Columns, - Columns, - Columns + Columns, + Columns, + Columns, + Columns, + Columns ]; export const getTlsColumns = (tableId: string): TlsColumns => [ diff --git a/x-pack/plugins/security_solution/public/network/components/tls_table/mock.ts b/x-pack/plugins/security_solution/public/network/components/tls_table/mock.ts index a90907eb38854..0e16d76d300de 100644 --- a/x-pack/plugins/security_solution/public/network/components/tls_table/mock.ts +++ b/x-pack/plugins/security_solution/public/network/components/tls_table/mock.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TlsData } from '../../../graphql/types'; +import { NetworkTlsStrategyResponse } from '../../../../common/search_strategy'; -export const mockTlsData: TlsData = { +export const mockTlsData: NetworkTlsStrategyResponse = { totalCount: 2, edges: [ { @@ -51,4 +51,5 @@ export const mockTlsData: TlsData = { fakeTotalCount: 50, showMorePagesIndicator: true, }, + rawResponse: {} as NetworkTlsStrategyResponse['rawResponse'], }; diff --git a/x-pack/plugins/security_solution/public/network/containers/details/index.tsx b/x-pack/plugins/security_solution/public/network/containers/details/index.tsx index 597f85ff082e2..f6ea86bd552f4 100644 --- a/x-pack/plugins/security_solution/public/network/containers/details/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/details/index.tsx @@ -19,7 +19,11 @@ import { NetworkDetailsRequestOptions, NetworkDetailsStrategyResponse, } from '../../../../common/search_strategy'; -import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../src/plugins/data/common'; import * as i18n from './translations'; import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; @@ -88,7 +92,7 @@ export const useNetworkDetails = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setNetworkDetailsResponse((prevResponse) => ({ @@ -99,7 +103,7 @@ export const useNetworkDetails = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx index 295cbff76f6aa..2afbff3138c6b 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/dns/index.tsx @@ -20,7 +20,11 @@ import { import { ESTermQuery } from '../../../../../common/typed_json'; import * as i18n from './translations'; -import { AbortError } from '../../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../../src/plugins/data/common'; import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; @@ -89,7 +93,7 @@ export const useNetworkKpiDns = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setNetworkKpiDnsResponse((prevResponse) => ({ @@ -100,7 +104,7 @@ export const useNetworkKpiDns = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx index 8ab94432746f4..26b57ef36b09d 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/network_events/index.tsx @@ -20,7 +20,11 @@ import { import { ESTermQuery } from '../../../../../common/typed_json'; import * as i18n from './translations'; -import { AbortError } from '../../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../../src/plugins/data/common'; import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; @@ -96,7 +100,7 @@ export const useNetworkKpiNetworkEvents = ({ ) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setNetworkKpiNetworkEventsResponse((prevResponse) => ({ @@ -107,7 +111,7 @@ export const useNetworkKpiNetworkEvents = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx index f7630352fc3c4..c97c1e43e699a 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/tls_handshakes/index.tsx @@ -20,7 +20,11 @@ import { import { ESTermQuery } from '../../../../../common/typed_json'; import * as i18n from './translations'; -import { AbortError } from '../../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../../src/plugins/data/common'; import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; @@ -96,7 +100,7 @@ export const useNetworkKpiTlsHandshakes = ({ ) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setNetworkKpiTlsHandshakesResponse((prevResponse) => ({ @@ -107,7 +111,7 @@ export const useNetworkKpiTlsHandshakes = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx index 5f1bd782b9abd..4e8b4ad38b711 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_flows/index.tsx @@ -20,7 +20,11 @@ import { import { ESTermQuery } from '../../../../../common/typed_json'; import * as i18n from './translations'; -import { AbortError } from '../../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../../src/plugins/data/common'; import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; @@ -96,7 +100,7 @@ export const useNetworkKpiUniqueFlows = ({ ) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setNetworkKpiUniqueFlowsResponse((prevResponse) => ({ @@ -107,7 +111,7 @@ export const useNetworkKpiUniqueFlows = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx index f32f43d811137..b518f95212129 100644 --- a/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/kpi_network/unique_private_ips/index.tsx @@ -21,7 +21,11 @@ import { import { ESTermQuery } from '../../../../../common/typed_json'; import * as i18n from './translations'; -import { AbortError } from '../../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../../src/plugins/data/common'; import { getInspectResponse } from '../../../../helpers'; import { InspectResponse } from '../../../../types'; @@ -103,7 +107,7 @@ export const useNetworkKpiUniquePrivateIps = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setNetworkKpiUniquePrivateIpsResponse((prevResponse) => ({ @@ -118,7 +122,7 @@ export const useNetworkKpiUniquePrivateIps = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx index 6f48cba2ebda1..209f8da0d8fae 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx @@ -23,7 +23,11 @@ import { NetworkDnsStrategyResponse, MatrixOverOrdinalHistogramData, } from '../../../../common/search_strategy/security_solution/network'; -import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../src/plugins/data/common'; import * as i18n from './translations'; import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; @@ -130,7 +134,7 @@ export const useNetworkDns = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setNetworkDnsResponse((prevResponse) => ({ @@ -144,7 +148,7 @@ export const useNetworkDns = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx index d3e8067d1802e..9244d571bb67b 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_http/index.tsx @@ -23,7 +23,11 @@ import { NetworkHttpStrategyResponse, SortField, } from '../../../../common/search_strategy'; -import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../src/plugins/data/common'; import * as i18n from './translations'; import { InspectResponse } from '../../../types'; import { getInspectResponse } from '../../../helpers'; @@ -130,7 +134,7 @@ export const useNetworkHttp = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setNetworkHttpResponse((prevResponse) => ({ @@ -143,7 +147,7 @@ export const useNetworkHttp = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx index 747f5e4f502dd..8138d30f2c510 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_countries/index.tsx @@ -24,7 +24,11 @@ import { NetworkTopCountriesStrategyResponse, PageInfoPaginated, } from '../../../../common/search_strategy'; -import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../src/plugins/data/common'; import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; import * as i18n from './translations'; @@ -129,7 +133,7 @@ export const useNetworkTopCountries = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setNetworkTopCountriesResponse((prevResponse) => ({ @@ -142,7 +146,7 @@ export const useNetworkTopCountries = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx index cc0da816c57ec..76c2ae2871a38 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_top_n_flow/index.tsx @@ -24,7 +24,11 @@ import { NetworkTopNFlowStrategyResponse, PageInfoPaginated, } from '../../../../common/search_strategy'; -import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../src/plugins/data/common'; import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; import * as i18n from './translations'; @@ -127,7 +131,7 @@ export const useNetworkTopNFlow = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setNetworkTopNFlowResponse((prevResponse) => ({ @@ -140,7 +144,7 @@ export const useNetworkTopNFlow = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/network/containers/tls/index.gql_query.ts b/x-pack/plugins/security_solution/public/network/containers/tls/index.gql_query.ts deleted file mode 100644 index f513a94d69667..0000000000000 --- a/x-pack/plugins/security_solution/public/network/containers/tls/index.gql_query.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import gql from 'graphql-tag'; - -export const tlsQuery = gql` - query GetTlsQuery( - $sourceId: ID! - $filterQuery: String - $flowTarget: FlowTargetSourceDest! - $ip: String! - $pagination: PaginationInputPaginated! - $sort: TlsSortField! - $timerange: TimerangeInput! - $defaultIndex: [String!]! - $inspect: Boolean! - ) { - source(id: $sourceId) { - id - Tls( - filterQuery: $filterQuery - flowTarget: $flowTarget - ip: $ip - pagination: $pagination - sort: $sort - timerange: $timerange - defaultIndex: $defaultIndex - ) { - totalCount - edges { - node { - _id - subjects - ja3 - issuers - notAfter - } - cursor { - value - } - } - pageInfo { - activePage - fakeTotalCount - showMorePagesIndicator - } - inspect @include(if: $inspect) { - dsl - response - } - } - } - } -`; diff --git a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx index df02acf208603..4c9658aa9b42c 100644 --- a/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/tls/index.tsx @@ -14,7 +14,7 @@ import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; import { inputsModel, State } from '../../../common/store'; import { useKibana } from '../../../common/lib/kibana'; import { createFilter } from '../../../common/containers/helpers'; -import { TlsEdges, PageInfoPaginated, FlowTargetSourceDest } from '../../../graphql/types'; +import { PageInfoPaginated, FlowTargetSourceDest } from '../../../graphql/types'; import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; import { networkModel, networkSelectors } from '../../store'; import { @@ -22,7 +22,11 @@ import { NetworkTlsRequestOptions, NetworkTlsStrategyResponse, } from '../../../../common/search_strategy/security_solution/network'; -import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../src/plugins/data/common'; import * as i18n from './translations'; import { getInspectResponse } from '../../../helpers'; @@ -36,7 +40,7 @@ export interface NetworkTlsArgs { loadPage: (newActivePage: number) => void; pageInfo: PageInfoPaginated; refetch: inputsModel.Refetch; - tls: TlsEdges[]; + tls: NetworkTlsStrategyResponse['edges']; totalCount: number; } @@ -77,6 +81,7 @@ export const useNetworkTls = ({ factoryQueryType: NetworkQueries.tls, filterQuery: createFilter(filterQuery), flowTarget, + id, ip, pagination: generateTablePaginationOptions(activePage, limit), sort, @@ -129,7 +134,7 @@ export const useNetworkTls = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setNetworkTlsResponse((prevResponse) => ({ @@ -142,7 +147,7 @@ export const useNetworkTls = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/network/containers/users/index.tsx b/x-pack/plugins/security_solution/public/network/containers/users/index.tsx index 608ccdb084709..a289f8d16e9b2 100644 --- a/x-pack/plugins/security_solution/public/network/containers/users/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/users/index.tsx @@ -23,7 +23,11 @@ import { NetworkUsersRequestOptions, NetworkUsersStrategyResponse, } from '../../../../common/search_strategy/security_solution/network'; -import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../src/plugins/data/common'; import * as i18n from './translations'; import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; @@ -126,7 +130,7 @@ export const useNetworkUsers = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setNetworkUsersResponse((prevResponse) => ({ @@ -139,7 +143,7 @@ export const useNetworkUsers = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/network/pages/network.tsx b/x-pack/plugins/security_solution/public/network/pages/network.tsx index e04350fd38df5..8aed6385ea24d 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.tsx @@ -19,7 +19,7 @@ import { HeaderPage } from '../../common/components/header_page'; import { LastEventTime } from '../../common/components/last_event_time'; import { SiemNavigation } from '../../common/components/navigation'; -import { KpiNetworkComponent } from '../components/kpi_network'; +import { NetworkKpiComponent } from '../components/kpi_network'; import { SiemSearchBar } from '../../common/components/search_bar'; import { WrapperPage } from '../../common/components/wrapper_page'; import { useFullScreen } from '../../common/containers/use_full_screen'; @@ -125,7 +125,7 @@ const NetworkComponent = React.memo( - { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setHostOverviewResponse((prevResponse) => ({ @@ -107,7 +111,7 @@ export const useHostOverview = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx index c61606e0c31dd..ae1fe942d8403 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_network/index.tsx @@ -18,7 +18,11 @@ import { useKibana } from '../../../common/lib/kibana'; import { inputsModel } from '../../../common/store/inputs'; import { createFilter } from '../../../common/containers/helpers'; import { ESQuery } from '../../../../common/typed_json'; -import { AbortError } from '../../../../../../../src/plugins/data/common'; +import { + AbortError, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../src/plugins/data/common'; import { getInspectResponse } from '../../../helpers'; import { InspectResponse } from '../../../types'; import * as i18n from './translations'; @@ -87,7 +91,7 @@ export const useNetworkOverview = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setNetworkOverviewResponse((prevResponse) => ({ @@ -98,7 +102,7 @@ export const useNetworkOverview = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts index 0883a3787fcce..fd086bd9b984e 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children.ts @@ -9,7 +9,6 @@ import { ResolverTree, ResolverEntityIndex, } from '../../../../common/endpoint/types'; -import { mockEndpointEvent } from '../../mocks/endpoint_event'; import { mockTreeWithNoAncestorsAnd2Children } from '../../mocks/resolver_tree'; import { DataAccessLayer } from '../../types'; @@ -54,13 +53,7 @@ export function noAncestorsTwoChildren(): { dataAccessLayer: DataAccessLayer; me relatedEvents(entityID: string): Promise { return Promise.resolve({ entityID, - events: [ - mockEndpointEvent({ - entityID, - name: 'event', - timestamp: 0, - }), - ], + events: [], nextEvent: null, }); }, diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts index ec0fa93485783..86450b25eb1da 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_in_index_called_awesome_index.ts @@ -61,7 +61,7 @@ export function noAncestorsTwoChildenInIndexCalledAwesomeIndex(): { events: [ mockEndpointEvent({ entityID, - name: 'event', + processName: 'event', timestamp: 0, }), ], diff --git a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts index 95ec0cd1a5f77..ec773a09ae8e0 100644 --- a/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts +++ b/x-pack/plugins/security_solution/public/resolver/data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin.ts @@ -66,7 +66,7 @@ export function noAncestorsTwoChildrenWithRelatedEventsOnOrigin(): { entityID, events, nextEvent: null, - } as ResolverRelatedEvents); + }); }, /** diff --git a/x-pack/plugins/security_solution/public/resolver/index.ts b/x-pack/plugins/security_solution/public/resolver/index.ts index 409f82c9d1560..08a3722f40493 100644 --- a/x-pack/plugins/security_solution/public/resolver/index.ts +++ b/x-pack/plugins/security_solution/public/resolver/index.ts @@ -7,7 +7,7 @@ import { Provider } from 'react-redux'; import { ResolverPluginSetup } from './types'; import { resolverStoreFactory } from './store/index'; import { ResolverWithoutProviders } from './view/resolver_without_providers'; -import { noAncestorsTwoChildren } from './data_access_layer/mocks/no_ancestors_two_children'; +import { noAncestorsTwoChildrenWithRelatedEventsOnOrigin } from './data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin'; /** * These exports are used by the plugin 'resolverTest' defined in x-pack's plugin_functional suite. @@ -23,7 +23,7 @@ export function resolverPluginSetup(): ResolverPluginSetup { ResolverWithoutProviders, mocks: { dataAccessLayer: { - noAncestorsTwoChildren, + noAncestorsTwoChildrenWithRelatedEventsOnOrigin, }, }, }; diff --git a/x-pack/plugins/security_solution/public/resolver/mocks/endpoint_event.ts b/x-pack/plugins/security_solution/public/resolver/mocks/endpoint_event.ts index 083f6b8baa59f..d19ca285ff3ff 100644 --- a/x-pack/plugins/security_solution/public/resolver/mocks/endpoint_event.ts +++ b/x-pack/plugins/security_solution/public/resolver/mocks/endpoint_event.ts @@ -4,31 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EndpointEvent } from '../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../common/endpoint/types'; /** * Simple mock endpoint event that works for tree layouts. */ export function mockEndpointEvent({ entityID, - name, - parentEntityId, - timestamp, - lifecycleType, + processName = 'process name', + parentEntityID, + timestamp = 0, + eventType = 'start', + eventCategory = 'process', pid = 0, + eventID = 'event id', }: { entityID: string; - name: string; - parentEntityId?: string; - timestamp: number; - lifecycleType?: string; + processName?: string; + parentEntityID?: string; + timestamp?: number; + eventType?: string; + eventCategory?: string; pid?: number; -}): EndpointEvent { + eventID?: string; +}): SafeResolverEvent { return { '@timestamp': timestamp, event: { - type: lifecycleType ? lifecycleType : 'start', - category: 'process', + type: eventType, + category: eventCategory, + id: eventID, }, agent: { id: 'agent.id', @@ -46,15 +51,15 @@ export function mockEndpointEvent({ entity_id: entityID, executable: 'executable', args: 'args', - name, + name: processName, pid, hash: { md5: 'hash.md5', }, parent: { pid: 0, - entity_id: parentEntityId, + entity_id: parentEntityID, }, }, - } as EndpointEvent; + }; } diff --git a/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts b/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts index 8bd5953e9cb41..8691ecac4d1cc 100644 --- a/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts +++ b/x-pack/plugins/security_solution/public/resolver/mocks/resolver_tree.ts @@ -5,7 +5,8 @@ */ import { mockEndpointEvent } from './endpoint_event'; -import { ResolverTree, ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types'; +import { ResolverTree, SafeResolverEvent } from '../../../common/endpoint/types'; +import * as eventModel from '../../../common/endpoint/models/event'; export function mockTreeWith2AncestorsAndNoChildren({ originID, @@ -16,34 +17,42 @@ export function mockTreeWith2AncestorsAndNoChildren({ firstAncestorID: string; originID: string; }): ResolverTree { - const secondAncestor: ResolverEvent = mockEndpointEvent({ + const secondAncestor: SafeResolverEvent = mockEndpointEvent({ entityID: secondAncestorID, - name: 'a', - parentEntityId: 'none', + processName: 'a', + parentEntityID: 'none', timestamp: 0, }); - const firstAncestor: ResolverEvent = mockEndpointEvent({ + const firstAncestor: SafeResolverEvent = mockEndpointEvent({ entityID: firstAncestorID, - name: 'b', - parentEntityId: secondAncestorID, + processName: 'b', + parentEntityID: secondAncestorID, timestamp: 1, }); - const originEvent: ResolverEvent = mockEndpointEvent({ + const originEvent: SafeResolverEvent = mockEndpointEvent({ entityID: originID, - name: 'c', - parentEntityId: firstAncestorID, + processName: 'c', + parentEntityID: firstAncestorID, timestamp: 2, }); - return ({ + return { entityID: originID, children: { childNodes: [], + nextChild: null, }, ancestry: { - ancestors: [{ lifecycle: [secondAncestor] }, { lifecycle: [firstAncestor] }], + nextAncestor: null, + ancestors: [ + { entityID: secondAncestorID, lifecycle: [secondAncestor] }, + { entityID: firstAncestorID, lifecycle: [firstAncestor] }, + ], }, lifecycle: [originEvent], - } as unknown) as ResolverTree; + relatedEvents: { events: [], nextEvent: null }, + relatedAlerts: { alerts: [], nextAlert: null }, + stats: { events: { total: 2, byCategory: {} }, totalAlerts: 0 }, + }; } export function mockTreeWithAllProcessesTerminated({ @@ -55,44 +64,44 @@ export function mockTreeWithAllProcessesTerminated({ firstAncestorID: string; originID: string; }): ResolverTree { - const secondAncestor: ResolverEvent = mockEndpointEvent({ + const secondAncestor: SafeResolverEvent = mockEndpointEvent({ entityID: secondAncestorID, - name: 'a', - parentEntityId: 'none', + processName: 'a', + parentEntityID: 'none', timestamp: 0, }); - const firstAncestor: ResolverEvent = mockEndpointEvent({ + const firstAncestor: SafeResolverEvent = mockEndpointEvent({ entityID: firstAncestorID, - name: 'b', - parentEntityId: secondAncestorID, + processName: 'b', + parentEntityID: secondAncestorID, timestamp: 1, }); - const originEvent: ResolverEvent = mockEndpointEvent({ + const originEvent: SafeResolverEvent = mockEndpointEvent({ entityID: originID, - name: 'c', - parentEntityId: firstAncestorID, + processName: 'c', + parentEntityID: firstAncestorID, timestamp: 2, }); - const secondAncestorTermination: ResolverEvent = mockEndpointEvent({ + const secondAncestorTermination: SafeResolverEvent = mockEndpointEvent({ entityID: secondAncestorID, - name: 'a', - parentEntityId: 'none', + processName: 'a', + parentEntityID: 'none', timestamp: 0, - lifecycleType: 'end', + eventType: 'end', }); - const firstAncestorTermination: ResolverEvent = mockEndpointEvent({ + const firstAncestorTermination: SafeResolverEvent = mockEndpointEvent({ entityID: firstAncestorID, - name: 'b', - parentEntityId: secondAncestorID, + processName: 'b', + parentEntityID: secondAncestorID, timestamp: 1, - lifecycleType: 'end', + eventType: 'end', }); - const originEventTermination: ResolverEvent = mockEndpointEvent({ + const originEventTermination: SafeResolverEvent = mockEndpointEvent({ entityID: originID, - name: 'c', - parentEntityId: firstAncestorID, + processName: 'c', + parentEntityID: firstAncestorID, timestamp: 2, - lifecycleType: 'end', + eventType: 'end', }); return ({ entityID: originID, @@ -109,26 +118,10 @@ export function mockTreeWithAllProcessesTerminated({ } as unknown) as ResolverTree; } -/** - * A valid category for a related event. E.g. "registry", "network", "file" - */ -type RelatedEventCategory = string; -/** - * A valid type for a related event. E.g. "start", "end", "access" - */ -type RelatedEventType = string; - /** * Add/replace related event info (on origin node) for any mock ResolverTree - * - * @param treeToAddRelatedEventsTo the ResolverTree to modify - * @param relatedEventsToAddByCategoryAndType Iterable of `[category, type]` pairs describing related events. e.g. [['dns','info'],['registry','access']] */ -function withRelatedEventsOnOrigin( - treeToAddRelatedEventsTo: ResolverTree, - relatedEventsToAddByCategoryAndType: Iterable<[RelatedEventCategory, RelatedEventType]> -): ResolverTree { - const events: SafeResolverEvent[] = []; +function withRelatedEventsOnOrigin(tree: ResolverTree, events: SafeResolverEvent[]): ResolverTree { const byCategory: Record = {}; const stats = { totalAlerts: 0, @@ -137,29 +130,19 @@ function withRelatedEventsOnOrigin( byCategory, }, }; - for (const [category, type] of relatedEventsToAddByCategoryAndType) { - events.push({ - '@timestamp': 1, - event: { - kind: 'event', - type, - category, - id: 'xyz', - }, - process: { - entity_id: treeToAddRelatedEventsTo.entityID, - }, - }); + for (const event of events) { stats.events.total++; - stats.events.byCategory[category] = stats.events.byCategory[category] - ? stats.events.byCategory[category] + 1 - : 1; + for (const category of eventModel.eventCategory(event)) { + stats.events.byCategory[category] = stats.events.byCategory[category] + ? stats.events.byCategory[category] + 1 + : 1; + } } return { - ...treeToAddRelatedEventsTo, + ...tree, stats, relatedEvents: { - events: events as ResolverEvent[], + events, nextEvent: null, }, }; @@ -174,38 +157,46 @@ export function mockTreeWithNoAncestorsAnd2Children({ firstChildID: string; secondChildID: string; }): ResolverTree { - const origin: ResolverEvent = mockEndpointEvent({ + const origin: SafeResolverEvent = mockEndpointEvent({ pid: 0, entityID: originID, - name: 'c.ext', - parentEntityId: 'none', + processName: 'c.ext', + parentEntityID: 'none', timestamp: 0, }); - const firstChild: ResolverEvent = mockEndpointEvent({ + const firstChild: SafeResolverEvent = mockEndpointEvent({ pid: 1, entityID: firstChildID, - name: 'd', - parentEntityId: originID, + processName: 'd', + parentEntityID: originID, timestamp: 1, }); - const secondChild: ResolverEvent = mockEndpointEvent({ + const secondChild: SafeResolverEvent = mockEndpointEvent({ pid: 2, entityID: secondChildID, - name: 'e', - parentEntityId: originID, + processName: 'e', + parentEntityID: originID, timestamp: 2, }); - return ({ + return { entityID: originID, children: { - childNodes: [{ lifecycle: [firstChild] }, { lifecycle: [secondChild] }], + childNodes: [ + { entityID: firstChildID, lifecycle: [firstChild] }, + { entityID: secondChildID, lifecycle: [secondChild] }, + ], + nextChild: null, }, ancestry: { ancestors: [], + nextAncestor: null, }, lifecycle: [origin], - } as unknown) as ResolverTree; + relatedEvents: { events: [], nextEvent: null }, + relatedAlerts: { alerts: [], nextAlert: null }, + stats: { events: { total: 2, byCategory: {} }, totalAlerts: 0 }, + }; } /** @@ -222,52 +213,52 @@ export function mockTreeWith1AncestorAnd2ChildrenAndAllNodesHave2GraphableEvents firstChildID: string; secondChildID: string; }): ResolverTree { - const ancestor: ResolverEvent = mockEndpointEvent({ + const ancestor: SafeResolverEvent = mockEndpointEvent({ entityID: ancestorID, - name: ancestorID, + processName: ancestorID, timestamp: 1, - parentEntityId: undefined, + parentEntityID: undefined, }); - const ancestorClone: ResolverEvent = mockEndpointEvent({ + const ancestorClone: SafeResolverEvent = mockEndpointEvent({ entityID: ancestorID, - name: ancestorID, + processName: ancestorID, timestamp: 1, - parentEntityId: undefined, + parentEntityID: undefined, }); - const origin: ResolverEvent = mockEndpointEvent({ + const origin: SafeResolverEvent = mockEndpointEvent({ entityID: originID, - name: originID, - parentEntityId: ancestorID, + processName: originID, + parentEntityID: ancestorID, timestamp: 0, }); - const originClone: ResolverEvent = mockEndpointEvent({ + const originClone: SafeResolverEvent = mockEndpointEvent({ entityID: originID, - name: originID, - parentEntityId: ancestorID, + processName: originID, + parentEntityID: ancestorID, timestamp: 0, }); - const firstChild: ResolverEvent = mockEndpointEvent({ + const firstChild: SafeResolverEvent = mockEndpointEvent({ entityID: firstChildID, - name: firstChildID, - parentEntityId: originID, + processName: firstChildID, + parentEntityID: originID, timestamp: 1, }); - const firstChildClone: ResolverEvent = mockEndpointEvent({ + const firstChildClone: SafeResolverEvent = mockEndpointEvent({ entityID: firstChildID, - name: firstChildID, - parentEntityId: originID, + processName: firstChildID, + parentEntityID: originID, timestamp: 1, }); - const secondChild: ResolverEvent = mockEndpointEvent({ + const secondChild: SafeResolverEvent = mockEndpointEvent({ entityID: secondChildID, - name: secondChildID, - parentEntityId: originID, + processName: secondChildID, + parentEntityID: originID, timestamp: 2, }); - const secondChildClone: ResolverEvent = mockEndpointEvent({ + const secondChildClone: SafeResolverEvent = mockEndpointEvent({ entityID: secondChildID, - name: secondChildID, - parentEntityId: originID, + processName: secondChildID, + parentEntityID: originID, timestamp: 2, }); @@ -330,9 +321,22 @@ export function mockTreeWithNoAncestorsAndTwoChildrenAndRelatedEventsOnOrigin({ firstChildID, secondChildID, }); - const withRelatedEvents: Array<[string, string]> = [ - ['registry', 'access'], - ['registry', 'access'], + const parentEntityID = eventModel.parentEntityIDSafeVersion(baseTree.lifecycle[0]); + const relatedEvents = [ + mockEndpointEvent({ + entityID: originID, + parentEntityID, + eventID: 'first related event', + eventType: 'access', + eventCategory: 'registry', + }), + mockEndpointEvent({ + entityID: originID, + parentEntityID, + eventID: 'second related event', + eventType: 'access', + eventCategory: 'registry', + }), ]; - return withRelatedEventsOnOrigin(baseTree, withRelatedEvents); + return withRelatedEventsOnOrigin(baseTree, relatedEvents); } diff --git a/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts b/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts index 4d48b34fb2841..380b15cf9da4c 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/process_event.test.ts @@ -6,11 +6,7 @@ import { eventType, orderByTime, userInfoForProcess } from './process_event'; import { mockProcessEvent } from './process_event_test_helpers'; -import { - LegacyEndpointEvent, - ResolverEvent, - SafeResolverEvent, -} from '../../../common/endpoint/types'; +import { LegacyEndpointEvent, SafeResolverEvent } from '../../../common/endpoint/types'; describe('process event', () => { describe('eventType', () => { @@ -45,7 +41,7 @@ describe('process event', () => { }); }); describe('orderByTime', () => { - let mock: (time: number, eventID: string) => ResolverEvent; + let mock: (time: number, eventID: string) => SafeResolverEvent; let events: SafeResolverEvent[]; beforeEach(() => { mock = (time, eventID) => { @@ -54,20 +50,20 @@ describe('process event', () => { event: { id: eventID, }, - } as ResolverEvent; + }; }; // 2 events each for numbers -1, 0, 1, and NaN // each event has a unique id, a through h // order is arbitrary events = [ - mock(-1, 'a') as SafeResolverEvent, - mock(0, 'c') as SafeResolverEvent, - mock(1, 'e') as SafeResolverEvent, - mock(NaN, 'g') as SafeResolverEvent, - mock(-1, 'b') as SafeResolverEvent, - mock(0, 'd') as SafeResolverEvent, - mock(1, 'f') as SafeResolverEvent, - mock(NaN, 'h') as SafeResolverEvent, + mock(-1, 'a'), + mock(0, 'c'), + mock(1, 'e'), + mock(NaN, 'g'), + mock(-1, 'b'), + mock(0, 'd'), + mock(1, 'f'), + mock(NaN, 'h'), ]; }); it('sorts events as expected', () => { diff --git a/x-pack/plugins/security_solution/public/resolver/models/process_event.ts b/x-pack/plugins/security_solution/public/resolver/models/process_event.ts index ea588731a55c8..1510fc7f9f365 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/process_event.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/process_event.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as event from '../../../common/endpoint/models/event'; +import { firstNonNullValue } from '../../../common/endpoint/models/ecs_safety_helpers'; + +import * as eventModel from '../../../common/endpoint/models/event'; import { ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types'; import { ResolverProcessType } from '../types'; @@ -12,19 +14,11 @@ import { ResolverProcessType } from '../types'; * Returns true if the process's eventType is either 'processCreated' or 'processRan'. * Resolver will only render 'graphable' process events. */ -export function isGraphableProcess(passedEvent: ResolverEvent) { +export function isGraphableProcess(passedEvent: SafeResolverEvent) { return eventType(passedEvent) === 'processCreated' || eventType(passedEvent) === 'processRan'; } -function isValue(field: string | string[], value: string) { - if (field instanceof Array) { - return field.length === 1 && field[0] === value; - } else { - return field === value; - } -} - -export function isTerminatedProcess(passedEvent: ResolverEvent) { +export function isTerminatedProcess(passedEvent: SafeResolverEvent) { return eventType(passedEvent) === 'processTerminated'; } @@ -33,7 +27,7 @@ export function isTerminatedProcess(passedEvent: ResolverEvent) { * may return NaN if the timestamp wasn't present or was invalid. */ export function datetime(passedEvent: SafeResolverEvent): number | null { - const timestamp = event.timestampSafeVersion(passedEvent); + const timestamp = eventModel.timestampSafeVersion(passedEvent); const time = timestamp === undefined ? 0 : new Date(timestamp).getTime(); @@ -44,8 +38,8 @@ export function datetime(passedEvent: SafeResolverEvent): number | null { /** * Returns a custom event type for a process event based on the event's metadata. */ -export function eventType(passedEvent: ResolverEvent): ResolverProcessType { - if (event.isLegacyEvent(passedEvent)) { +export function eventType(passedEvent: SafeResolverEvent): ResolverProcessType { + if (eventModel.isLegacyEventSafeVersion(passedEvent)) { const { endgame: { event_type_full: type, event_subtype_full: subType }, } = passedEvent; @@ -64,20 +58,20 @@ export function eventType(passedEvent: ResolverEvent): ResolverProcessType { return 'processCausedAlert'; } } else { - const { - event: { type, category, kind }, - } = passedEvent; - if (isValue(category, 'process')) { - if (isValue(type, 'start') || isValue(type, 'change') || isValue(type, 'creation')) { + const type = new Set(eventModel.eventType(passedEvent)); + const category = new Set(eventModel.eventCategory(passedEvent)); + const kind = new Set(eventModel.eventKind(passedEvent)); + if (category.has('process')) { + if (type.has('start') || type.has('change') || type.has('creation')) { return 'processCreated'; - } else if (isValue(type, 'info')) { + } else if (type.has('info')) { return 'processRan'; - } else if (isValue(type, 'end')) { + } else if (type.has('end')) { return 'processTerminated'; } else { return 'unknownProcessEvent'; } - } else if (kind === 'alert') { + } else if (kind.has('alert')) { return 'processCausedAlert'; } } @@ -88,7 +82,7 @@ export function eventType(passedEvent: ResolverEvent): ResolverProcessType { * Returns the process event's PID */ export function uniquePidForProcess(passedEvent: ResolverEvent): string { - if (event.isLegacyEvent(passedEvent)) { + if (eventModel.isLegacyEvent(passedEvent)) { return String(passedEvent.endgame.unique_pid); } else { return passedEvent.process.entity_id; @@ -98,45 +92,32 @@ export function uniquePidForProcess(passedEvent: ResolverEvent): string { /** * Returns the PID for the process on the host */ -export function processPid(passedEvent: ResolverEvent): number | undefined { - if (event.isLegacyEvent(passedEvent)) { - return passedEvent.endgame.pid; - } else { - return passedEvent.process.pid; - } +export function processPID(event: SafeResolverEvent): number | undefined { + return firstNonNullValue( + eventModel.isLegacyEventSafeVersion(event) ? event.endgame.pid : event.process?.pid + ); } /** * Returns the process event's parent PID */ export function uniqueParentPidForProcess(passedEvent: ResolverEvent): string | undefined { - if (event.isLegacyEvent(passedEvent)) { + if (eventModel.isLegacyEvent(passedEvent)) { return String(passedEvent.endgame.unique_ppid); } else { return passedEvent.process.parent?.entity_id; } } -/** - * Returns the process event's parent PID - */ -export function processParentPid(passedEvent: ResolverEvent): number | undefined { - if (event.isLegacyEvent(passedEvent)) { - return passedEvent.endgame.ppid; - } else { - return passedEvent.process.parent?.pid; - } -} - /** * Returns the process event's path on its host */ -export function processPath(passedEvent: ResolverEvent): string | undefined { - if (event.isLegacyEvent(passedEvent)) { - return passedEvent.endgame.process_path; - } else { - return passedEvent.process.executable; - } +export function processPath(passedEvent: SafeResolverEvent): string | undefined { + return firstNonNullValue( + eventModel.isLegacyEventSafeVersion(passedEvent) + ? passedEvent.endgame.process_path + : passedEvent.process?.executable + ); } /** @@ -148,19 +129,6 @@ export function userInfoForProcess( return passedEvent.user; } -/** - * Returns the MD5 hash for the `passedEvent` parameter, or undefined if it can't be located - * @param {ResolverEvent} passedEvent The `ResolverEvent` to get the MD5 value for - * @returns {string | undefined} The MD5 string for the event - */ -export function md5HashForProcess(passedEvent: ResolverEvent): string | undefined { - if (event.isLegacyEvent(passedEvent)) { - // There is not currently a key for this on Legacy event types - return undefined; - } - return passedEvent?.process?.hash?.md5; -} - /** * Returns the command line path and arguments used to run the `passedEvent` if any * @@ -168,7 +136,7 @@ export function md5HashForProcess(passedEvent: ResolverEvent): string | undefine * @returns {string | undefined} The arguments (including the path) used to run the process */ export function argsForProcess(passedEvent: ResolverEvent): string | undefined { - if (event.isLegacyEvent(passedEvent)) { + if (eventModel.isLegacyEvent(passedEvent)) { // There is not currently a key for this on Legacy event types return undefined; } @@ -184,8 +152,8 @@ export function orderByTime(first: SafeResolverEvent, second: SafeResolverEvent) if (firstDatetime === secondDatetime) { // break ties using an arbitrary (stable) comparison of `eventId` (which should be unique) - return String(event.eventIDSafeVersion(first)).localeCompare( - String(event.eventIDSafeVersion(second)) + return String(eventModel.eventIDSafeVersion(first)).localeCompare( + String(eventModel.eventIDSafeVersion(second)) ); } else if (firstDatetime === null || secondDatetime === null) { // sort `null`'s as higher than numbers diff --git a/x-pack/plugins/security_solution/public/resolver/models/resolver_tree.ts b/x-pack/plugins/security_solution/public/resolver/models/resolver_tree.ts index 446e371832d38..775b88246b61f 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/resolver_tree.ts +++ b/x-pack/plugins/security_solution/public/resolver/models/resolver_tree.ts @@ -6,12 +6,12 @@ import { ResolverTree, - ResolverEvent, ResolverNodeStats, ResolverLifecycleNode, ResolverChildNode, + SafeResolverEvent, } from '../../../common/endpoint/types'; -import { uniquePidForProcess } from './process_event'; +import * as eventModel from '../../../common/endpoint/models/event'; /** * ResolverTree is a type returned by the server. @@ -29,7 +29,7 @@ function lifecycleNodes(tree: ResolverTree): ResolverLifecycleNode[] { * All the process events */ export function lifecycleEvents(tree: ResolverTree) { - const events: ResolverEvent[] = [...tree.lifecycle]; + const events: SafeResolverEvent[] = [...tree.lifecycle]; for (const { lifecycle } of tree.children.childNodes) { events.push(...lifecycle); } @@ -66,7 +66,7 @@ export function mock({ /** * Events represented by the ResolverTree. */ - events: ResolverEvent[]; + events: SafeResolverEvent[]; children?: ResolverChildNode[]; /** * Optionally provide cursors for the 'children' and 'ancestry' edges. @@ -77,8 +77,12 @@ export function mock({ return null; } const first = events[0]; + const entityID = eventModel.entityIDSafeVersion(first); + if (!entityID) { + throw new Error('first mock event must include an entityID.'); + } return { - entityID: uniquePidForProcess(first), + entityID, // Required children: { childNodes: children, diff --git a/x-pack/plugins/security_solution/public/resolver/store/actions.ts b/x-pack/plugins/security_solution/public/resolver/store/actions.ts index 6a02d5b76bc4c..3348c962efdea 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/actions.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/actions.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { CameraAction } from './camera'; -import { ResolverEvent } from '../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../common/endpoint/types'; import { DataAction } from './data/action'; /** @@ -16,25 +16,7 @@ interface UserBroughtProcessIntoView { /** * Used to identify the process node that should be brought into view. */ - readonly process: ResolverEvent; - /** - * The time (since epoch in milliseconds) when the action was dispatched. - */ - readonly time: number; - }; -} - -/** - * When an examination of query params in the UI indicates that state needs to - * be updated to reflect the new selection - */ -interface AppDetectedNewIdFromQueryParams { - readonly type: 'appDetectedNewIdFromQueryParams'; - readonly payload: { - /** - * Used to identify the process the process that should be synced with state. - */ - readonly process: ResolverEvent; + readonly process: SafeResolverEvent; /** * The time (since epoch in milliseconds) when the action was dispatched. */ @@ -51,15 +33,6 @@ interface UserRequestedRelatedEventData { readonly payload: string; } -/** - * The action dispatched when the app requests related event data for one - * subject (whose entity_id should be included as `payload`) - */ -interface AppDetectedMissingEventData { - readonly type: 'appDetectedMissingEventData'; - readonly payload: string; -} - /** * When the user switches the "active descendant" of the Resolver. * The "active descendant" (from the point of view of the parent element) @@ -127,6 +100,4 @@ export type ResolverAction = | UserBroughtProcessIntoView | UserFocusedOnResolverNode | UserSelectedResolverNode - | UserRequestedRelatedEventData - | AppDetectedNewIdFromQueryParams - | AppDetectedMissingEventData; + | UserRequestedRelatedEventData; diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts index 59d1494ae8c27..0cb1cd1cec771 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/action.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/action.ts @@ -45,14 +45,6 @@ interface AppAbortedResolverDataRequest { readonly payload: TreeFetcherParameters; } -/** - * Will occur when a request for related event data is unsuccessful. - */ -interface ServerFailedToReturnRelatedEventData { - readonly type: 'serverFailedToReturnRelatedEventData'; - readonly payload: string; -} - /** * When related events are returned from the server */ @@ -64,7 +56,6 @@ interface ServerReturnedRelatedEventData { export type DataAction = | ServerReturnedResolverData | ServerFailedToReturnResolverData - | ServerFailedToReturnRelatedEventData | ServerReturnedRelatedEventData | AppRequestedResolverData | AppAbortedResolverDataRequest; diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts index 1e2de06ea4af5..5714345de0431 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts @@ -10,8 +10,7 @@ import { dataReducer } from './reducer'; import * as selectors from './selectors'; import { DataState } from '../../types'; import { DataAction } from './action'; -import { ResolverChildNode, ResolverEvent, ResolverTree } from '../../../../common/endpoint/types'; -import * as eventModel from '../../../../common/endpoint/models/event'; +import { ResolverChildNode, ResolverTree } from '../../../../common/endpoint/types'; import { values } from '../../../../common/endpoint/models/ecs_safety_helpers'; import { mockTreeFetcherParameters } from '../../mocks/tree_fetcher_parameters'; @@ -43,7 +42,7 @@ describe('Resolver Data Middleware', () => { const tree = mockResolverTree({ // Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with // a lot of the frontend functions. So casting it back to the unsafe type for now. - events: baseTree.allEvents as ResolverEvent[], + events: baseTree.allEvents, cursors: { childrenNextChild: 'aValidChildCursor', ancestryNextAncestor: 'aValidAncestorCursor', @@ -61,9 +60,6 @@ describe('Resolver Data Middleware', () => { describe('when data was received with stats mocked for the first child node', () => { let firstChildNodeInTree: TreeNode; - let eventStatsForFirstChildNode: { total: number; byCategory: Record }; - let categoryToOverCount: string; - let aggregateCategoryTotalForFirstChildNode: number; let tree: ResolverTree; /** @@ -73,13 +69,7 @@ describe('Resolver Data Middleware', () => { */ beforeEach(() => { - ({ - tree, - firstChildNodeInTree, - eventStatsForFirstChildNode, - categoryToOverCount, - aggregateCategoryTotalForFirstChildNode, - } = mockedTree()); + ({ tree, firstChildNodeInTree } = mockedTree()); if (tree) { dispatchTree(tree); } @@ -94,7 +84,7 @@ describe('Resolver Data Middleware', () => { entityID: firstChildNodeInTree.id, // Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with // a lot of the frontend functions. So casting it back to the unsafe type for now. - events: firstChildNodeInTree.relatedEvents as ResolverEvent[], + events: firstChildNodeInTree.relatedEvents, nextEvent: null, }, }; @@ -108,121 +98,6 @@ describe('Resolver Data Middleware', () => { expect(selectedEventsForFirstChildNode).toBe(firstChildNodeInTree.relatedEvents); }); - it('should indicate the correct related event count for each category', () => { - const selectedRelatedInfo = selectors.relatedEventInfoByEntityId(store.getState()); - const displayCountsForCategory = selectedRelatedInfo(firstChildNodeInTree.id) - ?.numberActuallyDisplayedForCategory!; - - const eventCategoriesForNode: string[] = Object.keys( - eventStatsForFirstChildNode.byCategory - ); - - for (const eventCategory of eventCategoriesForNode) { - expect(`${eventCategory}:${displayCountsForCategory(eventCategory)}`).toBe( - `${eventCategory}:${eventStatsForFirstChildNode.byCategory[eventCategory]}` - ); - } - }); - /** - * The general approach reflected here is to _avoid_ showing a limit warning - even if we hit - * the overall related event limit - as long as the number in our category matches what the stats - * say we have. E.g. If the stats say you have 20 dns events, and we receive 20 dns events, we - * don't need to display a limit warning for that, even if we hit some overall event limit of e.g. 100 - * while we were fetching the 20. - */ - it('should not indicate the limit has been exceeded because the number of related events received for the category is greater or equal to the stats count', () => { - const selectedRelatedInfo = selectors.relatedEventInfoByEntityId(store.getState()); - const shouldShowLimit = selectedRelatedInfo(firstChildNodeInTree.id) - ?.shouldShowLimitForCategory!; - for (const typeCounted of Object.keys(eventStatsForFirstChildNode.byCategory)) { - expect(shouldShowLimit(typeCounted)).toBe(false); - } - }); - it('should not indicate that there are any related events missing because the number of related events received for the category is greater or equal to the stats count', () => { - const selectedRelatedInfo = selectors.relatedEventInfoByEntityId(store.getState()); - const notDisplayed = selectedRelatedInfo(firstChildNodeInTree.id) - ?.numberNotDisplayedForCategory!; - for (const typeCounted of Object.keys(eventStatsForFirstChildNode.byCategory)) { - expect(notDisplayed(typeCounted)).toBe(0); - } - }); - it('should return an overall correct count for the number of related events', () => { - const aggregateTotalByEntityId = selectors.relatedEventAggregateTotalByEntityId( - store.getState() - ); - const countForId = aggregateTotalByEntityId(firstChildNodeInTree.id); - expect(countForId).toBe(aggregateCategoryTotalForFirstChildNode); - }); - }); - describe('when data was received and stats show more related events than the API can provide', () => { - beforeEach(() => { - // Add 1 to the stats for an event category so that the selectors think we are missing data. - // This mutates `tree`, and then we re-dispatch it - eventStatsForFirstChildNode.byCategory[categoryToOverCount] = - eventStatsForFirstChildNode.byCategory[categoryToOverCount] + 1; - - if (tree) { - dispatchTree(tree); - const relatedAction: DataAction = { - type: 'serverReturnedRelatedEventData', - payload: { - entityID: firstChildNodeInTree.id, - // Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with - // a lot of the frontend functions. So casting it back to the unsafe type for now. - events: firstChildNodeInTree.relatedEvents as ResolverEvent[], - nextEvent: 'aValidNextEventCursor', - }, - }; - store.dispatch(relatedAction); - } - }); - it('should have the correct related events', () => { - const selectedEventsByEntityId = selectors.relatedEventsByEntityId(store.getState()); - const selectedEventsForFirstChildNode = selectedEventsByEntityId.get( - firstChildNodeInTree.id - )!.events; - - expect(selectedEventsForFirstChildNode).toBe(firstChildNodeInTree.relatedEvents); - }); - it('should return related events for the category equal to the number of events of that type provided', () => { - const relatedEventsByCategory = selectors.relatedEventsByCategory(store.getState()); - const relatedEventsForOvercountedCategory = relatedEventsByCategory( - firstChildNodeInTree.id - )(categoryToOverCount); - expect(relatedEventsForOvercountedCategory.length).toBe( - eventStatsForFirstChildNode.byCategory[categoryToOverCount] - 1 - ); - }); - it('should return the correct related event detail metadata for a given related event', () => { - const relatedEventsByCategory = selectors.relatedEventsByCategory(store.getState()); - const someRelatedEventForTheFirstChild = relatedEventsByCategory(firstChildNodeInTree.id)( - categoryToOverCount - )[0]; - const relatedEventID = eventModel.eventId(someRelatedEventForTheFirstChild)!; - const relatedDisplayInfo = selectors.relatedEventDisplayInfoByEntityAndSelfID( - store.getState() - )(firstChildNodeInTree.id, relatedEventID); - const [, countOfSameType, , sectionData] = relatedDisplayInfo; - const hostEntries = sectionData.filter((section) => { - return section.sectionTitle === 'host'; - })[0].entries; - expect(hostEntries).toContainEqual({ title: 'os.platform', description: 'Windows' }); - expect(countOfSameType).toBe( - eventStatsForFirstChildNode.byCategory[categoryToOverCount] - 1 - ); - }); - it('should indicate the limit has been exceeded because the number of related events received for the category is less than what the stats count said it would be', () => { - const selectedRelatedInfo = selectors.relatedEventInfoByEntityId(store.getState()); - const shouldShowLimit = selectedRelatedInfo(firstChildNodeInTree.id) - ?.shouldShowLimitForCategory!; - expect(shouldShowLimit(categoryToOverCount)).toBe(true); - }); - it('should indicate that there are related events missing because the number of related events received for the category is less than what the stats count said it would be', () => { - const selectedRelatedInfo = selectors.relatedEventInfoByEntityId(store.getState()); - const notDisplayed = selectedRelatedInfo(firstChildNodeInTree.id) - ?.numberNotDisplayedForCategory!; - expect(notDisplayed(categoryToOverCount)).toBe(1); - }); }); }); }); @@ -241,7 +116,7 @@ function mockedTree() { const tree = mockResolverTree({ // Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with // a lot of the frontend functions. So casting it back to the unsafe type for now. - events: baseTree.allEvents as ResolverEvent[], + events: baseTree.allEvents, /** * Calculate children from the ResolverTree response using the children of the `Tree` we generated using the Resolver data generator code. * Compile (and attach) stats to the first child node. @@ -255,7 +130,7 @@ function mockedTree() { const childNode: Partial = {}; // Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with // a lot of the frontend functions. So casting it back to the unsafe type for now. - childNode.lifecycle = node.lifecycle as ResolverEvent[]; + childNode.lifecycle = node.lifecycle; // `TreeNode` has `id` which is the same as `entityID`. // The `ResolverChildNode` calls the entityID as `entityID`. @@ -281,8 +156,6 @@ function mockedTree() { return { tree: tree!, firstChildNodeInTree, - eventStatsForFirstChildNode: statsResults.eventStats, - aggregateCategoryTotalForFirstChildNode: statsResults.aggregateCategoryTotal, categoryToOverCount: statsResults.firstCategory, }; } @@ -309,7 +182,6 @@ function compileStatsForChild( }; /** The category of the first event. */ firstCategory: string; - aggregateCategoryTotal: number; } { const totalRelatedEvents = node.relatedEvents.length; // For the purposes of testing, we pick one category to fake an extra event for @@ -317,12 +189,6 @@ function compileStatsForChild( let firstCategory: string | undefined; - // This is the "aggregate total" which is displayed to users as the total count - // of related events for the node. It is tallied by incrementing for every discrete - // event.category in an event.category array (or just 1 for a plain string). E.g. two events - // categories 'file' and ['dns','network'] would have an `aggregate total` of 3. - let aggregateCategoryTotal: number = 0; - const compiledStats = node.relatedEvents.reduce( (counts: Record, relatedEvent) => { // get an array of categories regardless of whether category is a string or string[] @@ -336,7 +202,6 @@ function compileStatsForChild( // Increment the count of events with this category counts[category] = counts[category] ? counts[category] + 1 : 1; - aggregateCategoryTotal++; } return counts; }, @@ -354,6 +219,5 @@ function compileStatsForChild( byCategory: compiledStats, }, firstCategory, - aggregateCategoryTotal, }; } diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts index c8df95aaee6f4..1819407a19516 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.ts @@ -11,9 +11,7 @@ import * as treeFetcherParameters from '../../models/tree_fetcher_parameters'; const initialState: DataState = { relatedEvents: new Map(), - relatedEventsReady: new Map(), resolverComponentInstanceID: undefined, - tree: {}, }; export const dataReducer: Reducer = (state = initialState, action) => { @@ -44,7 +42,7 @@ export const dataReducer: Reducer = (state = initialS }; return nextState; } else if (action.type === 'appAbortedResolverDataRequest') { - if (treeFetcherParameters.equal(action.payload, state.tree.pendingRequestParameters)) { + if (treeFetcherParameters.equal(action.payload, state.tree?.pendingRequestParameters)) { // the request we were awaiting was aborted const nextState: DataState = { ...state, @@ -81,7 +79,7 @@ export const dataReducer: Reducer = (state = initialS return nextState; } else if (action.type === 'serverFailedToReturnResolverData') { /** Only handle this if we are expecting a response */ - if (state.tree.pendingRequestParameters !== undefined) { + if (state.tree?.pendingRequestParameters !== undefined) { const nextState: DataState = { ...state, tree: { @@ -97,19 +95,9 @@ export const dataReducer: Reducer = (state = initialS } else { return state; } - } else if ( - action.type === 'userRequestedRelatedEventData' || - action.type === 'appDetectedMissingEventData' - ) { - const nextState: DataState = { - ...state, - relatedEventsReady: new Map([...state.relatedEventsReady, [action.payload, false]]), - }; - return nextState; } else if (action.type === 'serverReturnedRelatedEventData') { const nextState: DataState = { ...state, - relatedEventsReady: new Map([...state.relatedEventsReady, [action.payload.entityID, true]]), relatedEvents: new Map([...state.relatedEvents, [action.payload.entityID, action.payload]]), }; return nextState; diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts index 539325faffdf0..d9717b52d9ce1 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.test.ts @@ -16,7 +16,7 @@ import { mockTreeWithAllProcessesTerminated, mockTreeWithNoProcessEvents, } from '../../mocks/resolver_tree'; -import { uniquePidForProcess } from '../../models/process_event'; +import * as eventModel from '../../../../common/endpoint/models/event'; import { EndpointEvent } from '../../../../common/endpoint/types'; import { mockTreeFetcherParameters } from '../../mocks/tree_fetcher_parameters'; @@ -411,7 +411,7 @@ describe('data state', () => { expect(graphables.length).toBe(3); for (const event of graphables) { expect(() => { - selectors.ariaFlowtoCandidate(state())(uniquePidForProcess(event)); + selectors.ariaFlowtoCandidate(state())(eventModel.entityIDSafeVersion(event)!); }).not.toThrow(); } }); diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts index d714ddb181470..c7829fa8a69b3 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/selectors.ts @@ -14,46 +14,36 @@ import { IndexedProcessNode, AABB, VisibleEntites, - SectionData, TreeFetcherParameters, } from '../../types'; -import { - isGraphableProcess, - isTerminatedProcess, - uniquePidForProcess, - uniqueParentPidForProcess, -} from '../../models/process_event'; +import { isGraphableProcess, isTerminatedProcess } from '../../models/process_event'; import * as indexedProcessTreeModel from '../../models/indexed_process_tree'; +import * as eventModel from '../../../../common/endpoint/models/event'; import { - ResolverEvent, ResolverTree, ResolverNodeStats, ResolverRelatedEvents, SafeResolverEvent, - EndpointEvent, - LegacyEndpointEvent, } from '../../../../common/endpoint/types'; import * as resolverTreeModel from '../../models/resolver_tree'; import * as treeFetcherParametersModel from '../../models/tree_fetcher_parameters'; import * as isometricTaxiLayoutModel from '../../models/indexed_process_tree/isometric_taxi_layout'; -import * as eventModel from '../../../../common/endpoint/models/event'; import * as vector2 from '../../models/vector2'; -import { formatDate } from '../../view/panels/panel_content_utilities'; /** * If there is currently a request. */ export function isTreeLoading(state: DataState): boolean { - return state.tree.pendingRequestParameters !== undefined; + return state.tree?.pendingRequestParameters !== undefined; } /** * If a request was made and it threw an error or returned a failure response code. */ export function hadErrorLoadingTree(state: DataState): boolean { - if (state.tree.lastResponse) { - return !state.tree.lastResponse.successful; + if (state.tree?.lastResponse) { + return !state.tree?.lastResponse.successful; } return false; } @@ -70,7 +60,7 @@ export function resolverComponentInstanceID(state: DataState): string { * we're currently interested in. */ const resolverTreeResponse = (state: DataState): ResolverTree | undefined => { - return state.tree.lastResponse?.successful ? state.tree.lastResponse.result : undefined; + return state.tree?.lastResponse?.successful ? state.tree?.lastResponse.result : undefined; }; /** @@ -102,7 +92,7 @@ export const terminatedProcesses = createSelector(resolverTreeResponse, function .lifecycleEvents(tree) .filter(isTerminatedProcess) .map((terminatedEvent) => { - return uniquePidForProcess(terminatedEvent); + return eventModel.entityIDSafeVersion(terminatedEvent); }) ); }); @@ -115,8 +105,8 @@ export const isProcessTerminated = createSelector(terminatedProcesses, function terminatedProcesses /* eslint-enable no-shadow */ ) { - return (entityId: string) => { - return terminatedProcesses.has(entityId); + return (entityID: string) => { + return terminatedProcesses.has(entityID); }; }); @@ -125,12 +115,14 @@ export const isProcessTerminated = createSelector(terminatedProcesses, function */ export const graphableProcesses = createSelector(resolverTreeResponse, function (tree?) { // Keep track of the last process event (in array order) for each entity ID - const events: Map = new Map(); + const events: Map = new Map(); if (tree) { for (const event of resolverTreeModel.lifecycleEvents(tree)) { if (isGraphableProcess(event)) { - const entityID = uniquePidForProcess(event); - events.set(entityID, event); + const entityID = eventModel.entityIDSafeVersion(event); + if (entityID !== undefined) { + events.set(entityID, event); + } } } return [...events.values()]; @@ -147,7 +139,7 @@ export const tree = createSelector(graphableProcesses, function indexedTree( graphableProcesses /* eslint-enable no-shadow */ ) { - return indexedProcessTreeModel.factory(graphableProcesses as SafeResolverEvent[]); + return indexedProcessTreeModel.factory(graphableProcesses); }); /** @@ -169,24 +161,18 @@ export const relatedEventsStats: ( ); /** - * This returns the "aggregate total" for related events, tallied as the sum - * of their individual `event.category`s. E.g. a [DNS, Network] would count as two - * towards the aggregate total. + * The total number of events related to a node. */ -export const relatedEventAggregateTotalByEntityId: ( +export const relatedEventTotalCount: ( state: DataState -) => (entityId: string) => number = createSelector(relatedEventsStats, (relatedStats) => { - return (entityId) => { - const statsForEntity = relatedStats(entityId); - if (statsForEntity === undefined) { - return 0; - } - return Object.values(statsForEntity?.events?.byCategory || {}).reduce( - (sum, val) => sum + val, - 0 - ); - }; -}); +) => (entityID: string) => number | undefined = createSelector( + relatedEventsStats, + (relatedStats) => { + return (entityID) => { + return relatedStats(entityID)?.events?.total; + }; + } +); /** * returns a map of entity_ids to related event data. @@ -197,98 +183,36 @@ export function relatedEventsByEntityId(data: DataState): Map
` entries - * @deprecated + * Get an event (from memory) by its `event.id`. + * @deprecated Use the API to find events by ID */ -const objectToDescriptionListEntries = function* ( - obj: object, - prefix = '' -): Generator<{ title: string; description: string }> { - const nextPrefix = prefix.length ? `${prefix}.` : ''; - for (const [metaKey, metaValue] of Object.entries(obj)) { - if (typeof metaValue === 'number' || typeof metaValue === 'string') { - yield { title: nextPrefix + metaKey, description: `${metaValue}` }; - } else if (metaValue instanceof Array) { - yield { - title: nextPrefix + metaKey, - description: metaValue - .filter((arrayEntry) => { - return typeof arrayEntry === 'number' || typeof arrayEntry === 'string'; - }) - .join(','), - }; - } else if (typeof metaValue === 'object') { - yield* objectToDescriptionListEntries(metaValue, nextPrefix + metaKey); +export const eventByID = createSelector(relatedEventsByEntityId, (relatedEvents) => { + // A map of nodeID to a map of eventID to events. Lazily populated. + const memo = new Map>(); + return ({ eventID, nodeID }: { eventID: string; nodeID: string }) => { + // We keep related events in a map by their nodeID. + const eventsWrapper = relatedEvents.get(nodeID); + if (!eventsWrapper) { + return undefined; } - } -}; - -/** - * Returns a function that returns the information needed to display related event details based on - * the related event's entityID and its own ID. - * @deprecated - */ -export const relatedEventDisplayInfoByEntityAndSelfID: ( - state: DataState -) => ( - entityId: string, - relatedEventId: string | number -) => [ - EndpointEvent | LegacyEndpointEvent | undefined, - number, - string | undefined, - SectionData, - string -] = createSelector(relatedEventsByEntityId, function relatedEventDetails( - /* eslint-disable no-shadow */ - relatedEventsByEntityId - /* eslint-enable no-shadow */ -) { - return defaultMemoize((entityId: string, relatedEventId: string | number) => { - const relatedEventsForThisProcess = relatedEventsByEntityId.get(entityId); - if (!relatedEventsForThisProcess) { - return [undefined, 0, undefined, [], '']; - } - const specificEvent = relatedEventsForThisProcess.events.find( - (evt) => eventModel.eventId(evt) === relatedEventId - ); - // For breadcrumbs: - const specificCategory = specificEvent && eventModel.primaryEventCategory(specificEvent); - const countOfCategory = relatedEventsForThisProcess.events.reduce((sumtotal, evt) => { - return eventModel.primaryEventCategory(evt) === specificCategory ? sumtotal + 1 : sumtotal; - }, 0); - // Assuming these details (agent, ecs, process) aren't as helpful, can revisit - const { agent, ecs, process, ...relevantData } = specificEvent as SafeResolverEvent & { - // Type this with various unknown keys so that ts will let us delete those keys - ecs: unknown; - process: unknown; - }; - - let displayDate = ''; - const sectionData: SectionData = Object.entries(relevantData) - .map(([sectionTitle, val]) => { - if (sectionTitle === '@timestamp') { - displayDate = formatDate(val); - return { sectionTitle: '', entries: [] }; + // When an event from a nodeID is requested, build a map for all events related to that node. + if (!memo.has(nodeID)) { + const map = new Map(); + for (const event of eventsWrapper.events) { + const id = eventModel.eventIDSafeVersion(event); + if (id !== undefined) { + map.set(id, event); } - if (typeof val !== 'object') { - return { sectionTitle, entries: [{ title: sectionTitle, description: `${val}` }] }; - } - return { sectionTitle, entries: [...objectToDescriptionListEntries(val)] }; - }) - .filter((v) => v.sectionTitle !== '' && v.entries.length); - - return [specificEvent, countOfCategory, specificCategory, sectionData, displayDate]; - }); + } + memo.set(nodeID, map); + } + const eventMap = memo.get(nodeID); + if (!eventMap) { + // This shouldn't be possible. + return undefined; + } + return eventMap.get(eventID); + }; }); /** @@ -298,44 +222,65 @@ export const relatedEventDisplayInfoByEntityAndSelfID: ( */ export const relatedEventsByCategory: ( state: DataState -) => (entityID: string) => (ecsCategory: string) => ResolverEvent[] = createSelector( +) => (node: string, eventCategory: string) => SafeResolverEvent[] = createSelector( relatedEventsByEntityId, function ( /* eslint-disable no-shadow */ relatedEventsByEntityId /* eslint-enable no-shadow */ ) { - return defaultMemoize((entityId: string) => { - return defaultMemoize((ecsCategory: string) => { - const relatedById = relatedEventsByEntityId.get(entityId); - // With no related events, we can't return related by category - if (!relatedById) { - return []; + // A map of nodeID -> event category -> SafeResolverEvent[] + const nodeMap: Map> = new Map(); + for (const [nodeID, events] of relatedEventsByEntityId) { + // A map of eventCategory -> SafeResolverEvent[] + let categoryMap = nodeMap.get(nodeID); + if (!categoryMap) { + categoryMap = new Map(); + nodeMap.set(nodeID, categoryMap); + } + + for (const event of events.events) { + for (const category of eventModel.eventCategory(event)) { + let eventsInCategory = categoryMap.get(category); + if (!eventsInCategory) { + eventsInCategory = []; + categoryMap.set(category, eventsInCategory); + } + eventsInCategory.push(event); } - return relatedById.events.reduce( - (eventsByCategory: ResolverEvent[], candidate: ResolverEvent) => { - if ( - [candidate && eventModel.allEventCategories(candidate)].flat().includes(ecsCategory) - ) { - eventsByCategory.push(candidate); - } - return eventsByCategory; - }, - [] - ); - }); - }); + } + } + + // Use the same empty array for all values that are missing + const emptyArray: SafeResolverEvent[] = []; + + return (entityID: string, category: string): SafeResolverEvent[] => { + const categoryMap = nodeMap.get(entityID); + if (!categoryMap) { + return emptyArray; + } + const eventsInCategory = categoryMap.get(category); + return eventsInCategory ?? emptyArray; + }; } ); -/** - * returns a map of entity_ids to booleans indicating if it is waiting on related event - * A value of `undefined` can be interpreted as `not yet requested` - * @deprecated - */ -export function relatedEventsReady(data: DataState): Map { - return data.relatedEventsReady; -} +export const relatedEventCountByType: ( + state: DataState +) => (nodeID: string, eventType: string) => number | undefined = createSelector( + relatedEventsStats, + (statsMap) => { + return (nodeID: string, eventType: string): number | undefined => { + const stats = statsMap(nodeID); + if (stats) { + const value = Object.prototype.hasOwnProperty.call(stats.events.byCategory, eventType); + if (typeof value === 'number' && Number.isFinite(value)) { + return value; + } + } + }; + } +); /** * `true` if there were more children than we got in the last request. @@ -355,113 +300,6 @@ export function hasMoreAncestors(state: DataState): boolean { return resolverTree ? resolverTreeModel.hasMoreAncestors(resolverTree) : false; } -interface RelatedInfoFunctions { - shouldShowLimitForCategory: (category: string) => boolean; - numberNotDisplayedForCategory: (category: string) => number; - numberActuallyDisplayedForCategory: (category: string) => number; -} -/** - * A map of `entity_id`s to functions that provide information about - * related events by ECS `.category` Primarily to avoid having business logic - * in UI components. - * @deprecated - */ -export const relatedEventInfoByEntityId: ( - state: DataState -) => (entityID: string) => RelatedInfoFunctions | null = createSelector( - relatedEventsByEntityId, - relatedEventsStats, - function selectLineageLimitInfo( - /* eslint-disable no-shadow */ - relatedEventsByEntityId, - relatedEventsStats - /* eslint-enable no-shadow */ - ) { - return (entityId) => { - const stats = relatedEventsStats(entityId); - if (!stats) { - return null; - } - const eventsResponseForThisEntry = relatedEventsByEntityId.get(entityId); - const hasMoreEvents = - eventsResponseForThisEntry && eventsResponseForThisEntry.nextEvent !== null; - /** - * Get the "aggregate" total for the event category (i.e. _all_ events that would qualify as being "in category") - * For a set like `[DNS,File][File,DNS][Registry]` The first and second events would contribute to the aggregate total for DNS being 2. - * This is currently aligned with how the backed provides this information. - * - * @param eventCategory {string} The ECS category like 'file','dns',etc. - */ - const aggregateTotalForCategory = (eventCategory: string): number => { - return stats.events.byCategory[eventCategory] || 0; - }; - - /** - * Get all the related events in the category provided. - * - * @param eventCategory {string} The ECS category like 'file','dns',etc. - */ - const unmemoizedMatchingEventsForCategory = (eventCategory: string): ResolverEvent[] => { - if (!eventsResponseForThisEntry) { - return []; - } - return eventsResponseForThisEntry.events.filter((resolverEvent) => { - for (const category of [eventModel.allEventCategories(resolverEvent)].flat()) { - if (category === eventCategory) { - return true; - } - } - return false; - }); - }; - - const matchingEventsForCategory = unmemoizedMatchingEventsForCategory; - - /** - * The number of events that occurred before the API limit was reached. - * The number of events that came back form the API that have `eventCategory` in their list of categories. - * - * @param eventCategory {string} The ECS category like 'file','dns',etc. - */ - const numberActuallyDisplayedForCategory = (eventCategory: string): number => { - return matchingEventsForCategory(eventCategory)?.length || 0; - }; - - /** - * The total number counted by the backend - the number displayed - * - * @param eventCategory {string} The ECS category like 'file','dns',etc. - */ - const numberNotDisplayedForCategory = (eventCategory: string): number => { - return ( - aggregateTotalForCategory(eventCategory) - - numberActuallyDisplayedForCategory(eventCategory) - ); - }; - - /** - * `true` when the `nextEvent` cursor appeared in the results and we are short on the number needed to - * fullfill the aggregate count. - * - * @param eventCategory {string} The ECS category like 'file','dns',etc. - */ - const shouldShowLimitForCategory = (eventCategory: string): boolean => { - if (hasMoreEvents && numberNotDisplayedForCategory(eventCategory) > 0) { - return true; - } - return false; - }; - - const entryValue = { - shouldShowLimitForCategory, - numberNotDisplayedForCategory, - numberActuallyDisplayedForCategory, - }; - return entryValue; - }; - } -); - /** * If the tree resource needs to be fetched then these are the parameters that should be used. */ @@ -470,14 +308,14 @@ export function treeParametersToFetch(state: DataState): TreeFetcherParameters | * If there are current tree parameters that don't match the parameters used in the pending request (if there is a pending request) and that don't match the parameters used in the last completed request (if there was a last completed request) then we need to fetch the tree resource using the current parameters. */ if ( - state.tree.currentParameters !== undefined && + state.tree?.currentParameters !== undefined && !treeFetcherParametersModel.equal( - state.tree.currentParameters, - state.tree.lastResponse?.parameters + state.tree?.currentParameters, + state.tree?.lastResponse?.parameters ) && !treeFetcherParametersModel.equal( - state.tree.currentParameters, - state.tree.pendingRequestParameters + state.tree?.currentParameters, + state.tree?.pendingRequestParameters ) ) { return state.tree.currentParameters; @@ -533,10 +371,11 @@ export const layout = createSelector( */ export const processEventForID: ( state: DataState -) => (nodeID: string) => ResolverEvent | null = createSelector( +) => (nodeID: string) => SafeResolverEvent | null = createSelector( tree, - (indexedProcessTree) => (nodeID: string) => - indexedProcessTreeModel.processEvent(indexedProcessTree, nodeID) as ResolverEvent + (indexedProcessTree) => (nodeID: string) => { + return indexedProcessTreeModel.processEvent(indexedProcessTree, nodeID); + } ); /** @@ -547,7 +386,7 @@ export const ariaLevel: (state: DataState) => (nodeID: string) => number | null processEventForID, ({ ariaLevels }, processEventGetter) => (nodeID: string) => { const node = processEventGetter(nodeID); - return node ? ariaLevels.get(node as SafeResolverEvent) ?? null : null; + return node ? ariaLevels.get(node) ?? null : null; } ); @@ -582,7 +421,7 @@ export const ariaFlowtoCandidate: ( * Getting the following sibling of a node has an `O(n)` time complexity where `n` is the number of children the parent of the node has. * For this reason, we calculate the following siblings of the node and all of its siblings at once and cache them. */ - const nodeEvent: ResolverEvent | null = eventGetter(nodeID); + const nodeEvent: SafeResolverEvent | null = eventGetter(nodeID); if (!nodeEvent) { // this should never happen. @@ -592,23 +431,30 @@ export const ariaFlowtoCandidate: ( // nodes with the same parent ID const children = indexedProcessTreeModel.children( indexedProcessTree, - uniqueParentPidForProcess(nodeEvent) + eventModel.parentEntityIDSafeVersion(nodeEvent) ); - let previousChild: ResolverEvent | null = null; + let previousChild: SafeResolverEvent | null = null; // Loop over all nodes that have the same parent ID (even if the parent ID is undefined or points to a node that isn't in the tree.) for (const child of children) { if (previousChild !== null) { // Set the `child` as the following sibling of `previousChild`. - memo.set(uniquePidForProcess(previousChild), uniquePidForProcess(child as ResolverEvent)); + const previousChildEntityID = eventModel.entityIDSafeVersion(previousChild); + const followingSiblingEntityID = eventModel.entityIDSafeVersion(child); + if (previousChildEntityID !== undefined && followingSiblingEntityID !== undefined) { + memo.set(previousChildEntityID, followingSiblingEntityID); + } } // Set the child as the previous child. - previousChild = child as ResolverEvent; + previousChild = child; } if (previousChild) { // if there is a previous child, it has no following sibling. - memo.set(uniquePidForProcess(previousChild), null); + const entityID = eventModel.entityIDSafeVersion(previousChild); + if (entityID !== undefined) { + memo.set(entityID, null); + } } return memoizedGetter(nodeID); @@ -708,10 +554,10 @@ export function treeRequestParametersToAbort(state: DataState): TreeFetcherParam * If there is a pending request, and its not for the current parameters (even, if the current parameters are undefined) then we should abort the request. */ if ( - state.tree.pendingRequestParameters !== undefined && + state.tree?.pendingRequestParameters !== undefined && !treeFetcherParametersModel.equal( - state.tree.pendingRequestParameters, - state.tree.currentParameters + state.tree?.pendingRequestParameters, + state.tree?.currentParameters ) ) { return state.tree.pendingRequestParameters; @@ -725,19 +571,19 @@ export function treeRequestParametersToAbort(state: DataState): TreeFetcherParam */ export const relatedEventTotalForProcess: ( state: DataState -) => (event: ResolverEvent) => number | null = createSelector( +) => (event: SafeResolverEvent) => number | null = createSelector( relatedEventsStats, (statsForProcess) => { - return (event: ResolverEvent) => { - const stats = statsForProcess(uniquePidForProcess(event)); - if (!stats) { + return (event: SafeResolverEvent) => { + const nodeID = eventModel.entityIDSafeVersion(event); + if (nodeID === undefined) { return null; } - let total = 0; - for (const value of Object.values(stats.events.byCategory)) { - total += value; + const stats = statsForProcess(nodeID); + if (!stats) { + return null; } - return total; + return stats.events.total; }; } ); diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts index 28948debae891..506acefe51676 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/visible_entities.test.ts @@ -8,7 +8,7 @@ import { Store, createStore } from 'redux'; import { ResolverAction } from '../actions'; import { resolverReducer } from '../reducer'; import { ResolverState } from '../../types'; -import { LegacyEndpointEvent, ResolverEvent } from '../../../../common/endpoint/types'; +import { LegacyEndpointEvent, SafeResolverEvent } from '../../../../common/endpoint/types'; import { visibleNodesAndEdgeLines } from '../selectors'; import { mockProcessEvent } from '../../models/process_event_test_helpers'; import { mock as mockResolverTree } from '../../models/resolver_tree'; @@ -102,7 +102,7 @@ describe('resolver visible entities', () => { }); describe('when rendering a large tree with a small viewport', () => { beforeEach(() => { - const events: ResolverEvent[] = [ + const events: SafeResolverEvent[] = [ processA, processB, processC, @@ -130,7 +130,7 @@ describe('resolver visible entities', () => { }); describe('when rendering a large tree with a large viewport', () => { beforeEach(() => { - const events: ResolverEvent[] = [ + const events: SafeResolverEvent[] = [ processA, processB, processC, diff --git a/x-pack/plugins/security_solution/public/resolver/store/methods.ts b/x-pack/plugins/security_solution/public/resolver/store/methods.ts index 8dd15b1a44d0c..f121b2aa86888 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/methods.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/methods.ts @@ -7,7 +7,7 @@ import { animatePanning } from './camera/methods'; import { layout } from './selectors'; import { ResolverState } from '../types'; -import { ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../common/endpoint/types'; const animationDuration = 1000; @@ -17,10 +17,10 @@ const animationDuration = 1000; export function animateProcessIntoView( state: ResolverState, startTime: number, - process: ResolverEvent + process: SafeResolverEvent ): ResolverState { const { processNodePositions } = layout(state); - const position = processNodePositions.get(process as SafeResolverEvent); + const position = processNodePositions.get(process); if (position) { return { ...state, diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts index ef6b1f5eb3c6f..5dca858b4fabe 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/index.ts @@ -6,9 +6,10 @@ import { Dispatch, MiddlewareAPI } from 'redux'; import { ResolverState, DataAccessLayer } from '../../types'; -import { ResolverRelatedEvents } from '../../../../common/endpoint/types'; import { ResolverTreeFetcher } from './resolver_tree_fetcher'; + import { ResolverAction } from '../actions'; +import { RelatedEventsFetcher } from './related_events_fetcher'; type MiddlewareFactory = ( dataAccessLayer: DataAccessLayer @@ -25,33 +26,12 @@ type MiddlewareFactory = ( export const resolverMiddlewareFactory: MiddlewareFactory = (dataAccessLayer: DataAccessLayer) => { return (api) => (next) => { const resolverTreeFetcher = ResolverTreeFetcher(dataAccessLayer, api); + const relatedEventsFetcher = RelatedEventsFetcher(dataAccessLayer, api); return async (action: ResolverAction) => { next(action); resolverTreeFetcher(); - - if ( - action.type === 'userRequestedRelatedEventData' || - action.type === 'appDetectedMissingEventData' - ) { - const entityIdToFetchFor = action.payload; - let result: ResolverRelatedEvents | undefined; - try { - result = await dataAccessLayer.relatedEvents(entityIdToFetchFor); - } catch { - api.dispatch({ - type: 'serverFailedToReturnRelatedEventData', - payload: action.payload, - }); - } - - if (result) { - api.dispatch({ - type: 'serverReturnedRelatedEventData', - payload: result, - }); - } - } + relatedEventsFetcher(); }; }; }; diff --git a/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts new file mode 100644 index 0000000000000..b83e3cff90736 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/store/middleware/related_events_fetcher.ts @@ -0,0 +1,49 @@ +/* + * 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 { Dispatch, MiddlewareAPI } from 'redux'; +import { isEqual } from 'lodash'; +import { ResolverRelatedEvents } from '../../../../common/endpoint/types'; + +import { ResolverState, DataAccessLayer, PanelViewAndParameters } from '../../types'; +import * as selectors from '../selectors'; +import { ResolverAction } from '../actions'; + +export function RelatedEventsFetcher( + dataAccessLayer: DataAccessLayer, + api: MiddlewareAPI, ResolverState> +): () => void { + let last: PanelViewAndParameters | undefined; + + // Call this after each state change. + // This fetches the ResolverTree for the current entityID + // if the entityID changes while + return async () => { + const state = api.getState(); + + const newParams = selectors.panelViewAndParameters(state); + const oldParams = last; + // Update this each time before fetching data (or even if we don't fetch data) so that subsequent actions that call this (concurrently) will have up to date info. + last = newParams; + + // If the panel view params have changed and the current panel view is either `nodeEventsOfType` or `eventDetail`, then fetch the related events for that nodeID. + if ( + !isEqual(newParams, oldParams) && + (newParams.panelView === 'nodeEventsOfType' || newParams.panelView === 'eventDetail') + ) { + const nodeID = newParams.panelParameters.nodeID; + + const result: ResolverRelatedEvents | undefined = await dataAccessLayer.relatedEvents(nodeID); + + if (result) { + api.dispatch({ + type: 'serverReturnedRelatedEventData', + payload: result, + }); + } + } + }; +} diff --git a/x-pack/plugins/security_solution/public/resolver/store/reducer.ts b/x-pack/plugins/security_solution/public/resolver/store/reducer.ts index bf62fd0e60df8..ae1e9a58a2097 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/reducer.ts @@ -9,7 +9,7 @@ import { cameraReducer } from './camera/reducer'; import { dataReducer } from './data/reducer'; import { ResolverAction } from './actions'; import { ResolverState, ResolverUIState } from '../types'; -import { uniquePidForProcess } from '../models/process_event'; +import * as eventModel from '../../../common/endpoint/models/event'; const uiReducer: Reducer = ( state = { @@ -37,17 +37,18 @@ const uiReducer: Reducer = ( selectedNode: action.payload, }; return next; - } else if ( - action.type === 'userBroughtProcessIntoView' || - action.type === 'appDetectedNewIdFromQueryParams' - ) { - const nodeID = uniquePidForProcess(action.payload.process); - const next: ResolverUIState = { - ...state, - ariaActiveDescendant: nodeID, - selectedNode: nodeID, - }; - return next; + } else if (action.type === 'userBroughtProcessIntoView') { + const nodeID = eventModel.entityIDSafeVersion(action.payload.process); + if (nodeID !== undefined) { + const next: ResolverUIState = { + ...state, + ariaActiveDescendant: nodeID, + selectedNode: nodeID, + }; + return next; + } else { + return state; + } } else if (action.type === 'appReceivedNewExternalProperties') { const next: ResolverUIState = { ...state, @@ -68,10 +69,7 @@ const concernReducers = combineReducers({ export const resolverReducer: Reducer = (state, action) => { const nextState = concernReducers(state, action); - if ( - action.type === 'userBroughtProcessIntoView' || - action.type === 'appDetectedNewIdFromQueryParams' - ) { + if (action.type === 'userBroughtProcessIntoView') { return animateProcessIntoView(nextState, action.payload.time, action.payload.process); } else { return nextState; diff --git a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts index 96b080206b61e..3c99a186ac0c2 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/selectors.ts @@ -9,7 +9,7 @@ import * as cameraSelectors from './camera/selectors'; import * as dataSelectors from './data/selectors'; import * as uiSelectors from './ui/selectors'; import { ResolverState, IsometricTaxiLayout } from '../types'; -import { ResolverEvent, ResolverNodeStats } from '../../../common/endpoint/types'; +import { ResolverNodeStats, SafeResolverEvent } from '../../../common/endpoint/types'; import { entityIDSafeVersion } from '../../../common/endpoint/models/event'; /** @@ -61,6 +61,11 @@ export const isProcessTerminated = composeSelectors( dataSelectors.isProcessTerminated ); +/** + * Retrieve an event from memory using the event's ID. + */ +export const eventByID = composeSelectors(dataStateSelector, dataSelectors.eventByID); + /** * Given a nodeID (aka entity_id) get the indexed process event. * Legacy functions take process events instead of nodeID, use this to get @@ -68,7 +73,7 @@ export const isProcessTerminated = composeSelectors( */ export const processEventForID: ( state: ResolverState -) => (nodeID: string) => ResolverEvent | null = composeSelectors( +) => (nodeID: string) => SafeResolverEvent | null = composeSelectors( dataStateSelector, dataSelectors.processEventForID ); @@ -119,30 +124,27 @@ export const relatedEventsStats: ( * of their individual `event.category`s. E.g. a [DNS, Network] would count as two * towards the aggregate total. */ -export const relatedEventAggregateTotalByEntityId: ( +export const relatedEventTotalCount: ( state: ResolverState -) => (nodeID: string) => number = composeSelectors( +) => (nodeID: string) => number | undefined = composeSelectors( dataStateSelector, - dataSelectors.relatedEventAggregateTotalByEntityId + dataSelectors.relatedEventTotalCount ); -/** - * Map of related events... by entity id - * @deprecated - */ -export const relatedEventsByEntityId = composeSelectors( +export const relatedEventCountByType: ( + state: ResolverState +) => (nodeID: string, eventType: string) => number | undefined = composeSelectors( dataStateSelector, - dataSelectors.relatedEventsByEntityId + dataSelectors.relatedEventCountByType ); /** - * Returns a function that returns the information needed to display related event details based on - * the related event's entityID and its own ID. + * Map of related events... by entity id * @deprecated */ -export const relatedEventDisplayInfoByEntityAndSelfId = composeSelectors( +export const relatedEventsByEntityId = composeSelectors( dataStateSelector, - dataSelectors.relatedEventDisplayInfoByEntityAndSelfID + dataSelectors.relatedEventsByEntityId ); /** @@ -155,26 +157,6 @@ export const relatedEventsByCategory = composeSelectors( dataSelectors.relatedEventsByCategory ); -/** - * Entity ids to booleans for waiting status - * @deprecated - */ -export const relatedEventsReady = composeSelectors( - dataStateSelector, - dataSelectors.relatedEventsReady -); - -/** - * Business logic lookup functions by ECS category by entity id. - * Example usage: - * const numberOfFileEvents = infoByEntityId.get(`someEntityId`)?.getAggregateTotalForCategory(`file`); - * @deprecated - */ -export const relatedEventInfoByEntityId = composeSelectors( - dataStateSelector, - dataSelectors.relatedEventInfoByEntityId -); - /** * Returns the id of the "current" tree node (fake-focused) */ diff --git a/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts b/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts index 6bc41832b92f2..a8882d835fce1 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/ui/selectors.ts @@ -8,9 +8,9 @@ import { decode, encode } from 'rison-node'; import { createSelector } from 'reselect'; import { PanelViewAndParameters, ResolverUIState } from '../../types'; -import { ResolverEvent } from '../../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../../common/endpoint/types'; import { isPanelViewAndParameters } from '../../models/location_search'; -import { eventId } from '../../../../common/endpoint/models/event'; +import { eventID } from '../../../../common/endpoint/models/event'; /** * id of the "current" tree node (fake-focused) @@ -124,12 +124,12 @@ export const relatedEventDetailHrefs: ( ) => ( category: string, nodeID: string, - events: ResolverEvent[] + events: SafeResolverEvent[] ) => Map = createSelector(relativeHref, (relativeHref) => { - return (category: string, nodeID: string, events: ResolverEvent[]) => { + return (category: string, nodeID: string, events: SafeResolverEvent[]) => { const hrefsByEntityID = new Map(); events.map((event) => { - const entityID = String(eventId(event)); + const entityID = String(eventID(event)); const eventDetailPanelParams: PanelViewAndParameters = { panelView: 'eventDetail', panelParameters: { diff --git a/x-pack/plugins/security_solution/public/resolver/types.ts b/x-pack/plugins/security_solution/public/resolver/types.ts index 952a1c5764d8e..4dc614abe3345 100644 --- a/x-pack/plugins/security_solution/public/resolver/types.ts +++ b/x-pack/plugins/security_solution/public/resolver/types.ts @@ -211,9 +211,8 @@ export interface TreeFetcherParameters { */ export interface DataState { readonly relatedEvents: Map; - readonly relatedEventsReady: Map; - readonly tree: { + readonly tree?: { /** * The parameters passed from the resolver properties */ @@ -614,8 +613,9 @@ export interface ResolverPluginSetup { dataAccessLayer: { /** * A mock `DataAccessLayer` that returns a tree that has no ancestor nodes but which has 2 children nodes. + * The origin has 2 related registry events */ - noAncestorsTwoChildren: () => { dataAccessLayer: DataAccessLayer }; + noAncestorsTwoChildrenWithRelatedEventsOnOrigin: () => { dataAccessLayer: DataAccessLayer }; }; }; } diff --git a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx index 53b889004798f..777a7292e9c23 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/edge_line.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import React from 'react'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -53,7 +55,7 @@ const StyledElapsedTime = styled.div` /** * A placeholder line segment view that connects process nodes. */ -const EdgeLineComponent = React.memo( +export const EdgeLine = React.memo( ({ className, edgeLineMetadata, @@ -155,7 +157,3 @@ const EdgeLineComponent = React.memo( ); } ); - -EdgeLineComponent.displayName = 'EdgeLine'; - -export const EdgeLine = EdgeLineComponent; diff --git a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx index 75aecf6747cca..dbeca840a4b66 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/graph_controls.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + /* eslint-disable react/button-has-type */ import React, { useCallback, useMemo, useContext } from 'react'; @@ -54,7 +56,7 @@ const StyledGraphControls = styled.div` /** * Controls for zooming, panning, and centering in Resolver */ -const GraphControlsComponent = React.memo( +export const GraphControls = React.memo( ({ className, }: { @@ -204,7 +206,3 @@ const GraphControlsComponent = React.memo( ); } ); - -GraphControlsComponent.displayName = 'GraphControlsComponent'; - -export const GraphControls = GraphControlsComponent; diff --git a/x-pack/plugins/security_solution/public/resolver/view/limit_warnings.tsx b/x-pack/plugins/security_solution/public/resolver/view/limit_warnings.tsx index 3f2b7c769cad7..bc57c4e28b9cd 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/limit_warnings.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/limit_warnings.tsx @@ -4,9 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable react/display-name */ + import React from 'react'; -import { EuiCallOut } from '@elastic/eui'; import { FormattedMessage } from 'react-intl'; +import { LimitWarningsEuiCallOut } from './styles'; const lineageLimitMessage = ( ); -const LineageTitleMessage = React.memo(function LineageTitleMessage({ - numberOfEntries, -}: { - numberOfEntries: number; -}) { +const LineageTitleMessage = React.memo(function ({ numberOfEntries }: { numberOfEntries: number }) { return (

- + ); }); /** * Limit warning for hitting a limit of nodes in the tree */ -export const LimitWarning = React.memo(function LimitWarning({ - className, - numberDisplayed, -}: { - className?: string; - numberDisplayed: number; -}) { +export const LimitWarning = React.memo(function ({ numberDisplayed }: { numberDisplayed: number }) { return ( - } >

{lineageLimitMessage}

-
+ ); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/node.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/node.test.tsx new file mode 100644 index 0000000000000..0b381f6771f00 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/node.test.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { noAncestorsTwoChildren } from '../data_access_layer/mocks/no_ancestors_two_children'; +import { Simulator } from '../test_utilities/simulator'; +// Extend jest with a custom matcher +import '../test_utilities/extend_jest'; + +let simulator: Simulator; +let databaseDocumentID: string; + +// the resolver component instance ID, used by the react code to distinguish piece of global state from those used by other resolver instances +const resolverComponentInstanceID = 'resolverComponentInstanceID'; + +describe('Resolver, when analyzing a tree that has no ancestors and 2 children', () => { + beforeEach(async () => { + // create a mock data access layer + const { metadata: dataAccessLayerMetadata, dataAccessLayer } = noAncestorsTwoChildren(); + + // save a reference to the `_id` supported by the mock data layer + databaseDocumentID = dataAccessLayerMetadata.databaseDocumentID; + + // create a resolver simulator, using the data access layer and an arbitrary component instance ID + simulator = new Simulator({ + databaseDocumentID, + dataAccessLayer, + resolverComponentInstanceID, + indices: [], + }); + }); + + it('shows 1 node with the words "Analyzed Event" in the label', async () => { + await expect( + simulator.map(() => { + return simulator.testSubject('resolver:node:description').map((element) => element.text()); + }) + ).toYieldEqualTo(['Analyzed Event · Running Process', 'Running Process', 'Running Process']); + }); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx index 7cfbd9a794669..2f23469606aca 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panel.test.tsx @@ -5,7 +5,7 @@ */ import { createMemoryHistory, History as HistoryPackageHistoryInterface } from 'history'; -import { noAncestorsTwoChildren } from '../data_access_layer/mocks/no_ancestors_two_children'; +import { noAncestorsTwoChildrenWithRelatedEventsOnOrigin } from '../data_access_layer/mocks/no_ancestors_two_children_with_related_events_on_origin'; import { Simulator } from '../test_utilities/simulator'; // Extend jest with a custom matcher import '../test_utilities/extend_jest'; @@ -14,7 +14,7 @@ import { urlSearch } from '../test_utilities/url_search'; // the resolver component instance ID, used by the react code to distinguish piece of global state from those used by other resolver instances const resolverComponentInstanceID = 'resolverComponentInstanceID'; -describe(`Resolver: when analyzing a tree with no ancestors and two children, and when the component instance ID is ${resolverComponentInstanceID}`, () => { +describe(`Resolver: when analyzing a tree with no ancestors and two children and two related registry event on the origin, and when the component instance ID is ${resolverComponentInstanceID}`, () => { /** * Get (or lazily create and get) the simulator. */ @@ -32,7 +32,10 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children, an beforeEach(() => { // create a mock data access layer - const { metadata: dataAccessLayerMetadata, dataAccessLayer } = noAncestorsTwoChildren(); + const { + metadata: dataAccessLayerMetadata, + dataAccessLayer, + } = noAncestorsTwoChildrenWithRelatedEventsOnOrigin(); entityIDs = dataAccessLayerMetadata.entityIDs; @@ -184,6 +187,38 @@ describe(`Resolver: when analyzing a tree with no ancestors and two children, an }) ); }); + describe("and when the user clicks the link to the node's events", () => { + beforeEach(async () => { + const nodeEventsListLink = await simulator().resolve( + 'resolver:node-detail:node-events-link' + ); + + if (nodeEventsListLink) { + nodeEventsListLink.simulate('click', { button: 0 }); + } + }); + it('should show a link to view 2 registry events', async () => { + await expect( + simulator().map(() => { + // The link text is split across two columns. The first column is the count and the second column has the type. + const type = simulator().testSubject('resolver:panel:node-events:event-type-count'); + const link = simulator().testSubject('resolver:panel:node-events:event-type-link'); + return { + typeLength: type.length, + linkLength: link.length, + typeText: type.text(), + linkText: link.text(), + }; + }) + ).toYieldEqualTo({ + typeLength: 1, + linkLength: 1, + linkText: 'registry', + // EUI's Table adds the column name to the value. + typeText: 'Count2', + }); + }); + }); describe('and when the node list link has been clicked', () => { beforeEach(async () => { const nodeListLink = await simulator().resolve( diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/breadcrumbs.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/breadcrumbs.tsx new file mode 100644 index 0000000000000..ed39198009364 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/breadcrumbs.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. + */ + +/* eslint-disable react/display-name */ + +import { i18n } from '@kbn/i18n'; +import { EuiBreadcrumb, EuiBetaBadge } from '@elastic/eui'; +import React, { memo } from 'react'; +import { BetaHeader, ThemedBreadcrumbs } from './styles'; +import { useColors } from '../use_colors'; + +/** + * Breadcrumb menu + */ +export const Breadcrumbs = memo(function ({ breadcrumbs }: { breadcrumbs: EuiBreadcrumb[] }) { + const { resolverBreadcrumbBackground, resolverEdgeText } = useColors(); + return ( + <> + + + + + + ); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx index 4e9d64f5a76a4..cc5f39e985d9e 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/cube_for_process.tsx @@ -43,7 +43,7 @@ export const CubeForProcess = memo(function ({ className={className} width="2.15em" height="2.15em" - viewBox="0 0 100% 100%" + viewBox="0 0 34 34" data-test-subj={dataTestSubj} isOrigin={isOrigin} > diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/deep_object_entries.test.ts b/x-pack/plugins/security_solution/public/resolver/view/panels/deep_object_entries.test.ts new file mode 100644 index 0000000000000..1c4e1f4199bc4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/deep_object_entries.test.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 { deepObjectEntries } from './deep_object_entries'; + +describe('deepObjectEntries', () => { + const valuesAndExpected: Array<[ + objectValue: object, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expected: Array<[path: Array, fieldValue: unknown]> + ]> = [ + [{}, []], // No 'field' values found + [{ a: {} }, []], // No 'field' values found + [{ a: { b: undefined } }, []], // No 'field' values found + [{ a: { b: undefined, c: [] } }, []], // No 'field' values found + [{ a: { b: undefined, c: [null] } }, []], // No 'field' values found + [{ a: { b: undefined, c: [null, undefined, 1] } }, [[['a', 'c'], 1]]], // Only `1` is a non-null value. It is under `a.c` because we ignore array indices + [ + { a: { b: undefined, c: [null, undefined, 1, { d: ['e'] }] } }, + [ + // 1 and 'e' are valid fields. + [['a', 'c'], 1], + [['a', 'c', 'd'], 'e'], + ], + ], + ]; + + describe.each(valuesAndExpected)('when passed %j', (value, expected) => { + it(`should return ${JSON.stringify(expected)}`, () => { + expect(deepObjectEntries(value)).toEqual(expected); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/deep_object_entries.ts b/x-pack/plugins/security_solution/public/resolver/view/panels/deep_object_entries.ts new file mode 100644 index 0000000000000..a508b00be5739 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/deep_object_entries.ts @@ -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. + */ +/** + * Sort of like object entries, but does a DFS of an object. + * Instead of getting a key, an array of keys is returned. + * The array of keys represents the path to the value. + * `undefined` and `null` values are omitted. + */ +export function deepObjectEntries(root: object): Array<[path: string[], value: unknown]> { + const queue: Array<{ path: string[]; value: unknown }> = [{ path: [], value: root }]; + const result: Array<[path: string[], value: unknown]> = []; + while (queue.length) { + const next = queue.shift(); + if (next === undefined) { + // this should be impossible + throw new Error(); + } + const { path, value } = next; + if (Array.isArray(value)) { + // branch on arrays + queue.push( + ...value.map((element) => ({ + path: [...path], // unlike with object paths, don't add the number indices to `path` + value: element, + })) + ); + } else if (typeof value === 'object' && value !== null) { + // branch on non-null objects + queue.push( + ...Object.keys(value).map((key) => ({ + path: [...path, key], + value: (value as Record)[key], + })) + ); + } else if (value !== undefined && value !== null) { + // emit other non-null, defined, values + result.push([path, value]); + } + } + return result; +} diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/descriptive_name.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/descriptive_name.test.tsx new file mode 100644 index 0000000000000..e869ab1ecd456 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/descriptive_name.test.tsx @@ -0,0 +1,50 @@ +/* + * 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 { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; +import { DescriptiveName } from './descriptive_name'; +import { SafeResolverEvent } from '../../../../common/endpoint/types'; +import { mount, ReactWrapper } from 'enzyme'; +import { I18nProvider } from '@kbn/i18n/react'; + +describe('DescriptiveName', () => { + let generator: EndpointDocGenerator; + let wrapper: (event: SafeResolverEvent) => ReactWrapper; + beforeEach(() => { + generator = new EndpointDocGenerator('seed'); + wrapper = (event: SafeResolverEvent) => + mount( + + + + ); + }); + it('returns the right name for a registry event', () => { + const extensions = { registry: { key: `HKLM/Windows/Software/abc` } }; + const event = generator.generateEvent({ eventCategory: 'registry', extensions }); + expect(wrapper(event).text()).toEqual(`HKLM/Windows/Software/abc`); + }); + + it('returns the right name for a network event', () => { + const randomIP = `${generator.randomIP()}`; + const extensions = { network: { direction: 'outbound', forwarded_ip: randomIP } }; + const event = generator.generateEvent({ eventCategory: 'network', extensions }); + expect(wrapper(event).text()).toEqual(`outbound ${randomIP}`); + }); + + it('returns the right name for a file event', () => { + const extensions = { file: { path: 'C:\\My Documents\\business\\January\\processName' } }; + const event = generator.generateEvent({ eventCategory: 'file', extensions }); + expect(wrapper(event).text()).toEqual('C:\\My Documents\\business\\January\\processName'); + }); + + it('returns the right name for a dns event', () => { + const extensions = { dns: { question: { name: `${generator.randomIP()}` } } }; + const event = generator.generateEvent({ eventCategory: 'dns', extensions }); + expect(wrapper(event).text()).toEqual(extensions.dns.question.name); + }); +}); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/descriptive_name.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/descriptive_name.tsx new file mode 100644 index 0000000000000..195ebceee0610 --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/descriptive_name.tsx @@ -0,0 +1,114 @@ +/* + * 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 { FormattedMessage } from 'react-intl'; + +import React from 'react'; + +import { + isLegacyEventSafeVersion, + processNameSafeVersion, + entityIDSafeVersion, +} from '../../../../common/endpoint/models/event'; +import { SafeResolverEvent } from '../../../../common/endpoint/types'; + +/** + * Based on the ECS category of the event, attempt to provide a more descriptive name + * (e.g. the `event.registry.key` for `registry` or the `dns.question.name` for `dns`, etc.). + * This function returns the data in the form of `{subject, descriptor}` where `subject` will + * tend to be the more distinctive term (e.g. 137.213.212.7 for a network event) and the + * `descriptor` can be used to present more useful/meaningful view (e.g. `inbound 137.213.212.7` + * in the example above). + * see: https://www.elastic.co/guide/en/ecs/current/ecs-field-reference.html + * @param event The ResolverEvent to get the descriptive name for + */ +export function DescriptiveName({ event }: { event: SafeResolverEvent }) { + if (isLegacyEventSafeVersion(event)) { + return ( + + ); + } + + /** + * This list of attempts can be expanded/adjusted as the underlying model changes over time: + */ + + // Stable, per ECS 1.5: https://www.elastic.co/guide/en/ecs/current/ecs-allowed-values-event-category.html + + if (event.network?.forwarded_ip) { + return ( + + ); + } + + if (event.file?.path) { + return ( + + ); + } + + if (event.registry?.path) { + return ( + + ); + } + + if (event.registry?.key) { + return ( + + ); + } + + if (event.dns?.question?.name) { + return ( + + ); + } + return ( + + ); +} diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx index 24d2a4a8f43f0..72f0d54d51fa3 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/event_detail.tsx @@ -4,275 +4,103 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { memo, useMemo, useEffect, Fragment } from 'react'; +/* eslint-disable no-continue */ + +/* eslint-disable react/display-name */ + +import React, { memo, useMemo, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiText, EuiDescriptionList, EuiTextColor, EuiTitle } from '@elastic/eui'; import styled from 'styled-components'; import { useSelector } from 'react-redux'; import { FormattedMessage } from 'react-intl'; import { StyledPanel } from '../styles'; -import { StyledBreadcrumbs, BoldCode, StyledTime, GeneratedText } from './panel_content_utilities'; -import * as event from '../../../../common/endpoint/models/event'; +import { BoldCode, StyledTime, GeneratedText, formatDate } from './panel_content_utilities'; +import { Breadcrumbs } from './breadcrumbs'; +import * as eventModel from '../../../../common/endpoint/models/event'; import * as selectors from '../../store/selectors'; -import { useResolverDispatch } from '../use_resolver_dispatch'; -import { PanelContentError } from './panel_content_error'; import { PanelLoading } from './panel_loading'; import { ResolverState } from '../../types'; -import { useNavigateOrReplace } from '../use_navigate_or_replace'; - -// Adding some styles to prevent horizontal scrollbars, per request from UX review -const StyledDescriptionList = memo(styled(EuiDescriptionList)` - &.euiDescriptionList.euiDescriptionList--column dt.euiDescriptionList__title.desc-title { - max-width: 8em; - overflow-wrap: break-word; - } - &.euiDescriptionList.euiDescriptionList--column dd.euiDescriptionList__description { - max-width: calc(100% - 8.5em); - overflow-wrap: break-word; - } -`); - -// Also prevents horizontal scrollbars on long descriptive names -const StyledDescriptiveName = memo(styled(EuiText)` - padding-right: 1em; - overflow-wrap: break-word; -`); - -// Styling subtitles, per UX review: -const StyledFlexTitle = memo(styled('h3')` - display: flex; - flex-flow: row; - font-size: 1.2em; -`); -const StyledTitleRule = memo(styled('hr')` - &.euiHorizontalRule.euiHorizontalRule--full.euiHorizontalRule--marginSmall.override { - display: block; - flex: 1; - margin-left: 0.5em; - } -`); +import { DescriptiveName } from './descriptive_name'; +import { useLinkProps } from '../use_link_props'; +import { SafeResolverEvent } from '../../../../common/endpoint/types'; +import { deepObjectEntries } from './deep_object_entries'; -const TitleHr = memo(() => { - return ( - +export const EventDetail = memo(function EventDetail({ + nodeID, + eventID, + eventType, +}: { + nodeID: string; + eventID: string; + /** The event type to show in the breadcrumbs */ + eventType: string; +}) { + const event = useSelector((state: ResolverState) => + selectors.eventByID(state)({ nodeID, eventID }) ); + const processEvent = useSelector((state: ResolverState) => + selectors.processEventForID(state)(nodeID) + ); + if (event && processEvent) { + return ( + + ); + } else { + return ( + + + + ); + } }); -TitleHr.displayName = 'TitleHR'; - -/** - * Take description list entries and prepare them for display by - * seeding with `` tags. - * - * @param entries {title: string, description: string}[] - */ -function entriesForDisplay(entries: Array<{ title: string; description: string }>) { - return entries.map((entry) => { - return { - description: {entry.description}, - title: {entry.title}, - }; - }); -} /** * This view presents a detailed view of all the available data for a related event, split and titled by the "section" * it appears in the underlying ResolverEvent */ -export const EventDetail = memo(function ({ +const EventDetailContents = memo(function ({ nodeID, - eventID, + event, + eventType, + processEvent, }: { nodeID: string; - eventID: string; -}) { - const parentEvent = useSelector((state: ResolverState) => - selectors.processEventForID(state)(nodeID) - ); - - const relatedEventsStats = useSelector((state: ResolverState) => - selectors.relatedEventsStats(state)(nodeID) - ); - const countForParent: number = Object.values(relatedEventsStats?.events.byCategory || {}).reduce( - (sum, val) => sum + val, - 0 - ); - const processName = (parentEvent && event.eventName(parentEvent)) || '*'; - const processEntityId = (parentEvent && event.entityId(parentEvent)) || ''; - const totalCount = countForParent || 0; - const eventsString = i18n.translate( - 'xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.events', - { - defaultMessage: 'Events', - } - ); - const naString = i18n.translate( - 'xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.NA', - { - defaultMessage: 'N/A', - } - ); - - const relatedsReadyMap = useSelector(selectors.relatedEventsReady); - const relatedsReady = relatedsReadyMap.get(processEntityId!); - const dispatch = useResolverDispatch(); - const nodesHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ panelView: 'nodes' }) - ); - const nodesLinkNavProps = useNavigateOrReplace({ - search: nodesHref, - }); - + event: SafeResolverEvent; /** - * If we don't have the related events for the parent yet, use this effect - * to request them. + * Event type to use in the breadcrumbs */ - useEffect(() => { - if ( - typeof relatedsReady === 'undefined' && - processEntityId !== null && - processEntityId !== undefined - ) { - dispatch({ - type: 'appDetectedMissingEventData', - payload: processEntityId, - }); + eventType: string; + processEvent: SafeResolverEvent; +}) { + const formattedDate = useMemo(() => { + const timestamp = eventModel.timestampSafeVersion(event); + if (timestamp !== undefined) { + return formatDate(new Date(timestamp)); } - }, [relatedsReady, dispatch, processEntityId]); - - const [ - relatedEventToShowDetailsFor, - countBySameCategory, - relatedEventCategory = naString, - sections, - formattedDate, - ] = useSelector((state: ResolverState) => - selectors.relatedEventDisplayInfoByEntityAndSelfId(state)(nodeID, eventID) - ); - - const { subject = '', descriptor = '' } = relatedEventToShowDetailsFor - ? event.descriptiveName(relatedEventToShowDetailsFor) - : {}; - - const nodeDetailHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ - panelView: 'nodeDetail', - panelParameters: { nodeID: processEntityId }, - }) - ); - const nodeDetailLinkNavProps = useNavigateOrReplace({ - search: nodeDetailHref, - }); - - const nodeEventsHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ - panelView: 'nodeEvents', - panelParameters: { nodeID: processEntityId }, - }) - ); - const nodeEventsLinkNavProps = useNavigateOrReplace({ - search: nodeEventsHref, - }); - - const nodeEventsOfTypeHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ - panelView: 'nodeEventsOfType', - panelParameters: { nodeID: processEntityId, eventType: relatedEventCategory }, - }) - ); - const nodeEventsOfTypeLinkNavProps = useNavigateOrReplace({ - search: nodeEventsOfTypeHref, - }); - const crumbs = useMemo(() => { - return [ - { - text: eventsString, - ...nodesLinkNavProps, - }, - { - text: processName, - ...nodeDetailLinkNavProps, - }, - { - text: ( - <> - - - ), - ...nodeEventsLinkNavProps, - }, - { - text: ( - <> - - - ), - ...nodeEventsOfTypeLinkNavProps, - }, - { - text: relatedEventToShowDetailsFor ? ( - - ) : ( - naString - ), - onClick: () => {}, - }, - ]; - }, [ - processName, - eventsString, - totalCount, - countBySameCategory, - naString, - relatedEventCategory, - relatedEventToShowDetailsFor, - subject, - descriptor, - nodeEventsOfTypeLinkNavProps, - nodeEventsLinkNavProps, - nodeDetailLinkNavProps, - nodesLinkNavProps, - ]); - - if (!relatedsReady) { - return ; - } - - /** - * Could happen if user e.g. loads a URL with a bad crumbEvent - */ - if (!relatedEventToShowDetailsFor) { - const errString = i18n.translate( - 'xpack.securitySolution.endpoint.resolver.panel.relatedDetail.missing', - { - defaultMessage: 'Related event not found.', - } - ); - return ; - } + }, [event]); return ( - + @@ -288,23 +116,49 @@ export const EventDetail = memo(function ({ - + - {sections.map(({ sectionTitle, entries }, index) => { - const displayEntries = entriesForDisplay(entries); + + + ); +}); + +function EventDetailFields({ event }: { event: SafeResolverEvent }) { + const sections = useMemo(() => { + const returnValue: Array<{ + namespace: React.ReactNode; + descriptions: Array<{ title: React.ReactNode; description: React.ReactNode }>; + }> = []; + for (const [key, value] of Object.entries(event)) { + // ignore these keys + if (key === 'agent' || key === 'ecs' || key === 'process' || key === '@timestamp') { + continue; + } + + const section = { + // Group the fields by their top-level namespace + namespace: {key}, + descriptions: deepObjectEntries(value).map(([path, fieldValue]) => ({ + title: {path.join('.')}, + description: {String(fieldValue)}, + })), + }; + returnValue.push(section); + } + return returnValue; + }, [event]); + return ( + <> + {sections.map(({ namespace, descriptions }, index) => { return ( {index === 0 ? null : } - {sectionTitle} + {namespace} @@ -315,12 +169,136 @@ export const EventDetail = memo(function ({ align="left" titleProps={{ className: 'desc-title' }} compressed - listItems={displayEntries} + listItems={descriptions} /> {index === sections.length - 1 ? null : } ); })} - + + ); +} + +function EventDetailBreadcrumbs({ + nodeID, + nodeName, + event, + breadcrumbEventCategory, +}: { + nodeID: string; + nodeName?: string; + event: SafeResolverEvent; + breadcrumbEventCategory: string; +}) { + const countByCategory = useSelector((state: ResolverState) => + selectors.relatedEventCountByType(state)(nodeID, breadcrumbEventCategory) + ); + const relatedEventCount: number | undefined = useSelector((state: ResolverState) => + selectors.relatedEventTotalCount(state)(nodeID) + ); + const nodesLinkNavProps = useLinkProps({ + panelView: 'nodes', + }); + + const nodeDetailLinkNavProps = useLinkProps({ + panelView: 'nodeDetail', + panelParameters: { nodeID }, + }); + + const nodeEventsLinkNavProps = useLinkProps({ + panelView: 'nodeEvents', + panelParameters: { nodeID }, + }); + + const nodeEventsOfTypeLinkNavProps = useLinkProps({ + panelView: 'nodeEventsOfType', + panelParameters: { nodeID, eventType: breadcrumbEventCategory }, + }); + const breadcrumbs = useMemo(() => { + return [ + { + text: i18n.translate( + 'xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.events', + { + defaultMessage: 'Events', + } + ), + ...nodesLinkNavProps, + }, + { + text: nodeName, + ...nodeDetailLinkNavProps, + }, + { + text: ( + + ), + ...nodeEventsLinkNavProps, + }, + { + text: ( + + ), + ...nodeEventsOfTypeLinkNavProps, + }, + { + text: , + }, + ]; + }, [ + breadcrumbEventCategory, + countByCategory, + event, + nodeDetailLinkNavProps, + nodeEventsLinkNavProps, + nodeName, + relatedEventCount, + nodesLinkNavProps, + nodeEventsOfTypeLinkNavProps, + ]); + return ; +} + +const StyledDescriptionList = memo(styled(EuiDescriptionList)` + &.euiDescriptionList.euiDescriptionList--column dt.euiDescriptionList__title.desc-title { + max-width: 8em; + overflow-wrap: break-word; + } + &.euiDescriptionList.euiDescriptionList--column dd.euiDescriptionList__description { + max-width: calc(100% - 8.5em); + overflow-wrap: break-word; + } +`); + +// Also prevents horizontal scrollbars on long descriptive names +const StyledDescriptiveName = memo(styled(EuiText)` + padding-right: 1em; + overflow-wrap: break-word; +`); + +const StyledFlexTitle = memo(styled('h3')` + display: flex; + flex-flow: row; + font-size: 1.2em; +`); +const StyledTitleRule = memo(styled('hr')` + &.euiHorizontalRule.euiHorizontalRule--full.euiHorizontalRule--marginSmall.override { + display: block; + flex: 1; + margin-left: 0.5em; + } +`); + +const TitleHr = memo(() => { + return ( + ); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx index da5cb1acfed6d..df9cbe9ced541 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/index.tsx @@ -11,16 +11,13 @@ import { useSelector } from 'react-redux'; import * as selectors from '../../store/selectors'; import { NodeEventsOfType } from './node_events_of_type'; import { NodeEvents } from './node_events'; -import { NodeDetail } from './node_details'; +import { NodeDetail } from './node_detail'; import { NodeList } from './node_list'; import { EventDetail } from './event_detail'; import { PanelViewAndParameters } from '../../types'; /** - * - * This component implements the strategy laid out above by determining the "right" view and doing some other housekeeping e.g. effects to keep the UI-selected node in line with what's indicated by the URL parameters. - * - * @returns {JSX.Element} The "right" table content to show based on the query params as described above + * Show the panel that matches the `panelViewAndParameters` (derived from the browser's location.search) */ export const PanelRouter = memo(function () { const params: PanelViewAndParameters = useSelector(selectors.panelViewAndParameters); @@ -40,6 +37,7 @@ export const PanelRouter = memo(function () { ); } else { diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_details.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx similarity index 74% rename from x-pack/plugins/security_solution/public/resolver/view/panels/node_details.tsx rename to x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx index 48d5089eb5641..04e9de61f6256 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_details.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_detail.tsx @@ -15,23 +15,17 @@ import styled from 'styled-components'; import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list'; import { StyledDescriptionList, StyledTitle } from './styles'; import * as selectors from '../../store/selectors'; -import * as event from '../../../../common/endpoint/models/event'; -import { formatDate, StyledBreadcrumbs, GeneratedText } from './panel_content_utilities'; -import { - processPath, - processPid, - userInfoForProcess, - processParentPid, - md5HashForProcess, - argsForProcess, -} from '../../models/process_event'; +import * as eventModel from '../../../../common/endpoint/models/event'; +import { formatDate, GeneratedText } from './panel_content_utilities'; +import { Breadcrumbs } from './breadcrumbs'; +import { processPath, processPID } from '../../models/process_event'; import { CubeForProcess } from './cube_for_process'; -import { ResolverEvent } from '../../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../../common/endpoint/types'; import { useCubeAssets } from '../use_cube_assets'; import { ResolverState } from '../../types'; import { PanelLoading } from './panel_loading'; import { StyledPanel } from '../styles'; -import { useNavigateOrReplace } from '../use_navigate_or_replace'; +import { useLinkProps } from '../use_link_props'; const StyledCubeForProcess = styled(CubeForProcess)` position: relative; @@ -44,7 +38,11 @@ export const NodeDetail = memo(function ({ nodeID }: { nodeID: string }) { ); return ( - {processEvent === null ? : } + {processEvent === null ? ( + + ) : ( + + )} ); }); @@ -53,21 +51,22 @@ export const NodeDetail = memo(function ({ nodeID }: { nodeID: string }) { * A description list view of all the Metadata that goes with a particular process event, like: * Created, PID, User/Domain, etc. */ -const NodeDetailView = memo(function NodeDetailView({ +const NodeDetailView = memo(function ({ processEvent, + nodeID, }: { - processEvent: ResolverEvent; + processEvent: SafeResolverEvent; + nodeID: string; }) { - const processName = event.eventName(processEvent); - const entityId = event.entityId(processEvent); + const processName = eventModel.processNameSafeVersion(processEvent); const isProcessTerminated = useSelector((state: ResolverState) => - selectors.isProcessTerminated(state)(entityId) + selectors.isProcessTerminated(state)(nodeID) ); const relatedEventTotal = useSelector((state: ResolverState) => { - return selectors.relatedEventAggregateTotalByEntityId(state)(entityId); + return selectors.relatedEventTotalCount(state)(nodeID); }); const processInfoEntry: EuiDescriptionListProps['listItems'] = useMemo(() => { - const eventTime = event.eventTimestamp(processEvent); + const eventTime = eventModel.eventTimestamp(processEvent); const dateTime = eventTime === undefined ? null : formatDate(eventTime); const createdEntry = { @@ -82,32 +81,32 @@ const NodeDetailView = memo(function NodeDetailView({ const pidEntry = { title: 'process.pid', - description: processPid(processEvent), + description: processPID(processEvent), }; const userEntry = { title: 'user.name', - description: userInfoForProcess(processEvent)?.name, + description: eventModel.userName(processEvent), }; const domainEntry = { title: 'user.domain', - description: userInfoForProcess(processEvent)?.domain, + description: eventModel.userDomain(processEvent), }; const parentPidEntry = { title: 'process.parent.pid', - description: processParentPid(processEvent), + description: eventModel.parentPID(processEvent), }; const md5Entry = { title: 'process.hash.md5', - description: md5HashForProcess(processEvent), + description: eventModel.md5HashForProcess(processEvent), }; const commandLineEntry = { title: 'process.args', - description: argsForProcess(processEvent), + description: eventModel.argsForProcess(processEvent), }; // This is the data in {title, description} form for the EuiDescriptionList to display @@ -134,12 +133,8 @@ const NodeDetailView = memo(function NodeDetailView({ return processDescriptionListData; }, [processEvent]); - const nodesHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ panelView: 'nodes' }) - ); - - const nodesLinkNavProps = useNavigateOrReplace({ - search: nodesHref, + const nodesLinkNavProps = useLinkProps({ + panelView: 'nodes', }); const crumbs = useMemo(() => { @@ -162,27 +157,20 @@ const NodeDetailView = memo(function NodeDetailView({ defaultMessage="Details for: {processName}" /> ), - onClick: () => {}, }, ]; }, [processName, nodesLinkNavProps]); const { descriptionText } = useCubeAssets(isProcessTerminated, false); - const nodeDetailHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ - panelView: 'nodeEvents', - panelParameters: { nodeID: entityId }, - }) - ); - - const nodeDetailNavProps = useNavigateOrReplace({ - search: nodeDetailHref!, + const nodeDetailNavProps = useLinkProps({ + panelView: 'nodeEvents', + panelParameters: { nodeID }, }); const titleID = useMemo(() => htmlIdGenerator('resolverTable')(), []); return ( <> - + @@ -201,7 +189,7 @@ const NodeDetailView = memo(function NodeDetailView({ - + @@ -26,11 +28,21 @@ export function NodeEvents({ nodeID }: { nodeID: string }) { selectors.relatedEventsStats(state)(nodeID) ); if (processEvent === null || relatedEventsStats === undefined) { - return ; + return ( + + + + ); } else { return ( - + + + ); } @@ -47,120 +59,29 @@ export function NodeEvents({ nodeID }: { nodeID: string }) { * | 2 | Network | * */ -const EventCountsForProcess = memo(function EventCountsForProcess({ - processEvent, +const EventCategoryLinks = memo(function ({ + nodeID, relatedStats, }: { - processEvent: ResolverEvent; + nodeID: string; relatedStats: ResolverNodeStats; }) { interface EventCountsTableView { - name: string; + eventType: string; count: number; } - const relatedEventsState = { stats: relatedStats.events.byCategory }; - const processName = processEvent && event.eventName(processEvent); - const processEntityId = event.entityId(processEvent); - /** - * totalCount: This will reflect the aggregated total by category for all related events - * e.g. [dns,file],[dns,file],[registry] will have an aggregate total of 5. This is to keep the - * total number consistent with the "broken out" totals we see elsewhere in the app. - * E.g. on the rleated list by type, the above would show as: - * 2 dns - * 2 file - * 1 registry - * So it would be extremely disorienting to show the user a "3" above that as a total. - */ - const totalCount = Object.values(relatedStats.events.byCategory).reduce( - (sum, val) => sum + val, - 0 - ); - const eventsString = i18n.translate( - 'xpack.securitySolution.endpoint.resolver.panel.processEventCounts.events', - { - defaultMessage: 'Events', - } - ); - const eventsHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ panelView: 'nodes' }) - ); - - const eventLinkNavProps = useNavigateOrReplace({ - search: eventsHref, - }); - - const processDetailHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ - panelView: 'nodeDetail', - panelParameters: { nodeID: processEntityId }, - }) - ); - - const processDetailNavProps = useNavigateOrReplace({ - search: processDetailHref, - }); - - const nodeDetailHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ - panelView: 'nodeEvents', - panelParameters: { nodeID: processEntityId }, - }) - ); - - const nodeDetailNavProps = useNavigateOrReplace({ - search: nodeDetailHref!, - }); - const crumbs = useMemo(() => { - return [ - { - text: eventsString, - ...eventLinkNavProps, - }, - { - text: processName, - ...processDetailNavProps, - }, - { - text: ( - - ), - ...nodeDetailNavProps, - }, - ]; - }, [ - processName, - totalCount, - eventsString, - eventLinkNavProps, - nodeDetailNavProps, - processDetailNavProps, - ]); const rows = useMemo(() => { - return Object.entries(relatedEventsState.stats).map( + return Object.entries(relatedStats.events.byCategory).map( ([eventType, count]): EventCountsTableView => { return { - name: eventType, + eventType, count, }; } ); - }, [relatedEventsState]); - - const eventDetailHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ - panelView: 'eventDetail', - panelParameters: { nodeID: processEntityId, eventType: name, eventID: processEntityId }, - }) - ); + }, [relatedStats.events.byCategory]); - const eventDetailNavProps = useNavigateOrReplace({ - search: eventDetailHref, - }); const columns = useMemo>>( () => [ { @@ -168,29 +89,100 @@ const EventCountsForProcess = memo(function EventCountsForProcess({ name: i18n.translate('xpack.securitySolution.endpoint.resolver.panel.table.row.count', { defaultMessage: 'Count', }), + 'data-test-subj': 'resolver:panel:node-events:event-type-count', width: '20%', sortable: true, }, { - field: 'name', + field: 'eventType', name: i18n.translate('xpack.securitySolution.endpoint.resolver.panel.table.row.eventType', { defaultMessage: 'Event Type', }), width: '80%', sortable: true, - render(name: string) { - return {name}; + render(eventType: string) { + return ( + + {eventType} + + ); }, }, ], - [eventDetailNavProps] + [nodeID] ); + return items={rows} columns={columns} sorting />; +}); + +const NodeEventsBreadcrumbs = memo(function ({ + nodeID, + nodeName, + totalEventCount, +}: { + nodeID: string; + nodeName: React.ReactNode; + totalEventCount: number; +}) { return ( - <> - - - items={rows} columns={columns} sorting /> - + + ), + ...useLinkProps({ + panelView: 'nodeEvents', + panelParameters: { nodeID }, + }), + }, + ]} + /> ); }); -EventCountsForProcess.displayName = 'EventCountsForProcess'; + +const NodeEventsLink = memo( + ({ + nodeID, + eventType, + children, + }: { + nodeID: string; + eventType: string; + children: React.ReactNode; + }) => { + const props = useLinkProps({ + panelView: 'nodeEventsOfType', + panelParameters: { + nodeID, + eventType, + }, + }); + return ( + + {children} + + ); + } +); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx index afff8d4b75c15..281794ac24d24 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_events_of_type.tsx @@ -4,297 +4,225 @@ * you may not use this file except in compliance with the Elastic License. */ -/* eslint-disable react/display-name */ - -import React, { memo, useMemo, useEffect, Fragment } from 'react'; +import React, { memo, useCallback, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiText, EuiButtonEmpty, EuiHorizontalRule } from '@elastic/eui'; import { useSelector } from 'react-redux'; import { FormattedMessage } from 'react-intl'; -import styled from 'styled-components'; import { StyledPanel } from '../styles'; -import { formatDate, StyledBreadcrumbs, BoldCode, StyledTime } from './panel_content_utilities'; -import * as event from '../../../../common/endpoint/models/event'; -import { ResolverEvent, ResolverNodeStats } from '../../../../common/endpoint/types'; +import { formatDate, BoldCode, StyledTime } from './panel_content_utilities'; +import { Breadcrumbs } from './breadcrumbs'; +import * as eventModel from '../../../../common/endpoint/models/event'; +import { SafeResolverEvent } from '../../../../common/endpoint/types'; import * as selectors from '../../store/selectors'; -import { useResolverDispatch } from '../use_resolver_dispatch'; -import { RelatedEventLimitWarning } from '../limit_warnings'; import { ResolverState } from '../../types'; -import { useNavigateOrReplace } from '../use_navigate_or_replace'; -import { useRelatedEventDetailNavigation } from '../use_related_event_detail_navigation'; import { PanelLoading } from './panel_loading'; +import { DescriptiveName } from './descriptive_name'; +import { useLinkProps } from '../use_link_props'; /** - * This view presents a list of related events of a given type for a given process. - * It will appear like: - * - * | | - * | :----------------------------------------------------- | - * | **registry deletion** @ *3:32PM..* *HKLM/software...* | - * | **file creation** @ *3:34PM..* *C:/directory/file.exe* | + * Render a list of events that are related to `nodeID` and that have a category of `eventType`. */ - -interface MatchingEventEntry { - formattedDate: string; - eventType: string; - eventCategory: string; - name: { subject: string; descriptor?: string }; - setQueryParams: () => void; -} - -const StyledRelatedLimitWarning = styled(RelatedEventLimitWarning)` - flex-flow: row wrap; - display: block; - align-items: baseline; - margin-top: 1em; - - & .euiCallOutHeader { - display: inline; - margin-right: 0.25em; - } - - & .euiText { - display: inline; - } - - & .euiText p { - display: inline; - } -`; - -const NodeCategoryEntries = memo(function ({ - crumbs, - matchingEventEntries, +export const NodeEventsOfType = memo(function NodeEventsOfType({ + nodeID, eventType, - processEntityId, }: { - crumbs: Array<{ - text: string | JSX.Element | null; - onClick: (event: React.MouseEvent) => void; - href?: string; - }>; - matchingEventEntries: MatchingEventEntry[]; + nodeID: string; eventType: string; - processEntityId: string; }) { - const relatedLookupsByCategory = useSelector(selectors.relatedEventInfoByEntityId); - const lookupsForThisNode = relatedLookupsByCategory(processEntityId); - const shouldShowLimitWarning = lookupsForThisNode?.shouldShowLimitForCategory(eventType); - const numberDisplayed = lookupsForThisNode?.numberActuallyDisplayedForCategory(eventType); - const numberMissing = lookupsForThisNode?.numberNotDisplayedForCategory(eventType); - - return ( - <> - - {shouldShowLimitWarning && typeof numberDisplayed !== 'undefined' && numberMissing ? ( - - ) : null} - - <> - {matchingEventEntries.map((eventView, index) => { - const { subject, descriptor = '' } = eventView.name; - return ( - - - - - - - - - - - - - - {index === matchingEventEntries.length - 1 ? null : } - - ); - })} - - - ); -}); - -export function NodeEventsOfType({ nodeID, eventType }: { nodeID: string; eventType: string }) { const processEvent = useSelector((state: ResolverState) => selectors.processEventForID(state)(nodeID) ); - const relatedEventsStats = useSelector((state: ResolverState) => - selectors.relatedEventsStats(state)(nodeID) + const eventCount = useSelector( + (state: ResolverState) => selectors.relatedEventsStats(state)(nodeID)?.events.total + ); + const eventsInCategoryCount = useSelector( + (state: ResolverState) => + selectors.relatedEventsStats(state)(nodeID)?.events.byCategory[eventType] + ); + const events = useSelector( + useCallback( + (state: ResolverState) => { + return selectors.relatedEventsByCategory(state)(nodeID, eventType); + }, + [eventType, nodeID] + ) ); return ( - + {eventCount === undefined || processEvent === null ? ( + + ) : ( + <> + + + + + )} ); -} +}); -const NodeEventList = memo(function ({ - processEvent, +/** + * Rendered for each event in the list. + */ +const NodeEventsListItem = memo(function ({ + event, + nodeID, eventType, - relatedStats, }: { - processEvent: ResolverEvent | null; + event: SafeResolverEvent; + nodeID: string; eventType: string; - relatedStats: ResolverNodeStats | undefined; }) { - const processName = processEvent && event.eventName(processEvent); - const processEntityId = processEvent ? event.entityId(processEvent) : ''; - const nodesHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ panelView: 'nodes' }) - ); - - const nodesLinkNavProps = useNavigateOrReplace({ - search: nodesHref, + const timestamp = eventModel.eventTimestamp(event); + const date = timestamp !== undefined ? formatDate(timestamp) : timestamp; + const linkProps = useLinkProps({ + panelView: 'eventDetail', + panelParameters: { + nodeID, + eventType, + eventID: String(eventModel.eventID(event)), + }, }); - const totalCount = relatedStats - ? Object.values(relatedStats.events.byCategory).reduce((sum, val) => sum + val, 0) - : 0; - const eventsString = i18n.translate( - 'xpack.securitySolution.endpoint.resolver.panel.processEventListByType.events', - { - defaultMessage: 'Events', - } + return ( + <> + + + + + + + + + + + + + ); +}); - const relatedsReadyMap = useSelector(selectors.relatedEventsReady); - const relatedsReady = processEntityId && relatedsReadyMap.get(processEntityId); - - const dispatch = useResolverDispatch(); - - useEffect(() => { - if (typeof relatedsReady === 'undefined') { - dispatch({ - type: 'appDetectedMissingEventData', - payload: processEntityId, - }); - } - }, [relatedsReady, dispatch, processEntityId]); - - const relatedByCategory = useSelector(selectors.relatedEventsByCategory); - const eventsForCurrentCategory = relatedByCategory(processEntityId)(eventType); - const relatedEventDetailNavigation = useRelatedEventDetailNavigation({ - nodeID: processEntityId, - category: eventType, - events: eventsForCurrentCategory, - }); - +/** + * Renders a list of events with a separator in between. + */ +const NodeEventList = memo(function NodeEventList({ + eventType, + events, + nodeID, +}: { + eventType: string; /** - * A list entry will be displayed for each of these + * The events to list. */ - const matchingEventEntries: MatchingEventEntry[] = useMemo(() => { - return eventsForCurrentCategory.map((resolverEvent) => { - const eventTime = event.eventTimestamp(resolverEvent); - const formattedDate = typeof eventTime === 'undefined' ? '' : formatDate(eventTime); - const entityId = event.eventId(resolverEvent); - return { - formattedDate, - eventCategory: `${eventType}`, - eventType: `${event.ecsEventType(resolverEvent)}`, - name: event.descriptiveName(resolverEvent), - setQueryParams: () => relatedEventDetailNavigation(entityId), - }; - }); - }, [eventType, eventsForCurrentCategory, relatedEventDetailNavigation]); - - const nodeDetailHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ - panelView: 'nodeDetail', - panelParameters: { nodeID: processEntityId }, - }) + events: SafeResolverEvent[]; + nodeID: string; +}) { + return ( + <> + {events.map((event, index) => ( + + + {index === events.length - 1 ? null : } + + ))} + ); +}); - const nodeDetailNavProps = useNavigateOrReplace({ - search: nodeDetailHref, +/** + * Renders `Breadcrumbs`. + */ +const NodeEventsOfTypeBreadcrumbs = memo(function ({ + nodeName, + eventType, + eventCount, + nodeID, + /** + * The count of events in the category that this list is showing. + */ + eventsInCategoryCount, +}: { + nodeName: React.ReactNode; + eventType: string; + /** + * The events to list. + */ + eventCount: number; + nodeID: string; + /** + * The count of events in the category that this list is showing. + */ + eventsInCategoryCount: number | undefined; +}) { + const nodesLinkNavProps = useLinkProps({ + panelView: 'nodes', }); - const nodeEventsHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ - panelView: 'nodeEvents', - panelParameters: { nodeID: processEntityId }, - }) - ); - - const nodeEventsNavProps = useNavigateOrReplace({ - search: nodeEventsHref, + const nodeDetailNavProps = useLinkProps({ + panelView: 'nodeDetail', + panelParameters: { nodeID }, }); - const crumbs = useMemo(() => { - return [ - { - text: eventsString, - ...nodesLinkNavProps, - }, - { - text: processName, - ...nodeDetailNavProps, - }, - { - text: ( - - ), - ...nodeEventsNavProps, - }, - { - text: ( - - ), - onClick: () => {}, - }, - ]; - }, [ - eventType, - eventsString, - matchingEventEntries.length, - processName, - totalCount, - nodeDetailNavProps, - nodesLinkNavProps, - nodeEventsNavProps, - ]); - if (!relatedsReady) { - return ; - } + const nodeEventsNavProps = useLinkProps({ + panelView: 'nodeEvents', + panelParameters: { nodeID }, + }); return ( - + ), + ...nodeEventsNavProps, + }, + { + text: ( + + ), + }, + ]} /> ); }); diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx index 6113cea4c4edc..8fc6e7cc66c79 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/node_list.tsx @@ -4,9 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable @elastic/eui/href-or-on-click */ + +/* eslint-disable no-duplicate-imports */ + +import { useDispatch } from 'react-redux'; + /* eslint-disable react/display-name */ -import React, { memo, useMemo } from 'react'; +import React, { memo, useMemo, useCallback, useContext } from 'react'; import { EuiBasicTableColumn, EuiBadge, @@ -16,71 +22,31 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useSelector } from 'react-redux'; -import styled from 'styled-components'; +import { SideEffectContext } from '../side_effect_context'; import { StyledPanel } from '../styles'; -import * as event from '../../../../common/endpoint/models/event'; +import { + StyledLabelTitle, + StyledAnalyzedEvent, + StyledLabelContainer, + StyledButtonTextContainer, +} from './styles'; +import * as eventModel from '../../../../common/endpoint/models/event'; import * as selectors from '../../store/selectors'; -import { formatter, StyledBreadcrumbs } from './panel_content_utilities'; +import { formatter } from './panel_content_utilities'; +import { Breadcrumbs } from './breadcrumbs'; import { CubeForProcess } from './cube_for_process'; -import { SafeResolverEvent } from '../../../../common/endpoint/types'; import { LimitWarning } from '../limit_warnings'; import { ResolverState } from '../../types'; -import { useNavigateOrReplace } from '../use_navigate_or_replace'; +import { useLinkProps } from '../use_link_props'; import { useColors } from '../use_colors'; - -const StyledLimitWarning = styled(LimitWarning)` - flex-flow: row wrap; - display: block; - align-items: baseline; - margin-top: 1em; - - & .euiCallOutHeader { - display: inline; - margin-right: 0.25em; - } - - & .euiText { - display: inline; - } - - & .euiText p { - display: inline; - } -`; - -const StyledButtonTextContainer = styled.div` - align-items: center; - display: flex; - flex-direction: row; -`; - -const StyledAnalyzedEvent = styled.div` - color: ${(props) => props.color}; - font-size: 10.5px; - font-weight: 700; -`; - -const StyledLabelTitle = styled.div``; - -const StyledLabelContainer = styled.div` - display: inline-block; - flex: 3; - min-width: 0; - - ${StyledAnalyzedEvent}, - ${StyledLabelTitle} { - overflow: hidden; - text-align: left; - text-overflow: ellipsis; - white-space: nowrap; - } -`; +import { SafeResolverEvent } from '../../../../common/endpoint/types'; +import { ResolverAction } from '../../store/actions'; interface ProcessTableView { name?: string; timestamp?: Date; + nodeID: string; event: SafeResolverEvent; - href: string | undefined; } /** @@ -99,8 +65,8 @@ export const NodeList = memo(() => { ), sortable: true, truncateText: true, - render(name: string, item: ProcessTableView) { - return ; + render(name: string | undefined, item: ProcessTableView) { + return ; }, }, { @@ -132,42 +98,26 @@ export const NodeList = memo(() => { [] ); - const { processNodePositions } = useSelector(selectors.layout); - const nodeHrefs: Map = useSelector( - (state: ResolverState) => { - const relativeHref = selectors.relativeHref(state); - return new Map( - [...processNodePositions.keys()].map((processEvent) => { - const nodeID = event.entityIDSafeVersion(processEvent); - if (nodeID === undefined) { - return [processEvent, null]; - } - return [ - processEvent, - relativeHref({ - panelView: 'nodeDetail', - panelParameters: { - nodeID, - }, - }), - ]; - }) - ); - } - ); - const processTableView: ProcessTableView[] = useMemo( - () => - [...processNodePositions.keys()].map((processEvent) => { - const name = event.processNameSafeVersion(processEvent); - return { - name, - timestamp: event.timestampAsDateSafeVersion(processEvent), - event: processEvent, - href: nodeHrefs.get(processEvent) ?? undefined, - }; - }), - [processNodePositions, nodeHrefs] + const processTableView: ProcessTableView[] = useSelector( + useCallback((state: ResolverState) => { + const { processNodePositions } = selectors.layout(state); + const view: ProcessTableView[] = []; + for (const processEvent of processNodePositions.keys()) { + const name = eventModel.processNameSafeVersion(processEvent); + const nodeID = eventModel.entityIDSafeVersion(processEvent); + if (nodeID !== undefined) { + view.push({ + name, + timestamp: eventModel.timestampAsDateSafeVersion(processEvent), + nodeID, + event: processEvent, + }); + } + } + return view; + }, []) ); + const numberOfProcesses = processTableView.length; const crumbs = useMemo(() => { @@ -176,7 +126,6 @@ export const NodeList = memo(() => { text: i18n.translate('xpack.securitySolution.resolver.panel.nodeList.title', { defaultMessage: 'All Process Events', }), - onClick: () => {}, }, ]; }, []); @@ -187,8 +136,8 @@ export const NodeList = memo(() => { const rowProps = useMemo(() => ({ 'data-test-subj': 'resolver:node-list:item' }), []); return ( - - {showWarning && } + + {showWarning && } rowProps={rowProps} @@ -201,16 +150,40 @@ export const NodeList = memo(() => { ); }); -function NodeDetailLink({ name, item }: { name: string; item: ProcessTableView }) { - const entityID = event.entityIDSafeVersion(item.event); - const originID = useSelector(selectors.originID); - const isOrigin = originID === entityID; +function NodeDetailLink({ + name, + nodeID, + event, +}: { + name?: string; + nodeID: string; + event: SafeResolverEvent; +}) { + const isOrigin = useSelector((state: ResolverState) => { + return selectors.originID(state) === nodeID; + }); const isTerminated = useSelector((state: ResolverState) => - entityID === undefined ? false : selectors.isProcessTerminated(state)(entityID) + nodeID === undefined ? false : selectors.isProcessTerminated(state)(nodeID) ); const { descriptionText } = useColors(); + const linkProps = useLinkProps({ panelView: 'nodeDetail', panelParameters: { nodeID } }); + const dispatch: (action: ResolverAction) => void = useDispatch(); + const { timestamp } = useContext(SideEffectContext); + const handleOnClick = useCallback( + (mouseEvent: React.MouseEvent) => { + linkProps.onClick(mouseEvent); + dispatch({ + type: 'userBroughtProcessIntoView', + payload: { + process: event, + time: timestamp(), + }, + }); + }, + [timestamp, linkProps, dispatch, event] + ); return ( - + {name === '' ? ( {i18n.translate( diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_error.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_error.tsx index 3b10a8db2bf12..199758145f117 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_error.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_error.tsx @@ -7,11 +7,8 @@ import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiText, EuiButtonEmpty } from '@elastic/eui'; import React, { memo, useMemo } from 'react'; -import { useSelector } from 'react-redux'; -import { useNavigateOrReplace } from '../use_navigate_or_replace'; -import * as selectors from '../../store/selectors'; -import { ResolverState } from '../../types'; -import { StyledBreadcrumbs } from './panel_content_utilities'; +import { Breadcrumbs } from './breadcrumbs'; +import { useLinkProps } from '../use_link_props'; /** * Display an error in the panel when something goes wrong and give the user a way to "retreat" back to a default state. @@ -24,12 +21,10 @@ export const PanelContentError = memo(function ({ }: { translatedErrorMessage: string; }) { - const nodesHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ panelView: 'nodes' }) - ); - const nodesLinkNavProps = useNavigateOrReplace({ - search: nodesHref, + const nodesLinkNavProps = useLinkProps({ + panelView: 'nodes', }); + const crumbs = useMemo(() => { return [ { @@ -42,13 +37,12 @@ export const PanelContentError = memo(function ({ text: i18n.translate('xpack.securitySolution.endpoint.resolver.panel.error.error', { defaultMessage: 'Error', }), - onClick: () => {}, }, ]; }, [nodesLinkNavProps]); return ( <> - + {translatedErrorMessage} @@ -60,4 +54,3 @@ export const PanelContentError = memo(function ({ ); }); -PanelContentError.displayName = 'TableServiceError'; diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx index a7d76277c6ab1..5ca34b33b2396 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx @@ -7,10 +7,9 @@ /* eslint-disable react/display-name */ import { i18n } from '@kbn/i18n'; -import { EuiBreadcrumbs, EuiCode, EuiBetaBadge } from '@elastic/eui'; +import { EuiCode } from '@elastic/eui'; import styled from 'styled-components'; import React, { memo } from 'react'; -import { useColors } from '../use_colors'; /** * A bold version of EuiCode to display certain titles with @@ -21,30 +20,6 @@ export const BoldCode = styled(EuiCode)` } `; -const BetaHeader = styled(`header`)` - margin-bottom: 1em; -`; - -const ThemedBreadcrumbs = styled(EuiBreadcrumbs)<{ background: string; text: string }>` - &.euiBreadcrumbs { - background-color: ${(props) => props.background}; - color: ${(props) => props.text}; - padding: 1em; - border-radius: 5px; - } - - & .euiBreadcrumbSeparator { - background: ${(props) => props.text}; - } -`; - -const betaBadgeLabel = i18n.translate( - 'xpack.securitySolution.enpdoint.resolver.panelutils.betaBadgeLabel', - { - defaultMessage: 'BETA', - } -); - /** * A component that renders an element with breaking opportunities (``s) * spliced into text children at word boundaries. @@ -85,31 +60,6 @@ export const StyledTime = memo(styled('time')` text-align: start; `); -type Breadcrumbs = Parameters[0]['breadcrumbs']; -/** - * Breadcrumb menu with adjustments per direction from UX team - */ -export const StyledBreadcrumbs = memo(function StyledBreadcrumbs({ - breadcrumbs, -}: { - breadcrumbs: Breadcrumbs; -}) { - const { resolverBreadcrumbBackground, resolverEdgeText } = useColors(); - return ( - <> - - - - - - ); -}); - /** * Long formatter (to second) for DateTime */ @@ -122,12 +72,6 @@ export const formatter = new Intl.DateTimeFormat(i18n.getLocale(), { second: '2-digit', }); -const invalidDateText = i18n.translate( - 'xpack.securitySolution.enpdoint.resolver.panelutils.invaliddate', - { - defaultMessage: 'Invalid Date', - } -); /** * @returns {string} A nicely formatted string for a date */ @@ -140,6 +84,8 @@ export function formatDate( if (isFinite(date.getTime())) { return formatter.format(date); } else { - return invalidDateText; + return i18n.translate('xpack.securitySolution.enpdoint.resolver.panelutils.invaliddate', { + defaultMessage: 'Invalid Date', + }); } } diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_loading.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_loading.tsx index 864990e4d96ab..2de0bf5d320ea 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_loading.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_loading.tsx @@ -5,13 +5,10 @@ */ import React, { useMemo } from 'react'; -import { useSelector } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiTitle } from '@elastic/eui'; -import * as selectors from '../../store/selectors'; -import { StyledBreadcrumbs } from './panel_content_utilities'; -import { useNavigateOrReplace } from '../use_navigate_or_replace'; -import { ResolverState } from '../../types'; +import { Breadcrumbs } from './breadcrumbs'; +import { useLinkProps } from '../use_link_props'; export function PanelLoading() { const waitingString = i18n.translate( @@ -26,11 +23,8 @@ export function PanelLoading() { defaultMessage: 'Events', } ); - const nodesHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ panelView: 'nodes' }) - ); - const nodesLinkNavProps = useNavigateOrReplace({ - search: nodesHref, + const nodesLinkNavProps = useLinkProps({ + panelView: 'nodes', }); const waitCrumbs = useMemo(() => { return [ @@ -42,7 +36,7 @@ export function PanelLoading() { }, [nodesLinkNavProps, eventsString]); return ( <> - +

{waitingString}

diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/styles.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/styles.tsx index c5d5ae53a5580..03826dd38397b 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/styles.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/styles.tsx @@ -3,15 +3,89 @@ * 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 no-duplicate-imports */ + +import { EuiBreadcrumbs } from '@elastic/eui'; + import styled from 'styled-components'; import { EuiDescriptionList } from '@elastic/eui'; +/** + * Used by the nodeDetail view to show attributes of the related events. + */ export const StyledDescriptionList = styled(EuiDescriptionList)` &.euiDescriptionList.euiDescriptionList--column dt.euiDescriptionList__title.desc-title { max-width: 10em; } `; +/** + * Used by the nodeDetail view for the label of the node. + */ export const StyledTitle = styled('h4')` overflow-wrap: break-word; `; + +/** + * Used for a 'BETA' badge in the breadcrumbs of each panel. + */ +export const BetaHeader = styled(`header`)` + margin-bottom: 1em; +`; + +/** + * Styled version of EuiBreadcrumbs that is used by the breadcrumbs in each panel. + */ +export const ThemedBreadcrumbs = styled(EuiBreadcrumbs)<{ background: string; text: string }>` + &.euiBreadcrumbs { + background-color: ${(props) => props.background}; + color: ${(props) => props.text}; + padding: 1em; + border-radius: 5px; + } + + & .euiBreadcrumbSeparator { + background: ${(props) => props.text}; + } +`; + +/** + * Used in the links to nodes on the node list panel. + */ +export const StyledButtonTextContainer = styled.div` + align-items: center; + display: flex; + flex-direction: row; +`; + +/** + * Used in the node list panel to call out the event that is represented by the databaseDocumentID. + */ +export const StyledAnalyzedEvent = styled.div` + color: ${(props) => props.color}; + font-size: 10.5px; + font-weight: 700; +`; + +/** + * Used to style the node name in the node list panel view. + */ +export const StyledLabelTitle = styled.div``; + +/** + * Used by the node list view. Wraps the title of the node and the 'Analyzed event' marker. + */ +export const StyledLabelContainer = styled.div` + display: inline-block; + flex: 3; + min-width: 0; + + ${StyledAnalyzedEvent}, + ${StyledLabelTitle} { + overflow: hidden; + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; + } +`; diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 65ec395080f86..4d647760edb9c 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -12,15 +12,15 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { NodeSubMenu } from './submenu'; import { applyMatrix3 } from '../models/vector2'; import { Vector2, Matrix3, ResolverState } from '../types'; -import { ResolverEvent, SafeResolverEvent } from '../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../common/endpoint/types'; import { useResolverDispatch } from './use_resolver_dispatch'; import * as eventModel from '../../../common/endpoint/models/event'; import * as selectors from '../store/selectors'; -import { useNavigateOrReplace } from './use_navigate_or_replace'; import { fontSize } from './font_size'; import { useCubeAssets } from './use_cube_assets'; import { useSymbolIDs } from './use_symbol_ids'; import { useColors } from './use_colors'; +import { useLinkProps } from './use_link_props'; interface StyledActionsContainer { readonly color: string; @@ -192,7 +192,6 @@ const UnstyledProcessEventDot = React.memo( /** * Type in non-SVG components scales as follows: - * (These values were adjusted to match the proportions in the comps provided by UX/Design) * 18.75 : The smallest readable font size at which labels/descriptions can be read. Font size will not scale below this. * 12.5 : A 'slope' at which the font size will scale w.r.t. to zoom level otherwise */ @@ -239,15 +238,10 @@ const UnstyledProcessEventDot = React.memo( const isOrigin = nodeID === originID; const dispatch = useResolverDispatch(); - const processDetailHref = useSelector((state: ResolverState) => - selectors.relativeHref(state)({ - panelView: 'nodeDetail', - panelParameters: { nodeID }, - }) - ); - const processDetailNavProps = useNavigateOrReplace({ - search: processDetailHref, + const processDetailNavProps = useLinkProps({ + panelView: 'nodeDetail', + panelParameters: { nodeID }, }); const handleFocus = useCallback(() => { @@ -272,7 +266,7 @@ const UnstyledProcessEventDot = React.memo( ); const grandTotal: number | null = useSelector((state: ResolverState) => - selectors.relatedEventTotalForProcess(state)(event as ResolverEvent) + selectors.relatedEventTotalForProcess(state)(event) ); /* eslint-disable jsx-a11y/click-events-have-key-events */ @@ -376,12 +370,13 @@ const UnstyledProcessEventDot = React.memo( backgroundColor={colorMap.resolverBackground} color={colorMap.descriptionText} isDisplaying={isShowingDescriptionText} + data-test-subj="resolver:node:description" > diff --git a/x-pack/plugins/security_solution/public/resolver/view/styles.tsx b/x-pack/plugins/security_solution/public/resolver/view/styles.tsx index fb4d4d289d254..7def5d3362d4f 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/styles.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/styles.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiPanel } from '@elastic/eui'; +import { EuiPanel, EuiCallOut } from '@elastic/eui'; import styled from 'styled-components'; @@ -62,3 +62,26 @@ export const GraphContainer = styled.div` flex-grow: 1; contain: layout; `; + +/** + * See `RelatedEventLimitWarning` + */ +export const LimitWarningsEuiCallOut = styled(EuiCallOut)` + flex-flow: row wrap; + display: block; + align-items: baseline; + margin-top: 1em; + + & .euiCallOutHeader { + display: inline; + margin-right: 0.25em; + } + + & .euiText { + display: inline; + } + + & .euiText p { + display: inline; + } +`; diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx index 495cd238d22fc..5406b444cee56 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/use_camera.test.tsx @@ -11,7 +11,7 @@ import { useCamera, useAutoUpdatingClientRect } from './use_camera'; import { Provider } from 'react-redux'; import * as selectors from '../store/selectors'; import { Matrix3, ResolverStore, SideEffectSimulator } from '../types'; -import { ResolverEvent } from '../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../common/endpoint/types'; import { SideEffectContext } from './side_effect_context'; import { applyMatrix3 } from '../models/vector2'; import { sideEffectSimulatorFactory } from './side_effect_simulator_factory'; @@ -33,7 +33,7 @@ describe('useCamera on an unpainted element', () => { beforeEach(async () => { store = createStore(resolverReducer); - const Test = function Test() { + const Test = function () { const camera = useCamera(); const { ref, onMouseDown } = camera; projectionMatrix = camera.projectionMatrix; @@ -160,9 +160,9 @@ describe('useCamera on an unpainted element', () => { expect(simulator.mock.requestAnimationFrame).not.toHaveBeenCalled(); }); describe('when the camera begins animation', () => { - let process: ResolverEvent; + let process: SafeResolverEvent; beforeEach(() => { - const events: ResolverEvent[] = []; + const events: SafeResolverEvent[] = []; const numberOfEvents: number = 10; for (let index = 0; index < numberOfEvents; index++) { @@ -190,9 +190,9 @@ describe('useCamera on an unpainted element', () => { } else { throw new Error('failed to create tree'); } - const processes: ResolverEvent[] = [ + const processes: SafeResolverEvent[] = [ ...selectors.layout(store.getState()).processNodePositions.keys(), - ] as ResolverEvent[]; + ]; process = processes[processes.length - 1]; if (!process) { throw new Error('missing the process to bring into view'); diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_link_props.ts b/x-pack/plugins/security_solution/public/resolver/view/use_link_props.ts new file mode 100644 index 0000000000000..5645edec7e1ca --- /dev/null +++ b/x-pack/plugins/security_solution/public/resolver/view/use_link_props.ts @@ -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 { useSelector } from 'react-redux'; +import { MouseEventHandler } from 'react'; +import { useNavigateOrReplace } from './use_navigate_or_replace'; + +import * as selectors from '../store/selectors'; +import { PanelViewAndParameters, ResolverState } from '../types'; + +type EventHandlerCallback = MouseEventHandler; + +/** + * Get an `onClick` function and an `href` string. Use these as props for `` elements. + * `onClick` will use navigate to the `panelViewAndParameters` using `history.push`. + * the `href` points to `panelViewAndParameters`. + * Existing `search` parameters are maintained. + */ +export function useLinkProps( + panelViewAndParameters: PanelViewAndParameters +): { href: string; onClick: EventHandlerCallback } { + const search = useSelector((state: ResolverState) => + selectors.relativeHref(state)(panelViewAndParameters) + ); + + return useNavigateOrReplace({ + search, + }); +} diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_related_event_by_category_navigation.ts b/x-pack/plugins/security_solution/public/resolver/view/use_related_event_by_category_navigation.ts index f994350132c35..6810837ae031a 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_related_event_by_category_navigation.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/use_related_event_by_category_navigation.ts @@ -12,7 +12,7 @@ import * as selectors from '../store/selectors'; /** * A hook that takes a nodeID and a record of categories, and returns a function that * navigates to the proper url when called with a category. - * @deprecated + * @deprecated See `useLinkProps` */ export function useRelatedEventByCategoryNavigation({ nodeID, diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_related_event_detail_navigation.ts b/x-pack/plugins/security_solution/public/resolver/view/use_related_event_detail_navigation.ts deleted file mode 100644 index 9fc74a7567c47..0000000000000 --- a/x-pack/plugins/security_solution/public/resolver/view/use_related_event_detail_navigation.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { useCallback } from 'react'; -import { useSelector } from 'react-redux'; -import { useHistory } from 'react-router-dom'; -import { ResolverState } from '../types'; -import { ResolverEvent } from '../../../common/endpoint/types'; -import * as selectors from '../store/selectors'; - -/** - * @deprecated - */ -export function useRelatedEventDetailNavigation({ - nodeID, - category, - events, -}: { - nodeID: string; - category: string; - events: ResolverEvent[]; -}) { - const relatedEventDetailUrls = useSelector((state: ResolverState) => - selectors.relatedEventDetailHrefs(state)(category, nodeID, events) - ); - const history = useHistory(); - return useCallback( - (entityID: string | number | undefined) => { - if (entityID !== undefined) { - const urlForEntityID = relatedEventDetailUrls.get(String(entityID)); - if (urlForEntityID !== null && urlForEntityID !== undefined) { - return history.replace({ search: urlForEntityID }); - } - } - }, - [history, relatedEventDetailUrls] - ); -} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx index ea938be91abd1..6e802053ab29f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx @@ -114,7 +114,7 @@ export const ColumnHeadersComponent = ({ timelineId, toggleColumn, }: Props) => { - const [draggingIndex, setDraggingIndex] = useState(null); + const [draggingIndex, setDraggingIndex] = useState(null); const { timelineFullScreen, setTimelineFullScreen, @@ -145,9 +145,7 @@ export const ColumnHeadersComponent = ({ const renderClone: DraggableChildrenFn = useCallback( (dragProvided, _dragSnapshot, rubric) => { - // TODO: Remove after github.com/DefinitelyTyped/DefinitelyTyped/pull/43057 is merged - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const index = (rubric as any).source.index; + const index = rubric.source.index; const header = columnHeaders[index]; const onMount = () => setDraggingIndex(index); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx index 5c992fd640a97..234814a68877d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/styles.tsx @@ -113,10 +113,10 @@ export const EventsThGroupData = styled.div.attrs(({ className = '' }) => ({ } `; -export const EventsTh = styled.div.attrs(({ className = '' }) => ({ +export const EventsTh = styled.div.attrs<{ role: string }>(({ className = '' }) => ({ className: `siemEventsTable__th ${className}`, role: 'columnheader', -}))` +}))<{ role?: string }>` align-items: center; display: flex; flex-shrink: 0; diff --git a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx index af99c75ae701a..23c05805a5aa4 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx @@ -18,6 +18,7 @@ import { TimelineEventsDetailsRequestOptions, TimelineEventsDetailsStrategyResponse, } from '../../../../common/search_strategy'; +import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/plugins/data/public'; export interface EventsArgs { detailsData: TimelineEventsDetailsItem[] | null; } @@ -66,13 +67,13 @@ export const useTimelineEventsDetails = ({ ) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setTimelineDetailsResponse(response.data || []); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index f340096c75f2b..d56a601fda4a3 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -10,7 +10,11 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { ESQuery } from '../../../common/typed_json'; -import { IIndexPattern } from '../../../../../../src/plugins/data/public'; +import { + IIndexPattern, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../src/plugins/data/public'; import { DEFAULT_INDEX_KEY } from '../../../common/constants'; import { inputsModel } from '../../common/store'; @@ -167,7 +171,7 @@ export const useTimelineEvents = ({ }) .subscribe({ next: (response) => { - if (!response.isPartial && !response.isRunning) { + if (isCompleteResponse(response)) { if (!didCancel) { setLoading(false); setTimelineResponse((prevResponse) => ({ @@ -181,7 +185,7 @@ export const useTimelineEvents = ({ })); } searchSubscription$.unsubscribe(); - } else if (response.isPartial && !response.isRunning) { + } else if (isErrorResponse(response)) { if (!didCancel) { setLoading(false); } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts index b796913118c99..5bc911fb075b5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts @@ -9,7 +9,7 @@ import { ILegacyScopedClusterClient } from 'kibana/server'; import { parentEntityIDSafeVersion, entityIDSafeVersion, - getAncestryAsArray, + ancestry, } from '../../../../../common/endpoint/models/event'; import { SafeResolverAncestry, @@ -35,7 +35,8 @@ export class AncestryQueryHandler implements QueryHandler legacyEndpointID: string | undefined, originNode: SafeResolverLifecycleNode | undefined ) { - this.ancestorsToFind = getAncestryAsArray(originNode?.lifecycle[0]).slice(0, levels); + const event = originNode?.lifecycle[0]; + this.ancestorsToFind = (event ? ancestry(event) : []).slice(0, levels); this.query = new LifecycleQuery(indexPattern, legacyEndpointID); // add the origin node to the response if it exists @@ -108,7 +109,7 @@ export class AncestryQueryHandler implements QueryHandler this.levels = this.levels - ancestryNodes.size; // the results come back in ascending order on timestamp so the first entry in the // results should be the further ancestor (most distant grandparent) - this.ancestorsToFind = getAncestryAsArray(results[0]).slice(0, this.levels); + this.ancestorsToFind = ancestry(results[0]).slice(0, this.levels); }; /** diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts index f54472141c1de..1a871891b1ed5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts @@ -4,12 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - parentEntityIDSafeVersion, - isProcessRunning, - getAncestryAsArray, - entityIDSafeVersion, -} from '../../../../../common/endpoint/models/event'; +import * as eventModel from '../../../../../common/endpoint/models/event'; import { SafeResolverChildren, SafeResolverChildNode, @@ -72,7 +67,7 @@ export class ChildrenNodesHelper { */ addLifecycleEvents(lifecycle: SafeResolverEvent[]) { for (const event of lifecycle) { - const entityID = entityIDSafeVersion(event); + const entityID = eventModel.entityIDSafeVersion(event); if (entityID) { const cachedChild = this.getOrCreateChildNode(entityID); cachedChild.lifecycle.push(event); @@ -93,19 +88,19 @@ export class ChildrenNodesHelper { const nonLeafNodes: Set = new Set(); const isDistantGrandchild = (event: ChildEvent) => { - const ancestry = getAncestryAsArray(event); + const ancestry = eventModel.ancestry(event); return ancestry.length > 0 && queriedNodes.has(ancestry[ancestry.length - 1]); }; for (const event of startEvents) { - const parentID = parentEntityIDSafeVersion(event); - const entityID = entityIDSafeVersion(event); - if (parentID && entityID && isProcessRunning(event)) { + const parentID = eventModel.parentEntityIDSafeVersion(event); + const entityID = eventModel.entityIDSafeVersion(event); + if (parentID && entityID && eventModel.isProcessRunning(event)) { // don't actually add the start event to the node, because that'll be done in // a different call const childNode = this.getOrCreateChildNode(entityID); - const ancestry = getAncestryAsArray(event); + const ancestry = eventModel.ancestry(event); // This is to handle the following unlikely but possible scenario: // if an alert was generated by the kernel process (parent process of all other processes) then // the direct children of that process would only have an ancestry array of [parent_kernel], a single value in the array. diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts index ec4d1efb81b11..7cf3d467c10c2 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts @@ -6,6 +6,7 @@ import { RequestHandler, RequestHandlerContext } from 'kibana/server'; import { + DeleteTrustedAppsRequestParams, GetTrustedAppsListRequest, GetTrustedListAppsResponse, PostTrustedAppCreateRequest, @@ -13,7 +14,6 @@ import { import { EndpointAppContext } from '../../types'; import { exceptionItemToTrustedAppItem, newTrustedAppItemToExceptionItem } from './utils'; import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; -import { DeleteTrustedAppsRequestParams } from './types'; import { ExceptionListClient } from '../../../../../lists/server'; const exceptionListClientFromContext = (context: RequestHandlerContext): ExceptionListClient => { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts index 35d0bf1116148..98c9b79f32d6b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts @@ -18,6 +18,7 @@ import { TRUSTED_APPS_LIST_API, } from '../../../../common/endpoint/constants'; import { + DeleteTrustedAppsRequestParams, GetTrustedAppsListRequest, PostTrustedAppCreateRequest, } from '../../../../common/endpoint/types'; @@ -26,8 +27,10 @@ import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/const import { EndpointAppContext } from '../../types'; import { ExceptionListClient, ListClient } from '../../../../../lists/server'; import { listMock } from '../../../../../lists/server/mocks'; -import { ExceptionListItemSchema } from '../../../../../lists/common/schemas/response'; -import { DeleteTrustedAppsRequestParams } from './types'; +import { + ExceptionListItemSchema, + FoundExceptionListItemSchema, +} from '../../../../../lists/common/schemas/response'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; type RequestHandlerContextWithLists = ReturnType & { @@ -125,6 +128,97 @@ describe('when invoking endpoint trusted apps route handlers', () => { }); }); + it('should map Exception List Item to Trusted App item', async () => { + const request = createListRequest(10, 100); + const emptyResponse: FoundExceptionListItemSchema = { + data: [ + { + _tags: ['os:windows'], + _version: undefined, + comments: [], + created_at: '2020-09-21T19:43:48.240Z', + created_by: 'test', + description: '', + entries: [ + { + field: 'process.hash.sha256', + operator: 'included', + type: 'match', + value: 'a4370c0cf81686c0b696fa6261c9d3e0d810ae704ab8301839dffd5d5112f476', + }, + { + field: 'process.hash.sha1', + operator: 'included', + type: 'match', + value: 'aedb279e378bed6c2db3c9dc9e12ba635e0b391c', + }, + { + field: 'process.hash.md5', + operator: 'included', + type: 'match', + value: '741462ab431a22233c787baab9b653c7', + }, + ], + id: '1', + item_id: '11', + list_id: 'trusted apps test', + meta: undefined, + name: 'test', + namespace_type: 'agnostic', + tags: [], + tie_breaker_id: '1', + type: 'simple', + updated_at: '2020-09-21T19:43:48.240Z', + updated_by: 'test', + }, + ], + page: 10, + per_page: 100, + total: 0, + }; + + exceptionsListClient.findExceptionListItem.mockResolvedValue(emptyResponse); + await routeHandler(context, request, response); + + expect(response.ok).toHaveBeenCalledWith({ + body: { + data: [ + { + created_at: '2020-09-21T19:43:48.240Z', + created_by: 'test', + description: '', + entries: [ + { + field: 'process.hash.*', + operator: 'included', + type: 'match', + value: 'a4370c0cf81686c0b696fa6261c9d3e0d810ae704ab8301839dffd5d5112f476', + }, + { + field: 'process.hash.*', + operator: 'included', + type: 'match', + value: 'aedb279e378bed6c2db3c9dc9e12ba635e0b391c', + }, + { + field: 'process.hash.*', + operator: 'included', + type: 'match', + value: '741462ab431a22233c787baab9b653c7', + }, + ], + id: '1', + name: 'test', + os: 'windows', + }, + ], + page: 10, + per_page: 100, + total: 0, + }, + }); + }); + it('should log unexpected error if one occurs', async () => { exceptionsListClient.findExceptionListItem.mockImplementation(() => { throw new Error('expected error'); @@ -138,24 +232,26 @@ describe('when invoking endpoint trusted apps route handlers', () => { describe('when creating a trusted app', () => { let routeHandler: RequestHandler; - const createNewTrustedAppBody = (): PostTrustedAppCreateRequest => ({ + const createNewTrustedAppBody = (): { + -readonly [k in keyof PostTrustedAppCreateRequest]: PostTrustedAppCreateRequest[k]; + } => ({ name: 'Some Anti-Virus App', description: 'this one is ok', os: 'windows', entries: [ { - field: 'process.path', + field: 'process.path.text', type: 'match', operator: 'included', value: 'c:/programs files/Anti-Virus', }, ], }); - const createPostRequest = () => { + const createPostRequest = (body?: PostTrustedAppCreateRequest) => { return httpServerMock.createKibanaRequest({ path: TRUSTED_APPS_LIST_API, method: 'post', - body: createNewTrustedAppBody(), + body: body ?? createNewTrustedAppBody(), }); }; @@ -197,7 +293,7 @@ describe('when invoking endpoint trusted apps route handlers', () => { description: 'this one is ok', entries: [ { - field: 'process.path', + field: 'process.path.text', operator: 'included', type: 'match', value: 'c:/programs files/Anti-Virus', @@ -224,7 +320,7 @@ describe('when invoking endpoint trusted apps route handlers', () => { description: 'this one is ok', entries: [ { - field: 'process.path', + field: 'process.path.text', operator: 'included', type: 'match', value: 'c:/programs files/Anti-Virus', @@ -247,6 +343,134 @@ describe('when invoking endpoint trusted apps route handlers', () => { expect(response.internalError).toHaveBeenCalled(); expect(endpointAppContext.logFactory.get('trusted_apps').error).toHaveBeenCalled(); }); + + it('should trim trusted app entry name', async () => { + const newTrustedApp = createNewTrustedAppBody(); + newTrustedApp.name = `\n ${newTrustedApp.name} \r\n`; + const request = createPostRequest(newTrustedApp); + await routeHandler(context, request, response); + expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0].name).toEqual( + 'Some Anti-Virus App' + ); + }); + + it('should trim condition entry values', async () => { + const newTrustedApp = createNewTrustedAppBody(); + newTrustedApp.entries.push({ + field: 'process.path.text', + value: '\n some value \r\n ', + operator: 'included', + type: 'match', + }); + const request = createPostRequest(newTrustedApp); + await routeHandler(context, request, response); + expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0].entries).toEqual([ + { + field: 'process.path.text', + operator: 'included', + type: 'match', + value: 'c:/programs files/Anti-Virus', + }, + { + field: 'process.path.text', + value: 'some value', + operator: 'included', + type: 'match', + }, + ]); + }); + + it('should convert hash values to lowercase', async () => { + const newTrustedApp = createNewTrustedAppBody(); + newTrustedApp.entries.push({ + field: 'process.hash.*', + value: '741462AB431A22233C787BAAB9B653C7', + operator: 'included', + type: 'match', + }); + const request = createPostRequest(newTrustedApp); + await routeHandler(context, request, response); + expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0].entries).toEqual([ + { + field: 'process.path.text', + operator: 'included', + type: 'match', + value: 'c:/programs files/Anti-Virus', + }, + { + field: 'process.hash.md5', + value: '741462ab431a22233c787baab9b653c7', + operator: 'included', + type: 'match', + }, + ]); + }); + + it('should detect md5 hash', async () => { + const newTrustedApp = createNewTrustedAppBody(); + newTrustedApp.entries = [ + { + field: 'process.hash.*', + value: '741462ab431a22233c787baab9b653c7', + operator: 'included', + type: 'match', + }, + ]; + const request = createPostRequest(newTrustedApp); + await routeHandler(context, request, response); + expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0].entries).toEqual([ + { + field: 'process.hash.md5', + value: '741462ab431a22233c787baab9b653c7', + operator: 'included', + type: 'match', + }, + ]); + }); + + it('should detect sha1 hash', async () => { + const newTrustedApp = createNewTrustedAppBody(); + newTrustedApp.entries = [ + { + field: 'process.hash.*', + value: 'aedb279e378bed6c2db3c9dc9e12ba635e0b391c', + operator: 'included', + type: 'match', + }, + ]; + const request = createPostRequest(newTrustedApp); + await routeHandler(context, request, response); + expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0].entries).toEqual([ + { + field: 'process.hash.sha1', + value: 'aedb279e378bed6c2db3c9dc9e12ba635e0b391c', + operator: 'included', + type: 'match', + }, + ]); + }); + + it('should detect sha256 hash', async () => { + const newTrustedApp = createNewTrustedAppBody(); + newTrustedApp.entries = [ + { + field: 'process.hash.*', + value: 'a4370c0cf81686c0b696fa6261c9d3e0d810ae704ab8301839dffd5d5112f476', + operator: 'included', + type: 'match', + }, + ]; + const request = createPostRequest(newTrustedApp); + await routeHandler(context, request, response); + expect(exceptionsListClient.createExceptionListItem.mock.calls[0][0].entries).toEqual([ + { + field: 'process.hash.sha256', + value: 'a4370c0cf81686c0b696fa6261c9d3e0d810ae704ab8301839dffd5d5112f476', + operator: 'included', + type: 'match', + }, + ]); + }); }); describe('when deleting a trusted app', () => { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/types.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/types.ts deleted file mode 100644 index 13c8bcfc20793..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/types.ts +++ /dev/null @@ -1,10 +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 { TypeOf } from '@kbn/config-schema'; -import { DeleteTrustedAppsRequestSchema } from '../../../../common/endpoint/schema/trusted_apps'; - -export type DeleteTrustedAppsRequestParams = TypeOf; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/utils.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/utils.ts index 794c1db4b49aa..2b8129ab950c6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/utils.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/utils.ts @@ -10,7 +10,7 @@ import { NewTrustedApp, TrustedApp } from '../../../../common/endpoint/types'; import { ExceptionListClient } from '../../../../../lists/server'; import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; -type NewExecptionItem = Parameters[0]; +type NewExceptionItem = Parameters[0]; /** * Map an ExcptionListItem to a TrustedApp item @@ -23,7 +23,15 @@ export const exceptionItemToTrustedAppItem = ( const { entries, description, created_by, created_at, name, _tags, id } = exceptionListItem; const os = osFromTagsList(_tags); return { - entries, + entries: entries.map((entry) => { + if (entry.field.startsWith('process.hash')) { + return { + ...entry, + field: 'process.hash.*', + }; + } + return entry; + }), description, created_at, created_by, @@ -51,22 +59,46 @@ export const newTrustedAppItemToExceptionItem = ({ entries, name, description = '', -}: NewTrustedApp): NewExecptionItem => { +}: NewTrustedApp): NewExceptionItem => { return { _tags: tagsListFromOs(os), comments: [], description, - entries, + // @ts-ignore + entries: entries.map(({ value, ...newEntry }) => { + let newValue = value.trim(); + + if (newEntry.field === 'process.hash.*') { + newValue = newValue.toLowerCase(); + newEntry.field = `process.hash.${hashType(newValue)}`; + } + + return { + ...newEntry, + value: newValue, + }; + }), itemId: uuid.v4(), listId: ENDPOINT_TRUSTED_APPS_LIST_ID, meta: undefined, - name, + name: name.trim(), namespaceType: 'agnostic', tags: [], type: 'simple', }; }; -const tagsListFromOs = (os: NewTrustedApp['os']): NewExecptionItem['_tags'] => { +const tagsListFromOs = (os: NewTrustedApp['os']): NewExceptionItem['_tags'] => { return [`os:${os}`]; }; + +const hashType = (hash: string): 'md5' | 'sha256' | 'sha1' | undefined => { + switch (hash.length) { + case 32: + return 'md5'; + case 40: + return 'sha1'; + case 64: + return 'sha256'; + } +}; diff --git a/x-pack/plugins/security_solution/server/graphql/index.ts b/x-pack/plugins/security_solution/server/graphql/index.ts index 7e25735707893..e949150c47c6c 100644 --- a/x-pack/plugins/security_solution/server/graphql/index.ts +++ b/x-pack/plugins/security_solution/server/graphql/index.ts @@ -26,8 +26,6 @@ import { toNumberSchema } from './scalar_to_number_array'; import { sourceStatusSchema } from './source_status'; import { sourcesSchema } from './sources'; import { timelineSchema } from './timeline'; -import { tlsSchema } from './tls'; -import { uncommonProcessesSchema } from './uncommon_processes'; import { whoAmISchema } from './who_am_i'; import { matrixHistogramSchema } from './matrix_histogram'; export const schemas = [ @@ -53,7 +51,5 @@ export const schemas = [ sourceStatusSchema, sharedSchema, timelineSchema, - tlsSchema, - uncommonProcessesSchema, whoAmISchema, ]; diff --git a/x-pack/plugins/security_solution/server/graphql/tls/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/tls/resolvers.ts deleted file mode 100644 index bfa3fddc3c8a5..0000000000000 --- a/x-pack/plugins/security_solution/server/graphql/tls/resolvers.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { SourceResolvers } from '../../graphql/types'; -import { AppResolverOf, ChildResolverOf } from '../../lib/framework'; -import { TLS, TlsRequestOptions } from '../../lib/tls'; -import { createOptionsPaginated } from '../../utils/build_query/create_options'; -import { QuerySourceResolver } from '../sources/resolvers'; - -export type QueryTlsResolver = ChildResolverOf< - AppResolverOf, - QuerySourceResolver ->; - -export interface TlsResolversDeps { - tls: TLS; -} - -export const createTlsResolvers = ( - libs: TlsResolversDeps -): { - Source: { - Tls: QueryTlsResolver; - }; -} => ({ - Source: { - async Tls(source, args, { req }, info) { - const options: TlsRequestOptions = { - ...createOptionsPaginated(source, args, info), - ip: args.ip, - sort: args.sort, - flowTarget: args.flowTarget, - }; - return libs.tls.getTls(req, options); - }, - }, -}); diff --git a/x-pack/plugins/security_solution/server/graphql/tls/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/tls/schema.gql.ts deleted file mode 100644 index 452c615c65aa5..0000000000000 --- a/x-pack/plugins/security_solution/server/graphql/tls/schema.gql.ts +++ /dev/null @@ -1,47 +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 gql from 'graphql-tag'; - -export const tlsSchema = gql` - enum TlsFields { - _id - } - type TlsNode { - _id: String - timestamp: Date - notAfter: [String!] - subjects: [String!] - ja3: [String!] - issuers: [String!] - } - input TlsSortField { - field: TlsFields! - direction: Direction! - } - type TlsEdges { - node: TlsNode! - cursor: CursorType! - } - type TlsData { - edges: [TlsEdges!]! - totalCount: Float! - pageInfo: PageInfoPaginated! - inspect: Inspect - } - extend type Source { - Tls( - filterQuery: String - id: String - ip: String! - pagination: PaginationInputPaginated! - sort: TlsSortField! - flowTarget: FlowTargetSourceDest! - timerange: TimerangeInput! - defaultIndex: [String!]! - ): TlsData! - } -`; diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index 7638ebd03f6b1..5887feb63c2a1 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -97,12 +97,6 @@ export interface NetworkHttpSortField { direction: Direction; } -export interface TlsSortField { - field: TlsFields; - - direction: Direction; -} - export interface PageInfoTimeline { pageIndex: number; @@ -356,10 +350,6 @@ export enum NetworkDnsFields { dnsBytesOut = 'dnsBytesOut', } -export enum TlsFields { - _id = '_id', -} - export enum DataProviderType { default = 'default', template = 'template', @@ -570,10 +560,6 @@ export interface Source { OverviewNetwork?: Maybe; OverviewHost?: Maybe; - - Tls: TlsData; - /** Gets UncommonProcesses based on a timerange, or all UncommonProcesses if no criteria is specified */ - UncommonProcesses: UncommonProcessesData; /** Just a simple example to get the app name */ whoAmI?: Maybe; } @@ -1930,64 +1916,6 @@ export interface OverviewHostData { inspect?: Maybe; } -export interface TlsData { - edges: TlsEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe; -} - -export interface TlsEdges { - node: TlsNode; - - cursor: CursorType; -} - -export interface TlsNode { - _id?: Maybe; - - timestamp?: Maybe; - - notAfter?: Maybe; - - subjects?: Maybe; - - ja3?: Maybe; - - issuers?: Maybe; -} - -export interface UncommonProcessesData { - edges: UncommonProcessesEdges[]; - - totalCount: number; - - pageInfo: PageInfoPaginated; - - inspect?: Maybe; -} - -export interface UncommonProcessesEdges { - node: UncommonProcessItem; - - cursor: CursorType; -} - -export interface UncommonProcessItem { - _id: string; - - instances: number; - - process: ProcessEcsFields; - - hosts: HostEcsFields[]; - - user?: Maybe; -} - export interface SayMyName { /** The id of the source */ appName: string; @@ -2575,32 +2503,6 @@ export interface OverviewHostSourceArgs { defaultIndex: string[]; } -export interface TlsSourceArgs { - filterQuery?: Maybe; - - id?: Maybe; - - ip: string; - - pagination: PaginationInputPaginated; - - sort: TlsSortField; - - flowTarget: FlowTargetSourceDest; - - timerange: TimerangeInput; - - defaultIndex: string[]; -} -export interface UncommonProcessesSourceArgs { - timerange: TimerangeInput; - - pagination: PaginationInputPaginated; - - filterQuery?: Maybe; - - defaultIndex: string[]; -} export interface IndicesExistSourceStatusArgs { defaultIndex: string[]; } @@ -3041,10 +2943,6 @@ export namespace SourceResolvers { OverviewNetwork?: OverviewNetworkResolver, TypeParent, TContext>; OverviewHost?: OverviewHostResolver, TypeParent, TContext>; - - Tls?: TlsResolver; - /** Gets UncommonProcesses based on a timerange, or all UncommonProcesses if no criteria is specified */ - UncommonProcesses?: UncommonProcessesResolver; /** Just a simple example to get the app name */ whoAmI?: WhoAmIResolver, TypeParent, TContext>; } @@ -3426,45 +3324,6 @@ export namespace SourceResolvers { defaultIndex: string[]; } - export type TlsResolver = Resolver< - R, - Parent, - TContext, - TlsArgs - >; - export interface TlsArgs { - filterQuery?: Maybe; - - id?: Maybe; - - ip: string; - - pagination: PaginationInputPaginated; - - sort: TlsSortField; - - flowTarget: FlowTargetSourceDest; - - timerange: TimerangeInput; - - defaultIndex: string[]; - } - - export type UncommonProcessesResolver< - R = UncommonProcessesData, - Parent = Source, - TContext = SiemContext - > = Resolver; - export interface UncommonProcessesArgs { - timerange: TimerangeInput; - - pagination: PaginationInputPaginated; - - filterQuery?: Maybe; - - defaultIndex: string[]; - } - export type WhoAmIResolver< R = Maybe, Parent = Source, @@ -8021,197 +7880,6 @@ export namespace OverviewHostDataResolvers { > = Resolver; } -export namespace TlsDataResolvers { - export interface Resolvers { - edges?: EdgesResolver; - - totalCount?: TotalCountResolver; - - pageInfo?: PageInfoResolver; - - inspect?: InspectResolver, TypeParent, TContext>; - } - - export type EdgesResolver = Resolver< - R, - Parent, - TContext - >; - export type TotalCountResolver = Resolver< - R, - Parent, - TContext - >; - export type PageInfoResolver< - R = PageInfoPaginated, - Parent = TlsData, - TContext = SiemContext - > = Resolver; - export type InspectResolver< - R = Maybe, - Parent = TlsData, - TContext = SiemContext - > = Resolver; -} - -export namespace TlsEdgesResolvers { - export interface Resolvers { - node?: NodeResolver; - - cursor?: CursorResolver; - } - - export type NodeResolver = Resolver< - R, - Parent, - TContext - >; - export type CursorResolver = Resolver< - R, - Parent, - TContext - >; -} - -export namespace TlsNodeResolvers { - export interface Resolvers { - _id?: _IdResolver, TypeParent, TContext>; - - timestamp?: TimestampResolver, TypeParent, TContext>; - - notAfter?: NotAfterResolver, TypeParent, TContext>; - - subjects?: SubjectsResolver, TypeParent, TContext>; - - ja3?: Ja3Resolver, TypeParent, TContext>; - - issuers?: IssuersResolver, TypeParent, TContext>; - } - - export type _IdResolver, Parent = TlsNode, TContext = SiemContext> = Resolver< - R, - Parent, - TContext - >; - export type TimestampResolver< - R = Maybe, - Parent = TlsNode, - TContext = SiemContext - > = Resolver; - export type NotAfterResolver< - R = Maybe, - Parent = TlsNode, - TContext = SiemContext - > = Resolver; - export type SubjectsResolver< - R = Maybe, - Parent = TlsNode, - TContext = SiemContext - > = Resolver; - export type Ja3Resolver, Parent = TlsNode, TContext = SiemContext> = Resolver< - R, - Parent, - TContext - >; - export type IssuersResolver< - R = Maybe, - Parent = TlsNode, - TContext = SiemContext - > = Resolver; -} - -export namespace UncommonProcessesDataResolvers { - export interface Resolvers { - edges?: EdgesResolver; - - totalCount?: TotalCountResolver; - - pageInfo?: PageInfoResolver; - - inspect?: InspectResolver, TypeParent, TContext>; - } - - export type EdgesResolver< - R = UncommonProcessesEdges[], - Parent = UncommonProcessesData, - TContext = SiemContext - > = Resolver; - export type TotalCountResolver< - R = number, - Parent = UncommonProcessesData, - TContext = SiemContext - > = Resolver; - export type PageInfoResolver< - R = PageInfoPaginated, - Parent = UncommonProcessesData, - TContext = SiemContext - > = Resolver; - export type InspectResolver< - R = Maybe, - Parent = UncommonProcessesData, - TContext = SiemContext - > = Resolver; -} - -export namespace UncommonProcessesEdgesResolvers { - export interface Resolvers { - node?: NodeResolver; - - cursor?: CursorResolver; - } - - export type NodeResolver< - R = UncommonProcessItem, - Parent = UncommonProcessesEdges, - TContext = SiemContext - > = Resolver; - export type CursorResolver< - R = CursorType, - Parent = UncommonProcessesEdges, - TContext = SiemContext - > = Resolver; -} - -export namespace UncommonProcessItemResolvers { - export interface Resolvers { - _id?: _IdResolver; - - instances?: InstancesResolver; - - process?: ProcessResolver; - - hosts?: HostsResolver; - - user?: UserResolver, TypeParent, TContext>; - } - - export type _IdResolver< - R = string, - Parent = UncommonProcessItem, - TContext = SiemContext - > = Resolver; - export type InstancesResolver< - R = number, - Parent = UncommonProcessItem, - TContext = SiemContext - > = Resolver; - export type ProcessResolver< - R = ProcessEcsFields, - Parent = UncommonProcessItem, - TContext = SiemContext - > = Resolver; - export type HostsResolver< - R = HostEcsFields[], - Parent = UncommonProcessItem, - TContext = SiemContext - > = Resolver; - export type UserResolver< - R = Maybe, - Parent = UncommonProcessItem, - TContext = SiemContext - > = Resolver; -} - export namespace SayMyNameResolvers { export interface Resolvers { /** The id of the source */ @@ -9492,12 +9160,6 @@ export type IResolvers = { NetworkHttpItem?: NetworkHttpItemResolvers.Resolvers; OverviewNetworkData?: OverviewNetworkDataResolvers.Resolvers; OverviewHostData?: OverviewHostDataResolvers.Resolvers; - TlsData?: TlsDataResolvers.Resolvers; - TlsEdges?: TlsEdgesResolvers.Resolvers; - TlsNode?: TlsNodeResolvers.Resolvers; - UncommonProcessesData?: UncommonProcessesDataResolvers.Resolvers; - UncommonProcessesEdges?: UncommonProcessesEdgesResolvers.Resolvers; - UncommonProcessItem?: UncommonProcessItemResolvers.Resolvers; SayMyName?: SayMyNameResolvers.Resolvers; TimelineResult?: TimelineResultResolvers.Resolvers; ColumnHeaderResult?: ColumnHeaderResultResolvers.Resolvers; diff --git a/x-pack/plugins/security_solution/server/graphql/uncommon_processes/index.ts b/x-pack/plugins/security_solution/server/graphql/uncommon_processes/index.ts deleted file mode 100644 index d0da0efd8a560..0000000000000 --- a/x-pack/plugins/security_solution/server/graphql/uncommon_processes/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export { createUncommonProcessesResolvers } from './resolvers'; -export { uncommonProcessesSchema } from './schema.gql'; diff --git a/x-pack/plugins/security_solution/server/graphql/uncommon_processes/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/uncommon_processes/resolvers.ts deleted file mode 100644 index 03d3c3d1a1fe4..0000000000000 --- a/x-pack/plugins/security_solution/server/graphql/uncommon_processes/resolvers.ts +++ /dev/null @@ -1,35 +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 { SourceResolvers } from '../../graphql/types'; -import { AppResolverOf, ChildResolverOf } from '../../lib/framework'; -import { UncommonProcesses } from '../../lib/uncommon_processes'; -import { createOptionsPaginated } from '../../utils/build_query/create_options'; -import { QuerySourceResolver } from '../sources/resolvers'; - -type QueryUncommonProcessesResolver = ChildResolverOf< - AppResolverOf, - QuerySourceResolver ->; - -export interface UncommonProcessesResolversDeps { - uncommonProcesses: UncommonProcesses; -} - -export const createUncommonProcessesResolvers = ( - libs: UncommonProcessesResolversDeps -): { - Source: { - UncommonProcesses: QueryUncommonProcessesResolver; - }; -} => ({ - Source: { - async UncommonProcesses(source, args, { req }, info) { - const options = createOptionsPaginated(source, args, info); - return libs.uncommonProcesses.getUncommonProcesses(req, options); - }, - }, -}); diff --git a/x-pack/plugins/security_solution/server/graphql/uncommon_processes/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/uncommon_processes/schema.gql.ts deleted file mode 100644 index 36a3da6779172..0000000000000 --- a/x-pack/plugins/security_solution/server/graphql/uncommon_processes/schema.gql.ts +++ /dev/null @@ -1,39 +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 gql from 'graphql-tag'; - -export const uncommonProcessesSchema = gql` - type UncommonProcessItem { - _id: String! - instances: Float! - process: ProcessEcsFields! - hosts: [HostEcsFields!]! - user: UserEcsFields - } - - type UncommonProcessesEdges { - node: UncommonProcessItem! - cursor: CursorType! - } - - type UncommonProcessesData { - edges: [UncommonProcessesEdges!]! - totalCount: Float! - pageInfo: PageInfoPaginated! - inspect: Inspect - } - - extend type Source { - "Gets UncommonProcesses based on a timerange, or all UncommonProcesses if no criteria is specified" - UncommonProcesses( - timerange: TimerangeInput! - pagination: PaginationInputPaginated! - filterQuery: String - defaultIndex: [String!]! - ): UncommonProcessesData! - } -`; diff --git a/x-pack/plugins/security_solution/server/init_server.ts b/x-pack/plugins/security_solution/server/init_server.ts index 1463d7f0da284..7cb2127a3d9d7 100644 --- a/x-pack/plugins/security_solution/server/init_server.ts +++ b/x-pack/plugins/security_solution/server/init_server.ts @@ -25,10 +25,8 @@ import { createScalarToNumberArrayValueResolvers } from './graphql/scalar_to_num import { createSourceStatusResolvers } from './graphql/source_status'; import { createSourcesResolvers } from './graphql/sources'; import { createTimelineResolvers } from './graphql/timeline'; -import { createUncommonProcessesResolvers } from './graphql/uncommon_processes'; import { createWhoAmIResolvers } from './graphql/who_am_i'; import { AppBackendLibs } from './lib/types'; -import { createTlsResolvers } from './graphql/tls'; import { createMatrixHistogramResolvers } from './graphql/matrix_histogram'; export const initServer = (libs: AppBackendLibs) => { @@ -55,8 +53,6 @@ export const initServer = (libs: AppBackendLibs) => { createSourcesResolvers(libs) as IResolvers, createSourceStatusResolvers(libs) as IResolvers, createTimelineResolvers(libs) as IResolvers, - createTlsResolvers(libs) as IResolvers, - createUncommonProcessesResolvers(libs) as IResolvers, createWhoAmIResolvers() as IResolvers, createKpiHostsResolvers(libs) as IResolvers, ], diff --git a/x-pack/plugins/security_solution/server/lib/compose/kibana.ts b/x-pack/plugins/security_solution/server/lib/compose/kibana.ts index db76f6d52dbb0..430ada93b4514 100644 --- a/x-pack/plugins/security_solution/server/lib/compose/kibana.ts +++ b/x-pack/plugins/security_solution/server/lib/compose/kibana.ts @@ -17,7 +17,6 @@ import { ElasticsearchKpiHostsAdapter } from '../kpi_hosts/elasticsearch_adapter import { ElasticsearchIndexFieldAdapter, IndexFields } from '../index_fields'; import { ElasticsearchIpDetailsAdapter, IpDetails } from '../ip_details'; -import { ElasticsearchTlsAdapter, TLS } from '../tls'; import { KpiNetwork } from '../kpi_network'; import { ElasticsearchKpiNetworkAdapter } from '../kpi_network/elasticsearch_adapter'; @@ -27,7 +26,6 @@ import { ElasticsearchOverviewAdapter } from '../overview/elasticsearch_adapter' import { ElasticsearchSourceStatusAdapter, SourceStatus } from '../source_status'; import { ConfigurationSourcesAdapter, Sources } from '../sources'; import { AppBackendLibs, AppDomainLibs } from '../types'; -import { ElasticsearchUncommonProcessesAdapter, UncommonProcesses } from '../uncommon_processes'; import * as note from '../note/saved_object'; import * as pinnedEvent from '../pinned_event/saved_object'; import * as timeline from '../timeline/saved_object'; @@ -50,13 +48,11 @@ export function compose( fields: new IndexFields(new ElasticsearchIndexFieldAdapter(framework)), hosts: new Hosts(new ElasticsearchHostsAdapter(framework, endpointContext)), ipDetails: new IpDetails(new ElasticsearchIpDetailsAdapter(framework)), - tls: new TLS(new ElasticsearchTlsAdapter(framework)), kpiHosts: new KpiHosts(new ElasticsearchKpiHostsAdapter(framework)), kpiNetwork: new KpiNetwork(new ElasticsearchKpiNetworkAdapter(framework)), matrixHistogram: new MatrixHistogram(new ElasticsearchMatrixHistogramAdapter(framework)), network: new Network(new ElasticsearchNetworkAdapter(framework)), overview: new Overview(new ElasticsearchOverviewAdapter(framework)), - uncommonProcesses: new UncommonProcesses(new ElasticsearchUncommonProcessesAdapter(framework)), }; const libs: AppBackendLibs = { 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 bbdb8ea0a36ed..9ee8c5cf298a1 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 @@ -337,7 +337,7 @@ export const repeatedSearchResultsWithSortId = ( guids: string[], ips?: string[], destIps?: string[] -) => ({ +): SignalSearchResponse => ({ took: 10, timed_out: false, _shards: { @@ -364,7 +364,7 @@ export const repeatedSearchResultsWithNoSortId = ( pageSize: number, guids: string[], ips?: string[] -) => ({ +): SignalSearchResponse => ({ took: 10, timed_out: false, _shards: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_ml_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_ml_signals.ts index bd9bf50688b58..89e3d28f451e4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_ml_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_ml_signals.ts @@ -8,7 +8,7 @@ import dateMath from '@elastic/datemath'; import { KibanaRequest } from '../../../../../../../src/core/server'; import { MlPluginSetup } from '../../../../../ml/server'; -import { getAnomalies } from '../../machine_learning'; +import { AnomalyResults, getAnomalies } from '../../machine_learning'; export const findMlSignals = async ({ ml, @@ -24,7 +24,7 @@ export const findMlSignals = async ({ anomalyThreshold: number; from: string; to: string; -}) => { +}): Promise => { const { mlAnomalySearch } = ml.mlSystemProvider(request); const params = { jobIds: [jobId], @@ -32,7 +32,5 @@ export const findMlSignals = async ({ earliestMs: dateMath.parse(from)?.valueOf() ?? 0, latestMs: dateMath.parse(to)?.valueOf() ?? 0, }; - const relevantAnomalies = await getAnomalies(params, mlAnomalySearch); - - return relevantAnomalies; + return getAnomalies(params, mlAnomalySearch); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts index 251c043adb58b..604b452174045 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/find_threshold_signals.ts @@ -34,6 +34,7 @@ export const findThresholdSignals = async ({ }: FindThresholdSignalsParams): Promise<{ searchResult: SignalSearchResponse; searchDuration: string; + searchErrors: string[]; }> => { const aggregations = threshold && !isEmpty(threshold.field) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index 756aedd5273d3..d369a91335347 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -3,56 +3,18 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import moment from 'moment'; -import { AlertServices } from '../../../../../alerts/server'; -import { ListClient } from '../../../../../lists/server'; -import { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { RuleTypeParams, RefreshTypes } from '../types'; -import { Logger } from '../../../../../../../src/core/server'; import { singleSearchAfter } from './single_search_after'; import { singleBulkCreate } from './single_bulk_create'; -import { BuildRuleMessage } from './rule_messages'; -import { SignalSearchResponse } from './types'; import { filterEventsAgainstList } from './filter_events_with_list'; -import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; -import { getSignalTimeTuples } from './utils'; - -interface SearchAfterAndBulkCreateParams { - gap: moment.Duration | null; - previousStartedAt: Date | null | undefined; - ruleParams: RuleTypeParams; - services: AlertServices; - listClient: ListClient; - exceptionsList: ExceptionListItemSchema[]; - logger: Logger; - id: string; - inputIndexPattern: string[]; - signalsIndex: string; - name: string; - actions: RuleAlertAction[]; - createdAt: string; - createdBy: string; - updatedBy: string; - updatedAt: string; - interval: string; - enabled: boolean; - pageSize: number; - filter: unknown; - refresh: RefreshTypes; - tags: string[]; - throttle: string; - buildRuleMessage: BuildRuleMessage; -} - -export interface SearchAfterAndBulkCreateReturnType { - success: boolean; - searchAfterTimes: string[]; - bulkCreateTimes: string[]; - lastLookBackDate: Date | null | undefined; - createdSignalsCount: number; - errors: string[]; -} +import { + createSearchAfterReturnType, + createSearchAfterReturnTypeFromResponse, + createTotalHitsFromSearchResult, + getSignalTimeTuples, + mergeReturns, +} from './utils'; +import { SearchAfterAndBulkCreateParams, SearchAfterAndBulkCreateReturnType } from './types'; // search_after through documents and re-index using bulk endpoint. export const searchAfterAndBulkCreate = async ({ @@ -81,14 +43,7 @@ export const searchAfterAndBulkCreate = async ({ throttle, buildRuleMessage, }: SearchAfterAndBulkCreateParams): Promise => { - const toReturn: SearchAfterAndBulkCreateReturnType = { - success: true, - searchAfterTimes: [], - bulkCreateTimes: [], - lastLookBackDate: null, - createdSignalsCount: 0, - errors: [], - }; + let toReturn = createSearchAfterReturnType(); // sortId tells us where to start our next consecutive search_after query let sortId: string | undefined; @@ -108,13 +63,15 @@ export const searchAfterAndBulkCreate = async ({ buildRuleMessage, }); logger.debug(buildRuleMessage(`totalToFromTuples: ${totalToFromTuples.length}`)); + while (totalToFromTuples.length > 0) { const tuple = totalToFromTuples.pop(); if (tuple == null || tuple.to == null || tuple.from == null) { logger.error(buildRuleMessage(`[-] malformed date tuple`)); - toReturn.success = false; - toReturn.errors = [...new Set([...toReturn.errors, 'malformed date tuple'])]; - return toReturn; + return createSearchAfterReturnType({ + success: false, + errors: ['malformed date tuple'], + }); } signalsCreatedCount = 0; while (signalsCreatedCount < tuple.maxSignals) { @@ -122,29 +79,27 @@ export const searchAfterAndBulkCreate = async ({ logger.debug(buildRuleMessage(`sortIds: ${sortId}`)); // perform search_after with optionally undefined sortId - const { - searchResult, - searchDuration, - }: { searchResult: SignalSearchResponse; searchDuration: string } = await singleSearchAfter( - { - searchAfterSortId: sortId, - index: inputIndexPattern, - from: tuple.from.toISOString(), - to: tuple.to.toISOString(), - services, - logger, - filter, - pageSize: tuple.maxSignals < pageSize ? Math.ceil(tuple.maxSignals) : pageSize, // maximum number of docs to receive per search result. - timestampOverride: ruleParams.timestampOverride, - } - ); - toReturn.searchAfterTimes.push(searchDuration); - + const { searchResult, searchDuration, searchErrors } = await singleSearchAfter({ + searchAfterSortId: sortId, + index: inputIndexPattern, + from: tuple.from.toISOString(), + to: tuple.to.toISOString(), + services, + logger, + filter, + pageSize: tuple.maxSignals < pageSize ? Math.ceil(tuple.maxSignals) : pageSize, // maximum number of docs to receive per search result. + timestampOverride: ruleParams.timestampOverride, + }); + toReturn = mergeReturns([ + toReturn, + createSearchAfterReturnTypeFromResponse({ searchResult }), + createSearchAfterReturnType({ + searchAfterTimes: [searchDuration], + errors: searchErrors, + }), + ]); // determine if there are any candidate signals to be processed - const totalHits = - typeof searchResult.hits.total === 'number' - ? searchResult.hits.total - : searchResult.hits.total.value; + const totalHits = createTotalHitsFromSearchResult({ searchResult }); logger.debug(buildRuleMessage(`totalHits: ${totalHits}`)); logger.debug( buildRuleMessage(`searchResult.hit.hits.length: ${searchResult.hits.hits.length}`) @@ -168,17 +123,11 @@ export const searchAfterAndBulkCreate = async ({ ); break; } - toReturn.lastLookBackDate = - searchResult.hits.hits.length > 0 - ? new Date( - searchResult.hits.hits[searchResult.hits.hits.length - 1]?._source['@timestamp'] - ) - : null; // filter out the search results that match with the values found in the list. // the resulting set are signals to be indexed, given they are not duplicates // of signals already present in the signals index. - const filteredEvents: SignalSearchResponse = await filterEventsAgainstList({ + const filteredEvents = await filterEventsAgainstList({ listClient, exceptionsList, logger, @@ -222,19 +171,21 @@ export const searchAfterAndBulkCreate = async ({ tags, throttle, }); - logger.debug(buildRuleMessage(`created ${createdCount} signals`)); - toReturn.createdSignalsCount += createdCount; + toReturn = mergeReturns([ + toReturn, + createSearchAfterReturnType({ + success: bulkSuccess, + createdSignalsCount: createdCount, + bulkCreateTimes: bulkDuration ? [bulkDuration] : undefined, + errors: bulkErrors, + }), + ]); signalsCreatedCount += createdCount; + logger.debug(buildRuleMessage(`created ${createdCount} signals`)); logger.debug(buildRuleMessage(`signalsCreatedCount: ${signalsCreatedCount}`)); - if (bulkDuration) { - toReturn.bulkCreateTimes.push(bulkDuration); - } - logger.debug( buildRuleMessage(`filteredEvents.hits.hits: ${filteredEvents.hits.hits.length}`) ); - toReturn.success = toReturn.success && bulkSuccess; - toReturn.errors = [...new Set([...toReturn.errors, ...bulkErrors])]; } // we are guaranteed to have searchResult hits at this point @@ -249,9 +200,13 @@ export const searchAfterAndBulkCreate = async ({ } } catch (exc: unknown) { logger.error(buildRuleMessage(`[-] search_after and bulk threw an error ${exc}`)); - toReturn.success = false; - toReturn.errors = [...new Set([...toReturn.errors, `${exc}`])]; - return toReturn; + return mergeReturns([ + toReturn, + createSearchAfterReturnType({ + success: false, + errors: [`${exc}`], + }), + ]); } } } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index 3ff5d5d2a6e13..382acf2f38245 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -18,11 +18,8 @@ import { sortExceptionItems, } from './utils'; import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; -import { RuleExecutorOptions } from './types'; -import { - searchAfterAndBulkCreate, - SearchAfterAndBulkCreateReturnType, -} from './search_after_bulk_create'; +import { RuleExecutorOptions, SearchAfterAndBulkCreateReturnType } from './types'; +import { searchAfterAndBulkCreate } from './search_after_bulk_create'; import { scheduleNotificationActions } from '../notifications/schedule_notification_actions'; import { RuleAlertType } from '../rules/types'; import { findMlSignals } from './find_ml_signals'; @@ -36,7 +33,17 @@ jest.mock('./rule_status_saved_objects_client'); jest.mock('./rule_status_service'); jest.mock('./search_after_bulk_create'); jest.mock('./get_filter'); -jest.mock('./utils'); +jest.mock('./utils', () => { + const original = jest.requireActual('./utils'); + return { + ...original, + getGapBetweenRuns: jest.fn(), + getGapMaxCatchupRatio: jest.fn(), + getListsClient: jest.fn(), + getExceptions: jest.fn(), + sortExceptionItems: jest.fn(), + }; +}); jest.mock('../notifications/schedule_notification_actions'); jest.mock('./find_ml_signals'); jest.mock('./bulk_create_ml_signals'); @@ -383,6 +390,7 @@ describe('rules_notification_alert_type', () => { }, ]); (findMlSignals as jest.Mock).mockResolvedValue({ + _shards: {}, hits: { hits: [], }, @@ -401,6 +409,7 @@ describe('rules_notification_alert_type', () => { payload = getPayload(ruleAlert, alertServices) as jest.Mocked; jobsSummaryMock.mockResolvedValue([]); (findMlSignals as jest.Mock).mockResolvedValue({ + _shards: {}, hits: { hits: [], }, @@ -409,6 +418,7 @@ describe('rules_notification_alert_type', () => { success: true, bulkCreateDuration: 0, createdItemsCount: 0, + errors: [], }); await alert.executor(payload); expect(ruleStatusService.success).not.toHaveBeenCalled(); @@ -425,6 +435,7 @@ describe('rules_notification_alert_type', () => { }, ]); (findMlSignals as jest.Mock).mockResolvedValue({ + _shards: { failed: 0 }, hits: { hits: [{}], }, @@ -433,6 +444,7 @@ describe('rules_notification_alert_type', () => { success: true, bulkCreateDuration: 1, createdItemsCount: 1, + errors: [], }); await alert.executor(payload); expect(ruleStatusService.success).toHaveBeenCalled(); @@ -460,6 +472,7 @@ describe('rules_notification_alert_type', () => { }); jobsSummaryMock.mockResolvedValue([]); (findMlSignals as jest.Mock).mockResolvedValue({ + _shards: { failed: 0 }, hits: { hits: [{}], }, @@ -468,6 +481,7 @@ describe('rules_notification_alert_type', () => { success: true, bulkCreateDuration: 1, createdItemsCount: 1, + errors: [], }); await alert.executor(payload); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 196c17b42221b..97ab12f905358 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -22,10 +22,7 @@ import { import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; import { SetupPlugins } from '../../../plugin'; import { getInputIndex } from './get_input_output_index'; -import { - searchAfterAndBulkCreate, - SearchAfterAndBulkCreateReturnType, -} from './search_after_bulk_create'; +import { searchAfterAndBulkCreate } from './search_after_bulk_create'; import { getFilter } from './get_filter'; import { SignalRuleAlertTypeDefinition, RuleAlertAttributes } from './types'; import { @@ -34,6 +31,10 @@ import { getExceptions, getGapMaxCatchupRatio, MAX_RULE_GAP_RATIO, + createErrorsFromShard, + createSearchAfterReturnType, + mergeReturns, + createSearchAfterReturnTypeFromResponse, } from './utils'; import { signalParamsSchema } from './signal_params_schema'; import { siemRuleActionGroups } from './siem_rule_action_groups'; @@ -104,14 +105,7 @@ export const signalRulesAlertType = ({ } = params; const searchAfterSize = Math.min(maxSignals, DEFAULT_SEARCH_AFTER_PAGE_SIZE); let hasError: boolean = false; - let result: SearchAfterAndBulkCreateReturnType = { - success: false, - bulkCreateTimes: [], - searchAfterTimes: [], - lastLookBackDate: null, - createdSignalsCount: 0, - errors: [], - }; + let result = createSearchAfterReturnType(); const ruleStatusClient = ruleStatusSavedObjectsClientFactory(services.savedObjectsClient); const ruleStatusService = await ruleStatusServiceFactory({ alertId, @@ -255,12 +249,22 @@ export const signalRulesAlertType = ({ refresh, tags, }); - result.success = success; - result.errors = errors; - result.createdSignalsCount = createdItemsCount; - if (bulkCreateDuration) { - result.bulkCreateTimes.push(bulkCreateDuration); - } + // The legacy ES client does not define failures when it can be present on the structure, hence why I have the & { failures: [] } + const shardFailures = + (anomalyResults._shards as typeof anomalyResults._shards & { failures: [] }).failures ?? + []; + const searchErrors = createErrorsFromShard({ + errors: shardFailures, + }); + result = mergeReturns([ + result, + createSearchAfterReturnType({ + success: success && anomalyResults._shards.failed === 0, + errors: [...errors, ...searchErrors], + createdSignalsCount: createdItemsCount, + bulkCreateTimes: bulkCreateDuration ? [bulkCreateDuration] : [], + }), + ]); } else if (isEqlRule(type)) { throw new Error('EQL Rules are under development, execution is not yet implemented'); } else if (isThresholdRule(type) && threshold) { @@ -276,7 +280,7 @@ export const signalRulesAlertType = ({ lists: exceptionItems ?? [], }); - const { searchResult: thresholdResults } = await findThresholdSignals({ + const { searchResult: thresholdResults, searchErrors } = await findThresholdSignals({ inputIndexPattern: inputIndex, from, to, @@ -313,12 +317,16 @@ export const signalRulesAlertType = ({ refresh, tags, }); - result.success = success; - result.errors = errors; - result.createdSignalsCount = createdItemsCount; - if (bulkCreateDuration) { - result.bulkCreateTimes.push(bulkCreateDuration); - } + result = mergeReturns([ + result, + createSearchAfterReturnTypeFromResponse({ searchResult: thresholdResults }), + createSearchAfterReturnType({ + success, + errors: [...errors, ...searchErrors], + createdSignalsCount: createdItemsCount, + bulkCreateTimes: bulkCreateDuration ? [bulkCreateDuration] : [], + }), + ]); } else if (isThreatMatchRule(type)) { if ( threatQuery == null || diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts index 250b891eb1f2c..da81911f07ad9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.test.ts @@ -11,6 +11,7 @@ import { } from './__mocks__/es_results'; import { singleSearchAfter } from './single_search_after'; import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; +import { ShardError } from '../../types'; describe('singleSearchAfter', () => { const mockService: AlertServicesMock = alertsMock.createAlertServices(); @@ -20,10 +21,71 @@ describe('singleSearchAfter', () => { }); test('if singleSearchAfter works without a given sort id', async () => { - let searchAfterSortId; - mockService.callCluster.mockResolvedValue(sampleDocSearchResultsNoSortId); + mockService.callCluster.mockResolvedValue(sampleDocSearchResultsNoSortId()); const { searchResult } = await singleSearchAfter({ - searchAfterSortId, + searchAfterSortId: undefined, + index: [], + from: 'now-360s', + to: 'now', + services: mockService, + logger: mockLogger, + pageSize: 1, + filter: undefined, + timestampOverride: undefined, + }); + expect(searchResult).toEqual(sampleDocSearchResultsNoSortId()); + }); + test('if singleSearchAfter returns an empty failure array', async () => { + mockService.callCluster.mockResolvedValue(sampleDocSearchResultsNoSortId()); + const { searchErrors } = await singleSearchAfter({ + searchAfterSortId: undefined, + index: [], + from: 'now-360s', + to: 'now', + services: mockService, + logger: mockLogger, + pageSize: 1, + filter: undefined, + timestampOverride: undefined, + }); + expect(searchErrors).toEqual([]); + }); + test('if singleSearchAfter will return an error array', async () => { + const errors: ShardError[] = [ + { + shard: 1, + index: 'index-123', + node: 'node-123', + reason: { + type: 'some type', + reason: 'some reason', + index_uuid: 'uuid-123', + index: 'index-123', + caused_by: { + type: 'some type', + reason: 'some reason', + }, + }, + }, + ]; + mockService.callCluster.mockResolvedValue({ + took: 10, + timed_out: false, + _shards: { + total: 10, + successful: 10, + failed: 1, + skipped: 0, + failures: errors, + }, + hits: { + total: 100, + max_score: 100, + hits: [], + }, + }); + const { searchErrors } = await singleSearchAfter({ + searchAfterSortId: undefined, index: [], from: 'now-360s', to: 'now', @@ -33,11 +95,11 @@ describe('singleSearchAfter', () => { filter: undefined, timestampOverride: undefined, }); - expect(searchResult).toEqual(sampleDocSearchResultsNoSortId); + expect(searchErrors).toEqual(['reason: some reason, type: some type, caused by: some reason']); }); test('if singleSearchAfter works with a given sort id', async () => { const searchAfterSortId = '1234567891111'; - mockService.callCluster.mockResolvedValue(sampleDocSearchResultsWithSortId); + mockService.callCluster.mockResolvedValue(sampleDocSearchResultsWithSortId()); const { searchResult } = await singleSearchAfter({ searchAfterSortId, index: [], @@ -49,7 +111,7 @@ describe('singleSearchAfter', () => { filter: undefined, timestampOverride: undefined, }); - expect(searchResult).toEqual(sampleDocSearchResultsWithSortId); + expect(searchResult).toEqual(sampleDocSearchResultsWithSortId()); }); test('if singleSearchAfter throws error', async () => { const searchAfterSortId = '1234567891111'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts index 92ce7a2836115..f758adb21611c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts @@ -9,7 +9,7 @@ import { AlertServices } from '../../../../../alerts/server'; import { Logger } from '../../../../../../../src/core/server'; import { SignalSearchResponse } from './types'; import { buildEventsSearchQuery } from './build_events_query'; -import { makeFloatString } from './utils'; +import { createErrorsFromShard, makeFloatString } from './utils'; import { TimestampOverrideOrUndefined } from '../../../../common/detection_engine/schemas/common/schemas'; interface SingleSearchAfterParams { @@ -40,6 +40,7 @@ export const singleSearchAfter = async ({ }: SingleSearchAfterParams): Promise<{ searchResult: SignalSearchResponse; searchDuration: string; + searchErrors: string[]; }> => { try { const searchAfterQuery = buildEventsSearchQuery({ @@ -59,7 +60,14 @@ export const singleSearchAfter = async ({ searchAfterQuery ); const end = performance.now(); - return { searchResult: nextSearchAfterResult, searchDuration: makeFloatString(end - start) }; + const searchErrors = createErrorsFromShard({ + errors: nextSearchAfterResult._shards.failures ?? [], + }); + return { + searchResult: nextSearchAfterResult, + searchDuration: makeFloatString(end - start), + searchErrors, + }; } catch (exc) { logger.error(`[-] nextSearchAfter threw an error ${exc}`); throw exc; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts index 7542128d83769..a6d4a2ba58ddd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signal.ts @@ -9,12 +9,10 @@ import { getThreatList } from './get_threat_list'; import { buildThreatMappingFilter } from './build_threat_mapping_filter'; import { getFilter } from '../get_filter'; -import { - searchAfterAndBulkCreate, - SearchAfterAndBulkCreateReturnType, -} from '../search_after_bulk_create'; +import { searchAfterAndBulkCreate } from '../search_after_bulk_create'; import { CreateThreatSignalOptions, ThreatListItem } from './types'; import { combineResults } from './utils'; +import { SearchAfterAndBulkCreateReturnType } from '../types'; export const createThreatSignal = async ({ threatMapping, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts index 9027475d71c4a..f416ae6703b66 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts @@ -6,9 +6,9 @@ import { getThreatList } from './get_threat_list'; -import { SearchAfterAndBulkCreateReturnType } from '../search_after_bulk_create'; import { CreateThreatSignalsOptions } from './types'; import { createThreatSignal } from './create_threat_signal'; +import { SearchAfterAndBulkCreateReturnType } from '../types'; export const createThreatSignals = async ({ threatMapping, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts index 4c3cd9943adb4..d63f2d2b3b6aa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts @@ -19,10 +19,10 @@ import { import { PartialFilter, RuleTypeParams } from '../../types'; import { AlertServices } from '../../../../../../alerts/server'; import { ExceptionListItemSchema } from '../../../../../../lists/common/schemas'; -import { SearchAfterAndBulkCreateReturnType } from '../search_after_bulk_create'; import { ILegacyScopedClusterClient, Logger } from '../../../../../../../../src/core/server'; import { RuleAlertAction } from '../../../../../common/detection_engine/types'; import { BuildRuleMessage } from '../rule_messages'; +import { SearchAfterAndBulkCreateReturnType } from '../types'; export interface CreateThreatSignalsOptions { threatMapping: ThreatMapping; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts index 48bdf430b940e..27593b40b0c8f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchAfterAndBulkCreateReturnType } from '../search_after_bulk_create'; +import { SearchAfterAndBulkCreateReturnType } from '../types'; import { calculateAdditiveMax, combineResults } from './utils'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts index 38bbb70b6c4ec..401a4a1acb065 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SearchAfterAndBulkCreateReturnType } from '../search_after_bulk_create'; +import { SearchAfterAndBulkCreateReturnType } from '../types'; /** * Given two timers this will take the max of each and add them to each other and return that addition. diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index 23aa786558a99..6ebdca0764e9d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -5,12 +5,22 @@ */ import { DslQuery, Filter } from 'src/plugins/data/common'; +import moment from 'moment'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; -import { AlertType, AlertTypeState, AlertExecutorOptions } from '../../../../../alerts/server'; +import { + AlertType, + AlertTypeState, + AlertExecutorOptions, + AlertServices, +} from '../../../../../alerts/server'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; -import { RuleTypeParams } from '../types'; +import { RuleTypeParams, RefreshTypes } from '../types'; import { SearchResponse } from '../../types'; +import { ListClient } from '../../../../../lists/server'; +import { Logger } from '../../../../../../../src/core/server'; +import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; +import { BuildRuleMessage } from './rule_messages'; // used for gap detection code // eslint-disable-next-line @typescript-eslint/naming-convention @@ -179,3 +189,39 @@ export interface QueryFilter { must_not: Filter[]; }; } + +export interface SearchAfterAndBulkCreateParams { + gap: moment.Duration | null; + previousStartedAt: Date | null | undefined; + ruleParams: RuleTypeParams; + services: AlertServices; + listClient: ListClient; + exceptionsList: ExceptionListItemSchema[]; + logger: Logger; + id: string; + inputIndexPattern: string[]; + signalsIndex: string; + name: string; + actions: RuleAlertAction[]; + createdAt: string; + createdBy: string; + updatedBy: string; + updatedAt: string; + interval: string; + enabled: boolean; + pageSize: number; + filter: unknown; + refresh: RefreshTypes; + tags: string[]; + throttle: string; + buildRuleMessage: BuildRuleMessage; +} + +export interface SearchAfterAndBulkCreateReturnType { + success: boolean; + searchAfterTimes: string[]; + bulkCreateTimes: string[]; + lastLookBackDate: Date | null | undefined; + createdSignalsCount: number; + errors: string[]; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index 123b9c9bdffa2..97f3dbeaf4489 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -25,15 +25,25 @@ import { getListsClient, getSignalTimeTuples, getExceptions, + createErrorsFromShard, + createSearchAfterReturnTypeFromResponse, + createSearchAfterReturnType, + mergeReturns, + createTotalHitsFromSearchResult, } from './utils'; -import { BulkResponseErrorAggregation } from './types'; +import { BulkResponseErrorAggregation, SearchAfterAndBulkCreateReturnType } from './types'; import { sampleBulkResponse, sampleEmptyBulkResponse, sampleBulkError, sampleBulkErrorItem, mockLogger, + sampleDocSearchResultsWithSortId, + sampleEmptyDocSearchResults, + sampleDocSearchResultsNoSortIdNoHits, + repeatedSearchResultsWithSortId, } from './__mocks__/es_results'; +import { ShardError } from '../../types'; const buildRuleMessage = buildRuleMessageFactory({ id: 'fake id', @@ -783,4 +793,278 @@ describe('utils', () => { expect(exceptions).toEqual([]); }); }); + + describe('createErrorsFromShard', () => { + test('empty errors will return an empty array', () => { + const createdErrors = createErrorsFromShard({ errors: [] }); + expect(createdErrors).toEqual([]); + }); + + test('single error will return single converted array of a string of a reason', () => { + const errors: ShardError[] = [ + { + shard: 1, + index: 'index-123', + node: 'node-123', + reason: { + type: 'some type', + reason: 'some reason', + index_uuid: 'uuid-123', + index: 'index-123', + caused_by: { + type: 'some type', + reason: 'some reason', + }, + }, + }, + ]; + const createdErrors = createErrorsFromShard({ errors }); + expect(createdErrors).toEqual([ + 'reason: some reason, type: some type, caused by: some reason', + ]); + }); + + test('two errors will return two converted arrays to a string of a reason', () => { + const errors: ShardError[] = [ + { + shard: 1, + index: 'index-123', + node: 'node-123', + reason: { + type: 'some type', + reason: 'some reason', + index_uuid: 'uuid-123', + index: 'index-123', + caused_by: { + type: 'some type', + reason: 'some reason', + }, + }, + }, + { + shard: 2, + index: 'index-345', + node: 'node-345', + reason: { + type: 'some type 2', + reason: 'some reason 2', + index_uuid: 'uuid-345', + index: 'index-345', + caused_by: { + type: 'some type 2', + reason: 'some reason 2', + }, + }, + }, + ]; + const createdErrors = createErrorsFromShard({ errors }); + expect(createdErrors).toEqual([ + 'reason: some reason, type: some type, caused by: some reason', + 'reason: some reason 2, type: some type 2, caused by: some reason 2', + ]); + }); + }); + + describe('createSearchAfterReturnTypeFromResponse', () => { + test('empty results will return successful type', () => { + const searchResult = sampleEmptyDocSearchResults(); + const newSearchResult = createSearchAfterReturnTypeFromResponse({ searchResult }); + const expected: SearchAfterAndBulkCreateReturnType = { + bulkCreateTimes: [], + createdSignalsCount: 0, + errors: [], + lastLookBackDate: null, + searchAfterTimes: [], + success: true, + }; + expect(newSearchResult).toEqual(expected); + }); + + test('multiple results will return successful type with expected success', () => { + const searchResult = sampleDocSearchResultsWithSortId(); + const newSearchResult = createSearchAfterReturnTypeFromResponse({ searchResult }); + const expected: SearchAfterAndBulkCreateReturnType = { + bulkCreateTimes: [], + createdSignalsCount: 0, + errors: [], + lastLookBackDate: new Date('2020-04-20T21:27:45.000Z'), + searchAfterTimes: [], + success: true, + }; + expect(newSearchResult).toEqual(expected); + }); + + test('result with error will create success: false within the result set', () => { + const searchResult = sampleDocSearchResultsNoSortIdNoHits(); + searchResult._shards.failed = 1; + const { success } = createSearchAfterReturnTypeFromResponse({ searchResult }); + expect(success).toEqual(false); + }); + + test('result with error will create success: false within the result set if failed is 2 or more', () => { + const searchResult = sampleDocSearchResultsNoSortIdNoHits(); + searchResult._shards.failed = 2; + const { success } = createSearchAfterReturnTypeFromResponse({ searchResult }); + expect(success).toEqual(false); + }); + + test('result with error will create success: true within the result set if failed is 0', () => { + const searchResult = sampleDocSearchResultsNoSortIdNoHits(); + searchResult._shards.failed = 0; + const { success } = createSearchAfterReturnTypeFromResponse({ searchResult }); + expect(success).toEqual(true); + }); + }); + + describe('createSearchAfterReturnType', () => { + test('createSearchAfterReturnType will return full object when nothing is passed', () => { + const searchAfterReturnType = createSearchAfterReturnType(); + const expected: SearchAfterAndBulkCreateReturnType = { + bulkCreateTimes: [], + createdSignalsCount: 0, + errors: [], + lastLookBackDate: null, + searchAfterTimes: [], + success: true, + }; + expect(searchAfterReturnType).toEqual(expected); + }); + + test('createSearchAfterReturnType can override all values', () => { + const searchAfterReturnType = createSearchAfterReturnType({ + bulkCreateTimes: ['123'], + createdSignalsCount: 5, + errors: ['error 1'], + lastLookBackDate: new Date('2020-09-21T18:51:25.193Z'), + searchAfterTimes: ['123'], + success: false, + }); + const expected: SearchAfterAndBulkCreateReturnType = { + bulkCreateTimes: ['123'], + createdSignalsCount: 5, + errors: ['error 1'], + lastLookBackDate: new Date('2020-09-21T18:51:25.193Z'), + searchAfterTimes: ['123'], + success: false, + }; + expect(searchAfterReturnType).toEqual(expected); + }); + + test('createSearchAfterReturnType can override select values', () => { + const searchAfterReturnType = createSearchAfterReturnType({ + createdSignalsCount: 5, + errors: ['error 1'], + }); + const expected: SearchAfterAndBulkCreateReturnType = { + bulkCreateTimes: [], + createdSignalsCount: 5, + errors: ['error 1'], + lastLookBackDate: null, + searchAfterTimes: [], + success: true, + }; + expect(searchAfterReturnType).toEqual(expected); + }); + }); + + describe('mergeReturns', () => { + test('it merges a default "prev" and "next" correctly ', () => { + const merged = mergeReturns([createSearchAfterReturnType(), createSearchAfterReturnType()]); + const expected: SearchAfterAndBulkCreateReturnType = { + bulkCreateTimes: [], + createdSignalsCount: 0, + errors: [], + lastLookBackDate: null, + searchAfterTimes: [], + success: true, + }; + expect(merged).toEqual(expected); + }); + + test('it merges search in with two default search results where "prev" "success" is false correctly', () => { + const { success } = mergeReturns([ + createSearchAfterReturnType({ success: false }), + createSearchAfterReturnType(), + ]); + expect(success).toEqual(false); + }); + + test('it merges search in with two default search results where "next" "success" is false correctly', () => { + const { success } = mergeReturns([ + createSearchAfterReturnType(), + createSearchAfterReturnType({ success: false }), + ]); + expect(success).toEqual(false); + }); + + test('it merges search where the lastLookBackDate is the "next" date when given', () => { + const { lastLookBackDate } = mergeReturns([ + createSearchAfterReturnType({ + lastLookBackDate: new Date('2020-08-21T19:21:46.194Z'), + }), + createSearchAfterReturnType({ + lastLookBackDate: new Date('2020-09-21T19:21:46.194Z'), + }), + ]); + expect(lastLookBackDate).toEqual(new Date('2020-09-21T19:21:46.194Z')); + }); + + test('it merges search where the lastLookBackDate is the "prev" if given undefined for "next', () => { + const { lastLookBackDate } = mergeReturns([ + createSearchAfterReturnType({ + lastLookBackDate: new Date('2020-08-21T19:21:46.194Z'), + }), + createSearchAfterReturnType({ + lastLookBackDate: undefined, + }), + ]); + expect(lastLookBackDate).toEqual(new Date('2020-08-21T19:21:46.194Z')); + }); + + test('it merges search where values from "next" and "prev" are computed together', () => { + const merged = mergeReturns([ + createSearchAfterReturnType({ + bulkCreateTimes: ['123'], + createdSignalsCount: 3, + errors: ['error 1', 'error 2'], + lastLookBackDate: new Date('2020-08-21T18:51:25.193Z'), + searchAfterTimes: ['123'], + success: true, + }), + createSearchAfterReturnType({ + bulkCreateTimes: ['456'], + createdSignalsCount: 2, + errors: ['error 3'], + lastLookBackDate: new Date('2020-09-21T18:51:25.193Z'), + searchAfterTimes: ['567'], + success: true, + }), + ]); + const expected: SearchAfterAndBulkCreateReturnType = { + bulkCreateTimes: ['123', '456'], // concatenates the prev and next together + createdSignalsCount: 5, // Adds the 3 and 2 together + errors: ['error 1', 'error 2', 'error 3'], // concatenates the prev and next together + lastLookBackDate: new Date('2020-09-21T18:51:25.193Z'), // takes the next lastLookBackDate + searchAfterTimes: ['123', '567'], // concatenates the searchAfterTimes together + success: true, // Defaults to success true is all of it was successful + }; + expect(merged).toEqual(expected); + }); + }); + + describe('createTotalHitsFromSearchResult', () => { + test('it should return 0 for empty results', () => { + const result = createTotalHitsFromSearchResult({ + searchResult: sampleEmptyDocSearchResults(), + }); + expect(result).toEqual(0); + }); + + test('it should return 4 for 4 result sets', () => { + const result = createTotalHitsFromSearchResult({ + searchResult: repeatedSearchResultsWithSortId(4, 1, ['1', '2', '3', '4']), + }); + expect(result).toEqual(4); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index 9f1e5d6980466..2eabc03dccad7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -12,11 +12,18 @@ import { AlertServices, parseDuration } from '../../../../../alerts/server'; import { ExceptionListClient, ListClient, ListPluginSetup } from '../../../../../lists/server'; import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { ListArray } from '../../../../common/detection_engine/schemas/types/lists'; -import { BulkResponse, BulkResponseErrorAggregation, isValidUnit } from './types'; +import { + BulkResponse, + BulkResponseErrorAggregation, + isValidUnit, + SearchAfterAndBulkCreateReturnType, + SignalSearchResponse, +} from './types'; import { BuildRuleMessage } from './rule_messages'; import { parseScheduleDates } from '../../../../common/detection_engine/parse_schedule_dates'; import { hasLargeValueList } from '../../../../common/detection_engine/utils'; import { MAX_EXCEPTION_LIST_SIZE } from '../../../../../lists/common/constants'; +import { ShardError } from '../../types'; interface SortExceptionsReturn { exceptionsWithValueLists: ExceptionListItemSchema[]; @@ -439,3 +446,97 @@ export const getSignalTimeTuples = ({ ); return totalToFromTuples; }; + +/** + * Given errors from a search query this will return an array of strings derived from the errors. + * @param errors The errors to derive the strings from + */ +export const createErrorsFromShard = ({ errors }: { errors: ShardError[] }): string[] => { + return errors.map((error) => { + return `reason: ${error.reason.reason}, type: ${error.reason.caused_by.type}, caused by: ${error.reason.caused_by.reason}`; + }); +}; + +export const createSearchAfterReturnTypeFromResponse = ({ + searchResult, +}: { + searchResult: SignalSearchResponse; +}): SearchAfterAndBulkCreateReturnType => { + return createSearchAfterReturnType({ + success: searchResult._shards.failed === 0, + lastLookBackDate: + searchResult.hits.hits.length > 0 + ? new Date(searchResult.hits.hits[searchResult.hits.hits.length - 1]?._source['@timestamp']) + : undefined, + }); +}; + +export const createSearchAfterReturnType = ({ + success, + searchAfterTimes, + bulkCreateTimes, + lastLookBackDate, + createdSignalsCount, + errors, +}: { + success?: boolean | undefined; + searchAfterTimes?: string[] | undefined; + bulkCreateTimes?: string[] | undefined; + lastLookBackDate?: Date | undefined; + createdSignalsCount?: number | undefined; + errors?: string[] | undefined; +} = {}): SearchAfterAndBulkCreateReturnType => { + return { + success: success ?? true, + searchAfterTimes: searchAfterTimes ?? [], + bulkCreateTimes: bulkCreateTimes ?? [], + lastLookBackDate: lastLookBackDate ?? null, + createdSignalsCount: createdSignalsCount ?? 0, + errors: errors ?? [], + }; +}; + +export const mergeReturns = ( + searchAfters: SearchAfterAndBulkCreateReturnType[] +): SearchAfterAndBulkCreateReturnType => { + return searchAfters.reduce((prev, next) => { + const { + success: existingSuccess, + searchAfterTimes: existingSearchAfterTimes, + bulkCreateTimes: existingBulkCreateTimes, + lastLookBackDate: existingLastLookBackDate, + createdSignalsCount: existingCreatedSignalsCount, + errors: existingErrors, + } = prev; + + const { + success: newSuccess, + searchAfterTimes: newSearchAfterTimes, + bulkCreateTimes: newBulkCreateTimes, + lastLookBackDate: newLastLookBackDate, + createdSignalsCount: newCreatedSignalsCount, + errors: newErrors, + } = next; + + return { + success: existingSuccess && newSuccess, + searchAfterTimes: [...existingSearchAfterTimes, ...newSearchAfterTimes], + bulkCreateTimes: [...existingBulkCreateTimes, ...newBulkCreateTimes], + lastLookBackDate: newLastLookBackDate ?? existingLastLookBackDate, + createdSignalsCount: existingCreatedSignalsCount + newCreatedSignalsCount, + errors: [...new Set([...existingErrors, ...newErrors])], + }; + }); +}; + +export const createTotalHitsFromSearchResult = ({ + searchResult, +}: { + searchResult: SignalSearchResponse; +}): number => { + const totalHits = + typeof searchResult.hits.total === 'number' + ? searchResult.hits.total + : searchResult.hits.total.value; + return totalHits; +}; diff --git a/x-pack/plugins/security_solution/server/lib/tls/elasticsearch_adapter.test.ts b/x-pack/plugins/security_solution/server/lib/tls/elasticsearch_adapter.test.ts deleted file mode 100644 index 428685cbaddb8..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/tls/elasticsearch_adapter.test.ts +++ /dev/null @@ -1,63 +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 { buildTlsQuery } from './query_tls.dsl'; -import { ElasticsearchTlsAdapter } from './elasticsearch_adapter'; -import expect from '@kbn/expect'; -import { FrameworkRequest, FrameworkAdapter } from '../framework'; -import { mockRequest, mockResponse, mockOptions, expectedTlsEdges, mockTlsQuery } from './mock'; -import { TlsData } from '../../graphql/types'; - -jest.mock('./query_tls.dsl', () => { - return { - buildTlsQuery: jest.fn(), - }; -}); - -describe('elasticsearch_adapter', () => { - describe('#getTls', () => { - let data: TlsData; - const mockCallWithRequest = jest.fn(); - const mockFramework: FrameworkAdapter = { - callWithRequest: mockCallWithRequest, - registerGraphQLEndpoint: jest.fn(), - getIndexPatternsService: jest.fn(), - }; - - beforeAll(async () => { - (buildTlsQuery as jest.Mock).mockReset(); - (buildTlsQuery as jest.Mock).mockReturnValue(mockTlsQuery); - - mockCallWithRequest.mockResolvedValue(mockResponse); - jest.doMock('../framework', () => ({ - callWithRequest: mockCallWithRequest, - })); - - const EsTls = new ElasticsearchTlsAdapter(mockFramework); - data = await EsTls.getTls(mockRequest as FrameworkRequest, mockOptions); - }); - - afterAll(() => { - mockCallWithRequest.mockRestore(); - (buildTlsQuery as jest.Mock).mockClear(); - }); - - test('buildTlsQuery', () => { - expect((buildTlsQuery as jest.Mock).mock.calls[0][0]).to.eql(mockOptions); - }); - - test('will return tlsEdges correctly', () => { - expect(data.edges).to.eql(expectedTlsEdges); - }); - - test('will return inspect data', () => { - expect(data.inspect).to.eql({ - dsl: [JSON.stringify(mockTlsQuery, null, 2)], - response: [JSON.stringify(mockResponse, null, 2)], - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/tls/elasticsearch_adapter.ts b/x-pack/plugins/security_solution/server/lib/tls/elasticsearch_adapter.ts deleted file mode 100644 index ab9175951a8f5..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/tls/elasticsearch_adapter.ts +++ /dev/null @@ -1,82 +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 { getOr } from 'lodash/fp'; - -import { TlsData, TlsEdges } from '../../graphql/types'; -import { inspectStringifyObject } from '../../utils/build_query'; -import { DatabaseSearchResponse, FrameworkAdapter, FrameworkRequest } from '../framework'; -import { TermAggregation } from '../types'; -import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants'; -import { TlsRequestOptions } from './index'; - -import { TlsAdapter, TlsBuckets } from './types'; - -import { buildTlsQuery } from './query_tls.dsl'; - -export class ElasticsearchTlsAdapter implements TlsAdapter { - constructor(private readonly framework: FrameworkAdapter) {} - - public async getTls(request: FrameworkRequest, options: TlsRequestOptions): Promise { - if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { - throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); - } - const dsl = buildTlsQuery(options); - const response = await this.framework.callWithRequest( - request, - 'search', - dsl - ); - - const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; - const totalCount = getOr(0, 'aggregations.count.value', response); - const tlsEdges: TlsEdges[] = getTlsEdges(response, options); - const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; - const edges = tlsEdges.splice(cursorStart, querySize - cursorStart); - const inspect = { - dsl: [inspectStringifyObject(dsl)], - response: [inspectStringifyObject(response)], - }; - const showMorePagesIndicator = totalCount > fakeTotalCount; - return { - edges, - inspect, - pageInfo: { - activePage: activePage ? activePage : 0, - fakeTotalCount, - showMorePagesIndicator, - }, - totalCount, - }; - } -} - -const getTlsEdges = ( - response: DatabaseSearchResponse, - options: TlsRequestOptions -): TlsEdges[] => { - return formatTlsEdges(getOr([], 'aggregations.sha1.buckets', response)); -}; - -export const formatTlsEdges = (buckets: TlsBuckets[]): TlsEdges[] => { - return buckets.map((bucket: TlsBuckets) => { - const edge: TlsEdges = { - node: { - _id: bucket.key, - subjects: bucket.subjects.buckets.map(({ key }) => key), - ja3: bucket.ja3.buckets.map(({ key }) => key), - issuers: bucket.issuers.buckets.map(({ key }) => key), - // eslint-disable-next-line @typescript-eslint/naming-convention - notAfter: bucket.not_after.buckets.map(({ key_as_string }) => key_as_string), - }, - cursor: { - value: bucket.key, - tiebreaker: null, - }, - }; - return edge; - }); -}; diff --git a/x-pack/plugins/security_solution/server/lib/tls/index.ts b/x-pack/plugins/security_solution/server/lib/tls/index.ts deleted file mode 100644 index 25e3957cc99db..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/tls/index.ts +++ /dev/null @@ -1,26 +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 { FlowTargetSourceDest, TlsSortField, TlsData } from '../../graphql/types'; -import { FrameworkRequest, RequestOptionsPaginated } from '../framework'; - -import { TlsAdapter } from './types'; - -export * from './elasticsearch_adapter'; - -export interface TlsRequestOptions extends RequestOptionsPaginated { - ip?: string; - sort: TlsSortField; - flowTarget: FlowTargetSourceDest; -} - -export class TLS { - constructor(private readonly adapter: TlsAdapter) {} - - public async getTls(req: FrameworkRequest, options: TlsRequestOptions): Promise { - return this.adapter.getTls(req, options); - } -} diff --git a/x-pack/plugins/security_solution/server/lib/tls/mock.ts b/x-pack/plugins/security_solution/server/lib/tls/mock.ts deleted file mode 100644 index 62d5e1e61570a..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/tls/mock.ts +++ /dev/null @@ -1,481 +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 { Direction, TlsFields, FlowTargetSourceDest } from '../../graphql/types'; - -export const mockTlsQuery = { - allowNoIndices: true, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - ignoreUnavailable: true, - body: { - aggs: { - count: { cardinality: { field: 'tls.server_certificate.fingerprint.sha1' } }, - sha1: { - terms: { - field: 'tls.server_certificate.fingerprint.sha1', - size: 10, - order: { _key: 'desc' }, - }, - aggs: { - issuers: { terms: { field: 'tls.server.issuer' } }, - subjects: { terms: { field: 'tls.server.subject' } }, - not_after: { terms: { field: 'tls.server.not_after' } }, - ja3: { terms: { field: 'tls.server.ja3s' } }, - }, - }, - }, - query: { - bool: { filter: [{ range: { '@timestamp': { gte: 1570719927430, lte: 1570806327431 } } }] }, - }, - size: 0, - track_total_hits: false, - }, -}; - -export const expectedTlsEdges = [ - { - cursor: { - tiebreaker: null, - value: 'fff8dc95436e0e25ce46b1526a1a547e8cf3bb82', - }, - node: { - _id: 'fff8dc95436e0e25ce46b1526a1a547e8cf3bb82', - subjects: ['*.1.nflxso.net'], - issuers: ['DigiCert SHA2 Secure Server CA'], - ja3: ['95d2dd53a89b334cddd5c22e81e7fe61'], - notAfter: ['2019-10-27T12:00:00.000Z'], - }, - }, - { - cursor: { - tiebreaker: null, - value: 'fd8440c4b20978b173e0910e2639d114f0d405c5', - }, - node: { - _id: 'fd8440c4b20978b173e0910e2639d114f0d405c5', - subjects: ['cogocast.net'], - issuers: ['Amazon'], - ja3: ['a111d93cdf31f993c40a8a9ef13e8d7e'], - notAfter: ['2020-02-01T12:00:00.000Z'], - }, - }, - { - cursor: { tiebreaker: null, value: 'fcdc16645ebb3386adc96e7ba735c4745709b9dd' }, - node: { - _id: 'fcdc16645ebb3386adc96e7ba735c4745709b9dd', - subjects: ['player-devintever2.mountain.siriusxm.com'], - issuers: ['Trustwave Organization Validation SHA256 CA, Level 1'], - ja3: ['6fa3244afc6bb6f9fad207b6b52af26b'], - notAfter: ['2020-03-06T21:57:09.000Z'], - }, - }, - { - cursor: { tiebreaker: null, value: 'fccf375789cb7e671502a7b0cc969f218a4b2c70' }, - node: { - _id: 'fccf375789cb7e671502a7b0cc969f218a4b2c70', - subjects: ['appleid.apple.com'], - issuers: ['DigiCert SHA2 Extended Validation Server CA'], - ja3: ['6fa3244afc6bb6f9fad207b6b52af26b'], - notAfter: ['2020-07-04T12:00:00.000Z'], - }, - }, - { - cursor: { tiebreaker: null, value: 'fc4a296b706fa18ac50b96f5c0327c69db4a8981' }, - node: { - _id: 'fc4a296b706fa18ac50b96f5c0327c69db4a8981', - subjects: ['itunes.apple.com'], - issuers: ['DigiCert SHA2 Extended Validation Server CA'], - ja3: ['a441a33aaee795f498d6b764cc78989a'], - notAfter: ['2020-03-24T12:00:00.000Z'], - }, - }, - { - cursor: { tiebreaker: null, value: 'fc2cbc41f6a0e9c0118de4fe40f299f7207b797e' }, - node: { - _id: 'fc2cbc41f6a0e9c0118de4fe40f299f7207b797e', - subjects: ['incapsula.com'], - issuers: ['GlobalSign CloudSSL CA - SHA256 - G3'], - ja3: ['6fa3244afc6bb6f9fad207b6b52af26b'], - notAfter: ['2020-04-04T14:05:06.000Z'], - }, - }, - { - cursor: { tiebreaker: null, value: 'fb70d78ffa663a3a4374d841b3288d2de9759566' }, - node: { - _id: 'fb70d78ffa663a3a4374d841b3288d2de9759566', - subjects: ['*.siriusxm.com'], - issuers: ['DigiCert Baltimore CA-2 G2'], - ja3: ['535aca3d99fc247509cd50933cd71d37', '6fa3244afc6bb6f9fad207b6b52af26b'], - notAfter: ['2021-10-27T12:00:00.000Z'], - }, - }, - { - cursor: { tiebreaker: null, value: 'fb59038dcec33ab3a01a6ae60d0835ad0e04ccf0' }, - node: { - _id: 'fb59038dcec33ab3a01a6ae60d0835ad0e04ccf0', - subjects: ['photos.amazon.eu'], - issuers: ['Amazon'], - ja3: ['6fa3244afc6bb6f9fad207b6b52af26b'], - notAfter: ['2020-04-23T12:00:00.000Z'], - }, - }, - { - cursor: { tiebreaker: null, value: 'f9815293c883a6006f0b2d95a4895bdc501fd174' }, - node: { - _id: 'f9815293c883a6006f0b2d95a4895bdc501fd174', - subjects: ['cdn.hbo.com'], - issuers: ['Sectigo RSA Organization Validation Secure Server CA'], - ja3: ['6fa3244afc6bb6f9fad207b6b52af26b'], - notAfter: ['2021-02-10T23:59:59.000Z'], - }, - }, - { - cursor: { tiebreaker: null, value: 'f8db6a69797e383dca2529727369595733123386' }, - node: { - _id: 'f8db6a69797e383dca2529727369595733123386', - subjects: ['www.google.com'], - issuers: ['GTS CA 1O1'], - ja3: ['a111d93cdf31f993c40a8a9ef13e8d7e'], - notAfter: ['2019-12-10T13:32:54.000Z'], - }, - }, -]; - -export const mockRequest = { - body: { - operationName: 'GetTlsQuery', - variables: { - defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - filterQuery: '', - flowTarget: 'source', - inspect: false, - ip: '', - pagination: { activePage: 0, cursorStart: 0, fakePossibleCount: 50, querySize: 10 }, - sort: { field: '_id', direction: 'desc' }, - sourceId: 'default', - timerange: { interval: '12h', from: 1570716261267, to: 1570802661267 }, - }, - query: - 'query GetTlsQuery($sourceId: ID!, $filterQuery: String, $flowTarget: FlowTarget!, $ip: String!, $pagination: PaginationInputPaginated!, $sort: TlsSortField!, $timerange: TimerangeInput!, $defaultIndex: [String!]!, $inspect: Boolean!) {\n source(id: $sourceId) {\n id\n Tls(filterQuery: $filterQuery, flowTarget: $flowTarget, ip: $ip, pagination: $pagination, sort: $sort, timerange: $timerange, defaultIndex: $defaultIndex) {\n totalCount\n edges {\n node {\n _id\n subjects\n ja3\n issuers\n notAfter\n __typename\n }\n cursor {\n value\n __typename\n }\n __typename\n }\n pageInfo {\n activePage\n fakeTotalCount\n showMorePagesIndicator\n __typename\n }\n inspect @include(if: $inspect) {\n dsl\n response\n __typename\n }\n __typename\n }\n __typename\n }\n}\n', - }, -}; - -export const mockResponse = { - took: 92, - timed_out: false, - _shards: { total: 33, successful: 33, skipped: 0, failed: 0 }, - hits: { max_score: null, hits: [] }, - aggregations: { - sha1: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 4597, - buckets: [ - { - key: 'fff8dc95436e0e25ce46b1526a1a547e8cf3bb82', - doc_count: 1, - not_after: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 1572177600000, key_as_string: '2019-10-27T12:00:00.000Z', doc_count: 1 }, - ], - }, - issuers: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'DigiCert SHA2 Secure Server CA', doc_count: 1 }], - }, - subjects: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: '*.1.nflxso.net', doc_count: 1 }], - }, - ja3: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: '95d2dd53a89b334cddd5c22e81e7fe61', doc_count: 1 }], - }, - }, - { - key: 'fd8440c4b20978b173e0910e2639d114f0d405c5', - doc_count: 1, - not_after: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 1580558400000, key_as_string: '2020-02-01T12:00:00.000Z', doc_count: 1 }, - ], - }, - issuers: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'Amazon', doc_count: 1 }], - }, - subjects: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'cogocast.net', doc_count: 1 }], - }, - ja3: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'a111d93cdf31f993c40a8a9ef13e8d7e', doc_count: 1 }], - }, - }, - { - key: 'fcdc16645ebb3386adc96e7ba735c4745709b9dd', - doc_count: 1, - not_after: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 1583531829000, key_as_string: '2020-03-06T21:57:09.000Z', doc_count: 1 }, - ], - }, - issuers: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'Trustwave Organization Validation SHA256 CA, Level 1', doc_count: 1 }, - ], - }, - subjects: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'player-devintever2.mountain.siriusxm.com', doc_count: 1 }], - }, - ja3: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: '6fa3244afc6bb6f9fad207b6b52af26b', doc_count: 1 }], - }, - }, - { - key: 'fccf375789cb7e671502a7b0cc969f218a4b2c70', - doc_count: 1, - not_after: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 1593864000000, key_as_string: '2020-07-04T12:00:00.000Z', doc_count: 1 }, - ], - }, - issuers: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'DigiCert SHA2 Extended Validation Server CA', doc_count: 1 }], - }, - subjects: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'appleid.apple.com', doc_count: 1 }], - }, - ja3: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: '6fa3244afc6bb6f9fad207b6b52af26b', doc_count: 1 }], - }, - }, - { - key: 'fc4a296b706fa18ac50b96f5c0327c69db4a8981', - doc_count: 2, - not_after: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 1585051200000, key_as_string: '2020-03-24T12:00:00.000Z', doc_count: 2 }, - ], - }, - issuers: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'DigiCert SHA2 Extended Validation Server CA', doc_count: 2 }], - }, - subjects: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'itunes.apple.com', doc_count: 2 }], - }, - ja3: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'a441a33aaee795f498d6b764cc78989a', doc_count: 2 }], - }, - }, - { - key: 'fc2cbc41f6a0e9c0118de4fe40f299f7207b797e', - doc_count: 1, - not_after: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 1586009106000, key_as_string: '2020-04-04T14:05:06.000Z', doc_count: 1 }, - ], - }, - issuers: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'GlobalSign CloudSSL CA - SHA256 - G3', doc_count: 1 }], - }, - subjects: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'incapsula.com', doc_count: 1 }], - }, - ja3: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: '6fa3244afc6bb6f9fad207b6b52af26b', doc_count: 1 }], - }, - }, - { - key: 'fb70d78ffa663a3a4374d841b3288d2de9759566', - doc_count: 325, - not_after: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 1635336000000, key_as_string: '2021-10-27T12:00:00.000Z', doc_count: 325 }, - ], - }, - issuers: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'DigiCert Baltimore CA-2 G2', doc_count: 325 }], - }, - subjects: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: '*.siriusxm.com', doc_count: 325 }], - }, - ja3: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: '535aca3d99fc247509cd50933cd71d37', doc_count: 284 }, - { key: '6fa3244afc6bb6f9fad207b6b52af26b', doc_count: 39 }, - ], - }, - }, - { - key: 'fb59038dcec33ab3a01a6ae60d0835ad0e04ccf0', - doc_count: 5, - not_after: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 1587643200000, key_as_string: '2020-04-23T12:00:00.000Z', doc_count: 5 }, - ], - }, - issuers: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'Amazon', doc_count: 5 }], - }, - subjects: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'photos.amazon.eu', doc_count: 5 }], - }, - ja3: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: '6fa3244afc6bb6f9fad207b6b52af26b', doc_count: 5 }], - }, - }, - { - key: 'f9815293c883a6006f0b2d95a4895bdc501fd174', - doc_count: 29, - not_after: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 1613001599000, key_as_string: '2021-02-10T23:59:59.000Z', doc_count: 29 }, - ], - }, - issuers: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 'Sectigo RSA Organization Validation Secure Server CA', doc_count: 29 }, - ], - }, - subjects: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'cdn.hbo.com', doc_count: 29 }], - }, - ja3: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: '6fa3244afc6bb6f9fad207b6b52af26b', doc_count: 26 }], - }, - }, - { - key: 'f8db6a69797e383dca2529727369595733123386', - doc_count: 5, - not_after: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [ - { key: 1575984774000, key_as_string: '2019-12-10T13:32:54.000Z', doc_count: 5 }, - ], - }, - issuers: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'GTS CA 1O1', doc_count: 5 }], - }, - subjects: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'www.google.com', doc_count: 5 }], - }, - ja3: { - doc_count_error_upper_bound: 0, - sum_other_doc_count: 0, - buckets: [{ key: 'a111d93cdf31f993c40a8a9ef13e8d7e', doc_count: 5 }], - }, - }, - ], - }, - count: { value: 364 }, - }, -}; - -export const mockOptions = { - defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - sourceConfiguration: { - fields: { - container: 'docker.container.name', - host: 'beat.hostname', - message: ['message', '@message'], - pod: 'kubernetes.pod.name', - tiebreaker: '_doc', - timestamp: '@timestamp', - }, - }, - timerange: { interval: '12h', to: '2019-10-11T13:51:11.626Z', from: '2019-10-10T13:51:11.626Z' }, - pagination: { activePage: 0, cursorStart: 0, fakePossibleCount: 50, querySize: 10 }, - filterQuery: {}, - fields: [ - 'totalCount', - '_id', - 'subjects', - 'ja3', - 'issuers', - 'notAfter', - 'edges.cursor.value', - 'pageInfo.activePage', - 'pageInfo.fakeTotalCount', - 'pageInfo.showMorePagesIndicator', - 'inspect.dsl', - 'inspect.response', - ], - ip: '', - sort: { field: TlsFields._id, direction: Direction.desc }, - flowTarget: FlowTargetSourceDest.source, -}; diff --git a/x-pack/plugins/security_solution/server/lib/tls/query_tls.dsl.ts b/x-pack/plugins/security_solution/server/lib/tls/query_tls.dsl.ts deleted file mode 100644 index f6921ddcdf508..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/tls/query_tls.dsl.ts +++ /dev/null @@ -1,107 +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 { assertUnreachable } from '../../../common/utility_types'; -import { createQueryFilterClauses } from '../../utils/build_query'; - -import { TlsRequestOptions } from './index'; -import { TlsSortField, Direction, TlsFields } from '../../graphql/types'; - -const getAggs = (querySize: number, sort: TlsSortField) => ({ - count: { - cardinality: { - field: 'tls.server.hash.sha1', - }, - }, - sha1: { - terms: { - field: 'tls.server.hash.sha1', - size: querySize, - order: { - ...getQueryOrder(sort), - }, - }, - aggs: { - issuers: { - terms: { - field: 'tls.server.issuer', - }, - }, - subjects: { - terms: { - field: 'tls.server.subject', - }, - }, - not_after: { - terms: { - field: 'tls.server.not_after', - }, - }, - ja3: { - terms: { - field: 'tls.server.ja3s', - }, - }, - }, - }, -}); - -export const buildTlsQuery = ({ - ip, - sort, - filterQuery, - flowTarget, - pagination: { querySize }, - defaultIndex, - sourceConfiguration: { - fields: { timestamp }, - }, - timerange: { from, to }, -}: TlsRequestOptions) => { - const defaultFilter = [ - ...createQueryFilterClauses(filterQuery), - { - range: { - [timestamp]: { gte: from, lte: to, format: 'strict_date_optional_time' }, - }, - }, - ]; - - const filter = ip ? [...defaultFilter, { term: { [`${flowTarget}.ip`]: ip } }] : defaultFilter; - - const dslQuery = { - allowNoIndices: true, - index: defaultIndex, - ignoreUnavailable: true, - body: { - aggs: { - ...getAggs(querySize, sort), - }, - query: { - bool: { - filter, - }, - }, - size: 0, - track_total_hits: false, - }, - }; - - return dslQuery; -}; - -interface QueryOrder { - _key: Direction; -} - -const getQueryOrder = (sort: TlsSortField): QueryOrder => { - switch (sort.field) { - case TlsFields._id: - return { _key: sort.direction }; - default: - return assertUnreachable(sort.field); - } -}; diff --git a/x-pack/plugins/security_solution/server/lib/tls/types.ts b/x-pack/plugins/security_solution/server/lib/tls/types.ts deleted file mode 100644 index f18ddc04e14a0..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/tls/types.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { FrameworkRequest, RequestBasicOptions } from '../framework'; -import { TlsData } from '../../graphql/types'; - -export interface TlsAdapter { - getTls(request: FrameworkRequest, options: RequestBasicOptions): Promise; -} - -export interface TlsBuckets { - key: string; - timestamp?: { - value: number; - value_as_string: string; - }; - - subjects: { - buckets: Readonly>; - }; - - ja3: { - buckets: Readonly>; - }; - - issuers: { - buckets: Readonly>; - }; - - not_after: { - buckets: Readonly>; - }; -} diff --git a/x-pack/plugins/security_solution/server/lib/types.ts b/x-pack/plugins/security_solution/server/lib/types.ts index ff89512124b66..87e755360285f 100644 --- a/x-pack/plugins/security_solution/server/lib/types.ts +++ b/x-pack/plugins/security_solution/server/lib/types.ts @@ -20,11 +20,9 @@ import { Network } from './network'; import { Overview } from './overview'; import { SourceStatus } from './source_status'; import { Sources } from './sources'; -import { UncommonProcesses } from './uncommon_processes'; import { Note } from './note/saved_object'; import { PinnedEvent } from './pinned_event/saved_object'; import { Timeline } from './timeline/saved_object'; -import { TLS } from './tls'; import { MatrixHistogram } from './matrix_histogram'; export * from './hosts'; @@ -39,9 +37,7 @@ export interface AppDomainLibs { network: Network; kpiNetwork: KpiNetwork; overview: Overview; - uncommonProcesses: UncommonProcesses; kpiHosts: KpiHosts; - tls: TLS; } export interface AppBackendLibs extends AppDomainLibs { @@ -98,6 +94,23 @@ export interface ShardsResponse { successful: number; failed: number; skipped: number; + failures?: ShardError[]; +} + +export interface ShardError { + shard: number; + index: string; + node: string; + reason: { + type: string; + reason: string; + index_uuid: string; + index: string; + caused_by: { + type: string; + reason: string; + }; + }; } export interface Explanation { diff --git a/x-pack/plugins/security_solution/server/lib/uncommon_processes/elasticsearch_adapter.test.ts b/x-pack/plugins/security_solution/server/lib/uncommon_processes/elasticsearch_adapter.test.ts deleted file mode 100644 index 2a15f1fe074f8..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/uncommon_processes/elasticsearch_adapter.test.ts +++ /dev/null @@ -1,265 +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 { UncommonProcessesEdges } from '../../graphql/types'; -import { processFieldsMap } from '../ecs_fields'; - -import { formatUncommonProcessesData, getHosts } from './elasticsearch_adapter'; -import { UncommonProcessBucket, UncommonProcessHit } from './types'; - -describe('elasticsearch_adapter', () => { - describe('#getHosts', () => { - const bucket1: UncommonProcessBucket = { - key: '123', - hosts: { - buckets: [ - { - key: '123', - host: { - hits: { - total: 0, - max_score: 0, - hits: [ - { - _index: 'hit-1', - _type: 'type-1', - _id: 'id-1', - _score: 0, - _source: { - host: { - name: ['host-1'], - id: ['host-id-1'], - }, - }, - }, - ], - }, - }, - }, - ], - }, - process: { - hits: { - total: { - value: 1, - relation: 'eq', - }, - max_score: 5, - hits: [], - }, - }, - }; - const bucket2: UncommonProcessBucket = { - key: '345', - hosts: { - buckets: [ - { - key: '123', - host: { - hits: { - total: 0, - max_score: 0, - hits: [ - { - _index: 'hit-1', - _type: 'type-1', - _id: 'id-1', - _score: 0, - _source: { - host: { - name: ['host-1'], - id: ['host-id-1'], - }, - }, - }, - ], - }, - }, - }, - { - key: '345', - host: { - hits: { - total: 0, - max_score: 0, - hits: [ - { - _index: 'hit-2', - _type: 'type-2', - _id: 'id-2', - _score: 0, - _source: { - host: { - name: ['host-2'], - id: ['host-id-2'], - }, - }, - }, - ], - }, - }, - }, - ], - }, - process: { - hits: { - total: { - value: 1, - relation: 'eq', - }, - max_score: 5, - hits: [], - }, - }, - }; - const bucket3: UncommonProcessBucket = { - key: '789', - hosts: { - buckets: [ - { - key: '789', - host: { - hits: { - total: 0, - max_score: 0, - hits: [ - { - _index: 'hit-9', - _type: 'type-9', - _id: 'id-9', - _score: 0, - _source: { - // @ts-expect-error ts doesn't like seeing the object written this way, but sometimes this is the data we get! - 'host.id': ['host-id-9'], - 'host.name': ['host-9'], - }, - }, - ], - }, - }, - }, - ], - }, - process: { - hits: { - total: { - value: 1, - relation: 'eq', - }, - max_score: 5, - hits: [], - }, - }, - }; - - test('will return a single host correctly', () => { - const hosts = getHosts(bucket1.hosts.buckets); - expect(hosts).toEqual([{ id: ['123'], name: ['host-1'] }]); - }); - - test('will return two hosts correctly', () => { - const hosts = getHosts(bucket2.hosts.buckets); - expect(hosts).toEqual([ - { id: ['123'], name: ['host-1'] }, - { id: ['345'], name: ['host-2'] }, - ]); - }); - - test('will return a dot notation host', () => { - const hosts = getHosts(bucket3.hosts.buckets); - expect(hosts).toEqual([{ id: ['789'], name: ['host-9'] }]); - }); - - test('will return no hosts when given an empty array', () => { - const hosts = getHosts([]); - expect(hosts).toEqual([]); - }); - }); - - describe('#formatUncommonProcessesData', () => { - const hit: UncommonProcessHit = { - _index: 'index-123', - _type: 'type-123', - _id: 'id-123', - _score: 10, - total: { - value: 100, - relation: 'eq', - }, - host: [ - { id: ['host-id-1'], name: ['host-name-1'] }, - { id: ['host-id-1'], name: ['host-name-1'] }, - ], - _source: { - '@timestamp': 'time', - process: { - name: ['process-1'], - title: ['title-1'], - }, - }, - cursor: 'cursor-1', - sort: [0], - }; - - test('it formats a uncommon process data with a source of name correctly', () => { - const fields: readonly string[] = ['process.name']; - const data = formatUncommonProcessesData(fields, hit, processFieldsMap); - const expected: UncommonProcessesEdges = { - cursor: { tiebreaker: null, value: 'cursor-1' }, - node: { - _id: 'id-123', - hosts: [ - { id: ['host-id-1'], name: ['host-name-1'] }, - { id: ['host-id-1'], name: ['host-name-1'] }, - ], - process: { - name: ['process-1'], - }, - instances: 100, - }, - }; - expect(data).toEqual(expected); - }); - - test('it formats a uncommon process data with a source of name and title correctly', () => { - const fields: readonly string[] = ['process.name', 'process.title']; - const data = formatUncommonProcessesData(fields, hit, processFieldsMap); - const expected: UncommonProcessesEdges = { - cursor: { tiebreaker: null, value: 'cursor-1' }, - node: { - _id: 'id-123', - hosts: [ - { id: ['host-id-1'], name: ['host-name-1'] }, - { id: ['host-id-1'], name: ['host-name-1'] }, - ], - instances: 100, - process: { - name: ['process-1'], - title: ['title-1'], - }, - }, - }; - expect(data).toEqual(expected); - }); - - test('it formats a uncommon process data without any data if fields is empty', () => { - const fields: readonly string[] = []; - const data = formatUncommonProcessesData(fields, hit, processFieldsMap); - const expected: UncommonProcessesEdges = { - cursor: { - tiebreaker: null, - value: '', - }, - node: { - _id: '', - hosts: [], - instances: 0, - process: {}, - }, - }; - expect(data).toEqual(expected); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/uncommon_processes/elasticsearch_adapter.ts b/x-pack/plugins/security_solution/server/lib/uncommon_processes/elasticsearch_adapter.ts deleted file mode 100644 index 046823da7cb85..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/uncommon_processes/elasticsearch_adapter.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get, getOr } from 'lodash/fp'; - -import { UncommonProcessesData, UncommonProcessesEdges } from '../../graphql/types'; -import { mergeFieldsWithHit, inspectStringifyObject } from '../../utils/build_query'; -import { processFieldsMap, userFieldsMap } from '../ecs_fields'; -import { FrameworkAdapter, FrameworkRequest, RequestOptionsPaginated } from '../framework'; -import { HostHits, TermAggregation } from '../types'; -import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants'; -import { buildQuery } from './query.dsl'; -import { - UncommonProcessBucket, - UncommonProcessData, - UncommonProcessesAdapter, - UncommonProcessHit, -} from './types'; - -export class ElasticsearchUncommonProcessesAdapter implements UncommonProcessesAdapter { - constructor(private readonly framework: FrameworkAdapter) {} - - public async getUncommonProcesses( - request: FrameworkRequest, - options: RequestOptionsPaginated - ): Promise { - if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { - throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); - } - const dsl = buildQuery(options); - const response = await this.framework.callWithRequest( - request, - 'search', - dsl - ); - const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; - const totalCount = getOr(0, 'aggregations.process_count.value', response); - const buckets = getOr([], 'aggregations.group_by_process.buckets', response); - const hits = getHits(buckets); - - const uncommonProcessesEdges = hits.map((hit) => - formatUncommonProcessesData(options.fields, hit, { ...processFieldsMap, ...userFieldsMap }) - ); - - const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; - const edges = uncommonProcessesEdges.splice(cursorStart, querySize - cursorStart); - const inspect = { - dsl: [inspectStringifyObject(dsl)], - response: [inspectStringifyObject(response)], - }; - - const showMorePagesIndicator = totalCount > fakeTotalCount; - return { - edges, - inspect, - pageInfo: { - activePage: activePage ? activePage : 0, - fakeTotalCount, - showMorePagesIndicator, - }, - totalCount, - }; - } -} - -export const getHits = (buckets: readonly UncommonProcessBucket[]): readonly UncommonProcessHit[] => - buckets.map((bucket: Readonly) => ({ - _id: bucket.process.hits.hits[0]._id, - _index: bucket.process.hits.hits[0]._index, - _type: bucket.process.hits.hits[0]._type, - _score: bucket.process.hits.hits[0]._score, - _source: bucket.process.hits.hits[0]._source, - sort: bucket.process.hits.hits[0].sort, - cursor: bucket.process.hits.hits[0].cursor, - total: bucket.process.hits.total, - host: getHosts(bucket.hosts.buckets), - })); - -export const getHosts = (buckets: ReadonlyArray<{ key: string; host: HostHits }>) => - buckets.map((bucket) => { - const source = get('host.hits.hits[0]._source', bucket); - return { - id: [bucket.key], - name: get('host.name', source), - }; - }); - -export const formatUncommonProcessesData = ( - fields: readonly string[], - hit: UncommonProcessHit, - fieldMap: Readonly> -): UncommonProcessesEdges => - fields.reduce( - (flattenedFields, fieldName) => { - flattenedFields.node._id = hit._id; - flattenedFields.node.instances = getOr(0, 'total.value', hit); - flattenedFields.node.hosts = hit.host; - if (hit.cursor) { - flattenedFields.cursor.value = hit.cursor; - } - return mergeFieldsWithHit(fieldName, flattenedFields, fieldMap, hit); - }, - { - node: { - _id: '', - instances: 0, - process: {}, - hosts: [], - }, - cursor: { - value: '', - tiebreaker: null, - }, - } - ); diff --git a/x-pack/plugins/security_solution/server/lib/uncommon_processes/index.ts b/x-pack/plugins/security_solution/server/lib/uncommon_processes/index.ts deleted file mode 100644 index 0ba0e90f391e1..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/uncommon_processes/index.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 { UncommonProcessesData } from '../../graphql/types'; -import { FrameworkRequest, RequestOptionsPaginated } from '../framework'; -export * from './elasticsearch_adapter'; -import { UncommonProcessesAdapter } from './types'; - -export class UncommonProcesses { - constructor(private readonly adapter: UncommonProcessesAdapter) {} - - public async getUncommonProcesses( - req: FrameworkRequest, - options: RequestOptionsPaginated - ): Promise { - return this.adapter.getUncommonProcesses(req, options); - } -} diff --git a/x-pack/plugins/security_solution/server/lib/uncommon_processes/query.dsl.ts b/x-pack/plugins/security_solution/server/lib/uncommon_processes/query.dsl.ts deleted file mode 100644 index 4563c769cdc31..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/uncommon_processes/query.dsl.ts +++ /dev/null @@ -1,222 +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 { createQueryFilterClauses } from '../../utils/build_query'; -import { reduceFields } from '../../utils/build_query/reduce_fields'; -import { hostFieldsMap, processFieldsMap, userFieldsMap } from '../ecs_fields'; -import { RequestOptionsPaginated } from '../framework'; - -export const buildQuery = ({ - defaultIndex, - fields, - filterQuery, - pagination: { querySize }, - sourceConfiguration: { - fields: { timestamp }, - }, - timerange: { from, to }, -}: RequestOptionsPaginated) => { - const processUserFields = reduceFields(fields, { ...processFieldsMap, ...userFieldsMap }); - const hostFields = reduceFields(fields, hostFieldsMap); - const filter = [ - ...createQueryFilterClauses(filterQuery), - { - range: { - [timestamp]: { - gte: from, - lte: to, - format: 'strict_date_optional_time', - }, - }, - }, - ]; - - const agg = { - process_count: { - cardinality: { - field: 'process.name', - }, - }, - }; - - const dslQuery = { - allowNoIndices: true, - index: defaultIndex, - ignoreUnavailable: true, - body: { - aggregations: { - ...agg, - group_by_process: { - terms: { - size: querySize, - field: 'process.name', - order: [ - { - host_count: 'asc', - }, - { - _count: 'asc', - }, - { - _key: 'asc', - }, - ], - }, - aggregations: { - process: { - top_hits: { - size: 1, - sort: [{ '@timestamp': { order: 'desc' } }], - _source: processUserFields, - }, - }, - host_count: { - cardinality: { - field: 'host.name', - }, - }, - hosts: { - terms: { - field: 'host.name', - }, - aggregations: { - host: { - top_hits: { - size: 1, - _source: hostFields, - }, - }, - }, - }, - }, - }, - }, - query: { - bool: { - should: [ - { - bool: { - filter: [ - { - term: { - 'agent.type': 'auditbeat', - }, - }, - { - term: { - 'event.module': 'auditd', - }, - }, - { - term: { - 'event.action': 'executed', - }, - }, - ], - }, - }, - { - bool: { - filter: [ - { - term: { - 'agent.type': 'auditbeat', - }, - }, - { - term: { - 'event.module': 'system', - }, - }, - { - term: { - 'event.dataset': 'process', - }, - }, - { - term: { - 'event.action': 'process_started', - }, - }, - ], - }, - }, - { - bool: { - filter: [ - { - term: { - 'agent.type': 'winlogbeat', - }, - }, - { - term: { - 'event.code': '4688', - }, - }, - ], - }, - }, - { - bool: { - filter: [ - { - term: { - 'winlog.event_id': 1, - }, - }, - { - term: { - 'winlog.channel': 'Microsoft-Windows-Sysmon/Operational', - }, - }, - ], - }, - }, - { - bool: { - filter: [ - { - term: { - 'event.type': 'process_start', - }, - }, - { - term: { - 'event.category': 'process', - }, - }, - ], - }, - }, - { - bool: { - filter: [ - { - term: { - 'event.category': 'process', - }, - }, - { - term: { - 'event.type': 'start', - }, - }, - ], - }, - }, - ], - minimum_should_match: 1, - filter, - }, - }, - }, - size: 0, - track_total_hits: false, - }; - - return dslQuery; -}; diff --git a/x-pack/plugins/security_solution/server/lib/uncommon_processes/types.ts b/x-pack/plugins/security_solution/server/lib/uncommon_processes/types.ts deleted file mode 100644 index dc60de5963a18..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/uncommon_processes/types.ts +++ /dev/null @@ -1,54 +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 { ProcessEcsFields, UncommonProcessesData } from '../../graphql/types'; -import { FrameworkRequest, RequestOptionsPaginated } from '../framework'; -import { Hit, Hits, HostHits, SearchHit, TotalHit } from '../types'; - -export interface UncommonProcessesAdapter { - getUncommonProcesses( - req: FrameworkRequest, - options: RequestOptionsPaginated - ): Promise; -} - -type StringOrNumber = string | number; -export interface UncommonProcessHit extends Hit { - total: TotalHit; - host: Array<{ - id: string[] | string | null | undefined; - name: string[] | string | null | undefined; - }>; - _source: { - '@timestamp': string; - process: ProcessEcsFields; - }; - cursor: string; - sort: StringOrNumber[]; -} - -export type ProcessHits = Hits; - -export interface UncommonProcessBucket { - key: string; - hosts: { - buckets: Array<{ key: string; host: HostHits }>; - }; - process: ProcessHits; -} - -export interface UncommonProcessData extends SearchHit { - sort: string[]; - aggregations: { - process_count: { - value: number; - }; - group_by_process: { - after_key: string; - buckets: UncommonProcessBucket[]; - }; - }; -} diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.ts index df300c85e300f..08b83b489485a 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/dsl/query.dsl.ts @@ -12,7 +12,7 @@ import { sourceFieldsMap, hostFieldsMap } from '../../../../../../../common/ecs/ import { createQueryFilterClauses } from '../../../../../../utils/build_query'; import { reduceFields } from '../../../../../../utils/build_query/reduce_fields'; -import { authenticationFields } from '../helpers'; +import { authenticationsFields } from '../helpers'; import { extendMap } from '../../../../../../../common/ecs/ecs_fields/extend_map'; export const auditdFieldsMap: Readonly> = { @@ -32,7 +32,7 @@ export const buildQuery = ({ defaultIndex, docValueFields, }: HostAuthenticationsRequestOptions) => { - const esFields = reduceFields(authenticationFields, { ...hostFieldsMap, ...sourceFieldsMap }); + const esFields = reduceFields(authenticationsFields, { ...hostFieldsMap, ...sourceFieldsMap }); const filter = [ ...createQueryFilterClauses(filterQuery), diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts index d61914fda7d06..ce8900a578102 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts @@ -15,7 +15,7 @@ import { StrategyResponseType, } from '../../../../../../common/search_strategy/security_solution'; -export const authenticationFields = [ +export const authenticationsFields = [ '_id', 'failures', 'successes', @@ -31,7 +31,7 @@ export const authenticationFields = [ ]; export const formatAuthenticationData = ( - fields: readonly string[] = authenticationFields, + fields: readonly string[] = authenticationsFields, hit: AuthenticationHit, fieldMap: Readonly> ): AuthenticationsEdges => diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx index a43f53880587a..e09d8de7ba945 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx @@ -20,7 +20,7 @@ import { import { inspectStringifyObject } from '../../../../../utils/build_query'; import { SecuritySolutionFactory } from '../../types'; import { auditdFieldsMap, buildQuery as buildAuthenticationQuery } from './dsl/query.dsl'; -import { authenticationFields, formatAuthenticationData, getHits } from './helpers'; +import { authenticationsFields, formatAuthenticationData, getHits } from './helpers'; export const authentications: SecuritySolutionFactory = { buildDsl: (options: HostAuthenticationsRequestOptions) => { @@ -40,7 +40,7 @@ export const authentications: SecuritySolutionFactory - formatAuthenticationData(authenticationFields, hit, auditdFieldsMap) + formatAuthenticationData(authenticationsFields, hit, auditdFieldsMap) ); const edges = authenticationEdges.splice(cursorStart, querySize - cursorStart); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.test.ts index edcba88a0cd89..44c55ab6e7c9d 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.test.ts @@ -5,13 +5,16 @@ */ import { hostsFactory } from '.'; -import { HostsQueries } from '../../../../../common/search_strategy'; +import { HostsQueries, HostsKpiQueries } from '../../../../../common/search_strategy'; import { allHosts } from './all'; import { hostDetails } from './details'; import { hostOverview } from './overview'; import { firstLastSeenHost } from './last_first_seen'; import { uncommonProcesses } from './uncommon_processes'; import { authentications } from './authentications'; +import { hostsKpiAuthentications } from './kpi/authentications'; +import { hostsKpiHosts } from './kpi/hosts'; +import { hostsKpiUniqueIps } from './kpi/unique_ips'; jest.mock('./all'); jest.mock('./details'); @@ -19,6 +22,9 @@ jest.mock('./overview'); jest.mock('./last_first_seen'); jest.mock('./uncommon_processes'); jest.mock('./authentications'); +jest.mock('./kpi/authentications'); +jest.mock('./kpi/hosts'); +jest.mock('./kpi/unique_ips'); describe('hostsFactory', () => { test('should include correct apis', () => { @@ -29,6 +35,9 @@ describe('hostsFactory', () => { [HostsQueries.firstLastSeen]: firstLastSeenHost, [HostsQueries.uncommonProcesses]: uncommonProcesses, [HostsQueries.authentications]: authentications, + [HostsKpiQueries.kpiAuthentications]: hostsKpiAuthentications, + [HostsKpiQueries.kpiHosts]: hostsKpiHosts, + [HostsKpiQueries.kpiUniqueIps]: hostsKpiUniqueIps, }; expect(hostsFactory).toEqual(expectedHostsFactory); }); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts index 85619cfec62ce..ad6a6182d331b 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts @@ -7,6 +7,7 @@ import { FactoryQueryTypes, HostsQueries, + HostsKpiQueries, } from '../../../../../common/search_strategy/security_solution'; import { SecuritySolutionFactory } from '../types'; @@ -16,12 +17,21 @@ import { hostOverview } from './overview'; import { firstLastSeenHost } from './last_first_seen'; import { uncommonProcesses } from './uncommon_processes'; import { authentications } from './authentications'; +import { hostsKpiAuthentications } from './kpi/authentications'; +import { hostsKpiHosts } from './kpi/hosts'; +import { hostsKpiUniqueIps } from './kpi/unique_ips'; -export const hostsFactory: Record> = { +export const hostsFactory: Record< + HostsQueries | HostsKpiQueries, + SecuritySolutionFactory +> = { [HostsQueries.details]: hostDetails, [HostsQueries.hosts]: allHosts, [HostsQueries.overview]: hostOverview, [HostsQueries.firstLastSeen]: firstLastSeenHost, [HostsQueries.uncommonProcesses]: uncommonProcesses, [HostsQueries.authentications]: authentications, + [HostsKpiQueries.kpiAuthentications]: hostsKpiAuthentications, + [HostsKpiQueries.kpiHosts]: hostsKpiHosts, + [HostsKpiQueries.kpiUniqueIps]: hostsKpiUniqueIps, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/authentications/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/authentications/helpers.ts new file mode 100644 index 0000000000000..513e361b5be05 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/authentications/helpers.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 { + HostsKpiHistogram, + HostsKpiAuthenticationsHistogramCount, + HostsKpiHistogramData, +} from '../../../../../../../common/search_strategy'; + +export const formatAuthenticationsHistogramData = ( + data: Array> +): HostsKpiHistogramData[] | null => + data && data.length > 0 + ? data.map(({ key, count }) => ({ + x: key, + y: count.doc_count, + })) + : null; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/authentications/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/authentications/index.ts new file mode 100644 index 0000000000000..bafc9a3accc6e --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/authentications/index.ts @@ -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 { getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../../src/plugins/data/common'; +import { + HostsKpiQueries, + HostsKpiAuthenticationsStrategyResponse, + HostsKpiAuthenticationsRequestOptions, +} from '../../../../../../../common/search_strategy/security_solution/hosts'; +import { inspectStringifyObject } from '../../../../../../utils/build_query'; +import { SecuritySolutionFactory } from '../../../types'; +import { buildHostsKpiAuthenticationsQuery } from './query.hosts_kpi_authentications.dsl'; +import { formatAuthenticationsHistogramData } from './helpers'; + +export const hostsKpiAuthentications: SecuritySolutionFactory = { + buildDsl: (options: HostsKpiAuthenticationsRequestOptions) => + buildHostsKpiAuthenticationsQuery(options), + parse: async ( + options: HostsKpiAuthenticationsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const inspect = { + dsl: [inspectStringifyObject(buildHostsKpiAuthenticationsQuery(options))], + }; + + const authenticationsSuccessHistogram = getOr( + null, + 'aggregations.authentication_success_histogram.buckets', + response.rawResponse + ); + const authenticationsFailureHistogram = getOr( + null, + 'aggregations.authentication_failure_histogram.buckets', + response.rawResponse + ); + + return { + ...response, + inspect, + authenticationsSuccess: getOr( + null, + 'aggregations.authentication_success.doc_count', + response.rawResponse + ), + authenticationsSuccessHistogram: formatAuthenticationsHistogramData( + authenticationsSuccessHistogram + ), + authenticationsFailure: getOr( + null, + 'aggregations.authentication_failure.doc_count', + response.rawResponse + ), + authenticationsFailureHistogram: formatAuthenticationsHistogramData( + authenticationsFailureHistogram + ), + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/authentications/query.hosts_kpi_authentications.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/authentications/query.hosts_kpi_authentications.dsl.ts new file mode 100644 index 0000000000000..8da5f7f95c5d1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/authentications/query.hosts_kpi_authentications.dsl.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HostsKpiAuthenticationsRequestOptions } from '../../../../../../../common/search_strategy/security_solution/hosts'; +import { createQueryFilterClauses } from '../../../../../../utils/build_query'; + +export const buildHostsKpiAuthenticationsQuery = ({ + filterQuery, + timerange: { from, to }, + defaultIndex, +}: HostsKpiAuthenticationsRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + bool: { + filter: [ + { + term: { + 'event.category': 'authentication', + }, + }, + ], + }, + }, + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const dslQuery = { + index: defaultIndex, + allowNoIndices: true, + ignoreUnavailable: true, + body: { + aggs: { + authentication_success: { + filter: { + term: { + 'event.outcome': 'success', + }, + }, + }, + authentication_success_histogram: { + auto_date_histogram: { + field: '@timestamp', + buckets: '6', + }, + aggs: { + count: { + filter: { + term: { + 'event.outcome': 'success', + }, + }, + }, + }, + }, + authentication_failure: { + filter: { + term: { + 'event.outcome': 'failure', + }, + }, + }, + authentication_failure_histogram: { + auto_date_histogram: { + field: '@timestamp', + buckets: '6', + }, + aggs: { + count: { + filter: { + term: { + 'event.outcome': 'failure', + }, + }, + }, + }, + }, + }, + query: { + bool: { + filter, + }, + }, + size: 0, + track_total_hits: false, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/common/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/common/index.ts new file mode 100644 index 0000000000000..080ef05c99136 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/common/index.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 { + HostsKpiHistogram, + HostsKpiGeneralHistogramCount, + HostsKpiHistogramData, +} from '../../../../../../../common/search_strategy'; + +export const formatGeneralHistogramData = ( + data: Array> +): HostsKpiHistogramData[] | null => + data && data.length > 0 + ? data.map(({ key, count }) => ({ + x: key, + y: count.value, + })) + : null; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/hosts/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/hosts/index.ts new file mode 100644 index 0000000000000..6d91ebf09895e --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/hosts/index.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../../src/plugins/data/common'; +import { + HostsKpiQueries, + HostsKpiHostsStrategyResponse, + HostsKpiHostsRequestOptions, +} from '../../../../../../../common/search_strategy/security_solution/hosts'; +import { inspectStringifyObject } from '../../../../../../utils/build_query'; +import { SecuritySolutionFactory } from '../../../types'; +import { buildHostsKpiHostsQuery } from './query.hosts_kpi_hosts.dsl'; +import { formatGeneralHistogramData } from '../common'; + +export const hostsKpiHosts: SecuritySolutionFactory = { + buildDsl: (options: HostsKpiHostsRequestOptions) => buildHostsKpiHostsQuery(options), + parse: async ( + options: HostsKpiHostsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const inspect = { + dsl: [inspectStringifyObject(buildHostsKpiHostsQuery(options))], + }; + + const hostsHistogram = getOr( + null, + 'aggregations.hosts_histogram.buckets', + response.rawResponse + ); + return { + ...response, + inspect, + hosts: getOr(null, 'aggregations.hosts.value', response.rawResponse), + hostsHistogram: formatGeneralHistogramData(hostsHistogram), + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/hosts/query.hosts_kpi_hosts.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/hosts/query.hosts_kpi_hosts.dsl.ts new file mode 100644 index 0000000000000..704743cc434ed --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/hosts/query.hosts_kpi_hosts.dsl.ts @@ -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 { HostsKpiHostsRequestOptions } from '../../../../../../../common/search_strategy/security_solution/hosts'; +import { createQueryFilterClauses } from '../../../../../../utils/build_query'; + +export const buildHostsKpiHostsQuery = ({ + filterQuery, + timerange: { from, to }, + defaultIndex, +}: HostsKpiHostsRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const dslQuery = { + index: defaultIndex, + allowNoIndices: true, + ignoreUnavailable: true, + body: { + aggregations: { + hosts: { + cardinality: { + field: 'host.name', + }, + }, + hosts_histogram: { + auto_date_histogram: { + field: '@timestamp', + buckets: '6', + }, + aggs: { + count: { + cardinality: { + field: 'host.name', + }, + }, + }, + }, + }, + query: { + bool: { + filter, + }, + }, + size: 0, + track_total_hits: false, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/legacy/server/lib/constants/xpack_info.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/index.ts similarity index 67% rename from x-pack/legacy/server/lib/constants/xpack_info.ts rename to x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/index.ts index c58bb275245b6..f4793ecd53f8f 100644 --- a/x-pack/legacy/server/lib/constants/xpack_info.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/index.ts @@ -4,4 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export const XPACK_INFO_API_DEFAULT_POLL_FREQUENCY_IN_MILLIS = 30001; // 30 seconds +export * from './authentications'; +export * from './common'; +export * from './hosts'; +export * from './unique_ips'; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/unique_ips/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/unique_ips/index.ts new file mode 100644 index 0000000000000..2f890e6fdacca --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/unique_ips/index.ts @@ -0,0 +1,55 @@ +/* + * 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 { getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../../src/plugins/data/common'; +import { + HostsKpiQueries, + HostsKpiUniqueIpsStrategyResponse, + HostsKpiUniqueIpsRequestOptions, +} from '../../../../../../../common/search_strategy/security_solution/hosts'; +import { inspectStringifyObject } from '../../../../../../utils/build_query'; +import { SecuritySolutionFactory } from '../../../types'; +import { buildHostsKpiUniqueIpsQuery } from './query.hosts_kpi_unique_ips.dsl'; +import { formatGeneralHistogramData } from '../common'; + +export const hostsKpiUniqueIps: SecuritySolutionFactory = { + buildDsl: (options: HostsKpiUniqueIpsRequestOptions) => buildHostsKpiUniqueIpsQuery(options), + parse: async ( + options: HostsKpiUniqueIpsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const inspect = { + dsl: [inspectStringifyObject(buildHostsKpiUniqueIpsQuery(options))], + }; + + const uniqueSourceIpsHistogram = getOr( + null, + 'aggregations.unique_source_ips_histogram.buckets', + response.rawResponse + ); + + const uniqueDestinationIpsHistogram = getOr( + null, + 'aggregations.unique_destination_ips_histogram.buckets', + response.rawResponse + ); + + return { + ...response, + inspect, + uniqueSourceIps: getOr(null, 'aggregations.unique_source_ips.value', response.rawResponse), + uniqueSourceIpsHistogram: formatGeneralHistogramData(uniqueSourceIpsHistogram), + uniqueDestinationIps: getOr( + null, + 'aggregations.unique_destination_ips.value', + response.rawResponse + ), + uniqueDestinationIpsHistogram: formatGeneralHistogramData(uniqueDestinationIpsHistogram), + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/unique_ips/query.hosts_kpi_unique_ips.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/unique_ips/query.hosts_kpi_unique_ips.dsl.ts new file mode 100644 index 0000000000000..618c6cb51f666 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/unique_ips/query.hosts_kpi_unique_ips.dsl.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 { HostsKpiUniqueIpsRequestOptions } from '../../../../../../../common/search_strategy/security_solution/hosts'; +import { createQueryFilterClauses } from '../../../../../../utils/build_query'; + +export const buildHostsKpiUniqueIpsQuery = ({ + filterQuery, + timerange: { from, to }, + defaultIndex, +}: HostsKpiUniqueIpsRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const dslQuery = { + index: defaultIndex, + allowNoIndices: true, + ignoreUnavailable: true, + body: { + aggregations: { + unique_source_ips: { + cardinality: { + field: 'source.ip', + }, + }, + unique_source_ips_histogram: { + auto_date_histogram: { + field: '@timestamp', + buckets: '6', + }, + aggs: { + count: { + cardinality: { + field: 'source.ip', + }, + }, + }, + }, + unique_destination_ips: { + cardinality: { + field: 'destination.ip', + }, + }, + unique_destination_ips_histogram: { + auto_date_histogram: { + field: '@timestamp', + buckets: '6', + }, + aggs: { + count: { + cardinality: { + field: 'destination.ip', + }, + }, + }, + }, + }, + query: { + bool: { + filter, + }, + }, + size: 0, + track_total_hits: false, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.test.ts index 096ca570ae852..a6f44c78e5cc4 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.test.ts @@ -7,8 +7,8 @@ import { processFieldsMap } from '../../../../../../common/ecs/ecs_fields'; import { - UncommonProcessesEdges, - UncommonProcessHit, + HostsUncommonProcessesEdges, + HostsUncommonProcessHit, } from '../../../../../../common/search_strategy'; import { formatUncommonProcessesData, getHosts, UncommonProcessBucket } from './helpers'; @@ -183,7 +183,7 @@ describe('helpers', () => { }); describe('#formatUncommonProcessesData', () => { - const hit: UncommonProcessHit = { + const hit: HostsUncommonProcessHit = { _index: 'index-123', _type: 'type-123', _id: 'id-123', @@ -210,7 +210,7 @@ describe('helpers', () => { test('it formats a uncommon process data with a source of name correctly', () => { const fields: readonly string[] = ['process.name']; const data = formatUncommonProcessesData(fields, hit, processFieldsMap); - const expected: UncommonProcessesEdges = { + const expected: HostsUncommonProcessesEdges = { cursor: { tiebreaker: null, value: 'cursor-1' }, node: { _id: 'id-123', @@ -230,7 +230,7 @@ describe('helpers', () => { test('it formats a uncommon process data with a source of name and title correctly', () => { const fields: readonly string[] = ['process.name', 'process.title']; const data = formatUncommonProcessesData(fields, hit, processFieldsMap); - const expected: UncommonProcessesEdges = { + const expected: HostsUncommonProcessesEdges = { cursor: { tiebreaker: null, value: 'cursor-1' }, node: { _id: 'id-123', @@ -251,7 +251,7 @@ describe('helpers', () => { test('it formats a uncommon process data without any data if fields is empty', () => { const fields: readonly string[] = []; const data = formatUncommonProcessesData(fields, hit, processFieldsMap); - const expected: UncommonProcessesEdges = { + const expected: HostsUncommonProcessesEdges = { cursor: { tiebreaker: null, value: '', diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts index 5c3d76175b7e4..20b3f5b05bc87 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/helpers.ts @@ -9,8 +9,8 @@ import { set } from '@elastic/safer-lodash-set/fp'; import { mergeFieldsWithHit } from '../../../../../utils/build_query'; import { ProcessHits, - UncommonProcessesEdges, - UncommonProcessHit, + HostsUncommonProcessesEdges, + HostsUncommonProcessHit, } from '../../../../../../common/search_strategy/security_solution/hosts/uncommon_processes'; import { toArray } from '../../../../helpers/to_array'; import { HostHits } from '../../../../../../common/search_strategy'; @@ -25,7 +25,9 @@ export const uncommonProcessesFields = [ 'hosts.name', ]; -export const getHits = (buckets: readonly UncommonProcessBucket[]): readonly UncommonProcessHit[] => +export const getHits = ( + buckets: readonly UncommonProcessBucket[] +): readonly HostsUncommonProcessHit[] => buckets.map((bucket: Readonly) => ({ _id: bucket.process.hits.hits[0]._id, _index: bucket.process.hits.hits[0]._index, @@ -57,10 +59,10 @@ export const getHosts = (buckets: ReadonlyArray<{ key: string; host: HostHits }> export const formatUncommonProcessesData = ( fields: readonly string[], - hit: UncommonProcessHit, + hit: HostsUncommonProcessHit, fieldMap: Readonly> -): UncommonProcessesEdges => - fields.reduce( +): HostsUncommonProcessesEdges => + fields.reduce( (flattenedFields, fieldName) => { flattenedFields.node._id = hit._id; flattenedFields.node.instances = getOr(0, 'total.value', hit); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.test.ts index a5fa9b459d1bf..5016c8cc38ce4 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.test.ts @@ -6,7 +6,7 @@ import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; -import { HostUncommonProcessesRequestOptions } from '../../../../../../common/search_strategy/security_solution'; +import { HostsUncommonProcessesRequestOptions } from '../../../../../../common/search_strategy/security_solution'; import * as buildQuery from './dsl/query.dsl'; import { uncommonProcesses } from '.'; import { @@ -35,7 +35,7 @@ describe('uncommonProcesses search strategy', () => { ...mockOptions.pagination, querySize: DEFAULT_MAX_TABLE_QUERY_SIZE, }, - } as HostUncommonProcessesRequestOptions; + } as HostsUncommonProcessesRequestOptions; expect(() => { uncommonProcesses.buildDsl(overSizeOptions); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.ts index 5682e63b50ed0..add2cdb76628a 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/uncommon_processes/index.ts @@ -12,8 +12,8 @@ import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants import { HostsQueries } from '../../../../../../common/search_strategy/security_solution'; import { processFieldsMap, userFieldsMap } from '../../../../../../common/ecs/ecs_fields'; import { - HostUncommonProcessesRequestOptions, - HostUncommonProcessesStrategyResponse, + HostsUncommonProcessesRequestOptions, + HostsUncommonProcessesStrategyResponse, } from '../../../../../../common/search_strategy/security_solution/hosts/uncommon_processes'; import { inspectStringifyObject } from '../../../../../utils/build_query'; @@ -23,16 +23,16 @@ import { buildQuery } from './dsl/query.dsl'; import { formatUncommonProcessesData, getHits, uncommonProcessesFields } from './helpers'; export const uncommonProcesses: SecuritySolutionFactory = { - buildDsl: (options: HostUncommonProcessesRequestOptions) => { + buildDsl: (options: HostsUncommonProcessesRequestOptions) => { if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); } return buildQuery(options); }, parse: async ( - options: HostUncommonProcessesRequestOptions, + options: HostsUncommonProcessesRequestOptions, response: IEsSearchResponse - ): Promise => { + ): Promise => { const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; const totalCount = getOr(0, 'aggregations.process_count.value', response.rawResponse); const buckets = getOr([], 'aggregations.group_by_process.buckets', response.rawResponse); diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts index acb00a87bf7d9..5ef0b5375d796 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts @@ -104,7 +104,7 @@ export class SpacesClient { `SpacesClient.getAll(), using RBAC. returning 403/Forbidden. Not authorized for any spaces for ${purpose} purpose.` ); this.auditLogger.spacesAuthorizationFailure(username, 'getAll'); - throw Boom.forbidden(); + throw Boom.forbidden(); // Note: there is a catch for this in `SpacesSavedObjectsClient.find`; if we get rid of this error, remove that too } this.auditLogger.spacesAuthorizationSuccess(username, 'getAll', authorized as string[]); diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts index c9c17d091cd55..f7621f11a1c05 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.test.ts @@ -10,6 +10,8 @@ import { spacesServiceMock } from '../spaces_service/spaces_service.mock'; import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import { SavedObjectTypeRegistry } from 'src/core/server'; import { SpacesClient } from '../lib/spaces_client'; +import { spacesClientMock } from '../lib/spaces_client/spaces_client.mock'; +import Boom from 'boom'; const typeRegistry = new SavedObjectTypeRegistry(); typeRegistry.registerType({ @@ -129,6 +131,34 @@ const ERROR_NAMESPACE_SPECIFIED = 'Spaces currently determines the namespaces'; }); describe('#find', () => { + const EMPTY_RESPONSE = { saved_objects: [], total: 0, per_page: 20, page: 1 }; + + test(`returns empty result if user is unauthorized in this space`, async () => { + const { client, baseClient, spacesService } = await createSpacesSavedObjectsClient(); + const spacesClient = spacesClientMock.create(); + spacesClient.getAll.mockResolvedValue([]); + spacesService.scopedClient.mockResolvedValue(spacesClient); + + const options = Object.freeze({ type: 'foo', namespaces: ['some-ns'] }); + const actualReturnValue = await client.find(options); + + expect(actualReturnValue).toEqual(EMPTY_RESPONSE); + expect(baseClient.find).not.toHaveBeenCalled(); + }); + + test(`returns empty result if user is unauthorized in any space`, async () => { + const { client, baseClient, spacesService } = await createSpacesSavedObjectsClient(); + const spacesClient = spacesClientMock.create(); + spacesClient.getAll.mockRejectedValue(Boom.unauthorized()); + spacesService.scopedClient.mockResolvedValue(spacesClient); + + const options = Object.freeze({ type: 'foo', namespaces: ['some-ns'] }); + const actualReturnValue = await client.find(options); + + expect(actualReturnValue).toEqual(EMPTY_RESPONSE); + expect(baseClient.find).not.toHaveBeenCalled(); + }); + test(`passes options.type to baseClient if valid singular type specified`, async () => { const { client, baseClient } = await createSpacesSavedObjectsClient(); const expectedReturnValue = { diff --git a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts index 4e830d6149537..a65e0431aef92 100644 --- a/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts +++ b/x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import Boom from 'boom'; import { SavedObjectsBaseOptions, SavedObjectsBulkCreateObject, @@ -16,8 +17,9 @@ import { SavedObjectsUpdateOptions, SavedObjectsAddToNamespacesOptions, SavedObjectsDeleteFromNamespacesOptions, + SavedObjectsUtils, ISavedObjectTypeRegistry, -} from 'src/core/server'; +} from '../../../../../src/core/server'; import { SpacesServiceSetup } from '../spaces_service/spaces_service'; import { spaceIdToNamespace } from '../lib/utils/namespace'; import { SpacesClient } from '../lib/spaces_client'; @@ -164,19 +166,26 @@ export class SpacesSavedObjectsClient implements SavedObjectsClientContract { let namespaces = options.namespaces; if (namespaces) { const spacesClient = await this.getSpacesClient; - const availableSpaces = await spacesClient.getAll('findSavedObjects'); - if (namespaces.includes('*')) { - namespaces = availableSpaces.map((space) => space.id); - } else { - namespaces = namespaces.filter((namespace) => - availableSpaces.some((space) => space.id === namespace) - ); - } - // This forbidden error allows this scenario to be consistent - // with the way the SpacesClient behaves when no spaces are authorized - // there. - if (namespaces.length === 0) { - throw this.errors.decorateForbiddenError(new Error()); + + try { + const availableSpaces = await spacesClient.getAll('findSavedObjects'); + if (namespaces.includes('*')) { + namespaces = availableSpaces.map((space) => space.id); + } else { + namespaces = namespaces.filter((namespace) => + availableSpaces.some((space) => space.id === namespace) + ); + } + if (namespaces.length === 0) { + // return empty response, since the user is unauthorized in this space (or these spaces), but we don't return forbidden errors for `find` operations + return SavedObjectsUtils.createEmptyFindResponse(options); + } + } catch (err) { + if (Boom.isBoom(err) && err.output.payload.statusCode === 403) { + // return empty response, since the user is unauthorized in any space, but we don't return forbidden errors for `find` operations + return SavedObjectsUtils.createEmptyFindResponse(options); + } + throw err; } } else { namespaces = [this.spaceId]; diff --git a/x-pack/plugins/transform/common/api_schemas/update_transforms.ts b/x-pack/plugins/transform/common/api_schemas/update_transforms.ts index e303d94ef0536..39c5a9e7baa4f 100644 --- a/x-pack/plugins/transform/common/api_schemas/update_transforms.ts +++ b/x-pack/plugins/transform/common/api_schemas/update_transforms.ts @@ -8,12 +8,18 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { TransformPivotConfig } from '../types/transform'; -import { destSchema, settingsSchema, sourceSchema, syncSchema } from './transforms'; +import { settingsSchema, sourceSchema, syncSchema } from './transforms'; // POST _transform/{transform_id}/_update export const postTransformsUpdateRequestSchema = schema.object({ description: schema.maybe(schema.string()), - dest: schema.maybe(destSchema), + // we cannot reuse `destSchema` because `index` is optional for the update request + dest: schema.maybe( + schema.object({ + index: schema.string(), + pipeline: schema.maybe(schema.string()), + }) + ), frequency: schema.maybe(schema.string()), settings: schema.maybe(settingsSchema), source: schema.maybe(sourceSchema), diff --git a/x-pack/plugins/transform/common/constants.ts b/x-pack/plugins/transform/common/constants.ts index 5efb6f31c1e3f..f2dbc085ab46f 100644 --- a/x-pack/plugins/transform/common/constants.ts +++ b/x-pack/plugins/transform/common/constants.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; -import { LICENSE_TYPE_BASIC, LicenseType } from '../../../legacy/common/constants'; +import { LicenseType } from '../../licensing/common/types'; export const DEFAULT_REFRESH_INTERVAL_MS = 30000; export const MINIMUM_REFRESH_INTERVAL_MS = 1000; @@ -14,7 +14,7 @@ export const PROGRESS_REFRESH_INTERVAL_MS = 2000; export const PLUGIN = { ID: 'transform', - MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_BASIC as LicenseType, + MINIMUM_LICENSE_REQUIRED: 'basic' as LicenseType, getI18nName: (): string => { return i18n.translate('xpack.transform.appName', { defaultMessage: 'Transforms', diff --git a/x-pack/plugins/transform/common/utils/object_utils.ts b/x-pack/plugins/transform/common/utils/object_utils.ts index dfdcd0959260d..f21460e08098d 100644 --- a/x-pack/plugins/transform/common/utils/object_utils.ts +++ b/x-pack/plugins/transform/common/utils/object_utils.ts @@ -17,3 +17,21 @@ export const getNestedProperty = ( return value; }; + +export const setNestedProperty = (obj: Record, accessor: string, value: any) => { + let ref = obj; + const accessors = accessor.split('.'); + const len = accessors.length; + for (let i = 0; i < len - 1; i++) { + const attribute = accessors[i]; + if (ref[attribute] === undefined) { + ref[attribute] = {}; + } + + ref = ref[attribute]; + } + + ref[accessors[len - 1]] = value; + + return obj; +}; diff --git a/x-pack/plugins/transform/public/app/common/request.test.ts b/x-pack/plugins/transform/public/app/common/request.test.ts index 416927c460842..913ea8964eaf0 100644 --- a/x-pack/plugins/transform/public/app/common/request.test.ts +++ b/x-pack/plugins/transform/public/app/common/request.test.ts @@ -157,6 +157,8 @@ describe('Transform: Common', () => { isContinuousModeEnabled: false, transformId: 'the-transform-id', transformDescription: 'the-transform-description', + transformFrequency: '1m', + transformSettingsMaxPageSearchSize: 100, destinationIndex: 'the-destination-index', touched: true, valid: true, @@ -171,10 +173,14 @@ describe('Transform: Common', () => { expect(request).toEqual({ description: 'the-transform-description', dest: { index: 'the-destination-index' }, + frequency: '1m', pivot: { aggregations: { 'the-agg-agg-name': { avg: { field: 'the-agg-field' } } }, group_by: { 'the-group-by-agg-name': { terms: { field: 'the-group-by-field' } } }, }, + settings: { + max_page_search_size: 100, + }, source: { index: ['the-index-pattern-title'], query: { query_string: { default_operator: 'AND', query: 'the-search-query' } }, diff --git a/x-pack/plugins/transform/public/app/common/request.ts b/x-pack/plugins/transform/public/app/common/request.ts index 10f3a63477029..45160d125309d 100644 --- a/x-pack/plugins/transform/public/app/common/request.ts +++ b/x-pack/plugins/transform/public/app/common/request.ts @@ -145,6 +145,10 @@ export const getCreateTransformRequestBody = ( ...(transformDetailsState.transformDescription !== '' ? { description: transformDetailsState.transformDescription } : {}), + // conditionally add optional frequency + ...(transformDetailsState.transformFrequency !== '' + ? { frequency: transformDetailsState.transformFrequency } + : {}), dest: { index: transformDetailsState.destinationIndex, }, @@ -159,6 +163,14 @@ export const getCreateTransformRequestBody = ( }, } : {}), + // conditionally add additional settings + ...(transformDetailsState.transformSettingsMaxPageSearchSize + ? { + settings: { + max_page_search_size: transformDetailsState.transformSettingsMaxPageSearchSize, + }, + } + : {}), }); export function isHttpFetchError(error: any): error is HttpFetchError { diff --git a/x-pack/plugins/transform/public/app/common/validators.test.ts b/x-pack/plugins/transform/public/app/common/validators.test.ts index 3e8140d9d14aa..ad7a7f5872457 100644 --- a/x-pack/plugins/transform/public/app/common/validators.test.ts +++ b/x-pack/plugins/transform/public/app/common/validators.test.ts @@ -4,26 +4,63 @@ * you may not use this file except in compliance with the Elastic License. */ -import { delayValidator } from './validators'; +import { continuousModeDelayValidator, transformFrequencyValidator } from './validators'; -describe('delayValidator', () => { - test('it should allow 0 input without unit', () => { - expect(delayValidator('0')).toBe(true); +describe('continuousModeDelayValidator', () => { + it('should allow 0 input without unit', () => { + expect(continuousModeDelayValidator('0')).toBe(true); }); - test('it should allow 0 input with unit provided', () => { - expect(delayValidator('0s')).toBe(true); + it('should allow 0 input with unit provided', () => { + expect(continuousModeDelayValidator('0s')).toBe(true); }); - test('it should allow integer input with unit provided', () => { - expect(delayValidator('234nanos')).toBe(true); + it('should allow integer input with unit provided', () => { + expect(continuousModeDelayValidator('234nanos')).toBe(true); }); - test('it should not allow integer input without unit provided', () => { - expect(delayValidator('90000')).toBe(false); + it('should not allow integer input without unit provided', () => { + expect(continuousModeDelayValidator('90000')).toBe(false); }); - test('it should not allow float input', () => { - expect(delayValidator('122.5d')).toBe(false); + it('should not allow float input', () => { + expect(continuousModeDelayValidator('122.5d')).toBe(false); + }); +}); + +describe('transformFrequencyValidator', () => { + it('should fail when the input is not an integer and valid time unit.', () => { + expect(transformFrequencyValidator('0')).toBe(false); + expect(transformFrequencyValidator('0.1s')).toBe(false); + expect(transformFrequencyValidator('1.1m')).toBe(false); + expect(transformFrequencyValidator('10.1asdf')).toBe(false); + }); + + it('should only allow s/m/h as time unit.', () => { + expect(transformFrequencyValidator('1ms')).toBe(false); + expect(transformFrequencyValidator('1s')).toBe(true); + expect(transformFrequencyValidator('1m')).toBe(true); + expect(transformFrequencyValidator('1h')).toBe(true); + expect(transformFrequencyValidator('1d')).toBe(false); + }); + + it('should only allow values above 0 and up to 1 hour.', () => { + expect(transformFrequencyValidator('0s')).toBe(false); + expect(transformFrequencyValidator('1s')).toBe(true); + expect(transformFrequencyValidator('3599s')).toBe(true); + expect(transformFrequencyValidator('3600s')).toBe(true); + expect(transformFrequencyValidator('3601s')).toBe(false); + expect(transformFrequencyValidator('10000s')).toBe(false); + + expect(transformFrequencyValidator('0m')).toBe(false); + expect(transformFrequencyValidator('1m')).toBe(true); + expect(transformFrequencyValidator('59m')).toBe(true); + expect(transformFrequencyValidator('60m')).toBe(true); + expect(transformFrequencyValidator('61m')).toBe(false); + expect(transformFrequencyValidator('100m')).toBe(false); + + expect(transformFrequencyValidator('0h')).toBe(false); + expect(transformFrequencyValidator('1h')).toBe(true); + expect(transformFrequencyValidator('2h')).toBe(false); }); }); diff --git a/x-pack/plugins/transform/public/app/common/validators.ts b/x-pack/plugins/transform/public/app/common/validators.ts index a54c885686422..e9b1478489563 100644 --- a/x-pack/plugins/transform/public/app/common/validators.ts +++ b/x-pack/plugins/transform/public/app/common/validators.ts @@ -5,10 +5,54 @@ */ /** - * Validates time delay input. + * Validates continuous mode time delay input. * Doesn't allow floating intervals. * @param value User input value. */ -export function delayValidator(value: string): boolean { +export function continuousModeDelayValidator(value: string): boolean { return value.match(/^(0|\d*(nanos|micros|ms|s|m|h|d))$/) !== null; } + +/** + * Validates transform frequency input. + * Allows time units of s/m/h only. + * Must be above 0 and only up to 1h. + * @param value User input value. + */ +export const transformFrequencyValidator = (value: string): boolean => { + if (typeof value !== 'string' || value === null) { + return false; + } + + // split string by groups of numbers and letters + const regexStr = value.match(/[a-z]+|[^a-z]+/gi); + + // only valid if one group of numbers and one group of letters + if (regexStr === null || (Array.isArray(regexStr) && regexStr.length !== 2)) { + return false; + } + + const valueNumber = +regexStr[0]; + const valueTimeUnit = regexStr[1]; + + // only valid if number is an integer above 0 + if (isNaN(valueNumber) || !Number.isInteger(valueNumber) || valueNumber === 0) { + return false; + } + + // only valid if value is up to 1 hour + return ( + (valueTimeUnit === 's' && valueNumber <= 3600) || + (valueTimeUnit === 'm' && valueNumber <= 60) || + (valueTimeUnit === 'h' && valueNumber === 1) + ); +}; + +/** + * Validates transform max_page_search_size input. + * Must be a number between 10 and 10000. + * @param value User input value. + */ +export function transformSettingsMaxPageSearchSizeValidator(value: number): boolean { + return value >= 10 && value <= 10000; +} diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor/advanced_pivot_editor.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor/advanced_pivot_editor.tsx index 983d36a20e87f..cb888444ca867 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor/advanced_pivot_editor.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor/advanced_pivot_editor.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual } from 'lodash'; +import isEqual from 'lodash/isEqual'; import React, { memo, FC } from 'react'; import { EuiCodeEditor, EuiFormRow } from '@elastic/eui'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx index 30e8c2b594db7..6204aeff0306e 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx @@ -18,7 +18,7 @@ import { EuiSelectOption, } from '@elastic/eui'; -import { cloneDeep } from 'lodash'; +import cloneDeep from 'lodash/cloneDeep'; import { useUpdateEffect } from 'react-use'; import { AggName } from '../../../../../../common/types/aggregations'; import { dictionaryToArray } from '../../../../../../common/types/common'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx index ffd6c30db45f4..75251fce98424 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { StepCreateForm } from './step_create_form'; +import { StepCreateForm, StepCreateFormProps } from './step_create_form'; jest.mock('../../../../../shared_imports'); jest.mock('../../../../../app/app_dependencies'); @@ -15,10 +15,21 @@ jest.mock('../../../../../app/app_dependencies'); describe('Transform: ', () => { test('Minimal initialization', () => { // Arrange - const props = { + const props: StepCreateFormProps = { createIndexPattern: false, transformId: 'the-transform-id', - transformConfig: {}, + transformConfig: { + dest: { + index: 'the-destination-index', + }, + pivot: { + group_by: {}, + aggregations: {}, + }, + source: { + index: 'the-source-index', + }, + }, overrides: { created: false, started: false, indexPatternId: undefined }, onChange() {}, }; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx index 675bd0f9f88ed..418dcf78eda5c 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx @@ -9,13 +9,8 @@ import { i18n } from '@kbn/i18n'; import { EuiButton, - // Module '"@elastic/eui"' has no exported member 'EuiCard'. - // @ts-ignore EuiCard, EuiCopy, - // Module '"@elastic/eui"' has no exported member 'EuiDescribedFormGroup'. - // @ts-ignore - EuiDescribedFormGroup, EuiFlexGrid, EuiFlexGroup, EuiFlexItem, @@ -30,7 +25,10 @@ import { import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; -import type { PutTransformsResponseSchema } from '../../../../../../common/api_schemas/transforms'; +import type { + PutTransformsRequestSchema, + PutTransformsResponseSchema, +} from '../../../../../../common/api_schemas/transforms'; import { isGetTransformsStatsResponseSchema, isPutTransformsResponseSchema, @@ -45,6 +43,7 @@ import { useApi } from '../../../../hooks/use_api'; import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; import { RedirectToTransformManagement } from '../../../../common/navigation'; import { ToastNotificationText } from '../../../../components'; +import { DuplicateIndexPatternError } from '../../../../../../../../../src/plugins/data/public'; export interface StepDetailsExposedState { created: boolean; @@ -60,16 +59,16 @@ export function getDefaultStepCreateState(): StepDetailsExposedState { }; } -interface Props { +export interface StepCreateFormProps { createIndexPattern: boolean; transformId: string; - transformConfig: any; + transformConfig: PutTransformsRequestSchema; overrides: StepDetailsExposedState; timeFieldName?: string | undefined; onChange(s: StepDetailsExposedState): void; } -export const StepCreateForm: FC = React.memo( +export const StepCreateForm: FC = React.memo( ({ createIndexPattern, transformConfig, transformId, onChange, overrides, timeFieldName }) => { const defaults = { ...getDefaultStepCreateState(), ...overrides }; @@ -85,7 +84,6 @@ export const StepCreateForm: FC = React.memo( const deps = useAppDependencies(); const indexPatterns = deps.data.indexPatterns; - const uiSettings = deps.uiSettings; const toastNotifications = useToastNotifications(); useEffect(() => { @@ -191,35 +189,14 @@ export const StepCreateForm: FC = React.memo( const indexPatternName = transformConfig.dest.index; try { - const newIndexPattern = await indexPatterns.make(); - - Object.assign(newIndexPattern, { - id: '', - title: indexPatternName, - timeFieldName, - }); - const id = await newIndexPattern.create(); - - await indexPatterns.clearCache(); - - // id returns false if there's a duplicate index pattern. - if (id === false) { - toastNotifications.addDanger( - i18n.translate('xpack.transform.stepCreateForm.duplicateIndexPatternErrorMessage', { - defaultMessage: - 'An error occurred creating the Kibana index pattern {indexPatternName}: The index pattern already exists.', - values: { indexPatternName }, - }) - ); - setLoading(false); - return; - } - - // check if there's a default index pattern, if not, - // set the newly created one as the default index pattern. - if (!uiSettings.get('defaultIndex')) { - await uiSettings.set('defaultIndex', id); - } + const newIndexPattern = await indexPatterns.createAndSave( + { + title: indexPatternName, + timeFieldName, + }, + false, + true + ); toastNotifications.addSuccess( i18n.translate('xpack.transform.stepCreateForm.createIndexPatternSuccessMessage', { @@ -228,22 +205,32 @@ export const StepCreateForm: FC = React.memo( }) ); - setIndexPatternId(id); + setIndexPatternId(newIndexPattern.id); setLoading(false); return true; } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate('xpack.transform.stepCreateForm.createIndexPatternErrorMessage', { - defaultMessage: - 'An error occurred creating the Kibana index pattern {indexPatternName}:', - values: { indexPatternName }, - }), - text: toMountPoint( - - ), - }); - setLoading(false); - return false; + if (e instanceof DuplicateIndexPatternError) { + toastNotifications.addDanger( + i18n.translate('xpack.transform.stepCreateForm.duplicateIndexPatternErrorMessage', { + defaultMessage: + 'An error occurred creating the Kibana index pattern {indexPatternName}: The index pattern already exists.', + values: { indexPatternName }, + }) + ); + } else { + toastNotifications.addDanger({ + title: i18n.translate('xpack.transform.stepCreateForm.createIndexPatternErrorMessage', { + defaultMessage: + 'An error occurred creating the Kibana index pattern {indexPatternName}:', + values: { indexPatternName }, + }), + text: toMountPoint( + + ), + }); + setLoading(false); + return false; + } } }; @@ -267,8 +254,11 @@ export const StepCreateForm: FC = React.memo( ) { const percent = getTransformProgress({ - id: transformConfig.id, - config: transformConfig, + id: transformId, + config: { + ...transformConfig, + id: transformId, + }, stats: stats.transforms[0], }) || 0; setProgressPercentComplete(percent); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts index 1523a0d9a89f9..f00102a2f87c0 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual } from 'lodash'; +import isEqual from 'lodash/isEqual'; import { Dictionary } from '../../../../../../../common/types/common'; import { PivotSupportedAggs } from '../../../../../../../common/types/pivot_aggs'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx index d59f99192621c..f27d50ac2773c 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useContext, useEffect, useState } from 'react'; import { EuiComboBox, EuiFormRow } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { debounce } from 'lodash'; +import debounce from 'lodash/debounce'; import { useUpdateEffect } from 'react-use'; import { i18n } from '@kbn/i18n'; import { isEsSearchResponse } from '../../../../../../../../../common/api_schemas/type_guards'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts index 9a67e7af15543..1b6324bf30eaa 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; import { EuiComboBoxOptionOption } from '@elastic/eui'; import { IndexPattern, KBN_FIELD_TYPES, } from '../../../../../../../../../../src/plugins/data/public'; +import { getNestedProperty } from '../../../../../../../common/utils/object_utils'; + import { DropDownLabel, DropDownOption, @@ -42,7 +43,7 @@ export function getPivotDropdownOptions(indexPattern: IndexPattern) { fields.forEach((field) => { // Group by - const availableGroupByAggs: [] = get(pivotGroupByFieldSupport, field.type); + const availableGroupByAggs: [] = getNestedProperty(pivotGroupByFieldSupport, field.type); if (availableGroupByAggs !== undefined) { availableGroupByAggs.forEach((groupByAgg) => { @@ -63,7 +64,7 @@ export function getPivotDropdownOptions(indexPattern: IndexPattern) { // Aggregations const aggOption: DropDownOption = { label: field.name, options: [] }; - const availableAggs: [] = get(pivotAggsFieldSupport, field.type); + const availableAggs: [] = getNestedProperty(pivotAggsFieldSupport, field.type); if (availableAggs !== undefined) { availableAggs.forEach((agg) => { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx index 43d4f11cffc9d..00ab516f625fe 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx @@ -8,7 +8,16 @@ import React, { Fragment, FC, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiLink, EuiSwitch, EuiFieldText, EuiForm, EuiFormRow, EuiSelect } from '@elastic/eui'; +import { + EuiAccordion, + EuiLink, + EuiSwitch, + EuiFieldText, + EuiForm, + EuiFormRow, + EuiSelect, + EuiSpacer, +} from '@elastic/eui'; import { KBN_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/common'; import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; @@ -35,7 +44,11 @@ import { isTransformIdValid, } from '../../../../common'; import { EsIndexName, IndexPatternTitle } from './common'; -import { delayValidator } from '../../../../common/validators'; +import { + continuousModeDelayValidator, + transformFrequencyValidator, + transformSettingsMaxPageSearchSizeValidator, +} from '../../../../common/validators'; import { StepDefineExposedState } from '../step_define/common'; import { dictionaryToArray } from '../../../../../../common/types/common'; @@ -48,11 +61,15 @@ export interface StepDetailsExposedState { touched: boolean; transformId: TransformId; transformDescription: string; + transformFrequency: string; + transformSettingsMaxPageSearchSize: number; valid: boolean; - indexPatternDateField?: string | undefined; + indexPatternTimeField?: string | undefined; } const defaultContinuousModeDelay = '60s'; +const defaultTransformFrequency = '1m'; +const defaultTransformSettingsMaxPageSearchSize = 500; export function getDefaultStepDetailsState(): StepDetailsExposedState { return { @@ -62,10 +79,12 @@ export function getDefaultStepDetailsState(): StepDetailsExposedState { isContinuousModeEnabled: false, transformId: '', transformDescription: '', + transformFrequency: defaultTransformFrequency, + transformSettingsMaxPageSearchSize: defaultTransformSettingsMaxPageSearchSize, destinationIndex: '', touched: false, valid: false, - indexPatternDateField: undefined, + indexPatternTimeField: undefined, }; } @@ -113,8 +132,10 @@ export const StepDetailsForm: FC = React.memo( // Index pattern state const [indexPatternTitles, setIndexPatternTitles] = useState([]); const [createIndexPattern, setCreateIndexPattern] = useState(defaults.createIndexPattern); - const [previewDateColumns, setPreviewDateColumns] = useState([]); - const [indexPatternDateField, setIndexPatternDateField] = useState(); + const [indexPatternAvailableTimeFields, setIndexPatternAvailableTimeFields] = useState< + string[] + >([]); + const [indexPatternTimeField, setIndexPatternTimeField] = useState(); const onTimeFieldChanged = React.useCallback( (e: React.ChangeEvent) => { @@ -125,11 +146,11 @@ export const StepDetailsForm: FC = React.memo( } // Find the time field based on the selected value // this is to account for undefined when user chooses not to use a date field - const timeField = previewDateColumns.find((col) => col === value); + const timeField = indexPatternAvailableTimeFields.find((col) => col === value); - setIndexPatternDateField(timeField); + setIndexPatternTimeField(timeField); }, - [setIndexPatternDateField, previewDateColumns] + [setIndexPatternTimeField, indexPatternAvailableTimeFields] ); // Continuous mode state @@ -158,12 +179,12 @@ export const StepDetailsForm: FC = React.memo( if (isPostTransformsPreviewResponseSchema(transformPreview)) { const properties = transformPreview.generated_dest_index.mappings.properties; - const datetimeColumns: string[] = Object.keys(properties).filter( + const timeFields: string[] = Object.keys(properties).filter( (col) => properties[col].type === 'date' ); - setPreviewDateColumns(datetimeColumns); - setIndexPatternDateField(datetimeColumns[0]); + setIndexPatternAvailableTimeFields(timeFields); + setIndexPatternTimeField(timeFields[0]); } else { toastNotifications.addDanger({ title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingTransformPreview', { @@ -237,7 +258,7 @@ export const StepDetailsForm: FC = React.memo( isContinuousModeAvailable ? dateFieldNames[0] : '' ); const [continuousModeDelay, setContinuousModeDelay] = useState(defaults.continuousModeDelay); - const isContinuousModeDelayValid = delayValidator(continuousModeDelay); + const isContinuousModeDelayValid = continuousModeDelayValidator(continuousModeDelay); const transformIdExists = transformIds.some((id) => transformId === id); const transformIdEmpty = transformId === ''; @@ -248,10 +269,22 @@ export const StepDetailsForm: FC = React.memo( const indexNameValid = isValidIndexName(destinationIndex); const indexPatternTitleExists = indexPatternTitles.some((name) => destinationIndex === name); + const [transformFrequency, setTransformFrequency] = useState(defaults.transformFrequency); + const isTransformFrequencyValid = transformFrequencyValidator(transformFrequency); + + const [transformSettingsMaxPageSearchSize, setTransformSettingsMaxPageSearchSize] = useState( + defaults.transformSettingsMaxPageSearchSize + ); + const isTransformSettingsMaxPageSearchSizeValid = transformSettingsMaxPageSearchSizeValidator( + transformSettingsMaxPageSearchSize + ); + const valid = !transformIdEmpty && transformIdValid && !transformIdExists && + isTransformFrequencyValid && + isTransformSettingsMaxPageSearchSizeValid && !indexNameEmpty && indexNameValid && (!indexPatternTitleExists || !createIndexPattern) && @@ -266,10 +299,12 @@ export const StepDetailsForm: FC = React.memo( isContinuousModeEnabled, transformId, transformDescription, + transformFrequency, + transformSettingsMaxPageSearchSize, destinationIndex, touched: true, valid, - indexPatternDateField, + indexPatternTimeField, }); // custom comparison /* eslint-disable react-hooks/exhaustive-deps */ @@ -280,9 +315,11 @@ export const StepDetailsForm: FC = React.memo( isContinuousModeEnabled, transformId, transformDescription, + transformFrequency, + transformSettingsMaxPageSearchSize, destinationIndex, valid, - indexPatternDateField, + indexPatternTimeField, /* eslint-enable react-hooks/exhaustive-deps */ ]); @@ -413,13 +450,15 @@ export const StepDetailsForm: FC = React.memo( data-test-subj="transformCreateIndexPatternSwitch" /> - {createIndexPattern && !indexPatternTitleExists && previewDateColumns.length > 0 && ( - - )} + {createIndexPattern && + !indexPatternTitleExists && + indexPatternAvailableTimeFields.length > 0 && ( + + )} = React.memo( )} + + + + + + setTransformFrequency(e.target.value)} + aria-label={i18n.translate('xpack.transform.stepDetailsForm.frequencyAriaLabel', { + defaultMessage: 'Choose a frequency.', + })} + isInvalid={!isTransformFrequencyValid} + data-test-subj="transformFrequencyInput" + /> + + + + + setTransformSettingsMaxPageSearchSize(parseInt(e.target.value, 10)) + } + aria-label={i18n.translate( + 'xpack.transform.stepDetailsForm.maxPageSearchSizeAriaLabel', + { + defaultMessage: 'Choose a maximum page search size.', + } + )} + isInvalid={!isTransformFrequencyValid} + data-test-subj="transformMaxPageSearchSizeInput" + /> + +
); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx index bd6dc8f709e99..45cd8aa465522 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_summary.tsx @@ -8,83 +8,110 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiFieldText, EuiFormRow, EuiSelect } from '@elastic/eui'; +import { EuiAccordion, EuiFormRow, EuiSpacer } from '@elastic/eui'; import { StepDetailsExposedState } from './step_details_form'; -export const StepDetailsSummary: FC = React.memo( - ({ +export const StepDetailsSummary: FC = React.memo((props) => { + const { continuousModeDateField, createIndexPattern, isContinuousModeEnabled, transformId, transformDescription, + transformFrequency, + transformSettingsMaxPageSearchSize, destinationIndex, touched, - indexPatternDateField, - }) => { - if (touched === false) { - return null; - } + indexPatternTimeField, + } = props; - const destinationIndexHelpText = createIndexPattern - ? i18n.translate('xpack.transform.stepDetailsSummary.createIndexPatternMessage', { - defaultMessage: 'A Kibana index pattern will be created for this transform.', - }) - : ''; + if (touched === false) { + return null; + } - return ( -
- - - + const destinationIndexHelpText = createIndexPattern + ? i18n.translate('xpack.transform.stepDetailsSummary.createIndexPatternMessage', { + defaultMessage: 'A Kibana index pattern will be created for this transform.', + }) + : ''; + + return ( +
+ + {transformId} + + + {transformDescription !== '' && ( - + {transformDescription} + )} + + + {destinationIndex} + + {createIndexPattern && indexPatternTimeField !== undefined && indexPatternTimeField !== '' && ( - + {indexPatternTimeField} + )} + + {isContinuousModeEnabled && ( - + {continuousModeDateField} + )} - {isContinuousModeEnabled && ( - - - + + + - ); - } -); + paddingSize="s" + > + + {transformFrequency} + + + {transformSettingsMaxPageSearchSize} + + +
+ ); +}); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_time_field.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_time_field.tsx index 2572b3ee57d7d..8ee2093a1e802 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_time_field.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_time_field.tsx @@ -10,20 +10,20 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; interface Props { - previewDateColumns: string[]; - indexPatternDateField: string | undefined; + indexPatternAvailableTimeFields: string[]; + indexPatternTimeField: string | undefined; onTimeFieldChanged: (e: React.ChangeEvent) => void; } export const StepDetailsTimeField: FC = ({ - previewDateColumns, - indexPatternDateField, + indexPatternAvailableTimeFields, + indexPatternTimeField, onTimeFieldChanged, }) => { const noTimeFieldLabel = i18n.translate( 'xpack.transform.stepDetailsForm.noTimeFieldOptionLabel', { - defaultMessage: "I don't want to use the Time Filter", + defaultMessage: "I don't want to use the time filter", } ); @@ -42,26 +42,26 @@ export const StepDetailsTimeField: FC = ({ } helpText={ } > ({ text })), + ...indexPatternAvailableTimeFields.map((text) => ({ text })), disabledDividerOption, noTimeFieldOption, ]} - value={indexPatternDateField} + value={indexPatternTimeField} onChange={onTimeFieldChanged} - data-test-subj="transformIndexPatternDateFieldSelect" + data-test-subj="transformIndexPatternTimeFieldSelect" /> ); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx index 0ca018972cac9..2724470896833 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx @@ -165,7 +165,7 @@ export const Wizard: FC = React.memo(({ cloneConfig, searchItems }) transformConfig={transformConfig} onChange={setStepCreateState} overrides={stepCreateState} - timeFieldName={stepDetailsState.indexPatternDateField} + timeFieldName={stepDetailsState.indexPatternTimeField} /> ) : ( diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx index 5836898755224..a566afcfa6556 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx @@ -6,7 +6,7 @@ import React, { FC } from 'react'; -import { EuiForm } from '@elastic/eui'; +import { EuiForm, EuiAccordion, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -33,26 +33,11 @@ export const EditTransformFlyoutForm: FC = ({ onChange={(value) => dispatch({ field: 'description', value })} value={formFields.description.value} /> - dispatch({ field: 'docsPerSecond', value })} - value={formFields.docsPerSecond.value} - /> + = ({ defaultMessage: 'Frequency', })} onChange={(value) => dispatch({ field: 'frequency', value })} - placeholder="1m" + placeholder={i18n.translate( + 'xpack.transform.transformList.editFlyoutFormFrequencyPlaceholderText', + { + defaultMessage: 'Default: {defaultValue}', + values: { defaultValue: formFields.frequency.defaultValue }, + } + )} value={formFields.frequency.value} /> + + + + + dispatch({ field: 'destinationIndex', value })} + value={formFields.destinationIndex.value} + /> + + dispatch({ field: 'destinationPipeline', value })} + value={formFields.destinationPipeline.value} + /> + + + + + +
+ dispatch({ field: 'docsPerSecond', value })} + value={formFields.docsPerSecond.value} + /> + + dispatch({ field: 'maxPageSearchSize', value })} + value={formFields.maxPageSearchSize.value} + placeholder={i18n.translate( + 'xpack.transform.transformList.editFlyoutFormMaxPageSearchSizePlaceholderText', + { + defaultMessage: 'Default: {defaultValue}', + values: { defaultValue: formFields.maxPageSearchSize.defaultValue }, + } + )} + /> +
+
); }; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.ts index 12e60c2af5556..b7f82388b02bc 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.ts +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.ts @@ -11,8 +11,8 @@ import { formReducerFactory, frequencyValidator, getDefaultState, - numberAboveZeroValidator, - FormField, + integerAboveZeroValidator, + stringValidator, } from './use_edit_transform_flyout'; const getTransformConfigMock = (): TransformPivotConfig => ({ @@ -45,79 +45,149 @@ const getTransformConfigMock = (): TransformPivotConfig => ({ description: 'the-description', }); -const getDescriptionFieldMock = (value = ''): FormField => ({ - isOptional: true, - value, - errorMessages: [], - validator: 'string', -}); - -const getDocsPerSecondFieldMock = (value = ''): FormField => ({ - isOptional: true, - value, - errorMessages: [], - validator: 'numberAboveZero', -}); - -const getFrequencyFieldMock = (value = ''): FormField => ({ - isOptional: true, - value, - errorMessages: [], - validator: 'frequency', -}); - describe('Transform: applyFormFieldsToTransformConfig()', () => { - test('should exclude unchanged form fields', () => { + it('should exclude unchanged form fields', () => { const transformConfigMock = getTransformConfigMock(); - const updateConfig = applyFormFieldsToTransformConfig(transformConfigMock, { - description: getDescriptionFieldMock(transformConfigMock.description), - docsPerSecond: getDocsPerSecondFieldMock(), - frequency: getFrequencyFieldMock(), - }); + const formState = getDefaultState(transformConfigMock); + + const updateConfig = applyFormFieldsToTransformConfig( + transformConfigMock, + formState.formFields + ); // This case will return an empty object. In the actual UI, this case should not happen // because the Update-Button will be disabled when no form field was changed. expect(Object.keys(updateConfig)).toHaveLength(0); expect(updateConfig.description).toBe(undefined); + // Destination index `index` attribute is nested under `dest` so we're just checking against that. + expect(updateConfig.dest).toBe(undefined); // `docs_per_second` is nested under `settings` so we're just checking against that. expect(updateConfig.settings).toBe(undefined); expect(updateConfig.frequency).toBe(undefined); }); - test('should include previously nonexisting attributes', () => { + it('should include previously nonexisting attributes', () => { const { description, frequency, ...transformConfigMock } = getTransformConfigMock(); - const updateConfig = applyFormFieldsToTransformConfig(transformConfigMock, { - description: getDescriptionFieldMock('the-new-description'), - docsPerSecond: getDocsPerSecondFieldMock('10'), - frequency: getFrequencyFieldMock('1m'), + const formState = getDefaultState({ + ...transformConfigMock, + description: 'the-new-description', + dest: { + index: 'the-new-destination-index', + }, + frequency: '10m', + settings: { + docs_per_second: 10, + }, }); - expect(Object.keys(updateConfig)).toHaveLength(3); + const updateConfig = applyFormFieldsToTransformConfig( + transformConfigMock, + formState.formFields + ); + + expect(Object.keys(updateConfig)).toHaveLength(4); expect(updateConfig.description).toBe('the-new-description'); + expect(updateConfig.dest?.index).toBe('the-new-destination-index'); expect(updateConfig.settings?.docs_per_second).toBe(10); - expect(updateConfig.frequency).toBe('1m'); + expect(updateConfig.frequency).toBe('10m'); }); - test('should only include changed form fields', () => { + it('should only include changed form fields', () => { const transformConfigMock = getTransformConfigMock(); - const updateConfig = applyFormFieldsToTransformConfig(transformConfigMock, { - description: getDescriptionFieldMock('the-updated-description'), - docsPerSecond: getDocsPerSecondFieldMock(), - frequency: getFrequencyFieldMock(), + + const formState = getDefaultState({ + ...transformConfigMock, + description: 'the-updated-description', + dest: { + index: 'the-updated-destination-index', + pipeline: 'the-updated-destination-index', + }, }); - expect(Object.keys(updateConfig)).toHaveLength(1); + const updateConfig = applyFormFieldsToTransformConfig( + transformConfigMock, + formState.formFields + ); + + expect(Object.keys(updateConfig)).toHaveLength(2); expect(updateConfig.description).toBe('the-updated-description'); + expect(updateConfig.dest?.index).toBe('the-updated-destination-index'); // `docs_per_second` is nested under `settings` so we're just checking against that. expect(updateConfig.settings).toBe(undefined); expect(updateConfig.frequency).toBe(undefined); }); + + it('should include dependent form fields', () => { + const transformConfigMock = getTransformConfigMock(); + + const formState = getDefaultState({ + ...transformConfigMock, + dest: { + ...transformConfigMock.dest, + pipeline: 'the-updated-destination-index', + }, + }); + + const updateConfig = applyFormFieldsToTransformConfig( + transformConfigMock, + formState.formFields + ); + expect(Object.keys(updateConfig)).toHaveLength(1); + // It should include the dependent unchanged destination index + expect(updateConfig.dest?.index).toBe(transformConfigMock.dest.index); + expect(updateConfig.dest?.pipeline).toBe('the-updated-destination-index'); + }); + + it('should include the destination index when pipeline is unset', () => { + const transformConfigMock = { + ...getTransformConfigMock(), + dest: { + index: 'the-untouched-destination-index', + pipeline: 'the-original-pipeline', + }, + }; + + const formState = getDefaultState({ + ...transformConfigMock, + dest: { + ...transformConfigMock.dest, + pipeline: '', + }, + }); + + const updateConfig = applyFormFieldsToTransformConfig( + transformConfigMock, + formState.formFields + ); + expect(Object.keys(updateConfig)).toHaveLength(1); + // It should include the dependent unchanged destination index + expect(updateConfig.dest?.index).toBe(transformConfigMock.dest.index); + expect(typeof updateConfig.dest?.pipeline).toBe('undefined'); + }); + + it('should exclude unrelated dependent form fields', () => { + const transformConfigMock = getTransformConfigMock(); + + const formState = getDefaultState({ + ...transformConfigMock, + description: 'the-updated-description', + }); + + const updateConfig = applyFormFieldsToTransformConfig( + transformConfigMock, + formState.formFields + ); + expect(Object.keys(updateConfig)).toHaveLength(1); + // It should exclude the dependent unchanged destination section + expect(typeof updateConfig.dest).toBe('undefined'); + expect(updateConfig.description).toBe('the-updated-description'); + }); }); describe('Transform: formReducerFactory()', () => { - test('field updates should trigger form validation', () => { + it('field updates should trigger form validation', () => { const transformConfigMock = getTransformConfigMock(); const reducer = formReducerFactory(transformConfigMock); @@ -150,52 +220,73 @@ describe('Transform: formReducerFactory()', () => { }); }); +describe('Transfom: stringValidator()', () => { + it('should allow an empty string for optional fields', () => { + expect(stringValidator('')).toHaveLength(0); + }); + + it('should not allow an empty string for required fields', () => { + expect(stringValidator('', false)).toHaveLength(1); + }); +}); + describe('Transform: frequencyValidator()', () => { - test('it should only allow values between 1s and 1h', () => { - // frequencyValidator() returns an array of error messages so - // an array with a length of 0 means a successful validation. + const transformFrequencyValidator = (arg: string) => frequencyValidator(arg).length === 0; - // invalid - expect(frequencyValidator(0)).toHaveLength(1); - expect(frequencyValidator('0')).toHaveLength(1); - expect(frequencyValidator('0s')).toHaveLength(1); - expect(frequencyValidator(1)).toHaveLength(1); - expect(frequencyValidator('1')).toHaveLength(1); - expect(frequencyValidator('1ms')).toHaveLength(1); - expect(frequencyValidator('1d')).toHaveLength(1); - expect(frequencyValidator('60s')).toHaveLength(1); - expect(frequencyValidator('60m')).toHaveLength(1); - expect(frequencyValidator('60h')).toHaveLength(1); - expect(frequencyValidator('2h')).toHaveLength(1); - expect(frequencyValidator('h2')).toHaveLength(1); - expect(frequencyValidator('2h2')).toHaveLength(1); - expect(frequencyValidator('h2h')).toHaveLength(1); + it('should fail when the input is not an integer and valid time unit.', () => { + expect(transformFrequencyValidator('0')).toBe(false); + expect(transformFrequencyValidator('0.1s')).toBe(false); + expect(transformFrequencyValidator('1.1m')).toBe(false); + expect(transformFrequencyValidator('10.1asdf')).toBe(false); + }); - // valid - expect(frequencyValidator('1s')).toHaveLength(0); - expect(frequencyValidator('1m')).toHaveLength(0); - expect(frequencyValidator('1h')).toHaveLength(0); - expect(frequencyValidator('10s')).toHaveLength(0); - expect(frequencyValidator('10m')).toHaveLength(0); - expect(frequencyValidator('59s')).toHaveLength(0); - expect(frequencyValidator('59m')).toHaveLength(0); + it('should only allow s/m/h as time unit.', () => { + expect(transformFrequencyValidator('1ms')).toBe(false); + expect(transformFrequencyValidator('1s')).toBe(true); + expect(transformFrequencyValidator('1m')).toBe(true); + expect(transformFrequencyValidator('1h')).toBe(true); + expect(transformFrequencyValidator('1d')).toBe(false); + }); + + it('should only allow values above 0 and up to 1 hour.', () => { + expect(transformFrequencyValidator('0s')).toBe(false); + expect(transformFrequencyValidator('1s')).toBe(true); + expect(transformFrequencyValidator('3599s')).toBe(true); + expect(transformFrequencyValidator('3600s')).toBe(true); + expect(transformFrequencyValidator('3601s')).toBe(false); + expect(transformFrequencyValidator('10000s')).toBe(false); + + expect(transformFrequencyValidator('0m')).toBe(false); + expect(transformFrequencyValidator('1m')).toBe(true); + expect(transformFrequencyValidator('59m')).toBe(true); + expect(transformFrequencyValidator('60m')).toBe(true); + expect(transformFrequencyValidator('61m')).toBe(false); + expect(transformFrequencyValidator('100m')).toBe(false); + + expect(transformFrequencyValidator('0h')).toBe(false); + expect(transformFrequencyValidator('1h')).toBe(true); + expect(transformFrequencyValidator('2h')).toBe(false); }); }); -describe('Transform: numberValidator()', () => { - test('it should only allow numbers', () => { - // numberValidator() returns an array of error messages so +describe('Transform: integerAboveZeroValidator()', () => { + it('should only allow integers above zero', () => { + // integerAboveZeroValidator() returns an array of error messages so // an array with a length of 0 means a successful validation. // invalid - expect(numberAboveZeroValidator('a-string')).toHaveLength(1); - expect(numberAboveZeroValidator('0s')).toHaveLength(1); - expect(numberAboveZeroValidator('1m')).toHaveLength(1); - expect(numberAboveZeroValidator(-1)).toHaveLength(1); - expect(numberAboveZeroValidator(0)).toHaveLength(1); + expect(integerAboveZeroValidator('a-string')).toHaveLength(1); + expect(integerAboveZeroValidator('0s')).toHaveLength(1); + expect(integerAboveZeroValidator('1m')).toHaveLength(1); + expect(integerAboveZeroValidator('1.')).toHaveLength(1); + expect(integerAboveZeroValidator('1..')).toHaveLength(1); + expect(integerAboveZeroValidator('1.0')).toHaveLength(1); + expect(integerAboveZeroValidator(-1)).toHaveLength(1); + expect(integerAboveZeroValidator(0)).toHaveLength(1); + expect(integerAboveZeroValidator(0.1)).toHaveLength(1); // valid - expect(numberAboveZeroValidator(1)).toHaveLength(0); - expect(numberAboveZeroValidator('1')).toHaveLength(0); + expect(integerAboveZeroValidator(1)).toHaveLength(0); + expect(integerAboveZeroValidator('1')).toHaveLength(0); }); }); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts index d622a7e9cc040..c3430ed94c628 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts @@ -4,17 +4,70 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEqual } from 'lodash'; +import isEqual from 'lodash/isEqual'; +import merge from 'lodash/merge'; + import { useReducer } from 'react'; import { i18n } from '@kbn/i18n'; import { PostTransformsUpdateRequestSchema } from '../../../../../../common/api_schemas/update_transforms'; import { TransformPivotConfig } from '../../../../../../common/types/transform'; +import { getNestedProperty, setNestedProperty } from '../../../../../../common/utils/object_utils'; + +// This custom hook uses nested reducers to provide a generic framework to manage form state +// and apply it to a final possibly nested configuration object suitable for passing on +// directly to an API call. For now this is only used for the transform edit form. +// Once we apply the functionality to other places, e.g. the transform creation wizard, +// the generic framework code in this file should be moved to a dedicated location. + +// The outer most level reducer defines a flat structure of names for form fields. +// This is a flat structure regardless of whether the final request object will be nested. +// For example, `destinationIndex` and `destinationPipeline` will later be nested under `dest`. +interface EditTransformFlyoutFieldsState { + [key: string]: FormField; + description: FormField; + destinationIndex: FormField; + destinationPipeline: FormField; + frequency: FormField; + docsPerSecond: FormField; +} + +// The inner reducers apply validation based on supplied attributes of each field. +export interface FormField { + formFieldName: string; + configFieldName: string; + defaultValue: string; + dependsOn: string[]; + errorMessages: string[]; + isNullable: boolean; + isOptional: boolean; + validator: keyof typeof validate; + value: string; + valueParser: (value: string) => any; +} + +// The reducers and utility functions in this file provide the following features: +// - getDefaultState() +// Sets up the initial form state. It supports overrides to apply a pre-existing configuration. +// The implementation of this function is the only one that's specifically required to define +// the features of the transform edit form. All other functions are generic and could be reused +// in the future for other forms. +// +// - formReducerFactory() / formFieldReducer() +// These nested reducers take care of updating and validating the form state. +// +// - applyFormFieldsToTransformConfig() (iterates over getUpdateValue()) +// Once a user hits the update button, these functions take care of extracting the information +// necessary to create the update request. They take into account whether a field needs to +// be included at all in the request (for example, if it hadn't been changed). +// The code is also able to identify relationships/dependencies between form fields. +// For example, if the `pipeline` field was changed, it's necessary to make the `index` +// field part of the request, otherwise the update would fail. // A Validator function takes in a value to check and returns an array of error messages. // If no messages (empty array) get returned, the value is valid. -type Validator = (arg: any) => string[]; +type Validator = (value: any, isOptional?: boolean) => string[]; // Note on the form validation and input components used: // All inputs use `EuiFieldText` which means all form values will be treated as strings. @@ -25,22 +78,48 @@ type Validator = (arg: any) => string[]; const numberAboveZeroNotValidErrorMessage = i18n.translate( 'xpack.transform.transformList.editFlyoutFormNumberNotValidErrorMessage', { - defaultMessage: 'Value needs to be a number above zero.', + defaultMessage: 'Value needs to be an integer above zero.', + } +); +export const integerAboveZeroValidator: Validator = (value) => + !isNaN(value) && Number.isInteger(+value) && +value > 0 && !(value + '').includes('.') + ? [] + : [numberAboveZeroNotValidErrorMessage]; + +const numberRange10To10000NotValidErrorMessage = i18n.translate( + 'xpack.transform.transformList.editFlyoutFormNumberRange10To10000NotValidErrorMessage', + { + defaultMessage: 'Value needs to be an integer between 10 and 10000.', } ); -export const numberAboveZeroValidator: Validator = (arg) => - !isNaN(arg) && parseInt(arg, 10) > 0 ? [] : [numberAboveZeroNotValidErrorMessage]; +export const integerRange10To10000Validator: Validator = (value) => + integerAboveZeroValidator(value).length === 0 && +value >= 10 && +value <= 10000 + ? [] + : [numberRange10To10000NotValidErrorMessage]; -// The way the current form is set up, this validator is just a sanity check, -// it should never trigger an error, because `EuiFieldText` always returns a string. +const requiredErrorMessage = i18n.translate( + 'xpack.transform.transformList.editFlyoutFormRequiredErrorMessage', + { + defaultMessage: 'Required field.', + } +); const stringNotValidErrorMessage = i18n.translate( 'xpack.transform.transformList.editFlyoutFormStringNotValidErrorMessage', { defaultMessage: 'Value needs to be of type string.', } ); -const stringValidator: Validator = (arg) => - typeof arg === 'string' ? [] : [stringNotValidErrorMessage]; +export const stringValidator: Validator = (value, isOptional = true) => { + if (typeof value !== 'string') { + return [stringNotValidErrorMessage]; + } + + if (value.length === 0 && !isOptional) { + return [requiredErrorMessage]; + } + + return []; +}; // Only allow frequencies in the form of 1s/1h etc. const frequencyNotValidErrorMessage = i18n.translate( @@ -57,55 +136,59 @@ export const frequencyValidator: Validator = (arg) => { // split string by groups of numbers and letters const regexStr = arg.match(/[a-z]+|[^a-z]+/gi); - return ( - // only valid if one group of numbers and one group of letters - regexStr !== null && - regexStr.length === 2 && - // only valid if time unit is one of s/m/h - ['s', 'm', 'h'].includes(regexStr[1]) && - // only valid if number is between 1 and 59 - parseInt(regexStr[0], 10) > 0 && - parseInt(regexStr[0], 10) < 60 && - // if time unit is 'h' then number must not be higher than 1 - !(parseInt(regexStr[0], 10) > 1 && regexStr[1] === 'h') - ? [] - : [frequencyNotValidErrorMessage] - ); -}; + // only valid if one group of numbers and one group of letters + if (regexStr === null || (Array.isArray(regexStr) && regexStr.length !== 2)) { + return [frequencyNotValidErrorMessage]; + } -type Validators = 'string' | 'frequency' | 'numberAboveZero'; + const valueNumber = +regexStr[0]; + const valueTimeUnit = regexStr[1]; + + // only valid if number is an integer above 0 + if (isNaN(valueNumber) || !Number.isInteger(valueNumber) || valueNumber === 0) { + return [frequencyNotValidErrorMessage]; + } -type Validate = { - [key in Validators]: Validator; + // only valid if value is up to 1 hour + return (valueTimeUnit === 's' && valueNumber <= 3600) || + (valueTimeUnit === 'm' && valueNumber <= 60) || + (valueTimeUnit === 'h' && valueNumber === 1) + ? [] + : [frequencyNotValidErrorMessage]; }; -const validate: Validate = { +const validate = { string: stringValidator, frequency: frequencyValidator, - numberAboveZero: numberAboveZeroValidator, -}; + integerAboveZero: integerAboveZeroValidator, + integerRange10To10000: integerRange10To10000Validator, +} as const; -export interface FormField { - errorMessages: string[]; - isOptional: boolean; - validator: keyof Validate; - value: string; -} +export const initializeField = ( + formFieldName: string, + configFieldName: string, + config: TransformPivotConfig, + overloads?: Partial +): FormField => { + const defaultValue = overloads?.defaultValue !== undefined ? overloads.defaultValue : ''; + const rawValue = getNestedProperty(config, configFieldName, undefined); + const value = rawValue !== null && rawValue !== undefined ? rawValue.toString() : ''; -const defaultField: FormField = { - errorMessages: [], - isOptional: true, - validator: 'string', - value: '', + return { + formFieldName, + configFieldName, + defaultValue, + dependsOn: [], + errorMessages: [], + isNullable: false, + isOptional: true, + validator: 'string', + value, + valueParser: (v) => v, + ...(overloads !== undefined ? { ...overloads } : {}), + }; }; -interface EditTransformFlyoutFieldsState { - [key: string]: FormField; - description: FormField; - frequency: FormField; - docsPerSecond: FormField; -} - export interface EditTransformFlyoutState { formFields: EditTransformFlyoutFieldsState; isFormTouched: boolean; @@ -119,48 +202,95 @@ interface Action { value: string; } +// Takes a value from form state and applies it to the structure +// of the expected final configuration request object. +// Considers options like if a value is nullable or optional. +const getUpdateValue = ( + attribute: keyof EditTransformFlyoutFieldsState, + config: TransformPivotConfig, + formState: EditTransformFlyoutFieldsState, + enforceFormValue = false +) => { + const formStateAttribute = formState[attribute]; + const fallbackValue = formStateAttribute.isNullable ? null : formStateAttribute.defaultValue; + + const formValue = + formStateAttribute.value !== '' + ? formStateAttribute.valueParser(formStateAttribute.value) + : fallbackValue; + + const configValue = getNestedProperty(config, formStateAttribute.configFieldName, fallbackValue); + + // only get depending values if we're not already in a call to get depending values. + const dependsOnConfig: PostTransformsUpdateRequestSchema = + enforceFormValue === false + ? formStateAttribute.dependsOn.reduce((_dependsOnConfig, dependsOnField) => { + return merge( + { ..._dependsOnConfig }, + getUpdateValue(dependsOnField, config, formState, true) + ); + }, {}) + : {}; + + if (formValue === formStateAttribute.defaultValue && formStateAttribute.isOptional) { + return formValue !== configValue ? dependsOnConfig : {}; + } + + return formValue !== configValue || enforceFormValue + ? setNestedProperty(dependsOnConfig, formStateAttribute.configFieldName, formValue) + : {}; +}; + // Takes in the form configuration and returns a // request object suitable to be sent to the // transform update API endpoint. export const applyFormFieldsToTransformConfig = ( config: TransformPivotConfig, - { description, docsPerSecond, frequency }: EditTransformFlyoutFieldsState -): PostTransformsUpdateRequestSchema => { - // if the input field was left empty, - // fall back to the default value of `null` - // which will disable throttling. - const docsPerSecondFormValue = - docsPerSecond.value !== '' ? parseInt(docsPerSecond.value, 10) : null; - const docsPerSecondConfigValue = config.settings?.docs_per_second ?? null; - - return { - // set the values only if they changed from the default - // and actually differ from the previous value. - ...(!(config.frequency === undefined && frequency.value === '') && - config.frequency !== frequency.value - ? { frequency: frequency.value } - : {}), - ...(!(config.description === undefined && description.value === '') && - config.description !== description.value - ? { description: description.value } - : {}), - ...(docsPerSecondFormValue !== docsPerSecondConfigValue - ? { settings: { docs_per_second: docsPerSecondFormValue } } - : {}), - }; -}; + formState: EditTransformFlyoutFieldsState +): PostTransformsUpdateRequestSchema => + // Iterates over all form fields and only if necessary applies them to + // the request object used for updating the transform. + Object.keys(formState).reduce( + (updateConfig, field) => merge({ ...updateConfig }, getUpdateValue(field, config, formState)), + {} + ); // Takes in a transform configuration and returns // the default state to populate the form. export const getDefaultState = (config: TransformPivotConfig): EditTransformFlyoutState => ({ formFields: { - description: { ...defaultField, value: config?.description ?? '' }, - frequency: { ...defaultField, value: config?.frequency ?? '', validator: 'frequency' }, - docsPerSecond: { - ...defaultField, - value: config?.settings?.docs_per_second?.toString() ?? '', - validator: 'numberAboveZero', - }, + // top level attributes + description: initializeField('description', 'description', config), + frequency: initializeField('frequency', 'frequency', config, { + defaultValue: '1m', + validator: 'frequency', + }), + + // dest.* + destinationIndex: initializeField('destinationIndex', 'dest.index', config, { + dependsOn: ['destinationPipeline'], + isOptional: false, + }), + destinationPipeline: initializeField('destinationPipeline', 'dest.pipeline', config, { + dependsOn: ['destinationIndex'], + }), + + // settings.* + docsPerSecond: initializeField('docsPerSecond', 'settings.docs_per_second', config, { + isNullable: true, + validator: 'integerAboveZero', + valueParser: (v) => (v === '' ? null : +v), + }), + maxPageSearchSize: initializeField( + 'maxPageSearchSize', + 'settings.max_page_search_size', + config, + { + defaultValue: '500', + validator: 'integerRange10To10000', + valueParser: (v) => +v, + } + ), }, isFormTouched: false, isFormValid: true, @@ -180,7 +310,7 @@ const formFieldReducer = (state: FormField, value: string): FormField => { errorMessages: state.isOptional && typeof value === 'string' && value.length === 0 ? [] - : validate[state.validator](value), + : validate[state.validator](value, state.isOptional), value, }; }; @@ -191,6 +321,8 @@ const formFieldReducer = (state: FormField, value: string): FormField => { // - sets `isFormValid` to have a flag if any of the form fields contains an error. export const formReducerFactory = (config: TransformPivotConfig) => { const defaultState = getDefaultState(config); + const defaultFieldValues = Object.values(defaultState.formFields).map((f) => f.value); + return (state: EditTransformFlyoutState, { field, value }: Action): EditTransformFlyoutState => { const formFields = { ...state.formFields, @@ -200,7 +332,10 @@ export const formReducerFactory = (config: TransformPivotConfig) => { return { ...state, formFields, - isFormTouched: !isEqual(defaultState.formFields, formFields), + isFormTouched: !isEqual( + defaultFieldValues, + Object.values(formFields).map((f) => f.value) + ), isFormValid: isFormValid(formFields), }; }; diff --git a/x-pack/plugins/transform/server/routes/api/error_utils.ts b/x-pack/plugins/transform/server/routes/api/error_utils.ts index 269cd28c4bda6..ef5927651df88 100644 --- a/x-pack/plugins/transform/server/routes/api/error_utils.ts +++ b/x-pack/plugins/transform/server/routes/api/error_utils.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { boomify, isBoom } from 'boom'; +import Boom from 'boom'; import { i18n } from '@kbn/i18n'; @@ -76,10 +76,65 @@ export function fillResultsWithTimeouts({ results, id, items, action }: Params) } export function wrapError(error: any): CustomHttpResponseOptions { - const boom = isBoom(error) ? error : boomify(error, { statusCode: error.status }); + const boom = Boom.isBoom(error) ? error : Boom.boomify(error, { statusCode: error.status }); return { body: boom, headers: boom.output.headers, statusCode: boom.output.statusCode, }; } + +function extractCausedByChain( + causedBy: Record = {}, + accumulator: string[] = [] +): string[] { + const { reason, caused_by } = causedBy; // eslint-disable-line @typescript-eslint/naming-convention + + if (reason) { + accumulator.push(reason); + } + + if (caused_by) { + return extractCausedByChain(caused_by, accumulator); + } + + return accumulator; +} + +/** + * Wraps an error thrown by the ES JS client into a Boom error response and returns it + * + * @param err Object Error thrown by ES JS client + * @param statusCodeToMessageMap Object Optional map of HTTP status codes => error messages + * @return Object Boom error response + */ +export function wrapEsError(err: any, statusCodeToMessageMap: Record = {}) { + const { statusCode, response } = err; + + const { + error: { + root_cause = [], // eslint-disable-line @typescript-eslint/naming-convention + caused_by = {}, // eslint-disable-line @typescript-eslint/naming-convention + } = {}, + } = JSON.parse(response); + + // If no custom message if specified for the error's status code, just + // wrap the error as a Boom error response, include the additional information from ES, and return it + if (!statusCodeToMessageMap[statusCode]) { + const boomError = Boom.boomify(err, { statusCode }); + + // The caused_by chain has the most information so use that if it's available. If not then + // settle for the root_cause. + const causedByChain = extractCausedByChain(caused_by); + const defaultCause = root_cause.length ? extractCausedByChain(root_cause[0]) : undefined; + + // @ts-expect-error cause is not defined on payload type + boomError.output.payload.cause = causedByChain.length ? causedByChain : defaultCause; + return boomError; + } + + // Otherwise, use the custom message to create a Boom error response and + // return it + const message = statusCodeToMessageMap[statusCode]; + return new Boom(message, { statusCode }); +} diff --git a/x-pack/plugins/transform/server/routes/api/field_histograms.ts b/x-pack/plugins/transform/server/routes/api/field_histograms.ts index 88352ec4af129..636af095e0053 100644 --- a/x-pack/plugins/transform/server/routes/api/field_histograms.ts +++ b/x-pack/plugins/transform/server/routes/api/field_histograms.ts @@ -9,8 +9,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { wrapEsError } from '../../../../../legacy/server/lib/create_router/error_wrappers'; - import { indexPatternTitleSchema, IndexPatternTitleSchema, @@ -24,7 +22,7 @@ import { RouteDependencies } from '../../types'; import { addBasePath } from '../index'; -import { wrapError } from './error_utils'; +import { wrapError, wrapEsError } from './error_utils'; export function registerFieldHistogramsRoutes({ router, license }: RouteDependencies) { router.post( diff --git a/x-pack/plugins/transform/server/routes/api/transforms.ts b/x-pack/plugins/transform/server/routes/api/transforms.ts index 3d2018eb5801f..31b2c2285a764 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms.ts @@ -12,7 +12,6 @@ import { SavedObjectsClientContract, LegacyAPICaller, } from 'kibana/server'; -import { wrapEsError } from '../../../../../legacy/server/lib/create_router/error_wrappers'; import { TRANSFORM_STATE } from '../../../common/constants'; import { TransformId } from '../../../common/types/transform'; @@ -54,7 +53,7 @@ import { RouteDependencies } from '../../types'; import { addBasePath } from '../index'; -import { isRequestTimeout, fillResultsWithTimeouts, wrapError } from './error_utils'; +import { isRequestTimeout, fillResultsWithTimeouts, wrapError, wrapEsError } from './error_utils'; import { registerTransformsAuditMessagesRoutes } from './transforms_audit_messages'; import { IIndexPattern } from '../../../../../../src/plugins/data/common/index_patterns'; diff --git a/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts b/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts index f01b2bdb73fd5..20cb6ffb4978b 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts @@ -6,13 +6,12 @@ import { transformIdParamSchema, TransformIdParamSchema } from '../../../common/api_schemas/common'; import { AuditMessage, TransformMessage } from '../../../common/types/messages'; -import { wrapEsError } from '../../../../../legacy/server/lib/create_router/error_wrappers'; import { RouteDependencies } from '../../types'; import { addBasePath } from '../index'; -import { wrapError } from './error_utils'; +import { wrapError, wrapEsError } from './error_utils'; const ML_DF_NOTIFICATION_INDEX_PATTERN = '.transform-notifications-read'; const SIZE = 500; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1223b3f42f8cd..ed66d56d552a5 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -499,6 +499,9 @@ "core.ui.securityNavList.label": "セキュリティ", "core.ui.welcomeErrorMessage": "Elasticが正常に読み込まれませんでした。詳細はサーバーアウトプットを確認してください。", "core.ui.welcomeMessage": "Elasticの読み込み中", + "core.status.greenTitle": "緑", + "core.status.redTitle": "赤", + "core.status.yellowTitle": "黄色", "dashboard.actions.toggleExpandPanelMenuItem.expandedDisplayName": "最小化", "dashboard.actions.toggleExpandPanelMenuItem.notExpandedDisplayName": "全画面", "dashboard.addExistingVisualizationLinkText": "既存のユーザーを追加", @@ -798,8 +801,6 @@ "data.indexPatterns.ensureDefaultIndexPattern.bannerLabel": "Kibanaでデータの可視化と閲覧を行うには、Elasticsearchからデータを取得するためのインデックスパターンの作成が必要です。", "data.indexPatterns.fetchFieldErrorTitle": "インデックスパターンのフィールド取得中にエラーが発生 {title} (ID: {id})", "data.indexPatterns.unableWriteLabel": "インデックスパターンを書き込めません!このインデックスパターンへの最新の変更を取得するには、ページを更新してください。", - "data.indexPatterns.unknownFieldErrorMessage": "インデックスパターン「{title}」のフィールド「{name}」が不明なフィールドタイプを使用しています。", - "data.indexPatterns.unknownFieldHeader": "不明なフィールドタイプ {type}", "data.noDataPopover.content": "この時間範囲にはデータが含まれていません表示するフィールドを増やし、グラフを作成するには、時間範囲を広げるか、調整してください。", "data.noDataPopover.dismissAction": "今後表示しない", "data.noDataPopover.subtitle": "ヒント", @@ -1462,7 +1463,6 @@ "discover.uninitializedText": "クエリを作成、フィルターを追加、または[更新]をクリックして、現在のクエリの結果を取得します。", "discover.uninitializedTitle": "検索開始", "discover.valueIsNotConfiguredIndexPatternIDWarningTitle": "{stateVal}は設定されたインデックスパターンIDではありません", - "embeddableApi.actions.applyFilterActionTitle": "現在のビューにフィルターを適用", "embeddableApi.addPanel.createNewDefaultOption": "新規作成...", "embeddableApi.addPanel.displayName": "パネルの追加", "embeddableApi.addPanel.noMatchingObjectsMessage": "一致するオブジェクトが見つかりませんでした。", @@ -2926,9 +2926,6 @@ "savedObjectsManagement.view.viewItemButtonLabel": "{title}を表示", "savedObjectsManagement.view.viewItemTitle": "{title}を表示", "usageCollection.stats.notReadyMessage": "まだ統計が準備できていません。後程再試行してください", - "core.status.greenTitle": "緑", - "core.status.redTitle": "赤", - "core.status.yellowTitle": "黄色", "share.advancedSettings.csv.quoteValuesText": "csvエクスポートに値を引用するかどうかです", "share.advancedSettings.csv.quoteValuesTitle": "CSVの値を引用", "share.advancedSettings.csv.separatorText": "エクスポートされた値をこの文字列で区切ります", @@ -4756,10 +4753,6 @@ "xpack.apm.metadataTable.section.urlLabel": "URL", "xpack.apm.metadataTable.section.userAgentLabel": "ユーザーエージェント", "xpack.apm.metadataTable.section.userLabel": "ユーザー", - "xpack.apm.metrics.durationByCountryMap.avgPageLoadByCountryLabel": "国ごとの平均ページ読み込み時間の分布", - "xpack.apm.metrics.durationByCountryMap.RegionMapChart.ToolTip.avgPageLoadDuration": "平均ページ読み込み時間:", - "xpack.apm.metrics.durationByCountryMap.RegionMapChart.ToolTip.countPageLoads": "{docCount} ページの読み込み", - "xpack.apm.metrics.pageLoadCharts.avgPageLoadByBrowser": "ブラウザごとの平均ページ読み込み時間の分布", "xpack.apm.metrics.plot.noDataLabel": "この時間範囲のデータがありません。", "xpack.apm.metrics.transactionChart.machineLearningLabel": "機械学習:", "xpack.apm.metrics.transactionChart.machineLearningTooltip": "平均期間の周りのストリームには予測バウンドが表示されます。異常スコアが>= 75の場合、注釈が表示されます。", @@ -4811,8 +4804,6 @@ "xpack.apm.serviceMap.avgMemoryUsagePopoverStat": "メモリー使用状況(平均)", "xpack.apm.serviceMap.avgReqPerMinutePopoverMetric": "1分あたりのリクエスト(平均)", "xpack.apm.serviceMap.avgTransDurationPopoverStat": "トランザクションの長さ(平均)", - "xpack.apm.serviceMap.betaBadge": "ベータ", - "xpack.apm.serviceMap.betaTooltipMessage": "現在、この機能はベータです。不具合を見つけた場合やご意見がある場合、サポートに問い合わせるか、またはディスカッションフォーラムにご報告ください。", "xpack.apm.serviceMap.center": "中央", "xpack.apm.serviceMap.download": "ダウンロード", "xpack.apm.serviceMap.emptyBanner.docsLink": "詳細はドキュメントをご覧ください", @@ -8822,7 +8813,6 @@ "xpack.infra.registerFeatures.logsDescription": "ログをリアルタイムでストリーするか、コンソール式の UI で履歴ビューをスクロールします。", "xpack.infra.registerFeatures.logsTitle": "ログ", "xpack.infra.sampleDataLinkLabel": "ログ", - "xpack.infra.savedView.changeView": "ビューの変更", "xpack.infra.savedView.currentView": "現在のビュー", "xpack.infra.savedView.defaultViewNameHosts": "デフォルトビュー", "xpack.infra.savedView.errorOnCreate.duplicateViewName": "その名前のビューは既に存在します", @@ -9296,15 +9286,7 @@ "xpack.ingestManager.setupPage.missingRequirementsKibanaTitle": "Kibana構成で、次の項目を有効にします。", "xpack.ingestManager.setupPage.tlsFlagText": "{kibanaSecurityLink}.{securityFlag}を{true}に設定します。開発目的では、危険な代替として{tlsFlag}を{true}に設定して、{tlsLink}を無効化できます。", "xpack.ingestManager.setupPage.tlsLink": "TLS", - "xpack.ingestManager.unenrollAgents.confirmModal.cancelButtonLabel": "キャンセル", - "xpack.ingestManager.unenrollAgents.confirmModal.confirmButtonLabel": "登録解除", - "xpack.ingestManager.unenrollAgents.confirmModal.deleteMultipleTitle": "{count, plural, one {# エージェント} other {# エージェント}}の登録を解除しますか?", - "xpack.ingestManager.unenrollAgents.confirmModal.deleteSingleTitle": "エージェント「{id}」の登録を解除しますか?", - "xpack.ingestManager.unenrollAgents.confirmModal.forceDeleteSingleTitle": "強制的にエージェント「{id}」の登録を解除しますか?", - "xpack.ingestManager.unenrollAgents.confirmModal.loadingButtonLabel": "読み込み中...", - "xpack.ingestManager.unenrollAgents.fatalErrorNotificationTitle": "エージェントの登録解除エラー", - "xpack.ingestManager.unenrollAgents.successForceSingleNotificationTitle": "エージェント「{id}」の登録を解除しました", - "xpack.ingestManager.unenrollAgents.successSingleNotificationTitle": "エージェント「{id}」を登録解除しています", + "xpack.ingestManager.unenrollAgents.cancelButtonLabel": "キャンセル", "xpack.ingestPipelines.app.checkingPrivilegesDescription": "権限を確認中…", "xpack.ingestPipelines.app.checkingPrivilegesErrorMessage": "サーバーからユーザー特権を取得中にエラーが発生。", "xpack.ingestPipelines.app.deniedPrivilegeDescription": "Ingest Pipelinesを使用するには、{privilegesCount, plural, one {このクラスター特権} other {これらのクラスター特権}}が必要です:{missingPrivileges}。", @@ -10534,8 +10516,6 @@ "xpack.ml.calendarsList.deleteCalendars.deletingCalendarSuccessNotificationMessage": "{messageId} が選択されました", "xpack.ml.calendarsList.deleteCalendarsModal.cancelButtonLabel": "キャンセル", "xpack.ml.calendarsList.deleteCalendarsModal.deleteButtonLabel": "削除", - "xpack.ml.calendarsList.deleteCalendarsModal.deleteCalendarsDescription": "{calendarsCount, plural, one {このカレンダー} other {これらのカレンダー}}を削除しますか?{calendarsList}", - "xpack.ml.calendarsList.deleteCalendarsModal.deleteCalendarTitle": "カレンダーの削除", "xpack.ml.calendarsList.errorWithLoadingListOfCalendarsErrorMessage": "カレンダーのリストの読み込み中にエラーが発生しました。", "xpack.ml.calendarsList.table.allJobsLabel": "すべてのジョブに適用", "xpack.ml.calendarsList.table.deleteButtonLabel": "削除", @@ -12594,7 +12574,6 @@ "xpack.monitoring.cluster.overview.esPanel.versionLabel": "バージョン", "xpack.monitoring.cluster.overview.esPanel.versionNotAvailableDescription": "N/A", "xpack.monitoring.cluster.overview.esPanel.warnLogsTooltipText": "警告ログの数です", - "xpack.monitoring.cluster.overview.healthStatusDescription": "ヘルス: {status}", "xpack.monitoring.cluster.overview.kibanaPanel.connectionsLabel": "接続", "xpack.monitoring.cluster.overview.kibanaPanel.instancesCountLinkAriaLabel": "Kibana インスタンス: {instancesCount}", "xpack.monitoring.cluster.overview.kibanaPanel.instancesCountLinkLabel": "インスタンス: {instancesCount}", @@ -12713,8 +12692,6 @@ "xpack.monitoring.elasticsearch.nodeDetailStatus.transportAddress": "トランスポートアドレス", "xpack.monitoring.elasticsearch.nodeDetailStatus.typeLabel": "タイプ", "xpack.monitoring.elasticsearch.nodes.alertsColumnTitle": "アラート", - "xpack.monitoring.elasticsearch.nodes.cells.maxText": "最高 {metric}", - "xpack.monitoring.elasticsearch.nodes.cells.minText": "最低 {metric}", "xpack.monitoring.elasticsearch.nodes.cpuThrottlingColumnTitle": "CPU スロットル", "xpack.monitoring.elasticsearch.nodes.cpuUsageColumnTitle": "CPU 使用状況", "xpack.monitoring.elasticsearch.nodes.diskFreeSpaceColumnTitle": "ディスクの空き容量", @@ -14895,7 +14872,7 @@ "xpack.securitySolution.auditd.violatedSeLinuxPolicyDescription": "selinuxポリシーに違反しました", "xpack.securitySolution.auditd.wasAuthorizedToUseDescription": "が以下の使用を承認されました。", "xpack.securitySolution.auditd.withResultDescription": "結果付き", - "xpack.securitySolution.authenticationsTable.authenticationFailures": "認証", + "xpack.securitySolution.authenticationsTable.authentications": "認証", "xpack.securitySolution.authenticationsTable.failures": "失敗", "xpack.securitySolution.authenticationsTable.lastFailedDestination": "前回失敗したデスティネーション", "xpack.securitySolution.authenticationsTable.lastFailedSource": "前回失敗したソース", @@ -15898,19 +15875,14 @@ "xpack.securitySolution.endpoint.resolver.panel.error.goBack": "このリンクをクリックすると、すべてのプロセスのリストに戻ります。", "xpack.securitySolution.endpoint.resolver.panel.processDescList.events": "イベント", "xpack.securitySolution.endpoint.resolver.panel.processEventCounts.events": "イベント", - "xpack.securitySolution.endpoint.resolver.panel.processEventListByType.eventDescriptiveName": "{descriptor} {subject}", "xpack.securitySolution.endpoint.resolver.panel.processEventListByType.events": "イベント", "xpack.securitySolution.endpoint.resolver.panel.relatedCounts.numberOfEventsInCrumb": "{totalCount}件のイベント", - "xpack.securitySolution.endpoint.resolver.panel.relatedDetail.missing": "関連イベントが見つかりません。", "xpack.securitySolution.endpoint.resolver.panel.relatedDetail.wait": "イベントを待機しています...", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.atTime": "@ {date}", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.categoryAndType": "{category} {eventType}", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.countByCategory": "{count} {category}", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.detailsForProcessName": "詳細:{processName}", - "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.eventDescriptiveName": "{descriptor} {subject}", - "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.eventDescriptiveNameInTitle": "{descriptor} {subject}", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.events": "イベント", - "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.NA": "N/A", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.numberOfEvents": "{totalCount}件のイベント", "xpack.securitySolution.endpoint.resolver.panel.relatedEventList.countByCategory": "{count} {category}", "xpack.securitySolution.endpoint.resolver.panel.relatedEventList.numberOfEvents": "{totalCount}件のイベント", @@ -16693,9 +16665,6 @@ "xpack.securitySolution.zeek.sfDescription": "通常のSYN/FIN完了", "xpack.securitySolution.zeek.shDescription": "接続元がFINに続きSYNを送信しました。レスポンダーからSYN-ACKはありません", "xpack.securitySolution.zeek.shrDescription": "レスポンダーがFINに続きSYNを送信しました。接続元からSYN-ACKはありません", - "xpack.server.checkLicense.errorExpiredMessage": "{licenseType} ライセンスが期限切れのため {pluginName} を使用できません", - "xpack.server.checkLicense.errorUnavailableMessage": "現在ライセンス情報が利用できないため {pluginName} を使用できません。", - "xpack.server.checkLicense.errorUnsupportedMessage": "ご使用の {licenseType} ライセンスは {pluginName} をサポートしていません。ライセンスをアップグレードしてください。", "xpack.snapshotRestore.addPolicy.breadcrumbTitle": "ポリシーを追加", "xpack.snapshotRestore.addPolicy.loadingIndicesDescription": "利用可能なインデックスを読み込み中…", "xpack.snapshotRestore.addPolicy.LoadingIndicesErrorMessage": "利用可能なインデックスを読み込み中にエラーが発生", @@ -17767,8 +17736,6 @@ "xpack.transform.stepDetailsForm.errorGettingIndexPatternTitles": "既存のインデックスパターンのタイトルの取得中にエラーが発生しました:", "xpack.transform.stepDetailsForm.errorGettingTransformList": "既存の変換 ID の取得中にエラーが発生しました:", "xpack.transform.stepDetailsForm.errorGettingTransformPreview": "変換プレビューの取得中にエラーが発生しました。", - "xpack.transform.stepDetailsForm.indexPatternTimeFilterHelpText": "時間フィルターはこのフィールドを使って時間でフィールドを絞ります。時間フィールドを使わないこともできますが、その場合データを時間範囲で絞ることができません。", - "xpack.transform.stepDetailsForm.indexPatternTimeFilterLabel": "時間フィルターのフィールド名", "xpack.transform.stepDetailsForm.indexPatternTitleError": "このタイトルのインデックスパターンが既に存在します。", "xpack.transform.stepDetailsForm.noTimeFieldOptionLabel": "時間フィルターを使用しない", "xpack.transform.stepDetailsForm.transformDescriptionInputAriaLabel": "オプションの変換の説明を選択してください。", @@ -17781,7 +17748,6 @@ "xpack.transform.stepDetailsSummary.continuousModeDateFieldLabel": "連続モード日付フィールド", "xpack.transform.stepDetailsSummary.createIndexPatternMessage": "このジョブの Kibana インデックスパターンが作成されます。", "xpack.transform.stepDetailsSummary.destinationIndexLabel": "デスティネーションインデックス", - "xpack.transform.stepDetailsSummary.indexPatternTimeFilterLabel": "時間フィルター", "xpack.transform.stepDetailsSummary.transformDescriptionLabel": "変換の説明", "xpack.transform.stepDetailsSummary.transformIdLabel": "ジョブ ID", "xpack.transform.tableActionLabel": "アクション", @@ -17810,8 +17776,6 @@ "xpack.transform.transformList.editFlyoutCancelButtonText": "キャンセル", "xpack.transform.transformList.editFlyoutFormDescriptionLabel": "説明", "xpack.transform.transformList.editFlyoutFormDocsPerSecondHelptext": "スロットリングを有効にするには、インプットドキュメントの毎秒あたりのドキュメントの上限を設定します。", - "xpack.transform.transformList.editFlyoutFormdocsPerSecondLabel": "毎秒あたりのドキュメント", - "xpack.transform.transformList.editFlyoutFormFrequencyHelptext": "変換が連続実行されているときにソースインデックスで変更を確認する間の間隔。また、変換が検索またはインデックス中に一時障害が発生した場合に、再試行する間隔も決定します。最小値は1秒で、最大値は1時間です。", "xpack.transform.transformList.editFlyoutFormFrequencyLabel": "頻度", "xpack.transform.transformList.editFlyoutFormFrequencyNotValidErrorMessage": "頻度値が無効です。", "xpack.transform.transformList.editFlyoutFormNumberNotValidErrorMessage": "値はゼロより大きい数値でなければなりません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index f1bab383d53f5..103ff4ab146a4 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -499,6 +499,9 @@ "core.ui.securityNavList.label": "安全", "core.ui.welcomeErrorMessage": "Elastic 未正确加载。检查服务器输出以了解详情。", "core.ui.welcomeMessage": "正在加载 Elastic", + "core.status.greenTitle": "绿", + "core.status.redTitle": "红", + "core.status.yellowTitle": "黄", "dashboard.actions.toggleExpandPanelMenuItem.expandedDisplayName": "最小化", "dashboard.actions.toggleExpandPanelMenuItem.notExpandedDisplayName": "全屏", "dashboard.addExistingVisualizationLinkText": "将现有", @@ -798,8 +801,6 @@ "data.indexPatterns.ensureDefaultIndexPattern.bannerLabel": "若要在 Kibana 中可视化和浏览数据,您需要创建索引模式,以从 Elasticsearch 检索数据。", "data.indexPatterns.fetchFieldErrorTitle": "提取索引模式 {title} (ID: {id}) 的字段时出错", "data.indexPatterns.unableWriteLabel": "无法写入索引模式!请刷新页面以获取此索引模式的最新更改。", - "data.indexPatterns.unknownFieldErrorMessage": "indexPattern “{title}” 中的字段 “{name}” 使用未知字段类型。", - "data.indexPatterns.unknownFieldHeader": "未知字段类型 {type}", "data.noDataPopover.content": "此时间范围不包含任何数据。增大或调整时间范围,以查看更多的字段并创建图表。", "data.noDataPopover.dismissAction": "不再显示", "data.noDataPopover.subtitle": "提示", @@ -1463,7 +1464,6 @@ "discover.uninitializedText": "编写查询,添加一些筛选,或只需单击“刷新”来检索当前查询的结果。", "discover.uninitializedTitle": "开始搜索", "discover.valueIsNotConfiguredIndexPatternIDWarningTitle": "{stateVal} 不是配置的索引模式 ID", - "embeddableApi.actions.applyFilterActionTitle": "将筛选应用于当前视图", "embeddableApi.addPanel.createNewDefaultOption": "新建", "embeddableApi.addPanel.displayName": "添加面板", "embeddableApi.addPanel.noMatchingObjectsMessage": "未找到任何匹配对象。", @@ -2927,9 +2927,6 @@ "savedObjectsManagement.view.viewItemButtonLabel": "查看“{title}”", "savedObjectsManagement.view.viewItemTitle": "查看“{title}”", "usageCollection.stats.notReadyMessage": "统计尚未就绪。请稍后重试", - "core.status.greenTitle": "绿", - "core.status.redTitle": "红", - "core.status.yellowTitle": "黄", "share.advancedSettings.csv.quoteValuesText": "在 CSV 导出中是否应使用引号引起值?", "share.advancedSettings.csv.quoteValuesTitle": "使用引号引起 CSV 值", "share.advancedSettings.csv.separatorText": "使用此字符串分隔导出的值", @@ -4758,10 +4755,6 @@ "xpack.apm.metadataTable.section.urlLabel": "URL", "xpack.apm.metadataTable.section.userAgentLabel": "用户代理", "xpack.apm.metadataTable.section.userLabel": "用户", - "xpack.apm.metrics.durationByCountryMap.avgPageLoadByCountryLabel": "页面加载平均时长分布(按国家/地区)", - "xpack.apm.metrics.durationByCountryMap.RegionMapChart.ToolTip.avgPageLoadDuration": "页面加载平均时长:", - "xpack.apm.metrics.durationByCountryMap.RegionMapChart.ToolTip.countPageLoads": "{docCount} 个页面加载", - "xpack.apm.metrics.pageLoadCharts.avgPageLoadByBrowser": "平均页面加载持续时间分布 - 按浏览器", "xpack.apm.metrics.plot.noDataLabel": "此时间范围内没有数据。", "xpack.apm.metrics.transactionChart.machineLearningLabel": "Machine Learning", "xpack.apm.metrics.transactionChart.machineLearningTooltip": "环绕平均持续时间的流显示预期边界。对 ≥ 75 的异常分数显示标注。", @@ -4814,8 +4807,6 @@ "xpack.apm.serviceMap.avgMemoryUsagePopoverStat": "内存使用率(平均值)", "xpack.apm.serviceMap.avgReqPerMinutePopoverMetric": "每分钟请求数(平均)", "xpack.apm.serviceMap.avgTransDurationPopoverStat": "事务持续时间(平均值)", - "xpack.apm.serviceMap.betaBadge": "公测版", - "xpack.apm.serviceMap.betaTooltipMessage": "此功能当前为公测版。如果遇到任何错误或有任何反馈,请报告问题或访问我们的论坛。", "xpack.apm.serviceMap.center": "中", "xpack.apm.serviceMap.download": "下载", "xpack.apm.serviceMap.emptyBanner.docsLink": "在文档中了解详情", @@ -8828,7 +8819,6 @@ "xpack.infra.registerFeatures.logsDescription": "实时流式传输日志或在类似控制台的工具中滚动浏览历史视图。", "xpack.infra.registerFeatures.logsTitle": "日志", "xpack.infra.sampleDataLinkLabel": "日志", - "xpack.infra.savedView.changeView": "更改视图", "xpack.infra.savedView.currentView": "当前视图", "xpack.infra.savedView.defaultViewNameHosts": "默认视图", "xpack.infra.savedView.errorOnCreate.duplicateViewName": "具有该名称的视图已存在。", @@ -9302,15 +9292,7 @@ "xpack.ingestManager.setupPage.missingRequirementsKibanaTitle": "在您的 Kibana 配置中,启用:", "xpack.ingestManager.setupPage.tlsFlagText": "{kibanaSecurityLink}。将 {securityFlag} 设置为 {true}。出于开发目的,作为非安全的备用方案可以通过将 {tlsFlag} 设置为 {true} 来禁用 {tlsLink}。", "xpack.ingestManager.setupPage.tlsLink": "TLS", - "xpack.ingestManager.unenrollAgents.confirmModal.cancelButtonLabel": "取消", - "xpack.ingestManager.unenrollAgents.confirmModal.confirmButtonLabel": "取消注册", - "xpack.ingestManager.unenrollAgents.confirmModal.deleteMultipleTitle": "取消注册 {count, plural, one {# 个代理} other {# 个代理}}?", - "xpack.ingestManager.unenrollAgents.confirmModal.deleteSingleTitle": "取消注册“{id}”?", - "xpack.ingestManager.unenrollAgents.confirmModal.forceDeleteSingleTitle": "强制取消注册代理“{id}”?", - "xpack.ingestManager.unenrollAgents.confirmModal.loadingButtonLabel": "正在加载……", - "xpack.ingestManager.unenrollAgents.fatalErrorNotificationTitle": "取消注册代理时出错", - "xpack.ingestManager.unenrollAgents.successForceSingleNotificationTitle": "代理“{id}”已取消注册", - "xpack.ingestManager.unenrollAgents.successSingleNotificationTitle": "取消注册代理“{id}”", + "xpack.ingestManager.unenrollAgents.cancelButtonLabel": "取消", "xpack.ingestPipelines.app.checkingPrivilegesDescription": "正在检查权限……", "xpack.ingestPipelines.app.checkingPrivilegesErrorMessage": "从服务器获取用户权限时出错。", "xpack.ingestPipelines.app.deniedPrivilegeDescription": "要使用“采集管道”,必须具有{privilegesCount, plural, one {以下集群权限} other {以下集群权限}}:{missingPrivileges}。", @@ -10540,8 +10522,6 @@ "xpack.ml.calendarsList.deleteCalendars.deletingCalendarSuccessNotificationMessage": "已删除 {messageId}", "xpack.ml.calendarsList.deleteCalendarsModal.cancelButtonLabel": "取消", "xpack.ml.calendarsList.deleteCalendarsModal.deleteButtonLabel": "删除", - "xpack.ml.calendarsList.deleteCalendarsModal.deleteCalendarsDescription": "是否删除{calendarsCount, plural, one {此日历} other {这些日历}}?{calendarsList}", - "xpack.ml.calendarsList.deleteCalendarsModal.deleteCalendarTitle": "删除日历", "xpack.ml.calendarsList.errorWithLoadingListOfCalendarsErrorMessage": "加载日历列表时出错。", "xpack.ml.calendarsList.table.allJobsLabel": "应用到所有作业", "xpack.ml.calendarsList.table.deleteButtonLabel": "删除", @@ -12603,7 +12583,6 @@ "xpack.monitoring.cluster.overview.esPanel.versionLabel": "版本", "xpack.monitoring.cluster.overview.esPanel.versionNotAvailableDescription": "不适用", "xpack.monitoring.cluster.overview.esPanel.warnLogsTooltipText": "警告日志数", - "xpack.monitoring.cluster.overview.healthStatusDescription": "运行状况为{status}", "xpack.monitoring.cluster.overview.kibanaPanel.connectionsLabel": "连接", "xpack.monitoring.cluster.overview.kibanaPanel.instancesCountLinkAriaLabel": "Kibana 实例:{instancesCount}", "xpack.monitoring.cluster.overview.kibanaPanel.instancesCountLinkLabel": "实例:{instancesCount}", @@ -12722,8 +12701,6 @@ "xpack.monitoring.elasticsearch.nodeDetailStatus.transportAddress": "传输地址", "xpack.monitoring.elasticsearch.nodeDetailStatus.typeLabel": "类型", "xpack.monitoring.elasticsearch.nodes.alertsColumnTitle": "告警", - "xpack.monitoring.elasticsearch.nodes.cells.maxText": "{metric} 最大值", - "xpack.monitoring.elasticsearch.nodes.cells.minText": "{metric} 最小值", "xpack.monitoring.elasticsearch.nodes.cpuThrottlingColumnTitle": "CPU 限制", "xpack.monitoring.elasticsearch.nodes.cpuUsageColumnTitle": "CPU 使用", "xpack.monitoring.elasticsearch.nodes.diskFreeSpaceColumnTitle": "磁盘可用空间", @@ -14904,7 +14881,7 @@ "xpack.securitySolution.auditd.violatedSeLinuxPolicyDescription": "已违反 selinux 策略", "xpack.securitySolution.auditd.wasAuthorizedToUseDescription": "有权使用", "xpack.securitySolution.auditd.withResultDescription": ",结果为", - "xpack.securitySolution.authenticationsTable.authenticationFailures": "身份验证", + "xpack.securitySolution.authenticationsTable.authentications": "身份验证", "xpack.securitySolution.authenticationsTable.failures": "错误", "xpack.securitySolution.authenticationsTable.lastFailedDestination": "上一失败目标", "xpack.securitySolution.authenticationsTable.lastFailedSource": "上一失败源", @@ -15908,19 +15885,14 @@ "xpack.securitySolution.endpoint.resolver.panel.error.goBack": "单击此链接以返回到所有进程的列表。", "xpack.securitySolution.endpoint.resolver.panel.processDescList.events": "事件", "xpack.securitySolution.endpoint.resolver.panel.processEventCounts.events": "事件", - "xpack.securitySolution.endpoint.resolver.panel.processEventListByType.eventDescriptiveName": "{descriptor} {subject}", "xpack.securitySolution.endpoint.resolver.panel.processEventListByType.events": "事件", "xpack.securitySolution.endpoint.resolver.panel.relatedCounts.numberOfEventsInCrumb": "{totalCount} 个事件", - "xpack.securitySolution.endpoint.resolver.panel.relatedDetail.missing": "找不到相关事件。", "xpack.securitySolution.endpoint.resolver.panel.relatedDetail.wait": "等候事件......", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.atTime": "@ {date}", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.categoryAndType": "{category} {eventType}", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.countByCategory": "{count} 个{category}", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.detailsForProcessName": "{processName} 的详情", - "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.eventDescriptiveName": "{descriptor} {subject}", - "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.eventDescriptiveNameInTitle": "{descriptor} {subject}", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.events": "事件", - "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.NA": "不可用", "xpack.securitySolution.endpoint.resolver.panel.relatedEventDetail.numberOfEvents": "{totalCount} 个事件", "xpack.securitySolution.endpoint.resolver.panel.relatedEventList.countByCategory": "{count} 个{category}", "xpack.securitySolution.endpoint.resolver.panel.relatedEventList.numberOfEvents": "{totalCount} 个事件", @@ -16703,9 +16675,6 @@ "xpack.securitySolution.zeek.sfDescription": "正常 SYN/FIN 完成", "xpack.securitySolution.zeek.shDescription": "发起方已发送 SYN,后跟 FIN,响应方未发送 SYN ACK", "xpack.securitySolution.zeek.shrDescription": "响应方已发送 SYN ACK,后跟 FIN,发起方未发送 SYN", - "xpack.server.checkLicense.errorExpiredMessage": "您不能使用 {pluginName},因为您的{licenseType}许可证已过期", - "xpack.server.checkLicense.errorUnavailableMessage": "您不能使用 {pluginName},因为许可证信息当前不可用。", - "xpack.server.checkLicense.errorUnsupportedMessage": "您的{licenseType}许可证不支持 {pluginName}。请升级您的许可证。", "xpack.snapshotRestore.addPolicy.breadcrumbTitle": "添加策略", "xpack.snapshotRestore.addPolicy.loadingIndicesDescription": "正在加载可用索引……", "xpack.snapshotRestore.addPolicy.LoadingIndicesErrorMessage": "加载可用索引时出错", @@ -17777,8 +17746,6 @@ "xpack.transform.stepDetailsForm.errorGettingIndexPatternTitles": "获取现有索引模式标题时发生错误:", "xpack.transform.stepDetailsForm.errorGettingTransformList": "获取现有转换 ID 时发生错误:", "xpack.transform.stepDetailsForm.errorGettingTransformPreview": "获取转换预览时发生错误", - "xpack.transform.stepDetailsForm.indexPatternTimeFilterHelpText": "时间筛选将使用此字段按时间筛选您的数据。您可以选择不使用时间字段,但将无法通过时间范围缩小您的数据范围。", - "xpack.transform.stepDetailsForm.indexPatternTimeFilterLabel": "时间筛选字段名称", "xpack.transform.stepDetailsForm.indexPatternTitleError": "具有此名称的索引模式已存在。", "xpack.transform.stepDetailsForm.noTimeFieldOptionLabel": "我不想使用时间筛选", "xpack.transform.stepDetailsForm.transformDescriptionInputAriaLabel": "选择可选的转换描述。", @@ -17791,7 +17758,6 @@ "xpack.transform.stepDetailsSummary.continuousModeDateFieldLabel": "连续模式日期字段", "xpack.transform.stepDetailsSummary.createIndexPatternMessage": "将为此作业创建 Kibana 索引模式。", "xpack.transform.stepDetailsSummary.destinationIndexLabel": "目标 IP", - "xpack.transform.stepDetailsSummary.indexPatternTimeFilterLabel": "时间筛选", "xpack.transform.stepDetailsSummary.transformDescriptionLabel": "转换描述", "xpack.transform.stepDetailsSummary.transformIdLabel": "作业 ID", "xpack.transform.tableActionLabel": "操作", @@ -17820,8 +17786,6 @@ "xpack.transform.transformList.editFlyoutCancelButtonText": "取消", "xpack.transform.transformList.editFlyoutFormDescriptionLabel": "描述", "xpack.transform.transformList.editFlyoutFormDocsPerSecondHelptext": "要启用节流,请设置输入文档限制(每秒文档数)。", - "xpack.transform.transformList.editFlyoutFormdocsPerSecondLabel": "每秒文档数", - "xpack.transform.transformList.editFlyoutFormFrequencyHelptext": "转换不间断运行时检查源索引更改的时间间隔。还决定转换在搜索或索引时发生临时失败时的重试时间间隔。最小值为 1s,最大值为 1h。", "xpack.transform.transformList.editFlyoutFormFrequencyLabel": "频率", "xpack.transform.transformList.editFlyoutFormFrequencyNotValidErrorMessage": "频率值无效。", "xpack.transform.transformList.editFlyoutFormNumberNotValidErrorMessage": "值需要是大于零的数字。", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx index 0742ed8a778ef..2bcd87830901b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx @@ -61,6 +61,7 @@ export const AddMessageVariables: React.FunctionComponent = ({ setIsVariablesPopoverOpen(true)} iconType="indexOpen" diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx index 495707db4975c..0a04db1b5ddfa 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx @@ -32,48 +32,47 @@ export const IndexParamsFields = ({ }; return ( - <> - 0 ? ((documents[0] as unknown) as string) : undefined + 0 ? ((documents[0] as unknown) as string) : undefined + } + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.documentsFieldLabel', + { + defaultMessage: 'Document to index', } - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.documentsFieldLabel', - { - defaultMessage: 'Document to index', - } - )} - aria-label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.jsonDocAriaLabel', - { - defaultMessage: 'Code editor', - } - )} - errors={errors.documents as string[]} - onDocumentsChange={onDocumentsChange} - helpText={ - - - + )} + aria-label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.jsonDocAriaLabel', + { + defaultMessage: 'Code editor', } - onBlur={() => { - if ( - !(documents && documents.length > 0 ? ((documents[0] as unknown) as string) : undefined) - ) { - // set document as empty to turn on the validation for non empty valid JSON object - onDocumentsChange('{}'); - } - }} - /> - + )} + errors={errors.documents as string[]} + onDocumentsChange={onDocumentsChange} + helpText={ + + + + } + onBlur={() => { + if ( + !(documents && documents.length > 0 ? ((documents[0] as unknown) as string) : undefined) + ) { + // set document as empty to turn on the validation for non empty valid JSON object + onDocumentsChange('{}'); + } + }} + /> ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/delete_modal_confirmation.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/delete_modal_confirmation.tsx index 5862a567f71ba..a093b9c511970 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/delete_modal_confirmation.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/delete_modal_confirmation.tsx @@ -5,7 +5,7 @@ */ import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { HttpSetup } from 'kibana/public'; import { useAppDependencies } from '../app_context'; @@ -17,6 +17,7 @@ export const DeleteModalConfirmation = ({ onErrors, singleTitle, multipleTitle, + setIsLoadingState, }: { idsToDelete: string[]; apiDeleteCall: ({ @@ -31,10 +32,17 @@ export const DeleteModalConfirmation = ({ onErrors: () => void; singleTitle: string; multipleTitle: string; + setIsLoadingState: (isLoading: boolean) => void; }) => { + const [deleteModalFlyoutVisible, setDeleteModalVisibility] = useState(false); + + useEffect(() => { + setDeleteModalVisibility(idsToDelete.length > 0); + }, [idsToDelete]); + const { http, toastNotifications } = useAppDependencies(); const numIdsToDelete = idsToDelete.length; - if (!numIdsToDelete) { + if (!deleteModalFlyoutVisible) { return null; } const confirmModalText = i18n.translate( @@ -65,12 +73,18 @@ export const DeleteModalConfirmation = ({ buttonColor="danger" data-test-subj="deleteIdsConfirmation" title={confirmButtonText} - onCancel={() => onCancel()} + onCancel={() => { + setDeleteModalVisibility(false); + onCancel(); + }} onConfirm={async () => { + setDeleteModalVisibility(false); + setIsLoadingState(true); const { successes, errors } = await apiDeleteCall({ ids: idsToDelete, http }); + setIsLoadingState(false); + const numSuccesses = successes.length; const numErrors = errors.length; - onDeleted(successes); if (numSuccesses > 0) { toastNotifications.addSuccess( i18n.translate( @@ -95,8 +109,9 @@ export const DeleteModalConfirmation = ({ } ) ); - onErrors(); + await onErrors(); } + await onDeleted(successes); }} cancelButtonText={cancelButtonText} confirmButtonText={confirmButtonText} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts index 43b22361aea36..ad3a5b40bd00d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts @@ -12,6 +12,7 @@ import { loadActionTypes, loadAllActions, updateActionConnector, + executeAction, } from './action_connector_api'; const http = httpServiceMock.createStartContract(); @@ -128,3 +129,32 @@ describe('deleteActions', () => { `); }); }); + +describe('executeAction', () => { + test('should call execute API', async () => { + const id = '123'; + const params = { + stringParams: 'someString', + numericParams: 123, + }; + + http.post.mockResolvedValueOnce({ + actionId: id, + status: 'ok', + }); + + const result = await executeAction({ id, http, params }); + expect(result).toEqual({ + actionId: id, + status: 'ok', + }); + expect(http.post.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/actions/action/123/_execute", + Object { + "body": "{\\"params\\":{\\"stringParams\\":\\"someString\\",\\"numericParams\\":123}}", + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.ts index 46a676ac06539..c2c7139d13bf0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.ts @@ -7,6 +7,7 @@ import { HttpSetup } from 'kibana/public'; import { BASE_ACTION_API_PATH } from '../constants'; import { ActionConnector, ActionConnectorWithoutId, ActionType } from '../../types'; +import { ActionTypeExecutorResult } from '../../../../../plugins/actions/common'; export async function loadActionTypes({ http }: { http: HttpSetup }): Promise { return await http.get(`${BASE_ACTION_API_PATH}/list_action_types`); @@ -65,3 +66,17 @@ export async function deleteActions({ ); return { successes, errors }; } + +export async function executeAction({ + id, + params, + http, +}: { + id: string; + http: HttpSetup; + params: Record; +}): Promise> { + return await http.post(`${BASE_ACTION_API_PATH}/action/${id}/_execute`, { + body: JSON.stringify({ params }), + }); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.scss new file mode 100644 index 0000000000000..873a3ceb762cd --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.scss @@ -0,0 +1,3 @@ +.connectorEditFlyoutTabs { + margin-bottom: '-25px'; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx index dd9eeae266987..0c2f4df0ca52b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx @@ -152,6 +152,6 @@ describe('connector_edit_flyout', () => { const preconfiguredBadge = wrapper.find('[data-test-subj="preconfiguredBadge"]'); expect(preconfiguredBadge.exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="saveEditedActionButton"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="saveAndCloseEditedActionButton"]').exists()).toBeFalsy(); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx index ca75e730062ab..fc902a4fabcd8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx @@ -19,15 +19,21 @@ import { EuiBetaBadge, EuiText, EuiLink, + EuiTabs, + EuiTab, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { Option, none, some } from 'fp-ts/lib/Option'; import { ActionConnectorForm, validateBaseProperties } from './action_connector_form'; +import { TestConnectorForm } from './test_connector_form'; import { ActionConnectorTableItem, ActionConnector, IErrorObject } from '../../../types'; import { connectorReducer } from './connector_reducer'; -import { updateActionConnector } from '../../lib/action_connector_api'; +import { updateActionConnector, executeAction } from '../../lib/action_connector_api'; import { hasSaveActionsCapability } from '../../lib/capabilities'; import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; import { PLUGIN } from '../../constants/plugin'; +import { ActionTypeExecutorResult } from '../../../../../actions/common'; +import './connector_edit_flyout.scss'; export interface ConnectorEditProps { initialConnector: ActionConnectorTableItem; @@ -40,7 +46,6 @@ export const ConnectorEditFlyout = ({ editFlyoutVisible, setEditFlyoutVisibility, }: ConnectorEditProps) => { - let hasErrors = false; const { http, toastNotifications, @@ -56,13 +61,26 @@ export const ConnectorEditFlyout = ({ connector: { ...initialConnector, secrets: {} }, }); const [isSaving, setIsSaving] = useState(false); + const [selectedTab, setTab] = useState<'config' | 'test'>('config'); + + const [hasChanges, setHasChanges] = useState(false); const setConnector = (key: string, value: any) => { dispatch({ command: { type: 'setConnector' }, payload: { key, value } }); }; + const [testExecutionActionParams, setTestExecutionActionParams] = useState< + Record + >({}); + const [testExecutionResult, setTestExecutionResult] = useState< + Option> + >(none); + const [isExecutingAction, setIsExecutinAction] = useState(false); + const closeFlyout = useCallback(() => { setEditFlyoutVisibility(false); setConnector('connector', { ...initialConnector, secrets: {} }); + setHasChanges(false); + setTestExecutionResult(none); // eslint-disable-next-line react-hooks/exhaustive-deps }, [setEditFlyoutVisibility]); @@ -71,11 +89,13 @@ export const ConnectorEditFlyout = ({ } const actionTypeModel = actionTypeRegistry.get(connector.actionTypeId); - const errors = { + const errorsInConnectorConfig = { ...actionTypeModel?.validateConnector(connector).errors, ...validateBaseProperties(connector).errors, } as IErrorObject; - hasErrors = !!Object.keys(errors).find((errorKey) => errors[errorKey].length >= 1); + const hasErrorsInConnectorConfig = !!Object.keys(errorsInConnectorConfig).find( + (errorKey) => errorsInConnectorConfig[errorKey].length >= 1 + ); const onActionConnectorSave = async (): Promise => await updateActionConnector({ http, connector, id: connector.id }) @@ -173,6 +193,32 @@ export const ConnectorEditFlyout = ({ ); + const onExecutAction = () => { + setIsExecutinAction(true); + return executeAction({ id: connector.id, params: testExecutionActionParams, http }).then( + (result) => { + setIsExecutinAction(false); + setTestExecutionResult(some(result)); + return result; + } + ); + }; + + const onSaveClicked = async (closeAfterSave: boolean = true) => { + setIsSaving(true); + const savedAction = await onActionConnectorSave(); + setIsSaving(false); + if (savedAction) { + setHasChanges(false); + if (closeAfterSave) { + closeFlyout(); + } + if (reloadConnectors) { + reloadConnectors(); + } + } + }; + return ( @@ -184,40 +230,78 @@ export const ConnectorEditFlyout = ({ ) : null} {flyoutTitle} + + setTab('config')} + data-test-subj="configureConnectorTab" + isSelected={'config' === selectedTab} + > + {i18n.translate('xpack.triggersActionsUI.sections.editConnectorForm.tabText', { + defaultMessage: 'Configuration', + })} + + setTab('test')} + data-test-subj="testConnectorTab" + isSelected={'test' === selectedTab} + > + {i18n.translate('xpack.triggersActionsUI.sections.testConnectorForm.tabText', { + defaultMessage: 'Test', + })} + + - {!connector.isPreconfigured ? ( - { + setHasChanges(true); + // if the user changes the connector, "forget" the last execution + // so the user comes back to a clean form ready to run a fresh test + setTestExecutionResult(none); + dispatch(changes); + }} + actionTypeRegistry={actionTypeRegistry} + http={http} + docLinks={docLinks} + capabilities={capabilities} + consumer={consumer} + /> + ) : ( + + + {i18n.translate( + 'xpack.triggersActionsUI.sections.editConnectorForm.descriptionText', + { + defaultMessage: 'This connector is readonly.', + } + )} + + + + + + ) + ) : ( + - ) : ( - - - {i18n.translate( - 'xpack.triggersActionsUI.sections.editConnectorForm.descriptionText', - { - defaultMessage: 'This connector is readonly.', - } - )} - - - - - )} @@ -232,35 +316,48 @@ export const ConnectorEditFlyout = ({ )} - {canSave && actionTypeModel && !connector.isPreconfigured ? ( - - { - setIsSaving(true); - const savedAction = await onActionConnectorSave(); - setIsSaving(false); - if (savedAction) { - closeFlyout(); - if (reloadConnectors) { - reloadConnectors(); - } - } - }} - > - - - - ) : null} + + + {canSave && actionTypeModel && !connector.isPreconfigured ? ( + + + { + await onSaveClicked(false); + }} + > + + + + + { + await onSaveClicked(); + }} + > + + + + + ) : null} + + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.test.tsx new file mode 100644 index 0000000000000..482bccb5517f1 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.test.tsx @@ -0,0 +1,212 @@ +/* + * 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, { lazy } from 'react'; +import { I18nProvider } from '@kbn/i18n/react'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import TestConnectorForm from './test_connector_form'; +import { none, some } from 'fp-ts/lib/Option'; +import { ActionConnector, ValidationResult } from '../../../types'; +import { actionTypeRegistryMock } from '../../action_type_registry.mock'; +import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context'; +import { EuiFormRow, EuiFieldText, EuiText, EuiLink, EuiForm, EuiSelect } from '@elastic/eui'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; + +const mockedActionParamsFields = lazy(async () => ({ + default() { + return ( + + + + + + + Link to some help + + } + > + + + + ); + }, +})); + +const actionType = { + id: 'my-action-type', + iconClass: 'test', + selectMessage: 'test', + validateConnector: (): ValidationResult => { + return { errors: {} }; + }, + validateParams: (): ValidationResult => { + const validationResult = { errors: {} }; + return validationResult; + }, + actionConnectorFields: null, + actionParamsFields: mockedActionParamsFields, +}; + +describe('test_connector_form', () => { + let deps: any; + let actionTypeRegistry; + beforeAll(async () => { + actionTypeRegistry = actionTypeRegistryMock.create(); + + const mocks = coreMock.createSetup(); + const [ + { + application: { capabilities }, + }, + ] = await mocks.getStartServices(); + deps = { + http: mocks.http, + toastNotifications: mocks.notifications.toasts, + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, + actionTypeRegistry, + capabilities, + }; + actionTypeRegistry.get.mockReturnValue(actionType); + }); + + it('renders initially as the action form and execute button and no result', async () => { + const connector = { + actionTypeId: actionType.id, + config: {}, + secrets: {}, + } as ActionConnector; + const wrapper = mountWithIntl( + + { + return new Promise(() => {}); + }, + docLinks: deps!.docLinks, + }} + > + {}} + isExecutingAction={false} + onExecutAction={async () => ({ + actionId: '', + status: 'ok', + })} + executionResult={none} + /> + + + ); + const executeActionButton = wrapper?.find('[data-test-subj="executeActionButton"]'); + expect(executeActionButton?.exists()).toBeTruthy(); + expect(executeActionButton?.first().prop('isDisabled')).toBe(false); + + const result = wrapper?.find('[data-test-subj="executionAwaiting"]'); + expect(result?.exists()).toBeTruthy(); + }); + + it('renders successful results', async () => { + const connector = { + actionTypeId: actionType.id, + config: {}, + secrets: {}, + } as ActionConnector; + const wrapper = mountWithIntl( + + { + return new Promise(() => {}); + }, + docLinks: deps!.docLinks, + }} + > + {}} + isExecutingAction={false} + onExecutAction={async () => ({ + actionId: '', + status: 'ok', + })} + executionResult={some({ + actionId: '', + status: 'ok', + })} + /> + + + ); + const result = wrapper?.find('[data-test-subj="executionSuccessfulResult"]'); + expect(result?.exists()).toBeTruthy(); + }); + + it('renders failure results', async () => { + const connector = { + actionTypeId: actionType.id, + config: {}, + secrets: {}, + } as ActionConnector; + const wrapper = mountWithIntl( + + { + return new Promise(() => {}); + }, + docLinks: deps!.docLinks, + }} + > + {}} + isExecutingAction={false} + onExecutAction={async () => ({ + actionId: '', + status: 'error', + message: 'Error Message', + })} + executionResult={some({ + actionId: '', + status: 'error', + message: 'Error Message', + })} + /> + + + ); + const result = wrapper?.find('[data-test-subj="executionFailureResult"]'); + expect(result?.exists()).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.tsx new file mode 100644 index 0000000000000..a73fd4e22e637 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.tsx @@ -0,0 +1,224 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { Fragment, Suspense } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiButton, + EuiSteps, + EuiLoadingSpinner, + EuiDescriptionList, + EuiCallOut, + EuiSpacer, +} from '@elastic/eui'; +import { Option, map, getOrElse } from 'fp-ts/lib/Option'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { ActionConnector } from '../../../types'; +import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; +import { ActionTypeExecutorResult } from '../../../../../actions/common'; + +export interface ConnectorAddFlyoutProps { + connector: ActionConnector; + executeEnabled: boolean; + isExecutingAction: boolean; + setActionParams: (params: Record) => void; + actionParams: Record; + onExecutAction: () => Promise>; + executionResult: Option>; +} + +export const TestConnectorForm = ({ + connector, + executeEnabled, + executionResult, + actionParams, + setActionParams, + onExecutAction, + isExecutingAction, +}: ConnectorAddFlyoutProps) => { + const { actionTypeRegistry, docLinks } = useActionsConnectorsContext(); + const actionTypeModel = actionTypeRegistry.get(connector.actionTypeId); + const ParamsFieldsComponent = actionTypeModel.actionParamsFields; + + const actionErrors = actionTypeModel?.validateParams(actionParams); + const hasErrors = !!Object.values(actionErrors.errors).find((errors) => errors.length > 0); + + const steps = [ + { + title: 'Create an action', + children: ParamsFieldsComponent ? ( + + + + + + } + > + + setActionParams({ + ...actionParams, + [field]: value, + }) + } + messageVariables={[]} + docLinks={docLinks} + actionConnector={connector} + /> + + ) : ( + +

This Connector does not require any Action Parameter.

+
+ ), + }, + { + title: 'Run the action', + children: ( + + {executeEnabled ? null : ( + + +

+ +

+
+ +
+ )} + + + + + +
+ ), + }, + { + title: 'Results', + children: pipe( + executionResult, + map((result) => + result.status === 'ok' ? ( + + ) : ( + + ) + ), + getOrElse(() => ) + ), + }, + ]; + + return ; +}; + +const AwaitingExecution = () => ( + +

+ +

+
+); + +const SuccessfulExecution = () => ( + +

+ +

+
+); + +const FailedExecussion = ({ + executionResult: { message, serviceMessage }, +}: { + executionResult: ActionTypeExecutorResult; +}) => { + const items = [ + { + title: i18n.translate( + 'xpack.triggersActionsUI.sections.testConnectorForm.executionFailureDescription', + { + defaultMessage: 'The following error was found:', + } + ), + description: + message ?? + i18n.translate( + 'xpack.triggersActionsUI.sections.testConnectorForm.executionFailureUnknownReason', + { + defaultMessage: 'Unknown reason', + } + ), + }, + ]; + if (serviceMessage) { + items.push({ + title: i18n.translate( + 'xpack.triggersActionsUI.sections.testConnectorForm.executionFailureAdditionalDetails', + { + defaultMessage: 'Details:', + } + ), + description: serviceMessage, + }); + } + return ( + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { TestConnectorForm as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx index 837529bfc938d..6bc9fd8e7e5a8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx @@ -194,55 +194,15 @@ export const ActionsConnectorsList: React.FunctionComponent = () => { truncateText: true, }, { - field: 'isPreconfigured', name: '', - render: (value: number, item: ActionConnectorTableItem) => { - if (item.isPreconfigured) { - return ( - - - - - - ); - } + render: (item: ActionConnectorTableItem) => { return ( - - - setConnectorsToDelete([item.id])} - iconType={'trash'} - /> - - + setConnectorsToDelete([item.id])} + /> ); }, @@ -344,28 +304,6 @@ export const ActionsConnectorsList: React.FunctionComponent = () => { /> ); - const noPermissionPrompt = ( - - - - } - body={ -

- -

- } - /> - ); - return (
{ 'xpack.triggersActionsUI.sections.actionsConnectorsList.multipleTitle', { defaultMessage: 'connectors' } )} + setIsLoadingState={(isLoading: boolean) => setIsLoadingActionTypes(isLoading)} /> {/* Render the view based on if there's data or if they can save */} @@ -411,7 +350,7 @@ export const ActionsConnectorsList: React.FunctionComponent = () => { {data.length === 0 && canSave && !isLoadingActions && !isLoadingActionTypes && ( setAddFlyoutVisibility(true)} /> )} - {data.length === 0 && !canSave && noPermissionPrompt} + {data.length === 0 && !canSave && } { function getActionsCountByActionType(actions: ActionConnector[], actionTypeId: string) { return actions.filter((action) => action.actionTypeId === actionTypeId).length; } + +const DeleteOperation: React.FunctionComponent<{ + item: ActionConnectorTableItem; + canDelete: boolean; + onDelete: () => void; +}> = ({ item, canDelete, onDelete }) => { + if (item.isPreconfigured) { + return ( + + + + ); + } + return ( + + + + + + ); +}; + +const NoPermissionPrompt: React.FunctionComponent<{}> = () => ( + + + + } + body={ +

+ +

+ } + /> +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 3d55c51e45281..7d0354589ecb8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -404,16 +404,16 @@ export const AlertsList: React.FunctionComponent = () => {
{ - loadAlertsData(); - setSelectedIds([]); setAlertsToDelete([]); + setSelectedIds([]); + await loadAlertsData(); }} onErrors={async () => { // Refresh the alerts from the server, some alerts may have beend deleted await loadAlertsData(); setAlertsToDelete([]); }} - onCancel={async () => { + onCancel={() => { setAlertsToDelete([]); }} apiDeleteCall={deleteAlerts} @@ -424,6 +424,9 @@ export const AlertsList: React.FunctionComponent = () => { multipleTitle={i18n.translate('xpack.triggersActionsUI.sections.alertsList.multipleTitle', { defaultMessage: 'alerts', })} + setIsLoadingState={(isLoading: boolean) => { + setAlertsState({ ...alertsState, isLoading }); + }} /> {loadedItems.length || isFilterApplied ? ( diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts index 83232bbce1ba7..cdd357f3560b8 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.test.ts @@ -437,8 +437,7 @@ describe('DynamicActionManager', () => { name: 'foo', config: {}, }; - - await expect(manager.createEvent(action, ['SELECT_RANGE_TRIGGER'])).rejects; + await expect(manager.createEvent(action, ['SELECT_RANGE_TRIGGER'])).rejects.toThrow(); }); }); }); @@ -704,4 +703,18 @@ describe('DynamicActionManager', () => { expect(basicAndGoldActions).toHaveLength(2); }); + + test("failing to revive/kill an action doesn't fail action manager", async () => { + const { manager, uiActions, storage } = setup([event1, event3, event2]); + + uiActions.registerActionFactory(actionFactoryDefinition1); + + await manager.start(); + + expect(uiActions.getTriggerActions('VALUE_CLICK_TRIGGER')).toHaveLength(2); + expect(await storage.list()).toEqual([event1, event3, event2]); + + await manager.stop(); + expect(uiActions.getTriggerActions('VALUE_CLICK_TRIGGER')).toHaveLength(0); + }); }); diff --git a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts index 471b929fdbc06..b414296690c9e 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/dynamic_actions/dynamic_action_manager.ts @@ -34,7 +34,13 @@ export interface DynamicActionManagerParams { storage: ActionStorage; uiActions: Pick< StartContract, - 'registerAction' | 'attachAction' | 'unregisterAction' | 'detachAction' | 'getActionFactory' + | 'registerAction' + | 'attachAction' + | 'unregisterAction' + | 'detachAction' + | 'hasAction' + | 'getActionFactory' + | 'hasActionFactory' >; isCompatible: (context: C) => Promise; } @@ -73,8 +79,17 @@ export class DynamicActionManager { const actionId = this.generateActionId(eventId); + if (!uiActions.hasActionFactory(action.factoryId)) { + // eslint-disable-next-line no-console + console.warn( + `Action factory for action [action.factoryId = ${action.factoryId}] doesn't exist. Skipping action [action.name = ${action.name}] revive.` + ); + return; + } + const factory = uiActions.getActionFactory(event.action.factoryId); const actionDefinition: ActionDefinition = factory.create(action as SerializedAction); + uiActions.registerAction({ ...actionDefinition, id: actionId, @@ -100,6 +115,7 @@ export class DynamicActionManager { protected killAction({ eventId, triggers }: SerializedEvent) { const { uiActions } = this.params; const actionId = this.generateActionId(eventId); + if (!uiActions.hasAction(actionId)) return; for (const trigger of triggers) uiActions.detachAction(trigger as any, actionId); uiActions.unregisterAction(actionId); @@ -157,6 +173,7 @@ export class DynamicActionManager { try { const events = await this.params.storage.list(); for (const event of events) this.reviveAction(event); + this.ui.transitions.finishFetching(events); } catch (error) { this.ui.transitions.failFetching(error instanceof Error ? error : { message: String(error) }); diff --git a/x-pack/plugins/ui_actions_enhanced/public/mocks.ts b/x-pack/plugins/ui_actions_enhanced/public/mocks.ts index 9eb0a06b6dbaf..1900f04b0c7d8 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/mocks.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/mocks.ts @@ -29,6 +29,7 @@ const createStartContract = (): Start => { ...uiActionsPluginMock.createStartContract(), getActionFactories: jest.fn(), getActionFactory: jest.fn(), + hasActionFactory: jest.fn(), FlyoutManageDrilldowns: jest.fn(), telemetry: jest.fn(), extract: jest.fn(), diff --git a/x-pack/plugins/ui_actions_enhanced/public/plugin.ts b/x-pack/plugins/ui_actions_enhanced/public/plugin.ts index b05c08c4c77d0..31236d2ea9779 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/plugin.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/plugin.ts @@ -61,7 +61,12 @@ export interface StartContract extends UiActionsStart, Pick< UiActionsServiceEnhancements, - 'getActionFactory' | 'getActionFactories' | 'telemetry' | 'extract' | 'inject' + | 'getActionFactory' + | 'hasActionFactory' + | 'getActionFactories' + | 'telemetry' + | 'extract' + | 'inject' > { FlyoutManageDrilldowns: ReturnType; } diff --git a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts index 5e40d803962de..cbbd88e65e841 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/services/ui_actions_service_enhancements.ts @@ -79,6 +79,10 @@ export class UiActionsServiceEnhancements return actionFactory; }; + public readonly hasActionFactory = (actionFactoryId: string): boolean => { + return this.actionFactories.has(actionFactoryId); + }; + /** * Returns an array of all action factories. */ diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx index 146cebabbb382..110eff36e3df9 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx @@ -239,7 +239,7 @@ export class UpgradeAssistantTabs extends React.Component { this.setState({ telemetryState: TelemetryState.Running }); - await this.props.http.fetch('/api/upgrade_assistant/telemetry/ui_open', { + await this.props.http.fetch('/api/upgrade_assistant/stats/ui_open', { method: 'PUT', body: JSON.stringify(set({}, tabName, true)), }); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/button.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/button.tsx index a20f4117f693d..747430f455f22 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/button.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/button.tsx @@ -239,7 +239,7 @@ export class ReindexButton extends React.Component { }); afterEach(() => jest.clearAllMocks()); - describe('PUT /api/upgrade_assistant/telemetry/ui_open', () => { + describe('PUT /api/upgrade_assistant/stats/ui_open', () => { it('returns correct payload with single option', async () => { const returnPayload = { overview: true, @@ -51,7 +51,7 @@ describe('Upgrade Assistant Telemetry API', () => { const resp = await routeDependencies.router.getHandler({ method: 'put', - pathPattern: '/api/upgrade_assistant/telemetry/ui_open', + pathPattern: '/api/upgrade_assistant/stats/ui_open', })( routeHandlerContextMock, createRequestMock({ body: returnPayload }), @@ -72,7 +72,7 @@ describe('Upgrade Assistant Telemetry API', () => { const resp = await routeDependencies.router.getHandler({ method: 'put', - pathPattern: '/api/upgrade_assistant/telemetry/ui_open', + pathPattern: '/api/upgrade_assistant/stats/ui_open', })( routeHandlerContextMock, createRequestMock({ @@ -93,7 +93,7 @@ describe('Upgrade Assistant Telemetry API', () => { const resp = await routeDependencies.router.getHandler({ method: 'put', - pathPattern: '/api/upgrade_assistant/telemetry/ui_open', + pathPattern: '/api/upgrade_assistant/stats/ui_open', })( routeHandlerContextMock, createRequestMock({ @@ -108,7 +108,7 @@ describe('Upgrade Assistant Telemetry API', () => { }); }); - describe('PUT /api/upgrade_assistant/telemetry/ui_reindex', () => { + describe('PUT /api/upgrade_assistant/stats/ui_reindex', () => { it('returns correct payload with single option', async () => { const returnPayload = { close: false, @@ -121,7 +121,7 @@ describe('Upgrade Assistant Telemetry API', () => { const resp = await routeDependencies.router.getHandler({ method: 'put', - pathPattern: '/api/upgrade_assistant/telemetry/ui_reindex', + pathPattern: '/api/upgrade_assistant/stats/ui_reindex', })( routeHandlerContextMock, createRequestMock({ @@ -147,7 +147,7 @@ describe('Upgrade Assistant Telemetry API', () => { const resp = await routeDependencies.router.getHandler({ method: 'put', - pathPattern: '/api/upgrade_assistant/telemetry/ui_reindex', + pathPattern: '/api/upgrade_assistant/stats/ui_reindex', })( routeHandlerContextMock, createRequestMock({ @@ -169,7 +169,7 @@ describe('Upgrade Assistant Telemetry API', () => { const resp = await routeDependencies.router.getHandler({ method: 'put', - pathPattern: '/api/upgrade_assistant/telemetry/ui_reindex', + pathPattern: '/api/upgrade_assistant/stats/ui_reindex', })( routeHandlerContextMock, createRequestMock({ diff --git a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts index 900a5e64c55c3..71f5de01f6a44 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts @@ -12,7 +12,7 @@ import { RouteDependencies } from '../types'; export function registerTelemetryRoutes({ router, getSavedObjectsService }: RouteDependencies) { router.put( { - path: '/api/upgrade_assistant/telemetry/ui_open', + path: '/api/upgrade_assistant/stats/ui_open', validate: { body: schema.object({ overview: schema.boolean({ defaultValue: false }), @@ -40,7 +40,7 @@ export function registerTelemetryRoutes({ router, getSavedObjectsService }: Rout router.put( { - path: '/api/upgrade_assistant/telemetry/ui_reindex', + path: '/api/upgrade_assistant/stats/ui_reindex', validate: { body: schema.object({ close: schema.boolean({ defaultValue: false }), diff --git a/x-pack/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx b/x-pack/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx index 4223e918393b6..edb7e13ed064f 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { LineSeries, CurveType } from '@elastic/charts'; +import { LineSeries, CurveType, Fit } from '@elastic/charts'; import { LocationDurationLine } from '../../../../common/types'; import { convertMicrosecondsToMilliseconds as microsToMillis } from '../../../lib/helper'; @@ -28,6 +28,7 @@ export const DurationLineSeriesList = ({ lines }: Props) => ( yAccessors={[1]} yScaleToDataExtent={false} yScaleType="linear" + fit={Fit.Linear} /> ))} diff --git a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx index 39b8a38f60982..3f252c2a436ae 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx @@ -17,6 +17,7 @@ import { EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useContext } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; +import numeral from '@elastic/numeral'; import moment from 'moment'; import { getChartDateLabel } from '../../../lib/helper'; import { ChartWrapper } from './chart_wrapper'; @@ -144,6 +145,8 @@ export const PingHistogramComponent: React.FC = ({ defaultMessage: 'Ping Y Axis', })} position="left" + tickFormat={(d) => numeral(d).format('0')} + labelFormat={(d) => numeral(d).format('0a')} title={i18n.translate('xpack.uptime.snapshotHistogram.yAxis.title', { defaultMessage: 'Pings', description: diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index b9dc7945e9579..3fc26811d46eb 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import Mustache from 'mustache'; import { UptimeAlertTypeFactory } from './types'; -import { esKuery, IIndexPattern } from '../../../../../../src/plugins/data/server'; +import { esKuery } from '../../../../../../src/plugins/data/server'; import { JsonObject } from '../../../../../../src/plugins/kibana_utils/common'; import { StatusCheckFilters, @@ -26,7 +26,7 @@ import { UNNAMED_LOCATION } from '../../../common/constants'; import { uptimeAlertWrapper } from './uptime_alert_wrapper'; import { MonitorStatusTranslations } from '../../../common/translations'; import { ESAPICaller } from '../adapters/framework'; -import { getUptimeIndexPattern } from '../requests/get_index_pattern'; +import { getUptimeIndexPattern, IndexPatternTitleAndFields } from '../requests/get_index_pattern'; import { UMServerLibs } from '../lib'; const { MONITOR_STATUS } = ACTION_GROUP_DEFINITIONS; @@ -58,7 +58,7 @@ export const hasFilters = (filters?: StatusCheckFilters) => { }; export const generateFilterDSL = async ( - getIndexPattern: () => Promise, + getIndexPattern: () => Promise, filters: StatusCheckFilters, search: string ): Promise => { diff --git a/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts b/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts index 345d02b990eb7..1d284143a1ab0 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_index_pattern.ts @@ -6,12 +6,17 @@ import { LegacyAPICaller, LegacyCallAPIOptions } from 'src/core/server'; import { UMElasticsearchQueryFn } from '../adapters'; -import { IndexPatternsFetcher, IIndexPattern } from '../../../../../../src/plugins/data/server'; +import { IndexPatternsFetcher, FieldDescriptor } from '../../../../../../src/plugins/data/server'; -export const getUptimeIndexPattern: UMElasticsearchQueryFn<{}, IIndexPattern | undefined> = async ({ - callES, - dynamicSettings, -}) => { +export interface IndexPatternTitleAndFields { + title: string; + fields: FieldDescriptor[]; +} + +export const getUptimeIndexPattern: UMElasticsearchQueryFn< + {}, + IndexPatternTitleAndFields | undefined +> = async ({ callES, dynamicSettings }) => { const callAsCurrentUser: LegacyAPICaller = async ( endpoint: string, clientParams: Record = {}, @@ -28,7 +33,7 @@ export const getUptimeIndexPattern: UMElasticsearchQueryFn<{}, IIndexPattern | u pattern: dynamicSettings.heartbeatIndices, }); - const indexPattern: IIndexPattern = { + const indexPattern: IndexPatternTitleAndFields = { fields, title: dynamicSettings.heartbeatIndices, }; diff --git a/x-pack/plugins/watcher/common/constants/plugin.ts b/x-pack/plugins/watcher/common/constants/plugin.ts index f89ef95e9261f..fa95b86c0673b 100644 --- a/x-pack/plugins/watcher/common/constants/plugin.ts +++ b/x-pack/plugins/watcher/common/constants/plugin.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LICENSE_TYPE_GOLD, LicenseType } from '../../../../legacy/common/constants'; +import { LicenseType } from '../../../licensing/common/types'; export const PLUGIN = { ID: 'watcher', - MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_GOLD as LicenseType, + MINIMUM_LICENSE_REQUIRED: 'gold' as LicenseType, getI18nName: (i18n: any): string => { return i18n.translate('xpack.watcher.appName', { defaultMessage: 'Watcher', diff --git a/x-pack/plugins/watcher/server/types.ts b/x-pack/plugins/watcher/server/types.ts index 167dcb3ab64c3..5ef3aef7de1c6 100644 --- a/x-pack/plugins/watcher/server/types.ts +++ b/x-pack/plugins/watcher/server/types.ts @@ -8,8 +8,6 @@ import { IRouter } from 'kibana/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; -import { XPackMainPlugin } from '../../../legacy/plugins/xpack_main/server/xpack_main'; - export interface Dependencies { licensing: LicensingPluginSetup; features: FeaturesPluginSetup; @@ -18,7 +16,6 @@ export interface Dependencies { export interface ServerShim { route: any; plugins: { - xpack_main: XPackMainPlugin; watcher: any; }; } diff --git a/x-pack/legacy/common/eui_styled_components/eui_styled_components.tsx b/x-pack/plugins/xpack_legacy/common/eui_styled_components.tsx similarity index 100% rename from x-pack/legacy/common/eui_styled_components/eui_styled_components.tsx rename to x-pack/plugins/xpack_legacy/common/eui_styled_components.tsx diff --git a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/index.js b/x-pack/plugins/xpack_legacy/common/index.ts similarity index 83% rename from x-pack/legacy/plugins/xpack_main/server/routes/api/v1/index.js rename to x-pack/plugins/xpack_legacy/common/index.ts index 80baf7bf1a64d..8c0dace27faf4 100644 --- a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/index.js +++ b/x-pack/plugins/xpack_legacy/common/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { xpackInfoRoute } from './xpack_info'; +export * from './eui_styled_components'; diff --git a/x-pack/tasks/build.ts b/x-pack/tasks/build.ts index 41bf9587cad1e..a3b08a16f4b08 100644 --- a/x-pack/tasks/build.ts +++ b/x-pack/tasks/build.ts @@ -61,9 +61,6 @@ async function copySourceAndBabelify() { 'plugins/**/*', 'plugins/reporting/.phantom/*', 'plugins/reporting/.chromium/*', - 'legacy/common/**/*', - 'legacy/plugins/**/*', - 'legacy/server/**/*', 'typings/**/*', ], { diff --git a/x-pack/tasks/helpers/flags.ts b/x-pack/tasks/helpers/flags.ts index 33ee126c2a2ee..8820a4d60aa40 100644 --- a/x-pack/tasks/helpers/flags.ts +++ b/x-pack/tasks/helpers/flags.ts @@ -4,14 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { resolve } from 'path'; - import log from 'fancy-log'; import getopts from 'getopts'; -import { toArray } from 'rxjs/operators'; - -// @ts-ignore complicated module doesn't have types yet -import { findPluginSpecs } from '../../../src/legacy/plugin_discovery'; /* Usage: @@ -53,18 +47,3 @@ export const FLAGS = { .map((id) => id.trim()) : undefined, }; - -export async function getEnabledPlugins() { - if (FLAGS.plugins) { - return FLAGS.plugins; - } - - const { spec$ } = findPluginSpecs({ - plugins: { - paths: [resolve(__dirname, '..', '..')], - }, - }); - - const enabledPlugins: Array<{ getId: () => string }> = await spec$.pipe(toArray()).toPromise(); - return enabledPlugins.map((spec) => spec.getId()); -} diff --git a/x-pack/test/accessibility/apps/dashboard_edit_panel.ts b/x-pack/test/accessibility/apps/dashboard_edit_panel.ts new file mode 100644 index 0000000000000..1c3456ad8d593 --- /dev/null +++ b/x-pack/test/accessibility/apps/dashboard_edit_panel.ts @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const { dashboard } = getPageObjects(['dashboard']); + const a11y = getService('a11y'); + const dashboardPanelActions = getService('dashboardPanelActions'); + const testSubjects = getService('testSubjects'); + const esArchiver = getService('esArchiver'); + const drilldowns = getService('dashboardDrilldownsManage'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['security', 'common']); + const toasts = getService('toasts'); + + describe('Dashboard Edit Panel', () => { + before(async () => { + await esArchiver.load('dashboard/drilldowns'); + await esArchiver.loadIfNeeded('logstash_functional'); + await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); + await PageObjects.common.navigateToApp('dashboard'); + await dashboard.loadSavedDashboard(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME); + await testSubjects.click('dashboardEditMode'); + }); + + after(async () => { + await esArchiver.unload('dashboard/drilldowns'); + }); + + // embeddable edit panel + it(' A11y test on dashboard edit panel menu options', async () => { + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await a11y.testAppSnapshot(); + }); + + // https://github.com/elastic/kibana/issues/77931 + it.skip('A11y test for edit visualization and save', async () => { + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await testSubjects.click('embeddablePanelAction-editPanel'); + await testSubjects.click('visualizesaveAndReturnButton'); + await a11y.testAppSnapshot(); + }); + + // clone panel + it(' A11y test on dashboard embeddable clone panel', async () => { + await testSubjects.click('embeddablePanelAction-clonePanel'); + await a11y.testAppSnapshot(); + await toasts.dismissAllToasts(); + await dashboardPanelActions.removePanelByTitle('Visualization PieChart (copy)'); + }); + + // edit panel title + it(' A11y test on dashboard embeddable edit dashboard title', async () => { + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await testSubjects.click('embeddablePanelAction-ACTION_CUSTOMIZE_PANEL'); + await a11y.testAppSnapshot(); + await testSubjects.click('customizePanelHideTitle'); + await a11y.testAppSnapshot(); + await testSubjects.click('saveNewTitleButton'); + }); + + // create drilldown + it('A11y test on dashboard embeddable open flyout and drilldown', async () => { + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await testSubjects.click('embeddablePanelAction-OPEN_FLYOUT_ADD_DRILLDOWN'); + await a11y.testAppSnapshot(); + await testSubjects.click('flyoutCloseButton'); + }); + + // clicking on more button + it('A11y test on dashboard embeddable more button', async () => { + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await testSubjects.click('embeddablePanelMore-mainMenu'); + await a11y.testAppSnapshot(); + }); + + // https://github.com/elastic/kibana/issues/77422 + it.skip('A11y test on dashboard embeddable custom time range', async () => { + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await testSubjects.click('embeddablePanelAction-CUSTOM_TIME_RANGE'); + await a11y.testAppSnapshot(); + }); + + // flow will change whenever the custom time range a11y issue gets fixed. + // Will need to click on gear icon and then click on more. + + // inspector panel + it('A11y test on dashboard embeddable open inspector', async () => { + await testSubjects.click('embeddablePanelAction-openInspector'); + await a11y.testAppSnapshot(); + await testSubjects.click('euiFlyoutCloseButton'); + }); + + // fullscreen + it('A11y test on dashboard embeddable fullscreen', async () => { + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await testSubjects.click('embeddablePanelMore-mainMenu'); + await testSubjects.click('embeddablePanelAction-togglePanel'); + await a11y.testAppSnapshot(); + }); + + // minimize fullscreen panel + it('A11y test on dashboard embeddable fullscreen minimize ', async () => { + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await testSubjects.click('embeddablePanelMore-mainMenu'); + await testSubjects.click('embeddablePanelAction-togglePanel'); + await a11y.testAppSnapshot(); + }); + + // replace panel + it('A11y test on dashboard embeddable replace panel', async () => { + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await testSubjects.click('embeddablePanelMore-mainMenu'); + await testSubjects.click('embeddablePanelAction-replacePanel'); + await a11y.testAppSnapshot(); + await testSubjects.click('euiFlyoutCloseButton'); + }); + + // delete from dashboard + it('A11y test on dashboard embeddable delete panel', async () => { + await testSubjects.click('embeddablePanelToggleMenuIcon'); + await testSubjects.click('embeddablePanelMore-mainMenu'); + await testSubjects.click('embeddablePanelAction-deletePanel'); + await a11y.testAppSnapshot(); + }); + }); +} diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index bae7b688fd28c..1163b74b24628 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -21,7 +21,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./apps/search_profiler'), require.resolve('./apps/uptime'), require.resolve('./apps/spaces'), + require.resolve('./apps/dashboard_edit_panel'), ], + pageObjects, services, diff --git a/x-pack/test/api_integration/apis/lens/telemetry.ts b/x-pack/test/api_integration/apis/lens/telemetry.ts index 0ae4753cd2967..5525a82b02ee8 100644 --- a/x-pack/test/api_integration/apis/lens/telemetry.ts +++ b/x-pack/test/api_integration/apis/lens/telemetry.ts @@ -60,7 +60,7 @@ export default ({ getService }: FtrProviderContext) => { it('should do nothing on empty post', async () => { await supertest - .post('/api/lens/telemetry') + .post('/api/lens/stats') .set(COMMON_HEADERS) .send({ events: {}, @@ -73,7 +73,7 @@ export default ({ getService }: FtrProviderContext) => { it('should write a document per results', async () => { await supertest - .post('/api/lens/telemetry') + .post('/api/lens/stats') .set(COMMON_HEADERS) .send({ events: { diff --git a/x-pack/test/api_integration/apis/maps/get_grid_tile.js b/x-pack/test/api_integration/apis/maps/get_grid_tile.js new file mode 100644 index 0000000000000..3eee56c962a27 --- /dev/null +++ b/x-pack/test/api_integration/apis/maps/get_grid_tile.js @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export default function ({ getService }) { + const supertest = getService('supertest'); + + describe('getGridTile', () => { + it('should validate params', async () => { + await supertest + .get( + `/api/maps/mvt/getGridTile?x=0&y=0&z=0&geometryFieldName=coordinates&index=logstash*&requestBody=(_source:(excludes:!()),aggs:(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:coordinates))),geotile_grid:(bounds:!n,field:coordinates,precision:!n,shard_size:65535,size:65535))),docvalue_fields:!((field:%27@timestamp%27,format:date_time),(field:timestamp,format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(timestamp:(format:strict_date_optional_time,gte:%272020-09-16T13:57:36.734Z%27,lte:%272020-09-23T13:57:36.734Z%27)))),must:!(),must_not:!(),should:!())),script_fields:(hour_of_day:(script:(lang:painless,source:%27doc[!%27timestamp!%27].value.getHour()%27))),size:0,stored_fields:!(%27*%27))&requestType=point&geoFieldType=geo_point` + ) + .set('kbn-xsrf', 'kibana') + .expect(200); + }); + + it('should not validate when required params are missing', async () => { + await supertest + .get( + `/api/maps/mvt/getGridTile?x=0&y=0&z=0&geometryFieldName=coordinates&index=logstash*&requestBody=(_source:(excludes:!()),aggs:(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:coordinates))),geotile_grid:(bounds:!n,field:coordinates,precision:!n,shard_size:65535,size:65535))),docvalue_fields:!((field:%27@timestamp%27,format:date_time),(field:timestamp,format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((match_all:()),(range:(timestamp:(format:strict_date_optional_time,gte:%272020-09-16T13:57:36.734Z%27,lte:%272020-09-23T13:57:36.734Z%27)))),must:!(),must_not:!(),should:!())),script_fields:(hour_of_day:(script:(lang:painless,source:%27doc[!%27timestamp!%27].value.getHour()%27))),size:0,stored_fields:!(%27*%27))&requestType=point` + ) + .set('kbn-xsrf', 'kibana') + .expect(400); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/maps/index.js b/x-pack/test/api_integration/apis/maps/index.js index 6c213380dd64e..9f1fe96c5637b 100644 --- a/x-pack/test/api_integration/apis/maps/index.js +++ b/x-pack/test/api_integration/apis/maps/index.js @@ -17,6 +17,7 @@ export default function ({ loadTestFile, getService }) { loadTestFile(require.resolve('./index_settings')); loadTestFile(require.resolve('./migrations')); loadTestFile(require.resolve('./get_tile')); + loadTestFile(require.resolve('./get_grid_tile')); }); }); } diff --git a/x-pack/test/api_integration/apis/security_solution/index.js b/x-pack/test/api_integration/apis/security_solution/index.js index b97795f325271..16a38c0fafbca 100644 --- a/x-pack/test/api_integration/apis/security_solution/index.js +++ b/x-pack/test/api_integration/apis/security_solution/index.js @@ -20,9 +20,9 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./overview_network')); loadTestFile(require.resolve('./timeline')); loadTestFile(require.resolve('./timeline_details')); - loadTestFile(require.resolve('./uncommon_processes')); + // loadTestFile(require.resolve('./uncommon_processes')); loadTestFile(require.resolve('./users')); - loadTestFile(require.resolve('./tls')); + // loadTestFile(require.resolve('./tls')); loadTestFile(require.resolve('./feature_controls')); }); } diff --git a/x-pack/test/api_integration/apis/security_solution/tls.ts b/x-pack/test/api_integration/apis/security_solution/tls.ts index e5f6233d50d59..ebaec7783427f 100644 --- a/x-pack/test/api_integration/apis/security_solution/tls.ts +++ b/x-pack/test/api_integration/apis/security_solution/tls.ts @@ -5,11 +5,14 @@ */ import expect from '@kbn/expect'; +// @ts-expect-error import { tlsQuery } from '../../../../plugins/security_solution/public/network/containers/tls/index.gql_query'; import { Direction, + // @ts-expect-error TlsFields, FlowTarget, + // @ts-expect-error GetTlsQuery, } from '../../../../plugins/security_solution/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/api_integration/apis/security_solution/uncommon_processes.ts b/x-pack/test/api_integration/apis/security_solution/uncommon_processes.ts index f1e064bcc37bb..1ed9a03ecf87e 100644 --- a/x-pack/test/api_integration/apis/security_solution/uncommon_processes.ts +++ b/x-pack/test/api_integration/apis/security_solution/uncommon_processes.ts @@ -6,7 +6,9 @@ import expect from '@kbn/expect'; +// @ts-expect-error import { uncommonProcessesQuery } from '../../../../plugins/security_solution/public/hosts/containers/uncommon_processes/index.gql_query'; +// @ts-expect-error import { GetUncommonProcessesQuery } from '../../../../plugins/security_solution/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/apm_api_integration/basic/tests/index.ts b/x-pack/test/apm_api_integration/basic/tests/index.ts index 8aa509b0899ce..9e1cb1f5872f1 100644 --- a/x-pack/test/apm_api_integration/basic/tests/index.ts +++ b/x-pack/test/apm_api_integration/basic/tests/index.ts @@ -45,7 +45,6 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont loadTestFile(require.resolve('./transaction_groups/transaction_charts')); loadTestFile(require.resolve('./transaction_groups/error_rate')); loadTestFile(require.resolve('./transaction_groups/breakdown')); - loadTestFile(require.resolve('./transaction_groups/avg_duration_by_browser')); }); describe('Observability overview', function () { diff --git a/x-pack/test/apm_api_integration/basic/tests/metrics_charts/metrics_charts.ts b/x-pack/test/apm_api_integration/basic/tests/metrics_charts/metrics_charts.ts index ad3d1b0ccc4d9..cae562b3f5dc5 100644 --- a/x-pack/test/apm_api_integration/basic/tests/metrics_charts/metrics_charts.ts +++ b/x-pack/test/apm_api_integration/basic/tests/metrics_charts/metrics_charts.ts @@ -19,8 +19,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - // FLAKY: https://github.com/elastic/kibana/issues/77870 - describe.skip('when data is loaded', () => { + describe('when data is loaded', () => { before(() => esArchiver.load('metrics_8.0.0')); after(() => esArchiver.unload('metrics_8.0.0')); @@ -70,7 +69,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { .toMatchInline(` Array [ 0.714, - 0.38770000000000004, + 0.3877, 0.75, 0.2543, ] @@ -100,8 +99,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) .toMatchInline(` Array [ - 0.7220939209255549, - 0.7181735467963479, + 0.722093920925555, + 0.718173546796348, ] `); }); @@ -162,9 +161,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { .toMatchInline(` Array [ 0.203, - 0.17877777777777779, + 0.178777777777778, 0.01, - 0.009000000000000001, + 0.009, ] `); }); @@ -175,8 +174,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { Array [ 0.193, 0.193, - 0.009000000000000001, - 0.009000000000000001, + 0.009, + 0.009, ] `); }); @@ -204,8 +203,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) .toMatchInline(` Array [ - 0.7079247035578369, - 0.7053959808411816, + 0.707924703557837, + 0.705395980841182, ] `); }); @@ -214,8 +213,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { const yValues = systemMemoryUsageChart?.series.map((serie) => first(serie.data)?.y); expectSnapshot(yValues).toMatchInline(` Array [ - 0.7079247035578369, - 0.7079247035578369, + 0.707924703557837, + 0.707924703557837, ] `); }); @@ -244,7 +243,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) .toMatchInline(` Array [ - 222501617.7777778, + 222501617.777778, 374341632, 1560281088, ] @@ -285,8 +284,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) .toMatchInline(` Array [ - 138573397.33333334, - 147677639.1111111, + 138573397.333333, + 147677639.111111, ] `); }); @@ -324,7 +323,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) .toMatchInline(` Array [ - 44.44444444444444, + 44.4444444444444, 45, ] `); @@ -423,16 +422,16 @@ export default function ApiTest({ getService }: FtrProviderContext) { expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) .toMatchInline(` Array [ - 0.11452389642649889, - 0.11400237609041514, + 0.114523896426499, + 0.114002376090415, ] `); const yValues = systemMemoryUsageChart?.series.map((serie) => first(serie.data)?.y); expectSnapshot(yValues).toMatchInline(` Array [ - 0.11383724014063981, - 0.11383724014063981, + 0.11383724014064, + 0.11383724014064, ] `); }); diff --git a/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts b/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts index 5b04213401660..41564af55562a 100644 --- a/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts +++ b/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts @@ -64,15 +64,15 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, Object { "x": "2020-09-15T08:54:00.000Z", - "y": 1.8666666666666667, + "y": 1.86666666666667, }, Object { "x": "2020-09-15T08:55:00.000Z", - "y": 0.9666666666666667, + "y": 0.966666666666667, }, Object { "x": "2020-09-15T08:56:00.000Z", - "y": 1.9333333333333333, + "y": 1.93333333333333, }, Object { "x": "2020-09-15T08:57:00.000Z", diff --git a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts index 9eb9d80e26b6c..0e0d5cb21b71a 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts @@ -91,43 +91,43 @@ export default function ApiTest({ getService }: FtrProviderContext) { Array [ Object { "avgResponseTime": Object { - "value": 213583.7652495379, + "value": 213583.765249538, }, "transactionErrorRate": Object { "value": 0, }, "transactionsPerMinute": Object { - "value": 18.033333333333335, + "value": 18.0333333333333, }, }, Object { "avgResponseTime": Object { - "value": 600255.7079646018, + "value": 600255.707964602, }, "transactionErrorRate": Object { "value": 0, }, "transactionsPerMinute": Object { - "value": 7.533333333333333, + "value": 7.53333333333333, }, }, Object { "avgResponseTime": Object { - "value": 1818501.060810811, + "value": 1818501.06081081, }, "transactionErrorRate": Object { - "value": 0.02027027027027027, + "value": 0.0202702702702703, }, "transactionsPerMinute": Object { - "value": 4.933333333333334, + "value": 4.93333333333333, }, }, Object { "avgResponseTime": Object { - "value": 290900.5714285714, + "value": 290900.571428571, }, "transactionErrorRate": Object { - "value": 0.013605442176870748, + "value": 0.0136054421768707, }, "transactionsPerMinute": Object { "value": 4.9, @@ -135,10 +135,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, Object { "avgResponseTime": Object { - "value": 1123903.7027027027, + "value": 1123903.7027027, }, "transactionErrorRate": Object { - "value": 0.009009009009009009, + "value": 0.00900900900900901, }, "transactionsPerMinute": Object { "value": 3.7, @@ -146,10 +146,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, Object { "avgResponseTime": Object { - "value": 80364.62962962964, + "value": 80364.6296296296, }, "transactionErrorRate": Object { - "value": 0.18518518518518517, + "value": 0.185185185185185, }, "transactionsPerMinute": Object { "value": 3.6, @@ -157,10 +157,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, Object { "avgResponseTime": Object { - "value": 1365102.9411764706, + "value": 1365102.94117647, }, "transactionsPerMinute": Object { - "value": 2.2666666666666666, + "value": 2.26666666666667, }, }, ] diff --git a/x-pack/test/apm_api_integration/basic/tests/traces/__snapshots__/top_traces.snap b/x-pack/test/apm_api_integration/basic/tests/traces/__snapshots__/top_traces.snap index cd5773d18d6b7..157bbccd109be 100644 --- a/x-pack/test/apm_api_integration/basic/tests/traces/__snapshots__/top_traces.snap +++ b/x-pack/test/apm_api_integration/basic/tests/traces/__snapshots__/top_traces.snap @@ -12,11 +12,11 @@ Array [ "serviceName": "opbeans-node", "transactionName": "POST /api/orders", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "averageResponseTime": 3347, - "impact": 0.003559081182448518, + "impact": 0.00355908118244852, "key": Object { "service.name": "opbeans-python", "transaction.name": "GET opbeans.views.stats", @@ -24,7 +24,7 @@ Array [ "serviceName": "opbeans-python", "transactionName": "GET opbeans.views.stats", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "averageResponseTime": 4479, @@ -36,11 +36,11 @@ Array [ "serviceName": "opbeans-node", "transactionName": "GET /api/customers/:id", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "averageResponseTime": 7287, - "impact": 0.009904230439845424, + "impact": 0.00990423043984542, "key": Object { "service.name": "opbeans-node", "transaction.name": "GET /api/products/top", @@ -48,11 +48,11 @@ Array [ "serviceName": "opbeans-node", "transactionName": "GET /api/products/top", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "averageResponseTime": 8023, - "impact": 0.011089517204678958, + "impact": 0.011089517204679, "key": Object { "service.name": "opbeans-ruby", "transaction.name": "Api::OrdersController#show", @@ -60,11 +60,11 @@ Array [ "serviceName": "opbeans-ruby", "transactionName": "Api::OrdersController#show", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "averageResponseTime": 8282, - "impact": 0.011506622193934236, + "impact": 0.0115066221939342, "key": Object { "service.name": "opbeans-node", "transaction.name": "GET /api/orders/:id", @@ -72,11 +72,11 @@ Array [ "serviceName": "opbeans-node", "transactionName": "GET /api/orders/:id", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "averageResponseTime": 12116, - "impact": 0.017681064390091532, + "impact": 0.0176810643900915, "key": Object { "service.name": "opbeans-ruby", "transaction.name": "Api::ProductsController#top", @@ -84,11 +84,11 @@ Array [ "serviceName": "opbeans-ruby", "transactionName": "Api::ProductsController#top", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "averageResponseTime": 6451, - "impact": 0.018946873353622995, + "impact": 0.018946873353623, "key": Object { "service.name": "opbeans-node", "transaction.name": "GET /api/products", @@ -96,11 +96,11 @@ Array [ "serviceName": "opbeans-node", "transactionName": "GET /api/products", "transactionType": "request", - "transactionsPerMinute": 0.06666666666666667, + "transactionsPerMinute": 0.0666666666666667, }, Object { "averageResponseTime": 13360, - "impact": 0.019684456693696034, + "impact": 0.019684456693696, "key": Object { "service.name": "opbeans-java", "transaction.name": "APIRestController#customers", @@ -108,11 +108,11 @@ Array [ "serviceName": "opbeans-java", "transactionName": "APIRestController#customers", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "averageResponseTime": 7903, - "impact": 0.023623602653998786, + "impact": 0.0236236026539988, "key": Object { "service.name": "opbeans-java", "transaction.name": "APIRestController#product", @@ -120,11 +120,11 @@ Array [ "serviceName": "opbeans-java", "transactionName": "APIRestController#product", "transactionType": "request", - "transactionsPerMinute": 0.06666666666666667, + "transactionsPerMinute": 0.0666666666666667, }, Object { "averageResponseTime": 17913, - "impact": 0.027016808107129565, + "impact": 0.0270168081071296, "key": Object { "service.name": "opbeans-node", "transaction.name": "GET /api/stats", @@ -132,11 +132,11 @@ Array [ "serviceName": "opbeans-node", "transactionName": "GET /api/stats", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { - "averageResponseTime": 6065.666666666667, - "impact": 0.02747417419573381, + "averageResponseTime": 6065.66666666667, + "impact": 0.0274741741957338, "key": Object { "service.name": "opbeans-java", "transaction.name": "APIRestController#topProducts", @@ -148,7 +148,7 @@ Array [ }, Object { "averageResponseTime": 2340.875, - "impact": 0.02832770950193187, + "impact": 0.0283277095019319, "key": Object { "service.name": "opbeans-java", "transaction.name": "ResourceHttpRequestHandler", @@ -156,11 +156,11 @@ Array [ "serviceName": "opbeans-java", "transactionName": "ResourceHttpRequestHandler", "transactionType": "request", - "transactionsPerMinute": 0.26666666666666666, + "transactionsPerMinute": 0.266666666666667, }, Object { - "averageResponseTime": 7340.666666666667, - "impact": 0.03363412239612548, + "averageResponseTime": 7340.66666666667, + "impact": 0.0336341223961255, "key": Object { "service.name": "opbeans-java", "transaction.name": "APIRestController#customerWhoBought", @@ -172,7 +172,7 @@ Array [ }, Object { "averageResponseTime": 7689, - "impact": 0.03531703634891222, + "impact": 0.0353170363489122, "key": Object { "service.name": "opbeans-node", "transaction.name": "GET /api/types", @@ -184,7 +184,7 @@ Array [ }, Object { "averageResponseTime": 11598, - "impact": 0.035524783621552876, + "impact": 0.0355247836215529, "key": Object { "service.name": "opbeans-node", "transaction.name": "GET /api/products/:id/customers", @@ -192,11 +192,11 @@ Array [ "serviceName": "opbeans-node", "transactionName": "GET /api/products/:id/customers", "transactionType": "request", - "transactionsPerMinute": 0.06666666666666667, + "transactionsPerMinute": 0.0666666666666667, }, Object { "averageResponseTime": 12077.5, - "impact": 0.03706919939257919, + "impact": 0.0370691993925792, "key": Object { "service.name": "opbeans-java", "transaction.name": "APIRestController#order", @@ -204,11 +204,11 @@ Array [ "serviceName": "opbeans-java", "transactionName": "APIRestController#order", "transactionType": "request", - "transactionsPerMinute": 0.06666666666666667, + "transactionsPerMinute": 0.0666666666666667, }, Object { "averageResponseTime": 6296.5, - "impact": 0.03872956712973051, + "impact": 0.0387295671297305, "key": Object { "service.name": "opbeans-ruby", "transaction.name": "Api::TypesController#index", @@ -216,11 +216,11 @@ Array [ "serviceName": "opbeans-ruby", "transactionName": "Api::TypesController#index", "transactionType": "request", - "transactionsPerMinute": 0.13333333333333333, + "transactionsPerMinute": 0.133333333333333, }, Object { "averageResponseTime": 28181, - "impact": 0.04355284683173653, + "impact": 0.0435528468317365, "key": Object { "service.name": "opbeans-python", "transaction.name": "GET opbeans.views.customer", @@ -228,11 +228,11 @@ Array [ "serviceName": "opbeans-python", "transactionName": "GET opbeans.views.customer", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "averageResponseTime": 7439, - "impact": 0.046089296090721335, + "impact": 0.0460892960907213, "key": Object { "service.name": "opbeans-go", "transaction.name": "GET /api/customers/:id", @@ -240,10 +240,10 @@ Array [ "serviceName": "opbeans-go", "transactionName": "GET /api/customers/:id", "transactionType": "request", - "transactionsPerMinute": 0.13333333333333333, + "transactionsPerMinute": 0.133333333333333, }, Object { - "averageResponseTime": 10471.333333333334, + "averageResponseTime": 10471.3333333333, "impact": 0.0487594121995447, "key": Object { "service.name": "opbeans-node", @@ -264,11 +264,11 @@ Array [ "serviceName": "opbeans-node", "transactionName": "GET /api/customers", "transactionType": "request", - "transactionsPerMinute": 0.06666666666666667, + "transactionsPerMinute": 0.0666666666666667, }, Object { "averageResponseTime": 11732.25, - "impact": 0.07374545045551247, + "impact": 0.0737454504555125, "key": Object { "service.name": "opbeans-java", "transaction.name": "APIRestController#customer", @@ -276,7 +276,7 @@ Array [ "serviceName": "opbeans-java", "transactionName": "APIRestController#customer", "transactionType": "request", - "transactionsPerMinute": 0.13333333333333333, + "transactionsPerMinute": 0.133333333333333, }, Object { "averageResponseTime": 47646, @@ -288,11 +288,11 @@ Array [ "serviceName": "opbeans-python", "transactionName": "GET opbeans.views.customers", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "averageResponseTime": 13160.75, - "impact": 0.08294752732271193, + "impact": 0.0829475273227119, "key": Object { "service.name": "opbeans-python", "transaction.name": "GET opbeans.views.products", @@ -300,11 +300,11 @@ Array [ "serviceName": "opbeans-python", "transactionName": "GET opbeans.views.products", "transactionType": "request", - "transactionsPerMinute": 0.13333333333333333, + "transactionsPerMinute": 0.133333333333333, }, Object { - "averageResponseTime": 4131.461538461538, - "impact": 0.08466426059895181, + "averageResponseTime": 4131.46153846154, + "impact": 0.0846642605989518, "key": Object { "service.name": "opbeans-go", "transaction.name": "GET /api/types/:id", @@ -312,11 +312,11 @@ Array [ "serviceName": "opbeans-go", "transactionName": "GET /api/types/:id", "transactionType": "request", - "transactionsPerMinute": 0.43333333333333335, + "transactionsPerMinute": 0.433333333333333, }, Object { "averageResponseTime": 13869.25, - "impact": 0.08751152554491062, + "impact": 0.0875115255449106, "key": Object { "service.name": "opbeans-ruby", "transaction.name": "Api::StatsController#index", @@ -324,11 +324,11 @@ Array [ "serviceName": "opbeans-ruby", "transactionName": "Api::StatsController#index", "transactionType": "request", - "transactionsPerMinute": 0.13333333333333333, + "transactionsPerMinute": 0.133333333333333, }, Object { - "averageResponseTime": 20643.333333333332, - "impact": 0.09790372050886552, + "averageResponseTime": 20643.3333333333, + "impact": 0.0979037205088655, "key": Object { "service.name": "opbeans-ruby", "transaction.name": "Api::ProductsController#show", @@ -340,7 +340,7 @@ Array [ }, Object { "averageResponseTime": 15596.5, - "impact": 0.09863808296099064, + "impact": 0.0986380829609906, "key": Object { "service.name": "opbeans-ruby", "transaction.name": "Api::TypesController#show", @@ -348,11 +348,11 @@ Array [ "serviceName": "opbeans-ruby", "transactionName": "Api::TypesController#show", "transactionType": "request", - "transactionsPerMinute": 0.13333333333333333, + "transactionsPerMinute": 0.133333333333333, }, Object { "averageResponseTime": 20989, - "impact": 0.09957375090986059, + "impact": 0.0995737509098606, "key": Object { "service.name": "opbeans-python", "transaction.name": "GET opbeans.views.orders", @@ -364,7 +364,7 @@ Array [ }, Object { "averageResponseTime": 74419, - "impact": 0.11801655529963453, + "impact": 0.118016555299635, "key": Object { "service.name": "opbeans-python", "transaction.name": "GET opbeans.views.product_type", @@ -372,11 +372,11 @@ Array [ "serviceName": "opbeans-python", "transactionName": "GET opbeans.views.product_type", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { - "averageResponseTime": 10678.42857142857, - "impact": 0.11854800181104089, + "averageResponseTime": 10678.4285714286, + "impact": 0.118548001811041, "key": Object { "service.name": "opbeans-go", "transaction.name": "GET /api/orders/:id", @@ -384,11 +384,11 @@ Array [ "serviceName": "opbeans-go", "transactionName": "GET /api/orders/:id", "transactionType": "request", - "transactionsPerMinute": 0.23333333333333334, + "transactionsPerMinute": 0.233333333333333, }, Object { - "averageResponseTime": 27078.666666666668, - "impact": 0.12899495187011034, + "averageResponseTime": 27078.6666666667, + "impact": 0.12899495187011, "key": Object { "service.name": "opbeans-ruby", "transaction.name": "Api::OrdersController#index", @@ -399,8 +399,8 @@ Array [ "transactionsPerMinute": 0.1, }, Object { - "averageResponseTime": 11827.42857142857, - "impact": 0.13150080269358994, + "averageResponseTime": 11827.4285714286, + "impact": 0.13150080269359, "key": Object { "service.name": "opbeans-go", "transaction.name": "GET /api/customers", @@ -408,11 +408,11 @@ Array [ "serviceName": "opbeans-go", "transactionName": "GET /api/customers", "transactionType": "request", - "transactionsPerMinute": 0.23333333333333334, + "transactionsPerMinute": 0.233333333333333, }, Object { "averageResponseTime": 21770.75, - "impact": 0.13841121778584634, + "impact": 0.138411217785846, "key": Object { "service.name": "opbeans-python", "transaction.name": "GET opbeans.views.product", @@ -420,11 +420,11 @@ Array [ "serviceName": "opbeans-python", "transactionName": "GET opbeans.views.product", "transactionType": "request", - "transactionsPerMinute": 0.13333333333333333, + "transactionsPerMinute": 0.133333333333333, }, Object { "averageResponseTime": 10252, - "impact": 0.1467613697908217, + "impact": 0.146761369790822, "key": Object { "service.name": "opbeans-go", "transaction.name": "GET /api/types", @@ -436,7 +436,7 @@ Array [ }, Object { "averageResponseTime": 100570, - "impact": 0.16013127566262603, + "impact": 0.160131275662626, "key": Object { "service.name": "opbeans-python", "transaction.name": "GET opbeans.views.top_products", @@ -444,11 +444,11 @@ Array [ "serviceName": "opbeans-python", "transactionName": "GET opbeans.views.top_products", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "averageResponseTime": 15505, - "impact": 0.1979283957314345, + "impact": 0.197928395731435, "key": Object { "service.name": "opbeans-ruby", "transaction.name": "Api::CustomersController#index", @@ -456,11 +456,11 @@ Array [ "serviceName": "opbeans-ruby", "transactionName": "Api::CustomersController#index", "transactionType": "request", - "transactionsPerMinute": 0.26666666666666666, + "transactionsPerMinute": 0.266666666666667, }, Object { "averageResponseTime": 22856.5, - "impact": 0.21902360134631826, + "impact": 0.219023601346318, "key": Object { "service.name": "opbeans-go", "transaction.name": "GET /api/products", @@ -472,7 +472,7 @@ Array [ }, Object { "averageResponseTime": 17250.125, - "impact": 0.2204118040518706, + "impact": 0.220411804051871, "key": Object { "service.name": "opbeans-ruby", "transaction.name": "Api::ProductsController#index", @@ -480,11 +480,11 @@ Array [ "serviceName": "opbeans-ruby", "transactionName": "Api::ProductsController#index", "transactionType": "request", - "transactionsPerMinute": 0.26666666666666666, + "transactionsPerMinute": 0.266666666666667, }, Object { - "averageResponseTime": 20089.555555555555, - "impact": 0.2893468583571687, + "averageResponseTime": 20089.5555555556, + "impact": 0.289346858357169, "key": Object { "service.name": "opbeans-ruby", "transaction.name": "Api::CustomersController#show", @@ -495,8 +495,8 @@ Array [ "transactionsPerMinute": 0.3, }, Object { - "averageResponseTime": 26487.85714285714, - "impact": 0.29676939463314395, + "averageResponseTime": 26487.8571428571, + "impact": 0.296769394633144, "key": Object { "service.name": "opbeans-go", "transaction.name": "GET /api/stats", @@ -504,11 +504,11 @@ Array [ "serviceName": "opbeans-go", "transactionName": "GET /api/stats", "transactionType": "request", - "transactionsPerMinute": 0.23333333333333334, + "transactionsPerMinute": 0.233333333333333, }, Object { - "averageResponseTime": 14957.538461538461, - "impact": 0.31131653504991197, + "averageResponseTime": 14957.5384615385, + "impact": 0.311316535049912, "key": Object { "service.name": "opbeans-go", "transaction.name": "GET /api/products/:id/customers", @@ -516,7 +516,7 @@ Array [ "serviceName": "opbeans-go", "transactionName": "GET /api/products/:id/customers", "transactionType": "request", - "transactionsPerMinute": 0.43333333333333335, + "transactionsPerMinute": 0.433333333333333, }, Object { "averageResponseTime": 30178.5, @@ -528,11 +528,11 @@ Array [ "serviceName": "opbeans-node", "transactionName": "GET /api", "transactionType": "request", - "transactionsPerMinute": 0.4666666666666667, + "transactionsPerMinute": 0.466666666666667, }, Object { "averageResponseTime": 32625.875, - "impact": 0.8388432258236366, + "impact": 0.838843225823637, "key": Object { "service.name": "opbeans-go", "transaction.name": "GET /api/products/:id", @@ -540,11 +540,11 @@ Array [ "serviceName": "opbeans-go", "transactionName": "GET /api/products/:id", "transactionType": "request", - "transactionsPerMinute": 0.5333333333333333, + "transactionsPerMinute": 0.533333333333333, }, Object { - "averageResponseTime": 121200.83333333333, - "impact": 1.1692918352841768, + "averageResponseTime": 121200.833333333, + "impact": 1.16929183528418, "key": Object { "service.name": "opbeans-python", "transaction.name": "GET opbeans.views.product_customers", @@ -555,8 +555,8 @@ Array [ "transactionsPerMinute": 0.2, }, Object { - "averageResponseTime": 38025.86666666667, - "impact": 3.6724805948748136, + "averageResponseTime": 38025.8666666667, + "impact": 3.67248059487481, "key": Object { "service.name": "opbeans-python", "transaction.name": "opbeans.tasks.sync_orders", @@ -579,8 +579,8 @@ Array [ "transactionsPerMinute": 0.3, }, Object { - "averageResponseTime": 691636.3636363636, - "impact": 12.25042667907868, + "averageResponseTime": 691636.363636364, + "impact": 12.2504266790787, "key": Object { "service.name": "opbeans-rum", "transaction.name": "/customers", @@ -588,11 +588,11 @@ Array [ "serviceName": "opbeans-rum", "transactionName": "/customers", "transactionType": "page-load", - "transactionsPerMinute": 0.36666666666666664, + "transactionsPerMinute": 0.366666666666667, }, Object { "averageResponseTime": 1590910.5, - "impact": 20.494746747861388, + "impact": 20.4947467478614, "key": Object { "service.name": "opbeans-go", "transaction.name": "GET /api/orders", @@ -600,11 +600,11 @@ Array [ "serviceName": "opbeans-go", "transactionName": "GET /api/orders", "transactionType": "request", - "transactionsPerMinute": 0.26666666666666666, + "transactionsPerMinute": 0.266666666666667, }, Object { - "averageResponseTime": 303589.16279069765, - "impact": 21.02144244954455, + "averageResponseTime": 303589.162790698, + "impact": 21.0214424495446, "key": Object { "service.name": "opbeans-ruby", "transaction.name": "Rack", @@ -612,11 +612,11 @@ Array [ "serviceName": "opbeans-ruby", "transactionName": "Rack", "transactionType": "request", - "transactionsPerMinute": 1.4333333333333333, + "transactionsPerMinute": 1.43333333333333, }, Object { "averageResponseTime": 1180200, - "impact": 28.507858596190804, + "impact": 28.5078585961908, "key": Object { "service.name": "opbeans-rum", "transaction.name": "/products", @@ -627,8 +627,8 @@ Array [ "transactionsPerMinute": 0.5, }, Object { - "averageResponseTime": 1073178.5714285714, - "impact": 48.390399898683754, + "averageResponseTime": 1073178.57142857, + "impact": 48.3903998986838, "key": Object { "service.name": "opbeans-rum", "transaction.name": "/dashboard", @@ -636,11 +636,11 @@ Array [ "serviceName": "opbeans-rum", "transactionName": "/dashboard", "transactionType": "page-load", - "transactionsPerMinute": 0.9333333333333333, + "transactionsPerMinute": 0.933333333333333, }, Object { - "averageResponseTime": 2676214.285714286, - "impact": 60.33667329750868, + "averageResponseTime": 2676214.28571429, + "impact": 60.3366732975087, "key": Object { "service.name": "opbeans-rum", "transaction.name": "/orders", @@ -648,11 +648,11 @@ Array [ "serviceName": "opbeans-rum", "transactionName": "/orders", "transactionType": "page-load", - "transactionsPerMinute": 0.4666666666666667, + "transactionsPerMinute": 0.466666666666667, }, Object { - "averageResponseTime": 928922.4347826086, - "impact": 68.81313564424958, + "averageResponseTime": 928922.434782609, + "impact": 68.8131356442496, "key": Object { "service.name": "opbeans-node", "transaction.name": "Process completed order", @@ -660,11 +660,11 @@ Array [ "serviceName": "opbeans-node", "transactionName": "Process completed order", "transactionType": "Worker", - "transactionsPerMinute": 1.5333333333333334, + "transactionsPerMinute": 1.53333333333333, }, Object { - "averageResponseTime": 1012219.0930232558, - "impact": 70.09342088866295, + "averageResponseTime": 1012219.09302326, + "impact": 70.0934208886629, "key": Object { "service.name": "opbeans-node", "transaction.name": "Process payment", @@ -672,11 +672,11 @@ Array [ "serviceName": "opbeans-node", "transactionName": "Process payment", "transactionType": "Worker", - "transactionsPerMinute": 1.4333333333333333, + "transactionsPerMinute": 1.43333333333333, }, Object { - "averageResponseTime": 126010.60833333334, - "impact": 73.05405786950051, + "averageResponseTime": 126010.608333333, + "impact": 73.0540578695005, "key": Object { "service.name": "opbeans-python", "transaction.name": "opbeans.tasks.update_stats", @@ -687,8 +687,8 @@ Array [ "transactionsPerMinute": 12, }, Object { - "averageResponseTime": 1041680.2444444444, - "impact": 75.48871418577934, + "averageResponseTime": 1041680.24444444, + "impact": 75.4887141857793, "key": Object { "service.name": "opbeans-node", "transaction.name": "Update shipping status", diff --git a/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts b/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts index 3429301e4a326..b6fccf8f5b581 100644 --- a/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts +++ b/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts @@ -73,7 +73,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { "serviceName": "opbeans-node", "transactionName": "POST /api/orders", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, } `); diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/avg_duration_by_browser.snap b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/avg_duration_by_browser.snap deleted file mode 100644 index ab7b71cdf9e53..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/avg_duration_by_browser.snap +++ /dev/null @@ -1,438 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Average duration by browser when data is loaded returns the average duration by browser 1`] = ` -Array [ - Object { - "data": Array [ - Object { - "x": 1600159980000, - "y": 1131000, - }, - Object { - "x": 1600160010000, - "y": 617000, - }, - Object { - "x": 1600160040000, - }, - Object { - "x": 1600160070000, - "y": 1963000, - }, - Object { - "x": 1600160100000, - "y": 1190000, - }, - Object { - "x": 1600160130000, - "y": 1697000, - }, - Object { - "x": 1600160160000, - }, - Object { - "x": 1600160190000, - "y": 710000, - }, - Object { - "x": 1600160220000, - "y": 675250, - }, - Object { - "x": 1600160250000, - }, - Object { - "x": 1600160280000, - }, - Object { - "x": 1600160310000, - "y": 1763500, - }, - Object { - "x": 1600160340000, - "y": 539333.3333333334, - }, - Object { - "x": 1600160370000, - }, - Object { - "x": 1600160400000, - }, - Object { - "x": 1600160430000, - "y": 1650000, - }, - Object { - "x": 1600160460000, - "y": 781000, - }, - Object { - "x": 1600160490000, - }, - Object { - "x": 1600160520000, - }, - Object { - "x": 1600160550000, - "y": 1710000, - }, - Object { - "x": 1600160580000, - "y": 718000, - }, - Object { - "x": 1600160610000, - "y": 11912000, - }, - Object { - "x": 1600160640000, - }, - Object { - "x": 1600160670000, - "y": 885000, - }, - Object { - "x": 1600160700000, - "y": 1043000, - }, - Object { - "x": 1600160730000, - "y": 406000, - }, - Object { - "x": 1600160760000, - }, - Object { - "x": 1600160790000, - "y": 1296000, - }, - Object { - "x": 1600160820000, - "y": 570500, - }, - Object { - "x": 1600160850000, - }, - Object { - "x": 1600160880000, - }, - Object { - "x": 1600160910000, - "y": 1110000, - }, - Object { - "x": 1600160940000, - "y": 533500, - }, - Object { - "x": 1600160970000, - "y": 782500, - }, - Object { - "x": 1600161000000, - }, - Object { - "x": 1600161030000, - "y": 1200000, - }, - Object { - "x": 1600161060000, - "y": 522000, - }, - Object { - "x": 1600161090000, - }, - Object { - "x": 1600161120000, - }, - Object { - "x": 1600161150000, - "y": 1006000, - }, - Object { - "x": 1600161180000, - "y": 1203000, - }, - Object { - "x": 1600161210000, - }, - Object { - "x": 1600161240000, - }, - Object { - "x": 1600161270000, - "y": 1908000, - }, - Object { - "x": 1600161300000, - "y": 549000, - }, - Object { - "x": 1600161330000, - "y": 685000, - }, - Object { - "x": 1600161360000, - }, - Object { - "x": 1600161390000, - "y": 2055000, - }, - Object { - "x": 1600161420000, - "y": 699750, - }, - Object { - "x": 1600161450000, - }, - Object { - "x": 1600161480000, - }, - Object { - "x": 1600161510000, - "y": 1557000, - }, - Object { - "x": 1600161540000, - "y": 4993000, - }, - Object { - "x": 1600161570000, - }, - Object { - "x": 1600161600000, - }, - Object { - "x": 1600161630000, - "y": 898000, - }, - Object { - "x": 1600161660000, - "y": 4100500, - }, - Object { - "x": 1600161690000, - }, - Object { - "x": 1600161720000, - }, - Object { - "x": 1600161750000, - "y": 1305000, - }, - Object { - "x": 1600161780000, - }, - ], - "title": "Electron", - }, -] -`; - -exports[`Average duration by browser when data is loaded returns the average duration by browser filtering by transaction name 2`] = ` -Array [ - Object { - "data": Array [ - Object { - "x": 1600159980000, - }, - Object { - "x": 1600160010000, - }, - Object { - "x": 1600160040000, - }, - Object { - "x": 1600160070000, - "y": 1096000, - }, - Object { - "x": 1600160100000, - }, - Object { - "x": 1600160130000, - }, - Object { - "x": 1600160160000, - }, - Object { - "x": 1600160190000, - "y": 710000, - }, - Object { - "x": 1600160220000, - }, - Object { - "x": 1600160250000, - }, - Object { - "x": 1600160280000, - }, - Object { - "x": 1600160310000, - "y": 1108000, - }, - Object { - "x": 1600160340000, - }, - Object { - "x": 1600160370000, - }, - Object { - "x": 1600160400000, - }, - Object { - "x": 1600160430000, - "y": 1221000, - }, - Object { - "x": 1600160460000, - }, - Object { - "x": 1600160490000, - }, - Object { - "x": 1600160520000, - }, - Object { - "x": 1600160550000, - "y": 1325000, - }, - Object { - "x": 1600160580000, - }, - Object { - "x": 1600160610000, - }, - Object { - "x": 1600160640000, - }, - Object { - "x": 1600160670000, - "y": 885000, - }, - Object { - "x": 1600160700000, - }, - Object { - "x": 1600160730000, - }, - Object { - "x": 1600160760000, - }, - Object { - "x": 1600160790000, - "y": 1296000, - }, - Object { - "x": 1600160820000, - }, - Object { - "x": 1600160850000, - }, - Object { - "x": 1600160880000, - }, - Object { - "x": 1600160910000, - "y": 1110000, - }, - Object { - "x": 1600160940000, - }, - Object { - "x": 1600160970000, - }, - Object { - "x": 1600161000000, - }, - Object { - "x": 1600161030000, - "y": 1200000, - }, - Object { - "x": 1600161060000, - }, - Object { - "x": 1600161090000, - }, - Object { - "x": 1600161120000, - }, - Object { - "x": 1600161150000, - "y": 1006000, - }, - Object { - "x": 1600161180000, - }, - Object { - "x": 1600161210000, - }, - Object { - "x": 1600161240000, - }, - Object { - "x": 1600161270000, - "y": 1908000, - }, - Object { - "x": 1600161300000, - }, - Object { - "x": 1600161330000, - }, - Object { - "x": 1600161360000, - }, - Object { - "x": 1600161390000, - "y": 1420000, - }, - Object { - "x": 1600161420000, - }, - Object { - "x": 1600161450000, - }, - Object { - "x": 1600161480000, - }, - Object { - "x": 1600161510000, - "y": 1215000, - }, - Object { - "x": 1600161540000, - }, - Object { - "x": 1600161570000, - }, - Object { - "x": 1600161600000, - }, - Object { - "x": 1600161630000, - "y": 898000, - }, - Object { - "x": 1600161660000, - }, - Object { - "x": 1600161690000, - }, - Object { - "x": 1600161720000, - }, - Object { - "x": 1600161750000, - "y": 1305000, - }, - Object { - "x": 1600161780000, - }, - ], - "title": "Electron", - }, -] -`; diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/breakdown.snap b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/breakdown.snap index 563bad8779e96..87938f6f1f122 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/breakdown.snap +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/breakdown.snap @@ -36,7 +36,7 @@ Object { }, Object { "x": 1600160190000, - "y": 0.4827586206896552, + "y": 0.482758620689655, }, Object { "x": 1600160220000, @@ -52,7 +52,7 @@ Object { }, Object { "x": 1600160310000, - "y": 0.17142857142857143, + "y": 0.171428571428571, }, Object { "x": 1600160340000, @@ -68,15 +68,15 @@ Object { }, Object { "x": 1600160430000, - "y": 0.41964285714285715, + "y": 0.419642857142857, }, Object { "x": 1600160460000, - "y": 0.7222222222222222, + "y": 0.722222222222222, }, Object { "x": 1600160490000, - "y": 0.8333333333333334, + "y": 0.833333333333333, }, Object { "x": 1600160520000, @@ -88,7 +88,7 @@ Object { }, Object { "x": 1600160580000, - "y": 0.11044776119402985, + "y": 0.11044776119403, }, Object { "x": 1600160610000, @@ -100,15 +100,15 @@ Object { }, Object { "x": 1600160670000, - "y": 0.15028901734104047, + "y": 0.15028901734104, }, Object { "x": 1600160700000, - "y": 0.38095238095238093, + "y": 0.380952380952381, }, Object { "x": 1600160730000, - "y": 0.06761565836298933, + "y": 0.0676156583629893, }, Object { "x": 1600160760000, @@ -116,7 +116,7 @@ Object { }, Object { "x": 1600160790000, - "y": 0.26373626373626374, + "y": 0.263736263736264, }, Object { "x": 1600160820000, @@ -124,7 +124,7 @@ Object { }, Object { "x": 1600160850000, - "y": 0.5294117647058824, + "y": 0.529411764705882, }, Object { "x": 1600160880000, @@ -132,11 +132,11 @@ Object { }, Object { "x": 1600160910000, - "y": 0.012096774193548387, + "y": 0.0120967741935484, }, Object { "x": 1600160940000, - "y": 0.26126126126126126, + "y": 0.261261261261261, }, Object { "x": 1600160970000, @@ -148,11 +148,11 @@ Object { }, Object { "x": 1600161030000, - "y": 0.16071428571428573, + "y": 0.160714285714286, }, Object { "x": 1600161060000, - "y": 0.040268456375838924, + "y": 0.0402684563758389, }, Object { "x": 1600161090000, @@ -164,11 +164,11 @@ Object { }, Object { "x": 1600161150000, - "y": 0.07894736842105263, + "y": 0.0789473684210526, }, Object { "x": 1600161180000, - "y": 0.4074074074074074, + "y": 0.407407407407407, }, Object { "x": 1600161210000, @@ -180,11 +180,11 @@ Object { }, Object { "x": 1600161270000, - "y": 0.6666666666666666, + "y": 0.666666666666667, }, Object { "x": 1600161300000, - "y": 0.8214285714285714, + "y": 0.821428571428571, }, Object { "x": 1600161330000, @@ -196,11 +196,11 @@ Object { }, Object { "x": 1600161390000, - "y": 0.17333333333333334, + "y": 0.173333333333333, }, Object { "x": 1600161420000, - "y": 0.14285714285714285, + "y": 0.142857142857143, }, Object { "x": 1600161450000, @@ -212,7 +212,7 @@ Object { }, Object { "x": 1600161510000, - "y": 0.42105263157894735, + "y": 0.421052631578947, }, Object { "x": 1600161540000, @@ -232,7 +232,7 @@ Object { }, Object { "x": 1600161660000, - "y": 0.018518518518518517, + "y": 0.0185185185185185, }, Object { "x": 1600161690000, @@ -244,11 +244,11 @@ Object { }, Object { "x": 1600161750000, - "y": 0.36764705882352944, + "y": 0.367647058823529, }, Object { "x": 1600161780000, - "y": 0.10526315789473684, + "y": 0.105263157894737, }, ], "hideLegend": false, @@ -289,7 +289,7 @@ Object { }, Object { "x": 1600160190000, - "y": 0.41379310344827586, + "y": 0.413793103448276, }, Object { "x": 1600160220000, @@ -305,7 +305,7 @@ Object { }, Object { "x": 1600160310000, - "y": 0.6285714285714286, + "y": 0.628571428571429, }, Object { "x": 1600160340000, @@ -341,7 +341,7 @@ Object { }, Object { "x": 1600160580000, - "y": 0.8895522388059701, + "y": 0.88955223880597, }, Object { "x": 1600160610000, @@ -353,7 +353,7 @@ Object { }, Object { "x": 1600160670000, - "y": 0.7052023121387283, + "y": 0.705202312138728, }, Object { "x": 1600160700000, @@ -361,7 +361,7 @@ Object { }, Object { "x": 1600160730000, - "y": 0.8718861209964412, + "y": 0.871886120996441, }, Object { "x": 1600160760000, @@ -369,7 +369,7 @@ Object { }, Object { "x": 1600160790000, - "y": 0.6703296703296703, + "y": 0.67032967032967, }, Object { "x": 1600160820000, @@ -385,11 +385,11 @@ Object { }, Object { "x": 1600160910000, - "y": 0.9879032258064516, + "y": 0.987903225806452, }, Object { "x": 1600160940000, - "y": 0.7387387387387387, + "y": 0.738738738738739, }, Object { "x": 1600160970000, @@ -401,7 +401,7 @@ Object { }, Object { "x": 1600161030000, - "y": 0.7946428571428571, + "y": 0.794642857142857, }, Object { "x": 1600161060000, @@ -417,7 +417,7 @@ Object { }, Object { "x": 1600161150000, - "y": 0.9210526315789473, + "y": 0.921052631578947, }, Object { "x": 1600161180000, @@ -449,11 +449,11 @@ Object { }, Object { "x": 1600161390000, - "y": 0.7466666666666667, + "y": 0.746666666666667, }, Object { "x": 1600161420000, - "y": 0.8571428571428571, + "y": 0.857142857142857, }, Object { "x": 1600161450000, @@ -465,7 +465,7 @@ Object { }, Object { "x": 1600161510000, - "y": 0.5789473684210527, + "y": 0.578947368421053, }, Object { "x": 1600161540000, @@ -485,7 +485,7 @@ Object { }, Object { "x": 1600161660000, - "y": 0.9814814814814815, + "y": 0.981481481481482, }, Object { "x": 1600161690000, @@ -497,11 +497,11 @@ Object { }, Object { "x": 1600161750000, - "y": 0.5588235294117647, + "y": 0.558823529411765, }, Object { "x": 1600161780000, - "y": 0.8947368421052632, + "y": 0.894736842105263, }, ], "hideLegend": false, @@ -542,7 +542,7 @@ Object { }, Object { "x": 1600160190000, - "y": 0.10344827586206896, + "y": 0.103448275862069, }, Object { "x": 1600160220000, @@ -574,15 +574,15 @@ Object { }, Object { "x": 1600160430000, - "y": 0.14285714285714285, + "y": 0.142857142857143, }, Object { "x": 1600160460000, - "y": 0.2777777777777778, + "y": 0.277777777777778, }, Object { "x": 1600160490000, - "y": 0.16666666666666666, + "y": 0.166666666666667, }, Object { "x": 1600160520000, @@ -606,15 +606,15 @@ Object { }, Object { "x": 1600160670000, - "y": 0.14450867052023122, + "y": 0.144508670520231, }, Object { "x": 1600160700000, - "y": 0.6190476190476191, + "y": 0.619047619047619, }, Object { "x": 1600160730000, - "y": 0.060498220640569395, + "y": 0.0604982206405694, }, Object { "x": 1600160760000, @@ -622,7 +622,7 @@ Object { }, Object { "x": 1600160790000, - "y": 0.06593406593406594, + "y": 0.0659340659340659, }, Object { "x": 1600160820000, @@ -630,7 +630,7 @@ Object { }, Object { "x": 1600160850000, - "y": 0.47058823529411764, + "y": 0.470588235294118, }, Object { "x": 1600160880000, @@ -654,7 +654,7 @@ Object { }, Object { "x": 1600161030000, - "y": 0.044642857142857144, + "y": 0.0446428571428571, }, Object { "x": 1600161060000, @@ -674,7 +674,7 @@ Object { }, Object { "x": 1600161180000, - "y": 0.5925925925925926, + "y": 0.592592592592593, }, Object { "x": 1600161210000, @@ -686,11 +686,11 @@ Object { }, Object { "x": 1600161270000, - "y": 0.3333333333333333, + "y": 0.333333333333333, }, Object { "x": 1600161300000, - "y": 0.17857142857142858, + "y": 0.178571428571429, }, Object { "x": 1600161330000, @@ -750,7 +750,7 @@ Object { }, Object { "x": 1600161750000, - "y": 0.07352941176470588, + "y": 0.0735294117647059, }, Object { "x": 1600161780000, diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/error_rate.snap b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/error_rate.snap index f9ab0ed8ff8cf..ab228385aaf56 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/error_rate.snap +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/error_rate.snap @@ -12,7 +12,7 @@ Array [ }, Object { "x": 1600160040000, - "y": 0.14285714285714285, + "y": 0.142857142857143, }, Object { "x": 1600160070000, @@ -44,11 +44,11 @@ Array [ }, Object { "x": 1600160280000, - "y": 0.16666666666666666, + "y": 0.166666666666667, }, Object { "x": 1600160310000, - "y": 0.3333333333333333, + "y": 0.333333333333333, }, Object { "x": 1600160340000, @@ -76,7 +76,7 @@ Array [ }, Object { "x": 1600160520000, - "y": 0.16666666666666666, + "y": 0.166666666666667, }, Object { "x": 1600160550000, diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/top_transaction_groups.snap b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/top_transaction_groups.snap index e37b2283f009a..93f22e67e1a02 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/top_transaction_groups.snap +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/top_transaction_groups.snap @@ -10,41 +10,41 @@ Array [ "serviceName": "opbeans-node", "transactionName": "POST /api/orders", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "averageResponseTime": 4479, - "impact": 0.1825278966745733, + "impact": 0.182527896674573, "key": "GET /api/customers/:id", "p95": 4448, "serviceName": "opbeans-node", "transactionName": "GET /api/customers/:id", "transactionType": "request", - "transactionsPerMinute": 0.03333333333333333, + "transactionsPerMinute": 0.0333333333333333, }, Object { "averageResponseTime": 2754.5, - "impact": 0.23878275411766442, + "impact": 0.238782754117664, "key": "GET /*", "p95": 2832, "serviceName": "opbeans-node", "transactionName": "GET /*", "transactionType": "request", - "transactionsPerMinute": 0.06666666666666667, + "transactionsPerMinute": 0.0666666666666667, }, Object { "averageResponseTime": 10841, - "impact": 1.122093248707094, + "impact": 1.12209324870709, "key": "GET /api/orders/:id", "p95": 13376, "serviceName": "opbeans-node", "transactionName": "GET /api/orders/:id", "transactionType": "request", - "transactionsPerMinute": 0.06666666666666667, + "transactionsPerMinute": 0.0666666666666667, }, Object { - "averageResponseTime": 10551.333333333334, - "impact": 1.6667276549425354, + "averageResponseTime": 10551.3333333333, + "impact": 1.66672765494254, "key": "GET /api/products/top", "p95": 19552, "serviceName": "opbeans-node", @@ -54,37 +54,37 @@ Array [ }, Object { "averageResponseTime": 15988, - "impact": 1.6843141249393074, + "impact": 1.68431412493931, "key": "GET /api/products/:id", "p95": 16000, "serviceName": "opbeans-node", "transactionName": "GET /api/products/:id", "transactionType": "request", - "transactionsPerMinute": 0.06666666666666667, + "transactionsPerMinute": 0.0666666666666667, }, Object { "averageResponseTime": 9499, - "impact": 2.013104650965918, + "impact": 2.01310465096592, "key": "GET /api/types", "p95": 14944, "serviceName": "opbeans-node", "transactionName": "GET /api/types", "transactionType": "request", - "transactionsPerMinute": 0.13333333333333333, + "transactionsPerMinute": 0.133333333333333, }, Object { "averageResponseTime": 12228, - "impact": 2.6092969071297842, + "impact": 2.60929690712978, "key": "GET /api/products/:id/customers", "p95": 17760, "serviceName": "opbeans-node", "transactionName": "GET /api/products/:id/customers", "transactionType": "request", - "transactionsPerMinute": 0.13333333333333333, + "transactionsPerMinute": 0.133333333333333, }, Object { - "averageResponseTime": 22555.666666666668, - "impact": 3.633626859892089, + "averageResponseTime": 22555.6666666667, + "impact": 3.63362685989209, "key": "GET /api/customers", "p95": 25984, "serviceName": "opbeans-node", @@ -94,17 +94,17 @@ Array [ }, Object { "averageResponseTime": 13852.6, - "impact": 3.7207945807456553, + "impact": 3.72079458074566, "key": "GET /api/types/:id", "p95": 21984, "serviceName": "opbeans-node", "transactionName": "GET /api/types/:id", "transactionType": "request", - "transactionsPerMinute": 0.16666666666666666, + "transactionsPerMinute": 0.166666666666667, }, Object { "averageResponseTime": 12228.5, - "impact": 3.9451586141206243, + "impact": 3.94515861412062, "key": "GET /api/orders", "p95": 16736, "serviceName": "opbeans-node", @@ -113,18 +113,18 @@ Array [ "transactionsPerMinute": 0.2, }, Object { - "averageResponseTime": 12491.42857142857, + "averageResponseTime": 12491.4285714286, "impact": 4.71355627370009, "key": "GET /api/products", "p95": 30448, "serviceName": "opbeans-node", "transactionName": "GET /api/products", "transactionType": "request", - "transactionsPerMinute": 0.23333333333333334, + "transactionsPerMinute": 0.233333333333333, }, Object { - "averageResponseTime": 23683.333333333332, - "impact": 11.579379700079686, + "averageResponseTime": 23683.3333333333, + "impact": 11.5793797000797, "key": "GET /api/stats", "p95": 36288, "serviceName": "opbeans-node", @@ -133,14 +133,14 @@ Array [ "transactionsPerMinute": 0.3, }, Object { - "averageResponseTime": 42606.74418604651, + "averageResponseTime": 42606.7441860465, "impact": 100, "key": "GET /api", "p95": 131008, "serviceName": "opbeans-node", "transactionName": "GET /api", "transactionType": "request", - "transactionsPerMinute": 1.4333333333333333, + "transactionsPerMinute": 1.43333333333333, }, ] `; diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/transaction_charts.snap b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/transaction_charts.snap index aaeac9edf01b8..9ed103b445575 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/transaction_charts.snap +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/transaction_charts.snap @@ -3,7 +3,7 @@ exports[`Transaction charts when data is loaded returns the correct data 4`] = ` Object { "apmTimeseries": Object { - "overallAvgDuration": 600255.7079646018, + "overallAvgDuration": 600255.707964602, "responseTimes": Object { "avg": Array [ Object { @@ -32,11 +32,11 @@ Object { }, Object { "x": 1600160160000, - "y": 467003.6666666667, + "y": 467003.666666667, }, Object { "x": 1600160190000, - "y": 863809.6666666666, + "y": 863809.666666667, }, Object { "x": 1600160220000, @@ -64,7 +64,7 @@ Object { }, Object { "x": 1600160400000, - "y": 368087.9090909091, + "y": 368087.909090909, }, Object { "x": 1600160430000, @@ -92,11 +92,11 @@ Object { }, Object { "x": 1600160610000, - "y": 882789.6666666666, + "y": 882789.666666667, }, Object { "x": 1600160640000, - "y": 238075.9090909091, + "y": 238075.909090909, }, Object { "x": 1600160670000, @@ -112,11 +112,11 @@ Object { }, Object { "x": 1600160760000, - "y": 282337.1666666667, + "y": 282337.166666667, }, Object { "x": 1600160790000, - "y": 987012.3333333334, + "y": 987012.333333333, }, Object { "x": 1600160820000, @@ -136,7 +136,7 @@ Object { }, Object { "x": 1600160940000, - "y": 1313632.6666666667, + "y": 1313632.66666667, }, Object { "x": 1600160970000, @@ -144,11 +144,11 @@ Object { }, Object { "x": 1600161000000, - "y": 611899.1428571428, + "y": 611899.142857143, }, Object { "x": 1600161030000, - "y": 273321.85714285716, + "y": 273321.857142857, }, Object { "x": 1600161060000, @@ -156,7 +156,7 @@ Object { }, Object { "x": 1600161090000, - "y": 1446104.6666666667, + "y": 1446104.66666667, }, Object { "x": 1600161120000, @@ -172,11 +172,11 @@ Object { }, Object { "x": 1600161210000, - "y": 1054428.6666666667, + "y": 1054428.66666667, }, Object { "x": 1600161240000, - "y": 816781.3333333334, + "y": 816781.333333333, }, Object { "x": 1600161270000, @@ -192,7 +192,7 @@ Object { }, Object { "x": 1600161360000, - "y": 714202.3333333334, + "y": 714202.333333333, }, Object { "x": 1600161390000, @@ -204,7 +204,7 @@ Object { }, Object { "x": 1600161450000, - "y": 836182.3333333334, + "y": 836182.333333333, }, Object { "x": 1600161480000, @@ -212,11 +212,11 @@ Object { }, Object { "x": 1600161510000, - "y": 615193.3333333334, + "y": 615193.333333333, }, Object { "x": 1600161540000, - "y": 946298.6666666666, + "y": 946298.666666667, }, Object { "x": 1600161570000, @@ -240,7 +240,7 @@ Object { }, Object { "x": 1600161720000, - "y": 450557.77777777775, + "y": 450557.777777778, }, Object { "x": 1600161750000, @@ -746,7 +746,7 @@ Object { }, "tpmBuckets": Array [ Object { - "avg": 2.8333333333333335, + "avg": 2.83333333333333, "dataPoints": Array [ Object { "x": 1600159980000, @@ -996,7 +996,7 @@ Object { "key": "HTTP 2xx", }, Object { - "avg": 0.23333333333333334, + "avg": 0.233333333333333, "dataPoints": Array [ Object { "x": 1600159980000, @@ -1246,7 +1246,7 @@ Object { "key": "HTTP 4xx", }, Object { - "avg": 4.466666666666667, + "avg": 4.46666666666667, "dataPoints": Array [ Object { "x": 1600159980000, diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/avg_duration_by_browser.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/avg_duration_by_browser.ts deleted file mode 100644 index 087bf1f0655e6..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/avg_duration_by_browser.ts +++ /dev/null @@ -1,67 +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 expect from '@kbn/expect'; -import archives_metadata from '../../../common/archives_metadata'; -import { expectSnapshot } from '../../../common/match_snapshot'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - const archiveName = 'apm_8.0.0'; - const metadata = archives_metadata[archiveName]; - - const start = encodeURIComponent(metadata.start); - const end = encodeURIComponent(metadata.end); - const transactionName = '/products'; - const uiFilters = encodeURIComponent(JSON.stringify({})); - - describe('Average duration by browser', () => { - describe('when data is not loaded', () => { - it('handles the empty state', async () => { - const response = await supertest.get( - `/api/apm/services/client/transaction_groups/avg_duration_by_browser?start=${start}&end=${end}&uiFilters=${uiFilters}` - ); - expect(response.status).to.be(200); - expect(response.body).to.eql([]); - }); - }); - - describe('when data is loaded', () => { - before(() => esArchiver.load(archiveName)); - after(() => esArchiver.unload(archiveName)); - - it('returns the average duration by browser', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-rum/transaction_groups/avg_duration_by_browser?start=${start}&end=${end}&uiFilters=${uiFilters}` - ); - - expect(response.status).to.be(200); - - expect(response.body.length).to.be.greaterThan(0); - - expectSnapshot(response.body).toMatch(); - - expectSnapshot(response.body.length).toMatchInline(`1`); - }); - - it('returns the average duration by browser filtering by transaction name', async () => { - const response = await supertest.get( - `/api/apm/services/opbeans-rum/transaction_groups/avg_duration_by_browser?start=${start}&end=${end}&uiFilters=${uiFilters}&transactionName=${transactionName}` - ); - - expect(response.status).to.be(200); - - expect(response.body.length).to.be.greaterThan(0); - - expectSnapshot(response.body.length).toMatchInline(`1`); - - expectSnapshot(response.body).toMatch(); - }); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts index 27a2eac3131f5..17ada95ca4958 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts @@ -80,7 +80,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('has the correct calculation for average', () => { - expectSnapshot(errorRateResponse.average).toMatchInline(`0.14086309523809523`); + expectSnapshot(errorRateResponse.average).toMatchInline(`0.140863095238095`); }); it('has the correct error rate', () => { diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts index 8dd52ef241c59..ef874695e6046 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts @@ -62,7 +62,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns the correct data', () => { expectSnapshot(response.body.apmTimeseries.overallAvgDuration).toMatchInline( - `600255.7079646018` + `600255.707964602` ); expectSnapshot(response.body.apmTimeseries.responseTimes.avg.length).toMatchInline(`61`); expectSnapshot(response.body.apmTimeseries.tpmBuckets.length).toMatchInline(`3`); diff --git a/x-pack/test/apm_api_integration/common/match_snapshot.ts b/x-pack/test/apm_api_integration/common/match_snapshot.ts index 4ac812a0ee168..d260a19b60df4 100644 --- a/x-pack/test/apm_api_integration/common/match_snapshot.ts +++ b/x-pack/test/apm_api_integration/common/match_snapshot.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SnapshotState, toMatchSnapshot, toMatchInlineSnapshot } from 'jest-snapshot'; +import { + SnapshotState, + toMatchSnapshot, + toMatchInlineSnapshot, + addSerializer, +} from 'jest-snapshot'; import path from 'path'; import expect from '@kbn/expect'; // @ts-expect-error @@ -62,6 +67,15 @@ export function registerMochaHooksForSnapshots() { { snapshotState: ISnapshotState; testsInFile: Test[] } > = {}; + addSerializer({ + serialize: (num: number) => { + return String(parseFloat(num.toPrecision(15))); + }, + test: (value: any) => { + return typeof value === 'number'; + }, + }); + registered = true; beforeEach(function () { diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_views.snap b/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_views.snap new file mode 100644 index 0000000000000..38b009fc73d34 --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_views.snap @@ -0,0 +1,280 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CSM page views when there is data returns page views 1`] = ` +Object { + "items": Array [ + Object { + "x": 1600149947000, + "y": 1, + }, + Object { + "x": 1600149957000, + "y": 0, + }, + Object { + "x": 1600149967000, + "y": 0, + }, + Object { + "x": 1600149977000, + "y": 0, + }, + Object { + "x": 1600149987000, + "y": 0, + }, + Object { + "x": 1600149997000, + "y": 0, + }, + Object { + "x": 1600150007000, + "y": 0, + }, + Object { + "x": 1600150017000, + "y": 0, + }, + Object { + "x": 1600150027000, + "y": 1, + }, + Object { + "x": 1600150037000, + "y": 0, + }, + Object { + "x": 1600150047000, + "y": 0, + }, + Object { + "x": 1600150057000, + "y": 0, + }, + Object { + "x": 1600150067000, + "y": 0, + }, + Object { + "x": 1600150077000, + "y": 1, + }, + Object { + "x": 1600150087000, + "y": 0, + }, + Object { + "x": 1600150097000, + "y": 0, + }, + Object { + "x": 1600150107000, + "y": 0, + }, + Object { + "x": 1600150117000, + "y": 0, + }, + Object { + "x": 1600150127000, + "y": 0, + }, + Object { + "x": 1600150137000, + "y": 0, + }, + Object { + "x": 1600150147000, + "y": 0, + }, + Object { + "x": 1600150157000, + "y": 0, + }, + Object { + "x": 1600150167000, + "y": 0, + }, + Object { + "x": 1600150177000, + "y": 1, + }, + Object { + "x": 1600150187000, + "y": 0, + }, + Object { + "x": 1600150197000, + "y": 0, + }, + Object { + "x": 1600150207000, + "y": 1, + }, + Object { + "x": 1600150217000, + "y": 0, + }, + Object { + "x": 1600150227000, + "y": 0, + }, + Object { + "x": 1600150237000, + "y": 1, + }, + ], + "topItems": Array [], +} +`; + +exports[`CSM page views when there is data returns page views with breakdown 1`] = ` +Object { + "items": Array [ + Object { + "Chrome": 1, + "x": 1600149947000, + "y": 1, + }, + Object { + "x": 1600149957000, + "y": 0, + }, + Object { + "x": 1600149967000, + "y": 0, + }, + Object { + "x": 1600149977000, + "y": 0, + }, + Object { + "x": 1600149987000, + "y": 0, + }, + Object { + "x": 1600149997000, + "y": 0, + }, + Object { + "x": 1600150007000, + "y": 0, + }, + Object { + "x": 1600150017000, + "y": 0, + }, + Object { + "Chrome": 1, + "x": 1600150027000, + "y": 1, + }, + Object { + "x": 1600150037000, + "y": 0, + }, + Object { + "x": 1600150047000, + "y": 0, + }, + Object { + "x": 1600150057000, + "y": 0, + }, + Object { + "x": 1600150067000, + "y": 0, + }, + Object { + "Chrome": 1, + "x": 1600150077000, + "y": 1, + }, + Object { + "x": 1600150087000, + "y": 0, + }, + Object { + "x": 1600150097000, + "y": 0, + }, + Object { + "x": 1600150107000, + "y": 0, + }, + Object { + "x": 1600150117000, + "y": 0, + }, + Object { + "x": 1600150127000, + "y": 0, + }, + Object { + "x": 1600150137000, + "y": 0, + }, + Object { + "x": 1600150147000, + "y": 0, + }, + Object { + "x": 1600150157000, + "y": 0, + }, + Object { + "x": 1600150167000, + "y": 0, + }, + Object { + "Chrome": 1, + "x": 1600150177000, + "y": 1, + }, + Object { + "x": 1600150187000, + "y": 0, + }, + Object { + "x": 1600150197000, + "y": 0, + }, + Object { + "Chrome Mobile": 1, + "x": 1600150207000, + "y": 1, + }, + Object { + "x": 1600150217000, + "y": 0, + }, + Object { + "x": 1600150227000, + "y": 0, + }, + Object { + "Chrome Mobile": 1, + "x": 1600150237000, + "y": 1, + }, + ], + "topItems": Array [ + "Chrome", + "Chrome Mobile", + ], +} +`; + +exports[`CSM page views when there is no data returns empty list 1`] = ` +Object { + "items": Array [], + "topItems": Array [], +} +`; + +exports[`CSM page views when there is no data returns empty list with breakdowns 1`] = ` +Object { + "items": Array [], + "topItems": Array [], +} +`; diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/page_views.ts b/x-pack/test/apm_api_integration/trial/tests/csm/page_views.ts new file mode 100644 index 0000000000000..ca5670d41d8ee --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/tests/csm/page_views.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { expectSnapshot } from '../../../common/match_snapshot'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default function rumServicesApiTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('CSM page views', () => { + describe('when there is no data', () => { + it('returns empty list', async () => { + const response = await supertest.get( + '/api/apm/rum-client/page-view-trends?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D' + ); + + expect(response.status).to.be(200); + expectSnapshot(response.body).toMatch(); + }); + it('returns empty list with breakdowns', async () => { + const response = await supertest.get( + '/api/apm/rum-client/page-view-trends?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D&breakdowns=%7B%22name%22%3A%22Browser%22%2C%22fieldName%22%3A%22user_agent.name%22%2C%22type%22%3A%22category%22%7D' + ); + + expect(response.status).to.be(200); + expectSnapshot(response.body).toMatch(); + }); + }); + + describe('when there is data', () => { + before(async () => { + await esArchiver.load('8.0.0'); + await esArchiver.load('rum_8.0.0'); + }); + after(async () => { + await esArchiver.unload('8.0.0'); + await esArchiver.unload('rum_8.0.0'); + }); + + it('returns page views', async () => { + const response = await supertest.get( + '/api/apm/rum-client/page-view-trends?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D' + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatch(); + }); + it('returns page views with breakdown', async () => { + const response = await supertest.get( + '/api/apm/rum-client/page-view-trends?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D&breakdowns=%7B%22name%22%3A%22Browser%22%2C%22fieldName%22%3A%22user_agent.name%22%2C%22type%22%3A%22category%22%7D' + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatch(); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/url_search.ts b/x-pack/test/apm_api_integration/trial/tests/csm/url_search.ts new file mode 100644 index 0000000000000..76dc758895e32 --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/tests/csm/url_search.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { expectSnapshot } from '../../../common/match_snapshot'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default function rumServicesApiTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('CSM url search api', () => { + describe('when there is no data', () => { + it('returns empty list', async () => { + const response = await supertest.get( + '/api/apm/rum-client/url-search?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D' + ); + + expect(response.status).to.be(200); + expectSnapshot(response.body).toMatchInline(` + Object { + "items": Array [], + "total": 0, + } + `); + }); + }); + + describe('when there is data', () => { + before(async () => { + await esArchiver.load('8.0.0'); + await esArchiver.load('rum_8.0.0'); + }); + after(async () => { + await esArchiver.unload('8.0.0'); + await esArchiver.unload('rum_8.0.0'); + }); + + it('returns top urls when no query', async () => { + const response = await supertest.get( + '/api/apm/rum-client/url-search?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D' + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatchInline(` + Object { + "items": Array [ + Object { + "count": 5, + "pld": 4924000, + "url": "http://localhost:5601/nfw/app/csm?rangeFrom=now-15m&rangeTo=now&serviceName=kibana-frontend-8_0_0", + }, + Object { + "count": 1, + "pld": 2760000, + "url": "http://localhost:5601/nfw/app/home", + }, + ], + "total": 2, + } + `); + }); + + it('returns specific results against query', async () => { + const response = await supertest.get( + '/api/apm/rum-client/url-search?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D&urlQuery=csm' + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatchInline(` + Object { + "items": Array [ + Object { + "count": 5, + "pld": 4924000, + "url": "http://localhost:5601/nfw/app/csm?rangeFrom=now-15m&rangeTo=now&serviceName=kibana-frontend-8_0_0", + }, + ], + "total": 1, + } + `); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/trial/tests/index.ts b/x-pack/test/apm_api_integration/trial/tests/index.ts index ae62253c62d81..69e54ea33c559 100644 --- a/x-pack/test/apm_api_integration/trial/tests/index.ts +++ b/x-pack/test/apm_api_integration/trial/tests/index.ts @@ -35,6 +35,8 @@ export default function observabilityApiIntegrationTests({ loadTestFile }: FtrPr loadTestFile(require.resolve('./csm/csm_services.ts')); loadTestFile(require.resolve('./csm/web_core_vitals.ts')); loadTestFile(require.resolve('./csm/long_task_metrics.ts')); + loadTestFile(require.resolve('./csm/url_search.ts')); + loadTestFile(require.resolve('./csm/page_views.ts')); }); }); } diff --git a/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap b/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap index bf42c08438156..8a3929f1e9ba6 100644 --- a/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap +++ b/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap @@ -75,8 +75,8 @@ Array [ "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -103,8 +103,8 @@ Array [ "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -117,7 +117,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -137,8 +137,8 @@ Array [ "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -151,7 +151,7 @@ Array [ "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -171,8 +171,8 @@ Array [ "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -185,7 +185,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -204,7 +204,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -232,7 +232,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -246,8 +246,8 @@ Array [ "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -266,7 +266,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -280,7 +280,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -300,7 +300,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -314,8 +314,8 @@ Array [ "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -333,7 +333,7 @@ Array [ "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -361,7 +361,7 @@ Array [ "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -375,8 +375,8 @@ Array [ "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -395,7 +395,7 @@ Array [ "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -409,7 +409,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -428,7 +428,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -455,7 +455,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -482,7 +482,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -510,7 +510,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -524,8 +524,8 @@ Array [ "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -544,7 +544,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -558,7 +558,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -578,7 +578,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -592,7 +592,7 @@ Array [ "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -611,8 +611,8 @@ Array [ "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -638,8 +638,8 @@ Array [ "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -652,8 +652,8 @@ Array [ "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -672,8 +672,8 @@ Array [ "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -686,7 +686,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -705,8 +705,8 @@ Array [ "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -719,7 +719,7 @@ Array [ "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -738,8 +738,8 @@ Array [ "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -752,7 +752,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -784,8 +784,8 @@ Array [ "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -816,7 +816,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -848,7 +848,7 @@ Array [ "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -880,7 +880,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -912,8 +912,8 @@ Array [ "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -942,7 +942,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -957,7 +957,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -972,7 +972,7 @@ Array [ "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -987,8 +987,8 @@ Array [ "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -1002,8 +1002,8 @@ Array [ "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -1061,8 +1061,8 @@ Object { "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -1089,8 +1089,8 @@ Object { "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -1103,7 +1103,7 @@ Object { "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1123,8 +1123,8 @@ Object { "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -1137,7 +1137,7 @@ Object { "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -1157,8 +1157,8 @@ Object { "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -1171,7 +1171,7 @@ Object { "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1190,7 +1190,7 @@ Object { "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1218,7 +1218,7 @@ Object { "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1232,8 +1232,8 @@ Object { "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -1252,7 +1252,7 @@ Object { "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1266,7 +1266,7 @@ Object { "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1286,7 +1286,7 @@ Object { "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1300,8 +1300,8 @@ Object { "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -1319,7 +1319,7 @@ Object { "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -1347,7 +1347,7 @@ Object { "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -1361,8 +1361,8 @@ Object { "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -1381,7 +1381,7 @@ Object { "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -1395,7 +1395,7 @@ Object { "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1414,7 +1414,7 @@ Object { "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1441,7 +1441,7 @@ Object { "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1468,7 +1468,7 @@ Object { "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1496,7 +1496,7 @@ Object { "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1510,8 +1510,8 @@ Object { "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -1530,7 +1530,7 @@ Object { "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1544,7 +1544,7 @@ Object { "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1564,7 +1564,7 @@ Object { "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1578,7 +1578,7 @@ Object { "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -1597,8 +1597,8 @@ Object { "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -1624,8 +1624,8 @@ Object { "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -1638,8 +1638,8 @@ Object { "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -1658,8 +1658,8 @@ Object { "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -1672,7 +1672,7 @@ Object { "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1691,8 +1691,8 @@ Object { "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -1705,7 +1705,7 @@ Object { "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -1724,8 +1724,8 @@ Object { "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -1738,7 +1738,7 @@ Object { "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1770,8 +1770,8 @@ Object { "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -1802,7 +1802,7 @@ Object { "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1834,7 +1834,7 @@ Object { "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -1866,7 +1866,7 @@ Object { "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1898,8 +1898,8 @@ Object { "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", @@ -1928,7 +1928,7 @@ Object { "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1943,7 +1943,7 @@ Object { "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -1958,7 +1958,7 @@ Object { "service.environment": "testing", "service.name": "opbeans-node", "serviceAnomalyStats": Object { - "actualValue": 32226.649122807008, + "actualValue": 32226.649122807, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", @@ -1973,8 +1973,8 @@ Object { "service.environment": "testing", "service.name": "opbeans-go", "serviceAnomalyStats": Object { - "actualValue": 3933482.1764705875, - "anomalyScore": 2.6101702751482714, + "actualValue": 3933482.17647059, + "anomalyScore": 2.61017027514827, "healthStatus": "healthy", "jobId": "apm-testing-d457-high_mean_transaction_duration", "transactionType": "request", @@ -1988,8 +1988,8 @@ Object { "service.environment": "production", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { - "actualValue": 684716.5813953485, - "anomalyScore": 0.20498907719907372, + "actualValue": 684716.581395349, + "anomalyScore": 0.204989077199074, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", "transactionType": "request", diff --git a/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts b/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts index 2e4a859f08cca..a8632d7a27c3c 100644 --- a/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts +++ b/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts @@ -177,7 +177,7 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) "service.environment": "production", "service.name": "opbeans-python", "serviceAnomalyStats": Object { - "actualValue": 66218.08333333333, + "actualValue": 66218.0833333333, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", @@ -192,7 +192,7 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) "service.environment": "production", "service.name": "opbeans-java", "serviceAnomalyStats": Object { - "actualValue": 14901.319999999996, + "actualValue": 14901.32, "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-229a-high_mean_transaction_duration", diff --git a/x-pack/test/functional/apps/discover/error_handling.ts b/x-pack/test/functional/apps/discover/error_handling.ts new file mode 100644 index 0000000000000..515e5e293ae28 --- /dev/null +++ b/x-pack/test/functional/apps/discover/error_handling.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'discover', 'timePicker']); + + describe('errors', function describeIndexTests() { + before(async function () { + await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.load('invalid_scripted_field'); + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await PageObjects.common.navigateToApp('discover'); + }); + + after(async function () { + await esArchiver.unload('invalid_scripted_field'); + }); + // this is the same test as in OSS but it catches different error message issue in different licences + describe('invalid scripted field error', () => { + it('is rendered', async () => { + const isFetchErrorVisible = await testSubjects.exists('discoverFetchError'); + expect(isFetchErrorVisible).to.be(true); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/discover/index.ts b/x-pack/test/functional/apps/discover/index.ts index 323b728e16454..759225d80fa20 100644 --- a/x-pack/test/functional/apps/discover/index.ts +++ b/x-pack/test/functional/apps/discover/index.ts @@ -13,5 +13,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./preserve_url')); loadTestFile(require.resolve('./async_scripted_fields')); loadTestFile(require.resolve('./reporting')); + loadTestFile(require.resolve('./error_handling')); }); } diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 42807a23cb13a..05047fab2517d 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -13,7 +13,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const listingTable = getService('listingTable'); const testSubjects = getService('testSubjects'); - describe('lens smokescreen tests', () => { + // Failing: See https://github.com/elastic/kibana/issues/77969 + describe.skip('lens smokescreen tests', () => { it('should allow creation of lens xy chart', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index 03b75601ec2a8..2d2d2f9d3cf9b 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -47,6 +47,7 @@ export default function ({ loadTestFile, getService }) { loadTestFile(require.resolve('./es_pew_pew_source')); loadTestFile(require.resolve('./joins')); loadTestFile(require.resolve('./mvt_scaling')); + loadTestFile(require.resolve('./mvt_super_fine')); loadTestFile(require.resolve('./add_layer_panel')); loadTestFile(require.resolve('./import_geojson')); loadTestFile(require.resolve('./layer_errors')); diff --git a/x-pack/test/functional/apps/maps/mvt_super_fine.js b/x-pack/test/functional/apps/maps/mvt_super_fine.js new file mode 100644 index 0000000000000..b5a7935a81eb5 --- /dev/null +++ b/x-pack/test/functional/apps/maps/mvt_super_fine.js @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +const MB_VECTOR_SOURCE_ID = 'g1xkv'; + +export default function ({ getPageObjects, getService }) { + const PageObjects = getPageObjects(['maps']); + const inspector = getService('inspector'); + const security = getService('security'); + + describe('mvt grid layer', () => { + before(async () => { + await security.testUser.setRoles( + ['global_maps_all', 'test_logstash_reader', 'geoshape_data_reader'], + false + ); + await PageObjects.maps.loadSavedMap('geo grid vector grid example (SUPER_FINE resolution)'); + }); + + after(async () => { + await inspector.close(); + await security.testUser.restoreDefaults(); + }); + + it('should render with mvt-source', async () => { + const mapboxStyle = await PageObjects.maps.getMapboxStyle(); + + //Source should be correct + expect(mapboxStyle.sources[MB_VECTOR_SOURCE_ID].tiles[0]).to.equal( + "/api/maps/mvt/getGridTile?x={x}&y={y}&z={z}&geometryFieldName=geo.coordinates&index=logstash-*&requestBody=(_source:(excludes:!()),aggs:(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:geo.coordinates)),max_of_bytes:(max:(field:bytes))),geotile_grid:(bounds:!n,field:geo.coordinates,precision:!n,shard_size:65535,size:65535))),docvalue_fields:!((field:'@timestamp',format:date_time),(field:'relatedContent.article:modified_time',format:date_time),(field:'relatedContent.article:published_time',format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((match_all:()),(range:('@timestamp':(format:strict_date_optional_time,gte:'2015-09-20T00:00:00.000Z',lte:'2015-09-20T01:00:00.000Z')))),must:!(),must_not:!(),should:!())),script_fields:(hour_of_day:(script:(lang:painless,source:'doc[!'@timestamp!'].value.getHour()'))),size:0,stored_fields:!('*'))&requestType=grid&geoFieldType=geo_point" + ); + + //Should correctly load meta for style-rule (sigma is set to 1, opacity to 1) + const fillLayer = mapboxStyle.layers.find( + (layer) => layer.id === MB_VECTOR_SOURCE_ID + '_fill' + ); + + expect(fillLayer.paint).to.eql({ + 'fill-color': [ + 'interpolate', + ['linear'], + [ + 'coalesce', + [ + 'case', + ['==', ['get', 'max_of_bytes'], null], + 1622, + ['max', ['min', ['to-number', ['get', 'max_of_bytes']], 9790], 1623], + ], + 1622, + ], + 1622, + 'rgba(0,0,0,0)', + 1623, + '#ecf1f7', + 2643.875, + '#d9e3ef', + 3664.75, + '#c5d5e7', + 4685.625, + '#b2c7df', + 5706.5, + '#9eb9d8', + 6727.375, + '#8bacd0', + 7748.25, + '#769fc8', + 8769.125, + '#6092c0', + ], + 'fill-opacity': 0.75, + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/index.ts b/x-pack/test/functional/apps/ml/index.ts index e224f5c8bb128..74dc0fc3ca9f0 100644 --- a/x-pack/test/functional/apps/ml/index.ts +++ b/x-pack/test/functional/apps/ml/index.ts @@ -46,5 +46,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./anomaly_detection')); loadTestFile(require.resolve('./data_visualizer')); loadTestFile(require.resolve('./data_frame_analytics')); + loadTestFile(require.resolve('./settings')); }); } diff --git a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts index eed7489b09fe6..c3dde872fa4a6 100644 --- a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts @@ -438,7 +438,7 @@ export default function ({ getService }: FtrProviderContext) { 'should display enabled elements of the edit calendar page' ); await ml.settingsFilterList.assertEditDescriptionButtonEnabled(true); - await ml.settingsFilterList.assertAddItemButtonEnabled(true); + await ml.settingsFilterList.assertAddItemsButtonEnabled(true); await ml.testExecution.logTestStep('should display the filter item in the list'); await ml.settingsFilterList.assertFilterItemExists(filterItems[0]); diff --git a/x-pack/test/functional/apps/ml/settings/calendar_creation.ts b/x-pack/test/functional/apps/ml/settings/calendar_creation.ts new file mode 100644 index 0000000000000..5b1e3b0a12b13 --- /dev/null +++ b/x-pack/test/functional/apps/ml/settings/calendar_creation.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { asyncForEach, createJobConfig } from './common'; + +export default function ({ getService }: FtrProviderContext) { + const ml = getService('ml'); + const esArchiver = getService('esArchiver'); + + const calendarId = 'test_calendar_id'; + const jobConfigs = [createJobConfig('test_calendar_ad_1'), createJobConfig('test_calendar_ad_2')]; + + describe('calendar creation', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + + await asyncForEach(jobConfigs, async (jobConfig) => { + await ml.api.createAnomalyDetectionJob(jobConfig); + }); + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + afterEach(async () => { + await ml.api.deleteCalendar(calendarId); + }); + + it('creates new calendar that applies to all jobs', async () => { + await ml.testExecution.logTestStep('calendar creation loads the calendar management page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToSettings(); + await ml.settings.navigateToCalendarManagement(); + + await ml.testExecution.logTestStep('calendar creation loads the new calendar edit page'); + await ml.settingsCalendar.assertCreateCalendarButtonEnabled(true); + await ml.settingsCalendar.navigateToCalendarCreationPage(); + + await ml.testExecution.logTestStep('calendar creation sets calendar to apply to all jobs'); + await ml.settingsCalendar.toggleApplyToAllJobsSwitch(true); + await ml.settingsCalendar.assertJobSelectionNotExists(); + await ml.settingsCalendar.assertJobGroupSelectionNotExists(); + + await ml.testExecution.logTestStep('calendar creation sets the calendar id and description'); + await ml.settingsCalendar.setCalendarId(calendarId); + await ml.settingsCalendar.setCalendarDescription('test calendar description'); + + await ml.testExecution.logTestStep('calendar creation creates new calendar event'); + await ml.settingsCalendar.openNewCalendarEventForm(); + await ml.settingsCalendar.setCalendarEventDescription('holiday'); + await ml.settingsCalendar.addNewCalendarEvent(); + await ml.settingsCalendar.assertEventRowExists('holiday'); + + await ml.testExecution.logTestStep( + 'calendar creation saves the new calendar and displays it in the list of calendars ' + ); + await ml.settingsCalendar.saveCalendar(); + + await ml.settingsCalendar.assertCalendarRowExists(calendarId); + }); + + it('creates new calendar that applies to specific jobs', async () => { + await ml.testExecution.logTestStep('calendar creation loads the calendar management page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToSettings(); + await ml.settings.navigateToCalendarManagement(); + + await ml.testExecution.logTestStep('calendar creation loads the new calendar edit page'); + await ml.settingsCalendar.assertCreateCalendarButtonEnabled(true); + await ml.settingsCalendar.navigateToCalendarCreationPage(); + + await ml.testExecution.logTestStep( + 'calendar creation verifies the job selection and job group section are displayed' + ); + await ml.settingsCalendar.assertJobSelectionExists(); + await ml.settingsCalendar.assertJobSelectionEnabled(true); + await ml.settingsCalendar.assertJobGroupSelectionExists(); + await ml.settingsCalendar.assertJobGroupSelectionEnabled(true); + + await ml.testExecution.logTestStep('calendar creation sets the calendar id'); + await ml.settingsCalendar.setCalendarId(calendarId); + + await ml.testExecution.logTestStep('calendar creation sets the job selection'); + await asyncForEach(jobConfigs, async (jobConfig) => { + await ml.settingsCalendar.selectJob(jobConfig.job_id); + }); + + await ml.settingsCalendar.saveCalendar(); + await ml.settingsCalendar.assertCalendarRowExists(calendarId); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/settings/calendar_delete.ts b/x-pack/test/functional/apps/ml/settings/calendar_delete.ts new file mode 100644 index 0000000000000..2cc4f91d5528f --- /dev/null +++ b/x-pack/test/functional/apps/ml/settings/calendar_delete.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { asyncForEach } from './common'; + +export default function ({ getService }: FtrProviderContext) { + const ml = getService('ml'); + + const testDataList = [1, 2].map((n) => ({ + calendarId: `test_delete_calendar_${n}`, + description: `test description ${n}`, + })); + + describe('calendar delete', function () { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); + + await asyncForEach(testDataList, async ({ calendarId, description }) => { + await ml.api.createCalendar(calendarId, { + description, + }); + }); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + + // clean up created calendars + await asyncForEach(testDataList, async ({ calendarId }) => { + await ml.api.deleteCalendar(calendarId); + }); + }); + + it('deletes multiple calendars', async () => { + await ml.testExecution.logTestStep('calendar delete loads the calendar list management page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToSettings(); + await ml.settings.navigateToCalendarManagement(); + + await ml.testExecution.logTestStep('calendar delete selects multiple calendars for deletion'); + await asyncForEach(testDataList, async ({ calendarId }) => { + await ml.settingsCalendar.assertCalendarRowExists(calendarId); + await ml.settingsCalendar.selectCalendarRow(calendarId); + }); + + await ml.testExecution.logTestStep('calendar delete clicks the delete button'); + await ml.settingsCalendar.deleteCalendar(); + + await ml.testExecution.logTestStep( + 'calendar delete validates the calendars are deleted from the table' + ); + await asyncForEach(testDataList, async ({ calendarId }) => { + await ml.settingsCalendar.assertCalendarRowNotExists(calendarId); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/settings/calendar_edit.ts b/x-pack/test/functional/apps/ml/settings/calendar_edit.ts new file mode 100644 index 0000000000000..f7c8c1f6f85f5 --- /dev/null +++ b/x-pack/test/functional/apps/ml/settings/calendar_edit.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { asyncForEach, createJobConfig } from './common'; + +export default function ({ getService }: FtrProviderContext) { + const ml = getService('ml'); + const esArchiver = getService('esArchiver'); + const comboBox = getService('comboBox'); + + const calendarId = 'test_edit_calendar_id'; + const testEvents = [ + { description: 'event_1', start_time: 1513641600000, end_time: 1513728000000 }, + { description: 'event_2', start_time: 1513814400000, end_time: 1513900800000 }, + ]; + const jobConfigs = [createJobConfig('test_calendar_ad_1'), createJobConfig('test_calendar_ad_2')]; + const newJobGroups = ['farequote']; + + describe('calendar edit', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + + await asyncForEach(jobConfigs, async (jobConfig) => { + await ml.api.createAnomalyDetectionJob(jobConfig); + }); + + await ml.api.createCalendar(calendarId, { + job_ids: jobConfigs.map((c) => c.job_id), + description: 'Test calendar', + }); + await ml.api.createCalendarEvents(calendarId, testEvents); + + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + afterEach(async () => { + await ml.api.deleteCalendar(calendarId); + }); + + it('updates jobs, groups and events', async () => { + await ml.testExecution.logTestStep('calendar edit loads the calendar management page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToSettings(); + await ml.settings.navigateToCalendarManagement(); + + await ml.testExecution.logTestStep('calendar edit opens existing calendar'); + await ml.settingsCalendar.openCalendarEditForm(calendarId); + + await ml.testExecution.logTestStep( + 'calendar edit deselects previous job selection and assigns new job groups' + ); + await comboBox.clear('mlCalendarJobSelection'); + await asyncForEach(newJobGroups, async (newJobGroup) => { + await ml.settingsCalendar.selectJobGroup(newJobGroup); + }); + + await ml.testExecution.logTestStep('calendar edit deletes old events'); + + await asyncForEach(testEvents, async ({ description }) => { + await ml.settingsCalendar.deleteCalendarEventRow(description); + }); + + await ml.testExecution.logTestStep('calendar edit creates new calendar event'); + await ml.settingsCalendar.openNewCalendarEventForm(); + await ml.settingsCalendar.setCalendarEventDescription('holiday'); + await ml.settingsCalendar.addNewCalendarEvent(); + await ml.settingsCalendar.assertEventRowExists('holiday'); + + await ml.testExecution.logTestStep( + 'calendar edit saves the new calendar and displays it in the list of calendars ' + ); + await ml.settingsCalendar.saveCalendar(); + await ml.settingsCalendar.assertCalendarRowExists(calendarId); + + await ml.testExecution.logTestStep('calendar edit re-opens the updated calendar'); + await ml.settingsCalendar.openCalendarEditForm(calendarId); + await ml.testExecution.logTestStep('calendar edit verifies the job selection is empty'); + await ml.settingsCalendar.assertJobSelection([]); + await ml.testExecution.logTestStep( + 'calendar edit verifies the job group selection was updated' + ); + await ml.settingsCalendar.assertJobGroupSelection(newJobGroups); + + await ml.testExecution.logTestStep('calendar edit verifies calendar updated correctly'); + await asyncForEach(testEvents, async ({ description }) => { + await ml.settingsCalendar.assertEventRowMissing(description); + }); + await ml.settingsCalendar.assertEventRowExists('holiday'); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/settings/common.ts b/x-pack/test/functional/apps/ml/settings/common.ts new file mode 100644 index 0000000000000..9fada028ff3da --- /dev/null +++ b/x-pack/test/functional/apps/ml/settings/common.ts @@ -0,0 +1,26 @@ +/* + * 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 async function asyncForEach(array: T[], callback: (item: T, index: number) => void) { + for (let index = 0; index < array.length; index++) { + await callback(array[index], index); + } +} + +export const createJobConfig = (jobId: string) => ({ + job_id: jobId, + description: + 'mean/min/max(responsetime) partition=airline on farequote dataset with 1h bucket span', + groups: ['farequote', 'automated', 'multi-metric'], + analysis_config: { + bucket_span: '1h', + influencers: ['airline'], + detectors: [{ function: 'mean', field_name: 'responsetime', partition_field_name: 'airline' }], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '20mb' }, + model_plot_config: { enabled: false }, +}); diff --git a/x-pack/test/functional/apps/ml/settings/filter_list_creation.ts b/x-pack/test/functional/apps/ml/settings/filter_list_creation.ts new file mode 100644 index 0000000000000..22affa1cada38 --- /dev/null +++ b/x-pack/test/functional/apps/ml/settings/filter_list_creation.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const ml = getService('ml'); + const filterId = 'test_create_filter'; + const description = 'test description'; + const keywords = ['filter word 1', 'filter word 2', 'filter word 3']; + + describe('filter list creation', function () { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + // clean up created filters + await ml.api.deleteFilter(filterId); + }); + + it('creates new filter list', async () => { + await ml.testExecution.logTestStep( + 'filter list creation loads the filter list management page' + ); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToSettings(); + await ml.settings.navigateToFilterListsManagement(); + + await ml.testExecution.logTestStep('filter list creation loads the filter creation page'); + await ml.settingsFilterList.navigateToFilterListCreationPage(); + + await ml.testExecution.logTestStep('filter list creation sets the list name and description'); + await ml.settingsFilterList.setFilterListId(filterId); + await ml.settingsFilterList.setFilterListDescription(description); + + await ml.testExecution.logTestStep('filter list creation adds items to the filter list'); + await ml.settingsFilterList.addFilterListKeywords(keywords); + await ml.testExecution.logTestStep('filter list creation saves the settings'); + await ml.settingsFilterList.saveFilterList(); + await ml.settingsFilterList.assertFilterListRowExists(filterId); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/settings/filter_list_delete.ts b/x-pack/test/functional/apps/ml/settings/filter_list_delete.ts new file mode 100644 index 0000000000000..9e30d2c8915d2 --- /dev/null +++ b/x-pack/test/functional/apps/ml/settings/filter_list_delete.ts @@ -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 { FtrProviderContext } from '../../../ftr_provider_context'; +import { asyncForEach } from './common'; + +export default function ({ getService }: FtrProviderContext) { + const ml = getService('ml'); + + const testDataList = [1, 2].map((n) => ({ + filterId: `test_delete_filter_${n}`, + description: `test description ${n}`, + items: ['filter word 1', 'filter word 2', 'filter word 3'], + })); + + describe('filter list delete', function () { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); + + for (let index = 0; index < testDataList.length; index++) { + const { filterId, description, items } = testDataList[index]; + + await ml.api.createFilter(filterId, { + description, + items, + }); + } + }); + + after(async () => { + await ml.api.cleanMlIndices(); + + // clean up created filters + await asyncForEach(testDataList, async ({ filterId }) => { + await ml.api.deleteFilter(filterId); + }); + }); + + it('deletes filter list with items', async () => { + await ml.testExecution.logTestStep( + 'filter list delete loads the filter list management page' + ); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToSettings(); + await ml.settings.navigateToFilterListsManagement(); + + await ml.testExecution.logTestStep( + 'filter list delete selects list entries and deletes them' + ); + for (const testData of testDataList) { + const { filterId } = testData; + await ml.settingsFilterList.selectFilterListRow(filterId); + } + await ml.settingsFilterList.deleteFilterList(); + + await ml.testExecution.logTestStep( + 'filter list delete validates selected filter lists are deleted' + ); + await asyncForEach(testDataList, async ({ filterId }) => { + await ml.settingsFilterList.assertFilterListRowNotExists(filterId); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/settings/filter_list_edit.ts b/x-pack/test/functional/apps/ml/settings/filter_list_edit.ts new file mode 100644 index 0000000000000..8c39c679ac6f2 --- /dev/null +++ b/x-pack/test/functional/apps/ml/settings/filter_list_edit.ts @@ -0,0 +1,78 @@ +/* + * 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 { asyncForEach } from './common'; + +export default function ({ getService }: FtrProviderContext) { + const ml = getService('ml'); + + const filterId = 'test_filter_list_edit'; + const keywordToDelete = 'keyword_to_delete'; + const oldKeyword = 'old_keyword'; + const oldDescription = 'Old filter list description'; + + const newKeywords = ['new_keyword1', 'new_keyword2']; + const newDescription = 'New filter list description'; + + describe('filter list edit', function () { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); + + await ml.api.createFilter(filterId, { + description: oldDescription, + items: [keywordToDelete, oldKeyword], + }); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + await ml.api.deleteFilter(filterId); + }); + + it('updates description and filter items', async () => { + await ml.testExecution.logTestStep('filter list edit loads the filter list management page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToSettings(); + await ml.settings.navigateToFilterListsManagement(); + + await ml.testExecution.logTestStep('filter list edit opens existing filter list'); + await ml.settingsFilterList.selectFilterListRowEditLink(filterId); + await ml.settingsFilterList.assertFilterItemExists(keywordToDelete); + await ml.settingsFilterList.assertFilterListDescriptionValue(oldDescription); + + await ml.testExecution.logTestStep('filter list edit deletes existing filter item'); + await ml.settingsFilterList.deleteFilterItem(keywordToDelete); + + await ml.testExecution.logTestStep('filter list edit sets new keywords and description'); + await ml.settingsFilterList.setFilterListDescription(newDescription); + await ml.settingsFilterList.addFilterListKeywords(newKeywords); + + await ml.testExecution.logTestStep( + 'filter list edit saves the new filter list and displays it in the list of entries' + ); + await ml.settingsFilterList.saveFilterList(); + await ml.settingsFilterList.assertFilterListRowExists(filterId); + + await ml.testExecution.logTestStep('filter list edit reopens the edited filter list'); + await ml.settingsFilterList.selectFilterListRowEditLink(filterId); + + await ml.testExecution.logTestStep( + 'filter list edit verifies the filter list description updated correctly' + ); + await ml.settingsFilterList.assertFilterListDescriptionValue(newDescription); + + await ml.testExecution.logTestStep( + 'filter list edit verifies the filter items updated correctly' + ); + await ml.settingsFilterList.assertFilterItemNotExists(keywordToDelete); + await asyncForEach([...newKeywords, oldKeyword], async (filterItem) => { + await ml.settingsFilterList.assertFilterItemExists(filterItem); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/settings/index.ts b/x-pack/test/functional/apps/ml/settings/index.ts new file mode 100644 index 0000000000000..5b2c7d15e1959 --- /dev/null +++ b/x-pack/test/functional/apps/ml/settings/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('settings', function () { + this.tags(['quynh', 'skipFirefox']); + + loadTestFile(require.resolve('./calendar_creation')); + loadTestFile(require.resolve('./calendar_edit')); + loadTestFile(require.resolve('./calendar_delete')); + + loadTestFile(require.resolve('./filter_list_creation')); + loadTestFile(require.resolve('./filter_list_edit')); + loadTestFile(require.resolve('./filter_list_delete')); + }); +} diff --git a/x-pack/test/functional/apps/monitoring/cluster/overview.js b/x-pack/test/functional/apps/monitoring/cluster/overview.js index 0e608e9a055fa..e30280802e8c8 100644 --- a/x-pack/test/functional/apps/monitoring/cluster/overview.js +++ b/x-pack/test/functional/apps/monitoring/cluster/overview.js @@ -30,7 +30,7 @@ export default function ({ getService, getPageObjects }) { }); it('shows elasticsearch panel with data', async () => { - expect(await overview.getEsStatus()).to.be('Health is green'); + expect(await overview.getEsStatus()).to.be('Healthy'); expect(await overview.getEsVersion()).to.be('7.0.0-alpha1'); expect(await overview.getEsUptime()).to.be('20 minutes'); expect(await overview.getEsNumberOfNodes()).to.be('Nodes: 2'); @@ -44,7 +44,7 @@ export default function ({ getService, getPageObjects }) { }); it('shows kibana panel', async () => { - expect(await overview.getEsStatus()).to.be('Health is green'); + expect(await overview.getEsStatus()).to.be('Healthy'); expect(await overview.getKbnRequests()).to.be('914'); expect(await overview.getKbnMaxResponseTime()).to.be('2873 ms'); expect(await overview.getKbnInstances()).to.be('Instances: 1'); @@ -81,7 +81,7 @@ export default function ({ getService, getPageObjects }) { }); it('shows elasticsearch panel with data', async () => { - expect(await overview.getEsStatus()).to.be('Health is yellow'); + expect(await overview.getEsStatus()).to.be('Missing replica shards'); expect(await overview.getEsVersion()).to.be('7.0.0-alpha1'); expect(await overview.getEsUptime()).to.be('5 minutes'); expect(await overview.getEsNumberOfNodes()).to.be('Nodes: 1'); @@ -95,7 +95,7 @@ export default function ({ getService, getPageObjects }) { }); it('shows kibana panel', async () => { - expect(await overview.getKbnStatus()).to.be('Health is green'); + expect(await overview.getKbnStatus()).to.be('Healthy'); expect(await overview.getKbnRequests()).to.be('174'); expect(await overview.getKbnMaxResponseTime()).to.be('2203 ms'); expect(await overview.getKbnInstances()).to.be('Instances: 1'); @@ -131,7 +131,7 @@ export default function ({ getService, getPageObjects }) { }); it('shows elasticsearch panel with data', async () => { - expect(await overview.getEsStatus()).to.be('Health is yellow'); + expect(await overview.getEsStatus()).to.be('Missing replica shards'); expect(await overview.getEsVersion()).to.be('7.0.0-alpha1'); expect(await overview.getEsUptime()).to.be('8 minutes'); expect(await overview.getEsNumberOfNodes()).to.be('Nodes: 1'); diff --git a/x-pack/test/functional/apps/monitoring/elasticsearch/nodes.js b/x-pack/test/functional/apps/monitoring/elasticsearch/nodes.js index 569447df0533f..8ae1f89c2f3b4 100644 --- a/x-pack/test/functional/apps/monitoring/elasticsearch/nodes.js +++ b/x-pack/test/functional/apps/monitoring/elasticsearch/nodes.js @@ -56,23 +56,39 @@ export default function ({ getService, getPageObjects }) { expect(rows.length).to.be(3); const nodesAll = await nodesList.getNodesAll(); + console.log(JSON.stringify(nodesAll, null, 2)); const tableData = [ { name: 'whatever-01', status: 'Status: Online', - cpu: '0% \n3% max\n0% min', - load: '3.28 \n3.71 max\n2.19 min', - memory: '39% \n52% max\n25% min', - disk: '173.9 GB \n173.9 GB max\n173.9 GB min', + cpu: '0%', + cpuText: 'Trending\nup\nMax value\n3%\nMin value\n0%\nApplies to current time period', + load: '3.28', + loadText: + 'Trending\nup\nMax value\n3.71\nMin value\n2.19\nApplies to current time period', + memory: '39%', + memoryText: + 'Trending\ndown\nMax value\n52%\nMin value\n25%\nApplies to current time period', + disk: '173.9 GB', + diskText: + 'Trending\ndown\nMax value\n173.9 GB\nMin value\n173.9 GB\nApplies to current time period', shards: '38', }, { name: 'whatever-02', status: 'Status: Online', - cpu: '2% \n3% max\n0% min', - load: '3.28 \n3.73 max\n2.29 min', - memory: '25% \n49% max\n25% min', - disk: '173.9 GB \n173.9 GB max\n173.9 GB min', + cpu: '2%', + cpuText: + 'Trending\ndown\nMax value\n3%\nMin value\n0%\nApplies to current time period', + load: '3.28', + loadText: + 'Trending\nup\nMax value\n3.73\nMin value\n2.29\nApplies to current time period', + memory: '25%', + memoryText: + 'Trending\ndown\nMax value\n49%\nMin value\n25%\nApplies to current time period', + disk: '173.9 GB', + diskText: + 'Trending\ndown\nMax value\n173.9 GB\nMin value\n173.9 GB\nApplies to current time period', shards: '38', }, { name: 'whatever-03', status: 'Status: Offline' }, @@ -81,9 +97,13 @@ export default function ({ getService, getPageObjects }) { expect(nodesAll[node].name).to.be(tableData[node].name); expect(nodesAll[node].status).to.be(tableData[node].status); expect(nodesAll[node].cpu).to.be(tableData[node].cpu); + expect(nodesAll[node].cpuText).to.be(tableData[node].cpuText); expect(nodesAll[node].load).to.be(tableData[node].load); + expect(nodesAll[node].loadText).to.be(tableData[node].loadText); expect(nodesAll[node].memory).to.be(tableData[node].memory); + expect(nodesAll[node].memoryText).to.be(tableData[node].memoryText); expect(nodesAll[node].disk).to.be(tableData[node].disk); + expect(nodesAll[node].diskText).to.be(tableData[node].diskText); expect(nodesAll[node].shards).to.be(tableData[node].shards); }); }); @@ -94,12 +114,20 @@ export default function ({ getService, getPageObjects }) { const nodesAll = await nodesList.getNodesAll(); const tableData = [ - { cpu: '2% \n3% max\n0% min' }, - { cpu: '0% \n3% max\n0% min' }, - { cpu: undefined }, + { + cpu: '2%', + cpuText: + 'Trending\ndown\nMax value\n3%\nMin value\n0%\nApplies to current time period', + }, + { + cpu: '0%', + cpuText: 'Trending\nup\nMax value\n3%\nMin value\n0%\nApplies to current time period', + }, + { cpu: undefined, cpuText: undefined }, ]; nodesAll.forEach((obj, node) => { expect(nodesAll[node].cpu).to.be(tableData[node].cpu); + expect(nodesAll[node].cpuText).to.be(tableData[node].cpuText); }); }); @@ -109,12 +137,21 @@ export default function ({ getService, getPageObjects }) { const nodesAll = await nodesList.getNodesAll(); const tableData = [ - { load: '3.28 \n3.71 max\n2.19 min' }, - { load: '3.28 \n3.73 max\n2.29 min' }, + { + load: '3.28', + loadText: + 'Trending\nup\nMax value\n3.71\nMin value\n2.19\nApplies to current time period', + }, + { + load: '3.28', + loadText: + 'Trending\nup\nMax value\n3.73\nMin value\n2.29\nApplies to current time period', + }, { load: undefined }, ]; nodesAll.forEach((obj, node) => { expect(nodesAll[node].load).to.be(tableData[node].load); + expect(nodesAll[node].loadText).to.be(tableData[node].loadText); }); }); }); @@ -155,12 +192,21 @@ export default function ({ getService, getPageObjects }) { const nodesAll = await nodesList.getNodesAll(); const tableData = [ - { memory: '39% \n52% max\n25% min' }, - { memory: '25% \n49% max\n25% min' }, - { memory: undefined }, + { + memory: '39%', + memoryText: + 'Trending\ndown\nMax value\n52%\nMin value\n25%\nApplies to current time period', + }, + { + memory: '25%', + memoryText: + 'Trending\ndown\nMax value\n49%\nMin value\n25%\nApplies to current time period', + }, + { memory: undefined, memoryText: undefined }, ]; nodesAll.forEach((obj, node) => { expect(nodesAll[node].memory).to.be(tableData[node].memory); + expect(nodesAll[node].memoryText).to.be(tableData[node].memoryText); }); }); @@ -170,12 +216,21 @@ export default function ({ getService, getPageObjects }) { const nodesAll = await nodesList.getNodesAll(); const tableData = [ - { disk: '173.9 GB \n173.9 GB max\n173.9 GB min' }, - { disk: '173.9 GB \n173.9 GB max\n173.9 GB min' }, + { + disk: '173.9 GB', + diskText: + 'Trending\ndown\nMax value\n173.9 GB\nMin value\n173.9 GB\nApplies to current time period', + }, + { + disk: '173.9 GB', + diskText: + 'Trending\ndown\nMax value\n173.9 GB\nMin value\n173.9 GB\nApplies to current time period', + }, { disk: undefined }, ]; nodesAll.forEach((obj, node) => { expect(nodesAll[node].disk).to.be(tableData[node].disk); + expect(nodesAll[node].diskText).to.be(tableData[node].diskText); }); }); diff --git a/x-pack/test/functional/apps/transform/editing.ts b/x-pack/test/functional/apps/transform/editing.ts index ac955bde4ad5d..498678e7ba4b5 100644 --- a/x-pack/test/functional/apps/transform/editing.ts +++ b/x-pack/test/functional/apps/transform/editing.ts @@ -99,6 +99,7 @@ export default function ({ getService }: FtrProviderContext) { await transform.testExecution.logTestStep( 'should update the transform documents per second' ); + await transform.editFlyout.openTransformEditAccordionAdvancedSettings(); await transform.editFlyout.assertTransformEditFlyoutInputExists('DocsPerSecond'); await transform.editFlyout.assertTransformEditFlyoutInputValue('DocsPerSecond', ''); await transform.editFlyout.setTransformEditFlyoutInputValue( diff --git a/x-pack/test/functional/es_archives/endpoint/telemetry/agent_only/data.json b/x-pack/test/functional/es_archives/endpoint/telemetry/agent_only/data.json new file mode 100644 index 0000000000000..9befbcc23470a --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/telemetry/agent_only/data.json @@ -0,0 +1,200 @@ +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:8a0a4a79-c1e3-4d13-951a-36c168b4be87", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-17T09:46:00.818Z", + "fleet-agents": { + "config_id": "25129070-f8c9-11ea-b3d9-2d2b39f3a393", + "default_api_key": "/XDWfZFwXdENtpxpo74Rl5fjAFX0mL8cwrPmOXXPtnri/QW6591rkVO7wcsZe8zN4A4Ld4SXBw1EpBMQILwtL2jVBj/t34qjXJDcQFkAf6CPmO1YP3ilC/L49tGQX0J3MTLVoSmwo/N9ni8v/ywdl8IkhB4dqTp6me/SrL+I31D6vUFxYupWdlg=", + "current_error_events": "[]", + "config_revision": 1, + "enrolled_at": "2020-09-17T09:37:50.171Z", + "default_api_key_id": "oShtm3QBy3Gd1kyfzhpC", + "last_checkin": "2020-09-17T09:46:00.818Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "_8ptm3QB3yUjiqk7XrOs", + "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.0", + "id": "8a0a4a79-c1e3-4d13-951a-36c168b4be87" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:f490a4f2-5022-4a15-b447-86204ebbdd35", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-17T09:46:00.818Z", + "fleet-agents": { + "config_id": "25129070-f8c9-11ea-b3d9-2d2b39f3a393", + "default_api_key": "S3lDCj6T1USDsJUQfe/W77/D0sDl6V6K4n+Sk6WxUlev1cInze2xS3DVqETXdK0n4hQrTuBwVMjroXf8I329Yi1qevbPTGH7NhR/dhAPWOQfuVWCWh3auRdBLF3vwupqFZXJPfmo4XFA+nL0o/DHgmhPK6Ur0aKQowrCYOyvSEu9krK1AtPfK8k=", + "current_error_events": "[]", + "config_revision": 1, + "enrolled_at": "2020-09-17T09:38:06.809Z", + "default_api_key_id": "xihum3QBy3Gd1kyfaiLd", + "last_checkin": "2020-09-17T09:46:00.818Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "WMptm3QB3yUjiqk7n7R7", + "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:3d24:2d96:b634:3165/64", + "fdbb:cb5c:fb4:68:6896:8ae7:78be:f9d0/64", + "fdbb:cb5c:fb4:68:78d9:60b3:632c:c908/64", + "fdbb:cb5c:fb4:68:f1a6:a826:7f63:6b77/64", + "fdbb:cb5c:fb4:68:997d:b66b:2555:6bb7/64", + "fdbb:cb5c:fb4:68:4c21:58bc:9122:1adf/64", + "fdbb:cb5c:fb4:68:d536:51f3:fcbe:7f3/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.0", + "id": "f490a4f2-5022-4a15-b447-86204ebbdd35" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:58bed8d4-fba1-40fa-a7b0-1ceec2fb89e5", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-17T09:46:00.946Z", + "fleet-agents": { + "config_id": "25129070-f8c9-11ea-b3d9-2d2b39f3a393", + "default_api_key": "vAM73FEHbqUVOnjN4FY0jdI0EtE+7MLqN5rNj4n6TJRz1Shx3rhNnDi6S555R9pGR2ZLtN57j9Z+5sO4nT9DYW3h8RK4Z15ifgnEvNVFR1Lsp7S/GUbPM6ooQMgNo63eoU8plYSfh1/98Cbe4dOwe+FhB5tbwGK1GqWb2TOshzH01JUbvfKb5jc=", + "current_error_events": "[]", + "config_revision": 1, + "enrolled_at": "2020-09-17T09:37:19.476Z", + "default_api_key_id": "LChtm3QBy3Gd1kyfGhoG", + "last_checkin": "2020-09-17T09:46:00.818Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "xspsm3QB3yUjiqk75rL_", + "packages": [ + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "DESKTOP-QBBSCUT", + "ip": [ + "fdbb:cb5c:fb4:68:6ca6:5ea3:ae36:af51/64", + "fdbb:cb5c:fb4:68:2c1b:d329:2b41:de28/128", + "fdbb:cb5c:fb4:68:6d89:d2f8:2825:d739/128", + "fdbb:cb5c:fb4:68:a484:d8c6:87c:c2ee/128", + "fdbb:cb5c:fb4:68:b03f:b84f:8b8a:77b9/128", + "fdbb:cb5c:fb4:68:b42f:f72:b857:40e9/128", + "fdbb:cb5c:fb4:68:e9b5:f8c7:f06d:6fe1/128", + "fdbb:cb5c:fb4:68:f05b:8555:cc24:dfa4/128", + "fe80::6ca6:5ea3:ae36:af51/64", + "10.0.7.235/22", + "::1/128", + "127.0.0.1/8" + ], + "hostname": "DESKTOP-QBBSCUT", + "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.0", + "id": "58bed8d4-fba1-40fa-a7b0-1ceec2fb89e5" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/endpoint/telemetry/agent_only/mappings.json b/x-pack/test/functional/es_archives/endpoint/telemetry/agent_only/mappings.json new file mode 100644 index 0000000000000..507611b8b4b57 --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/telemetry/agent_only/mappings.json @@ -0,0 +1,2587 @@ +{ + "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_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "43b8830d5d0df85a6823d290885fc9fd", + "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_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "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/endpoint_malware_disabled/data.json b/x-pack/test/functional/es_archives/endpoint/telemetry/endpoint_malware_disabled/data.json new file mode 100644 index 0000000000000..ce140e66d1441 --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/telemetry/endpoint_malware_disabled/data.json @@ -0,0 +1,272 @@ +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:8a0a4a79-c1e3-4d13-951a-36c168b4be87", + "source": { + "references": [], + "type": "fleet-agents", + "updated_at": "2020-09-17T15:04:31.916Z", + "fleet-agents": { + "config_id": "25129070-f8c9-11ea-b3d9-2d2b39f3a393", + "default_api_key": "/XDWfZFwXdENtpxpo74Rl5fjAFX0mL8cwrPmOXXPtnri/QW6591rkVO7wcsZe8zN4A4Ld4SXBw1EpBMQILwtL2jVBj/t34qjXJDcQFkAf6CPmO1YP3ilC/L49tGQX0J3MTLVoSmwo/N9ni8v/ywdl8IkhB4dqTp6me/SrL+I31D6vUFxYupWdlg=", + "current_error_events": "[]", + "config_revision": 3, + "enrolled_at": "2020-09-17T09:37:50.171Z", + "last_checkin": "2020-09-17T15:04:31.915Z", + "default_api_key_id": "oShtm3QBy3Gd1kyfzhpC", + "access_api_key_id": "_8ptm3QB3yUjiqk7XrOs", + "user_provided_metadata": {}, + "active": true, + "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.0", + "id": "8a0a4a79-c1e3-4d13-951a-36c168b4be87" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:58bed8d4-fba1-40fa-a7b0-1ceec2fb89e5", + "source": { + "references": [], + "type": "fleet-agents", + "updated_at": "2020-09-17T15:04:31.916Z", + "fleet-agents": { + "config_id": "25129070-f8c9-11ea-b3d9-2d2b39f3a393", + "default_api_key": "vAM73FEHbqUVOnjN4FY0jdI0EtE+7MLqN5rNj4n6TJRz1Shx3rhNnDi6S555R9pGR2ZLtN57j9Z+5sO4nT9DYW3h8RK4Z15ifgnEvNVFR1Lsp7S/GUbPM6ooQMgNo63eoU8plYSfh1/98Cbe4dOwe+FhB5tbwGK1GqWb2TOshzH01JUbvfKb5jc=", + "current_error_events": "[]", + "config_revision": 3, + "enrolled_at": "2020-09-17T09:37:19.476Z", + "last_checkin": "2020-09-17T15:04:31.915Z", + "default_api_key_id": "LChtm3QBy3Gd1kyfGhoG", + "access_api_key_id": "xspsm3QB3yUjiqk75rL_", + "user_provided_metadata": {}, + "active": true, + "packages": [ + "endpoint", + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "DESKTOP-QBBSCUT", + "ip": [ + "fdbb:cb5c:fb4:68:6ca6:5ea3:ae36:af51/64", + "fdbb:cb5c:fb4:68:2c1b:d329:2b41:de28/128", + "fdbb:cb5c:fb4:68:6d89:d2f8:2825:d739/128", + "fdbb:cb5c:fb4:68:a484:d8c6:87c:c2ee/128", + "fdbb:cb5c:fb4:68:b03f:b84f:8b8a:77b9/128", + "fdbb:cb5c:fb4:68:b42f:f72:b857:40e9/128", + "fdbb:cb5c:fb4:68:e9b5:f8c7:f06d:6fe1/128", + "fdbb:cb5c:fb4:68:f05b:8555:cc24:dfa4/128", + "fe80::6ca6:5ea3:ae36:af51/64", + "10.0.7.235/22", + "::1/128", + "127.0.0.1/8" + ], + "hostname": "DESKTOP-QBBSCUT", + "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.0", + "id": "58bed8d4-fba1-40fa-a7b0-1ceec2fb89e5" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:f490a4f2-5022-4a15-b447-86204ebbdd35", + "source": { + "references": [], + "type": "fleet-agents", + "updated_at": "2020-09-17T15:04:31.916Z", + "fleet-agents": { + "config_id": "25129070-f8c9-11ea-b3d9-2d2b39f3a393", + "default_api_key": "S3lDCj6T1USDsJUQfe/W77/D0sDl6V6K4n+Sk6WxUlev1cInze2xS3DVqETXdK0n4hQrTuBwVMjroXf8I329Yi1qevbPTGH7NhR/dhAPWOQfuVWCWh3auRdBLF3vwupqFZXJPfmo4XFA+nL0o/DHgmhPK6Ur0aKQowrCYOyvSEu9krK1AtPfK8k=", + "current_error_events": "[]", + "config_revision": 3, + "enrolled_at": "2020-09-17T09:38:06.809Z", + "last_checkin": "2020-09-17T15:04:31.915Z", + "default_api_key_id": "xihum3QBy3Gd1kyfaiLd", + "access_api_key_id": "WMptm3QB3yUjiqk7n7R7", + "user_provided_metadata": {}, + "active": true, + "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:3d24:2d96:b634:3165/64", + "fdbb:cb5c:fb4:68:6896:8ae7:78be:f9d0/64", + "fdbb:cb5c:fb4:68:78d9:60b3:632c:c908/64", + "fdbb:cb5c:fb4:68:f1a6:a826:7f63:6b77/64", + "fdbb:cb5c:fb4:68:997d:b66b:2555:6bb7/64", + "fdbb:cb5c:fb4:68:4c21:58bc:9122:1adf/64", + "fdbb:cb5c:fb4:68:d536:51f3:fcbe:7f3/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.0", + "id": "f490a4f2-5022-4a15-b447-86204ebbdd35" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:78858140-f8f6-11ea-b3d9-2d2b39f3a393", + "source": { + "fleet-agent-events": { + "config_id": "25129070-f8c9-11ea-b3d9-2d2b39f3a393", + "timestamp": "2020-09-17T10:59:18.7281236-04:00", + "subtype": "RUNNING", + "agent_id": "58bed8d4-fba1-40fa-a7b0-1ceec2fb89e5", + "message": "Application: endpoint-security--7.9.0[58bed8d4-fba1-40fa-a7b0-1ceec2fb89e5]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-17T14:59:18.26119700Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"a7343ed0-f8cb-11ea-b3d9-2d2b39f3a393\",\"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\":\"off\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"off\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[3617,34,0,0,1,2,1,1,4,1,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\":0.156187721707846,\"mean\":0.277454799155371}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":96870400,\"mean\":87327732}}},\"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.00545851528384279},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0.256550218340611},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0.0709645723019816},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0.0218364450267496},\"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.0163782278757438},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0.0163782278757438},\"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\":0},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"}],\"uptime\":{\"endpoint\":18317,\"system\":660526}},\"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 off 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\":\"Successfully stopped 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 stopped 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\":\"6a546aade5563d3e8dffc1fe2d93d33edda8f9ca3e17ac3cc9ac707620cb9ecd\"},{\"name\":\"endpointpe-v4-exceptionlist\",\"sha256\":\"04f9f87accc5d5aea433427bd1bd4ec6908f8528c78ceed26f70df7875a99385\"},{\"name\":\"endpointpe-v4-model\",\"sha256\":\"1471838597fcd79a54ea4a3ec9a9beee1a86feaedab6c98e61102559ced822a8\"},{\"name\":\"global-exceptionlist-windows\",\"sha256\":\"824859b0c6749cc31951d92a73bbdddfcfe9f38abfe432087934d4dab9766ce8\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-windows-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"a7343ed0-f8cb-11ea-b3d9-2d2b39f3a393\",\"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\":\"ea57f15d-71a2-4b5e-8101-7155d9cdd4a2\",\"version\":\"7.9.0\"},\"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)\"}}}}" + }, + "references": [], + "updated_at": "2020-09-17T15:00:05.588Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:9a206900-f8f6-11ea-b3d9-2d2b39f3a393", + "source": { + "fleet-agent-events": { + "config_id": "25129070-f8c9-11ea-b3d9-2d2b39f3a393", + "timestamp": "2020-09-17T11:00:52.879069-04:00", + "subtype": "RUNNING", + "agent_id": "f490a4f2-5022-4a15-b447-86204ebbdd35", + "message": "Application: endpoint-security--7.9.0[f490a4f2-5022-4a15-b447-86204ebbdd35]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-17T15:00:52.870465905Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"a7343ed0-f8cb-11ea-b3d9-2d2b39f3a393\",\"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\":\"off\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"off\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[3571,29,4,2,1,0,1,0,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\":0.76328957,\"mean\":0.580389267551738}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":58580992,\"mean\":62785576}}},\"threads\":[{\"cpu\":{\"mean\":0.00543212559074366},\"name\":\"Cron\"},{\"cpu\":{\"mean\":0.00543212559074366},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0.0217285023629746},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0.103210386224129},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0.211852898039003},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0.016296376772231},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0.217344055640078},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0.0326016083460117},\"name\":\"FileScoreAsyncEventThread\"},{\"cpu\":{\"mean\":0},\"name\":\"QuarantineManagerWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DelayedAlertEnrichment\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0.0271709596782958},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0.0271709596782958},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0.0271709596782958},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0.0271709596782958},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0.0271709596782958},\"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\":0.0979538528515455},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0.0326530612244898},\"name\":\"stateReportThread\"},{\"cpu\":{\"mean\":0.0166527893422148},\"name\":\"EventsPidMonitorThread\"},{\"cpu\":{\"mean\":0.133222314737719},\"name\":\"EventsDelayEventThread\"}],\"uptime\":{\"endpoint\":18409,\"system\":3198488}},\"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 off 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\":\"a7343ed0-f8cb-11ea-b3d9-2d2b39f3a393\",\"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\":\"2bd3e07d-b528-4cd7-8ca6-2af2236ca446\",\"version\":\"7.9.0\"},\"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\"}}}}" + }, + "references": [], + "updated_at": "2020-09-17T15:01:01.968Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:5619b4f0-f8f6-11ea-b3d9-2d2b39f3a393", + "source": { + "fleet-agent-events": { + "config_id": "25129070-f8c9-11ea-b3d9-2d2b39f3a393", + "timestamp": "2020-09-17T10:58:59.660363289-04:00", + "subtype": "RUNNING", + "agent_id": "8a0a4a79-c1e3-4d13-951a-36c168b4be87", + "message": "Application: endpoint-security--7.9.0[8a0a4a79-c1e3-4d13-951a-36c168b4be87]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-17T14:58:59.634499439Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"a7343ed0-f8cb-11ea-b3d9-2d2b39f3a393\",\"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\":\"off\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"off\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[3655,2,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":0.2,\"mean\":0.711852054295345}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":57106432,\"mean\":55650691}}},\"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.0109289617486339},\"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.00546448087431694},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0.60207991242474},\"name\":\"EventsLoopThread\"},{\"cpu\":{\"mean\":0},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"}],\"uptime\":{\"endpoint\":18300,\"system\":4390784}},\"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\":\"a7343ed0-f8cb-11ea-b3d9-2d2b39f3a393\",\"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\":\"145f6503-0c88-484d-a00a-40c68e3cc796\",\"version\":\"7.9.0\"},\"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\"}}}}" + }, + "references": [], + "updated_at": "2020-09-17T14:59:07.839Z", + "type": "fleet-agent-events" + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/endpoint/telemetry/endpoint_malware_disabled/mappings.json b/x-pack/test/functional/es_archives/endpoint/telemetry/endpoint_malware_disabled/mappings.json new file mode 100644 index 0000000000000..507611b8b4b57 --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/telemetry/endpoint_malware_disabled/mappings.json @@ -0,0 +1,2587 @@ +{ + "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_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "43b8830d5d0df85a6823d290885fc9fd", + "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_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "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/endpoint_malware_enabled/data.json b/x-pack/test/functional/es_archives/endpoint/telemetry/endpoint_malware_enabled/data.json new file mode 100644 index 0000000000000..752877a6014d1 --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/telemetry/endpoint_malware_enabled/data.json @@ -0,0 +1,272 @@ +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:d26f4ae1-f8cb-11ea-b3d9-2d2b39f3a393", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "25129070-f8c9-11ea-b3d9-2d2b39f3a393", + "timestamp": "2020-09-17T05:54:12.675688-04:00", + "subtype": "RUNNING", + "agent_id": "58bed8d4-fba1-40fa-a7b0-1ceec2fb89e5", + "message": "Application: endpoint-security--7.9.0[58bed8d4-fba1-40fa-a7b0-1ceec2fb89e5]: State changed to RUNNING: Protecting with policy {a7343ed0-f8cb-11ea-b3d9-2d2b39f3a393}", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-17T09:54:12.73687100Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"a7343ed0-f8cb-11ea-b3d9-2d2b39f3a393\",\"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\":[1,0,0,0,0,0,0,1,2,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\":0.156156153275005,\"mean\":30.7709502014208}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":117456896,\"mean\":87985152}}},\"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\":\"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\"},{\"name\":\"checkinAPIThread\"},{\"name\":\"actionsAPIThread\"},{\"name\":\"stateReportThread\"}],\"uptime\":{\"endpoint\":23,\"system\":642232}},\"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\":\"6a546aade5563d3e8dffc1fe2d93d33edda8f9ca3e17ac3cc9ac707620cb9ecd\"},{\"name\":\"endpointpe-v4-exceptionlist\",\"sha256\":\"04f9f87accc5d5aea433427bd1bd4ec6908f8528c78ceed26f70df7875a99385\"},{\"name\":\"endpointpe-v4-model\",\"sha256\":\"1471838597fcd79a54ea4a3ec9a9beee1a86feaedab6c98e61102559ced822a8\"},{\"name\":\"global-exceptionlist-windows\",\"sha256\":\"824859b0c6749cc31951d92a73bbdddfcfe9f38abfe432087934d4dab9766ce8\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-windows-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"a7343ed0-f8cb-11ea-b3d9-2d2b39f3a393\",\"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\":\"ea57f15d-71a2-4b5e-8101-7155d9cdd4a2\",\"version\":\"7.9.0\"},\"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-17T09:54:48.078Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:d5ec3ca0-f8cb-11ea-b3d9-2d2b39f3a393", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "25129070-f8c9-11ea-b3d9-2d2b39f3a393", + "timestamp": "2020-09-17T05:54:08.471216392-04:00", + "subtype": "RUNNING", + "agent_id": "8a0a4a79-c1e3-4d13-951a-36c168b4be87", + "message": "Application: endpoint-security--7.9.0[8a0a4a79-c1e3-4d13-951a-36c168b4be87]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-17T09:54:08.470069104Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"00000000-0000-0000-0000-000000000000\",\"policy\":{\"linux\":{\"events\":{},\"logging\":{\"file\":\"info\",\"stdout\":\"info\"}},\"mac\":{\"events\":{},\"logging\":{\"file\":\"info\",\"stdout\":\"info\"},\"malware\":{}},\"windows\":{\"events\":{},\"logging\":{\"file\":\"info\",\"stdout\":\"info\"},\"malware\":{}}}}]},\"metrics\":{\"cpu\":{},\"memory\":{},\"uptime\":{}},\"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 disabling process events; current state is disabled\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Success disabling network events; current state is disabled\",\"name\":\"detect_network_events\",\"status\":\"success\"},{\"message\":\"Success disabling file events; current state is disabled\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Success disabling file events; current state is disabled\",\"name\":\"configure_file_events\",\"status\":\"success\"},{\"message\":\"Success disabling network events; current state is disabled\",\"name\":\"configure_network_events\",\"status\":\"success\"},{\"message\":\"Success disabling process events; current state is disabled\",\"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\":\"00000000-0000-0000-0000-000000000000\",\"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\":\"145f6503-0c88-484d-a00a-40c68e3cc796\",\"version\":\"7.9.0\"},\"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-17T09:54:53.930Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:ba82e9e0-f8cc-11ea-b3d9-2d2b39f3a393", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "25129070-f8c9-11ea-b3d9-2d2b39f3a393", + "timestamp": "2020-09-17T06:00:29.153626-04:00", + "subtype": "RUNNING", + "agent_id": "f490a4f2-5022-4a15-b447-86204ebbdd35", + "message": "Application: endpoint-security--7.9.0[f490a4f2-5022-4a15-b447-86204ebbdd35]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-17T10:00:29.144191298Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"a7343ed0-f8cb-11ea-b3d9-2d2b39f3a393\",\"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\":[5,1,0,0,0,0,0,0,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\":0.25202279,\"mean\":2.207890305}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":39948288,\"mean\":37541888}}},\"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\"},{\"name\":\"checkinAPIThread\"},{\"name\":\"actionsAPIThread\"},{\"name\":\"stateReportThread\"}],\"uptime\":{\"endpoint\":34,\"system\":3180112}},\"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\":\"Successfully started file write event reporting\",\"name\":\"detect_file_write_events\",\"status\":\"success\"},{\"message\":\"Successfully started process event reporting\",\"name\":\"detect_process_events\",\"status\":\"success\"},{\"message\":\"Successfully started network event reporting\",\"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\":\"a7343ed0-f8cb-11ea-b3d9-2d2b39f3a393\",\"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\":\"2bd3e07d-b528-4cd7-8ca6-2af2236ca446\",\"version\":\"7.9.0\"},\"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-17T10:01:17.438Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:f490a4f2-5022-4a15-b447-86204ebbdd35", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-17T10:42:01.472Z", + "fleet-agents": { + "config_id": "25129070-f8c9-11ea-b3d9-2d2b39f3a393", + "default_api_key": "S3lDCj6T1USDsJUQfe/W77/D0sDl6V6K4n+Sk6WxUlev1cInze2xS3DVqETXdK0n4hQrTuBwVMjroXf8I329Yi1qevbPTGH7NhR/dhAPWOQfuVWCWh3auRdBLF3vwupqFZXJPfmo4XFA+nL0o/DHgmhPK6Ur0aKQowrCYOyvSEu9krK1AtPfK8k=", + "current_error_events": "[]", + "config_revision": 2, + "enrolled_at": "2020-09-17T09:38:06.809Z", + "default_api_key_id": "xihum3QBy3Gd1kyfaiLd", + "last_checkin": "2020-09-17T10:42:01.471Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "WMptm3QB3yUjiqk7n7R7", + "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:3d24:2d96:b634:3165/64", + "fdbb:cb5c:fb4:68:6896:8ae7:78be:f9d0/64", + "fdbb:cb5c:fb4:68:78d9:60b3:632c:c908/64", + "fdbb:cb5c:fb4:68:f1a6:a826:7f63:6b77/64", + "fdbb:cb5c:fb4:68:997d:b66b:2555:6bb7/64", + "fdbb:cb5c:fb4:68:4c21:58bc:9122:1adf/64", + "fdbb:cb5c:fb4:68:d536:51f3:fcbe:7f3/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.0", + "id": "f490a4f2-5022-4a15-b447-86204ebbdd35" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:8a0a4a79-c1e3-4d13-951a-36c168b4be87", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-17T10:42:01.472Z", + "fleet-agents": { + "config_id": "25129070-f8c9-11ea-b3d9-2d2b39f3a393", + "default_api_key": "/XDWfZFwXdENtpxpo74Rl5fjAFX0mL8cwrPmOXXPtnri/QW6591rkVO7wcsZe8zN4A4Ld4SXBw1EpBMQILwtL2jVBj/t34qjXJDcQFkAf6CPmO1YP3ilC/L49tGQX0J3MTLVoSmwo/N9ni8v/ywdl8IkhB4dqTp6me/SrL+I31D6vUFxYupWdlg=", + "current_error_events": "[]", + "config_revision": 2, + "enrolled_at": "2020-09-17T09:37:50.171Z", + "default_api_key_id": "oShtm3QBy3Gd1kyfzhpC", + "last_checkin": "2020-09-17T10:42:01.471Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "_8ptm3QB3yUjiqk7XrOs", + "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.0", + "id": "8a0a4a79-c1e3-4d13-951a-36c168b4be87" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:58bed8d4-fba1-40fa-a7b0-1ceec2fb89e5", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-17T10:42:28.549Z", + "fleet-agents": { + "config_id": "25129070-f8c9-11ea-b3d9-2d2b39f3a393", + "default_api_key": "vAM73FEHbqUVOnjN4FY0jdI0EtE+7MLqN5rNj4n6TJRz1Shx3rhNnDi6S555R9pGR2ZLtN57j9Z+5sO4nT9DYW3h8RK4Z15ifgnEvNVFR1Lsp7S/GUbPM6ooQMgNo63eoU8plYSfh1/98Cbe4dOwe+FhB5tbwGK1GqWb2TOshzH01JUbvfKb5jc=", + "current_error_events": "[]", + "config_revision": 2, + "enrolled_at": "2020-09-17T09:37:19.476Z", + "default_api_key_id": "LChtm3QBy3Gd1kyfGhoG", + "last_checkin": "2020-09-17T10:42:01.471Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "xspsm3QB3yUjiqk75rL_", + "packages": [ + "endpoint", + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "DESKTOP-QBBSCUT", + "ip": [ + "fdbb:cb5c:fb4:68:6ca6:5ea3:ae36:af51/64", + "fdbb:cb5c:fb4:68:2c1b:d329:2b41:de28/128", + "fdbb:cb5c:fb4:68:6d89:d2f8:2825:d739/128", + "fdbb:cb5c:fb4:68:a484:d8c6:87c:c2ee/128", + "fdbb:cb5c:fb4:68:b03f:b84f:8b8a:77b9/128", + "fdbb:cb5c:fb4:68:b42f:f72:b857:40e9/128", + "fdbb:cb5c:fb4:68:e9b5:f8c7:f06d:6fe1/128", + "fdbb:cb5c:fb4:68:f05b:8555:cc24:dfa4/128", + "fe80::6ca6:5ea3:ae36:af51/64", + "10.0.7.235/22", + "::1/128", + "127.0.0.1/8" + ], + "hostname": "DESKTOP-QBBSCUT", + "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.0", + "id": "58bed8d4-fba1-40fa-a7b0-1ceec2fb89e5" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/endpoint/telemetry/endpoint_malware_enabled/mappings.json b/x-pack/test/functional/es_archives/endpoint/telemetry/endpoint_malware_enabled/mappings.json new file mode 100644 index 0000000000000..507611b8b4b57 --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/telemetry/endpoint_malware_enabled/mappings.json @@ -0,0 +1,2587 @@ +{ + "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_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "43b8830d5d0df85a6823d290885fc9fd", + "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_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "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/endpoint_uninstalled/data.json b/x-pack/test/functional/es_archives/endpoint/telemetry/endpoint_uninstalled/data.json new file mode 100644 index 0000000000000..4f903d3bb52ce --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/telemetry/endpoint_uninstalled/data.json @@ -0,0 +1,269 @@ +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:f490a4f2-5022-4a15-b447-86204ebbdd35", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-17T16:50:40.506Z", + "fleet-agents": { + "config_id": "25129070-f8c9-11ea-b3d9-2d2b39f3a393", + "default_api_key": "S3lDCj6T1USDsJUQfe/W77/D0sDl6V6K4n+Sk6WxUlev1cInze2xS3DVqETXdK0n4hQrTuBwVMjroXf8I329Yi1qevbPTGH7NhR/dhAPWOQfuVWCWh3auRdBLF3vwupqFZXJPfmo4XFA+nL0o/DHgmhPK6Ur0aKQowrCYOyvSEu9krK1AtPfK8k=", + "current_error_events": "[]", + "config_revision": 4, + "enrolled_at": "2020-09-17T09:38:06.809Z", + "default_api_key_id": "xihum3QBy3Gd1kyfaiLd", + "last_checkin": "2020-09-17T16:50:32.344Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "WMptm3QB3yUjiqk7n7R7", + "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:3d24:2d96:b634:3165/64", + "fdbb:cb5c:fb4:68:6896:8ae7:78be:f9d0/64", + "fdbb:cb5c:fb4:68:78d9:60b3:632c:c908/64", + "fdbb:cb5c:fb4:68:f1a6:a826:7f63:6b77/64", + "fdbb:cb5c:fb4:68:997d:b66b:2555:6bb7/64", + "fdbb:cb5c:fb4:68:4c21:58bc:9122:1adf/64", + "fdbb:cb5c:fb4:68:d536:51f3:fcbe:7f3/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.0", + "id": "f490a4f2-5022-4a15-b447-86204ebbdd35" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:58bed8d4-fba1-40fa-a7b0-1ceec2fb89e5", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-17T16:50:33.463Z", + "fleet-agents": { + "config_id": "25129070-f8c9-11ea-b3d9-2d2b39f3a393", + "default_api_key": "vAM73FEHbqUVOnjN4FY0jdI0EtE+7MLqN5rNj4n6TJRz1Shx3rhNnDi6S555R9pGR2ZLtN57j9Z+5sO4nT9DYW3h8RK4Z15ifgnEvNVFR1Lsp7S/GUbPM6ooQMgNo63eoU8plYSfh1/98Cbe4dOwe+FhB5tbwGK1GqWb2TOshzH01JUbvfKb5jc=", + "current_error_events": "[]", + "config_revision": 4, + "enrolled_at": "2020-09-17T09:37:19.476Z", + "default_api_key_id": "LChtm3QBy3Gd1kyfGhoG", + "last_checkin": "2020-09-17T16:50:32.344Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "xspsm3QB3yUjiqk75rL_", + "packages": [ + "system" + ], + "type": "PERMANENT", + "local_metadata": { + "host": { + "name": "DESKTOP-QBBSCUT", + "ip": [ + "fdbb:cb5c:fb4:68:6ca6:5ea3:ae36:af51/64", + "fdbb:cb5c:fb4:68:2c1b:d329:2b41:de28/128", + "fdbb:cb5c:fb4:68:6d89:d2f8:2825:d739/128", + "fdbb:cb5c:fb4:68:a484:d8c6:87c:c2ee/128", + "fdbb:cb5c:fb4:68:b03f:b84f:8b8a:77b9/128", + "fdbb:cb5c:fb4:68:b42f:f72:b857:40e9/128", + "fdbb:cb5c:fb4:68:e9b5:f8c7:f06d:6fe1/128", + "fdbb:cb5c:fb4:68:f05b:8555:cc24:dfa4/128", + "fe80::6ca6:5ea3:ae36:af51/64", + "10.0.7.235/22", + "::1/128", + "127.0.0.1/8" + ], + "hostname": "DESKTOP-QBBSCUT", + "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.0", + "id": "58bed8d4-fba1-40fa-a7b0-1ceec2fb89e5" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agents:8a0a4a79-c1e3-4d13-951a-36c168b4be87", + "source": { + "type": "fleet-agents", + "references": [], + "updated_at": "2020-09-17T16:50:32.345Z", + "fleet-agents": { + "config_id": "25129070-f8c9-11ea-b3d9-2d2b39f3a393", + "default_api_key": "/XDWfZFwXdENtpxpo74Rl5fjAFX0mL8cwrPmOXXPtnri/QW6591rkVO7wcsZe8zN4A4Ld4SXBw1EpBMQILwtL2jVBj/t34qjXJDcQFkAf6CPmO1YP3ilC/L49tGQX0J3MTLVoSmwo/N9ni8v/ywdl8IkhB4dqTp6me/SrL+I31D6vUFxYupWdlg=", + "current_error_events": "[]", + "config_revision": 4, + "enrolled_at": "2020-09-17T09:37:50.171Z", + "default_api_key_id": "oShtm3QBy3Gd1kyfzhpC", + "last_checkin": "2020-09-17T16:50:32.344Z", + "active": true, + "user_provided_metadata": {}, + "access_api_key_id": "_8ptm3QB3yUjiqk7XrOs", + "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.0", + "id": "8a0a4a79-c1e3-4d13-951a-36c168b4be87" + } + } + } + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:d26db7e1-f905-11ea-b3d9-2d2b39f3a393", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "25129070-f8c9-11ea-b3d9-2d2b39f3a393", + "timestamp": "2020-09-17T12:49:38.703352816-04:00", + "subtype": "RUNNING", + "agent_id": "8a0a4a79-c1e3-4d13-951a-36c168b4be87", + "message": "Application: endpoint-security--7.9.0[8a0a4a79-c1e3-4d13-951a-36c168b4be87]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-17T14:58:59.634499439Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"a7343ed0-f8cb-11ea-b3d9-2d2b39f3a393\",\"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\":\"off\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"off\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[3655,2,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0],\"values\":[5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100]},\"latest\":0.2,\"mean\":0.711852054295345}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":57106432,\"mean\":55650691}}},\"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.0109289617486339},\"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.00546448087431694},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0.60207991242474},\"name\":\"EventsLoopThread\"},{\"cpu\":{\"mean\":0},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"}],\"uptime\":{\"endpoint\":18300,\"system\":4390784}},\"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\":\"a7343ed0-f8cb-11ea-b3d9-2d2b39f3a393\",\"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\":\"145f6503-0c88-484d-a00a-40c68e3cc796\",\"version\":\"7.9.0\"},\"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-17T16:49:58.878Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:cf5ddfd1-f905-11ea-b3d9-2d2b39f3a393", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "25129070-f8c9-11ea-b3d9-2d2b39f3a393", + "timestamp": "2020-09-17T12:49:09.3059615-04:00", + "subtype": "RUNNING", + "agent_id": "58bed8d4-fba1-40fa-a7b0-1ceec2fb89e5", + "message": "Application: endpoint-security--7.9.0[58bed8d4-fba1-40fa-a7b0-1ceec2fb89e5]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-17T14:59:18.26119700Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"a7343ed0-f8cb-11ea-b3d9-2d2b39f3a393\",\"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\":\"off\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"off\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[3617,34,0,0,1,2,1,1,4,1,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\":0.156187721707846,\"mean\":0.277454799155371}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":96870400,\"mean\":87327732}}},\"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.00545851528384279},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0.256550218340611},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0.0709645723019816},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0.0218364450267496},\"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.0163782278757438},\"name\":\"KernelSyncMessageThread\"},{\"cpu\":{\"mean\":0.0163782278757438},\"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\":0},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"stateReportThread\"}],\"uptime\":{\"endpoint\":18317,\"system\":660526}},\"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 off 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\":\"Successfully stopped 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 stopped 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\":\"6a546aade5563d3e8dffc1fe2d93d33edda8f9ca3e17ac3cc9ac707620cb9ecd\"},{\"name\":\"endpointpe-v4-exceptionlist\",\"sha256\":\"04f9f87accc5d5aea433427bd1bd4ec6908f8528c78ceed26f70df7875a99385\"},{\"name\":\"endpointpe-v4-model\",\"sha256\":\"1471838597fcd79a54ea4a3ec9a9beee1a86feaedab6c98e61102559ced822a8\"},{\"name\":\"global-exceptionlist-windows\",\"sha256\":\"824859b0c6749cc31951d92a73bbdddfcfe9f38abfe432087934d4dab9766ce8\"}],\"version\":\"1.0.0\"},\"user\":{\"identifiers\":[{\"name\":\"endpoint-exceptionlist-windows-v1\",\"sha256\":\"d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658\"}],\"version\":\"1.0.0\"}},\"id\":\"a7343ed0-f8cb-11ea-b3d9-2d2b39f3a393\",\"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\":\"ea57f15d-71a2-4b5e-8101-7155d9cdd4a2\",\"version\":\"7.9.0\"},\"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-17T16:49:53.741Z", + "type": "fleet-agent-events" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana_1", + "type": "_doc", + "id": "fleet-agent-events:9a206900-f8f6-11ea-b3d9-2d2b39f3a393", + "source": { + "references": [], + "fleet-agent-events": { + "config_id": "25129070-f8c9-11ea-b3d9-2d2b39f3a393", + "timestamp": "2020-09-17T11:00:52.879069-04:00", + "subtype": "RUNNING", + "agent_id": "f490a4f2-5022-4a15-b447-86204ebbdd35", + "message": "Application: endpoint-security--7.9.0[f490a4f2-5022-4a15-b447-86204ebbdd35]: State changed to RUNNING: ", + "type": "STATE", + "payload": "{\"endpoint-security\":{\"@timestamp\":\"2020-09-17T15:00:52.870465905Z\",\"Endpoint\":{\"configuration\":{\"inputs\":[{\"id\":\"a7343ed0-f8cb-11ea-b3d9-2d2b39f3a393\",\"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\":\"off\"}},\"windows\":{\"events\":{\"dll_and_driver_load\":true,\"dns\":true,\"file\":true,\"network\":true,\"process\":true,\"registry\":true,\"security\":true},\"logging\":{\"file\":\"info\"},\"malware\":{\"mode\":\"off\"}}}}]},\"metrics\":{\"cpu\":{\"endpoint\":{\"histogram\":{\"counts\":[3571,29,4,2,1,0,1,0,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\":0.76328957,\"mean\":0.580389267551738}},\"memory\":{\"endpoint\":{\"private\":{\"latest\":58580992,\"mean\":62785576}}},\"threads\":[{\"cpu\":{\"mean\":0.00543212559074366},\"name\":\"Cron\"},{\"cpu\":{\"mean\":0.00543212559074366},\"name\":\"File Cache\"},{\"cpu\":{\"mean\":0},\"name\":\"FileLogThread\"},{\"cpu\":{\"mean\":0.0217285023629746},\"name\":\"DocumentLoggingThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DocumentLoggingMaintenance\"},{\"cpu\":{\"mean\":0.103210386224129},\"name\":\"BulkConsumerThread\"},{\"cpu\":{\"mean\":0.211852898039003},\"name\":\"DocumentLoggingConsumerThread\"},{\"cpu\":{\"mean\":0.016296376772231},\"name\":\"ArtifactManifestDownload\"},{\"cpu\":{\"mean\":0},\"name\":\"PerformanceMonitorWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"MetadataThread\"},{\"cpu\":{\"mean\":0.217344055640078},\"name\":\"EventsQueueThread\"},{\"cpu\":{\"mean\":0.0326016083460117},\"name\":\"FileScoreAsyncEventThread\"},{\"cpu\":{\"mean\":0},\"name\":\"QuarantineManagerWorkerThread\"},{\"cpu\":{\"mean\":0},\"name\":\"DelayedAlertEnrichment\"},{\"cpu\":{\"mean\":0},\"name\":\"grpcConnectionManagerThread\"},{\"cpu\":{\"mean\":0.0271709596782958},\"name\":\"KernelPortConsumerThread\"},{\"cpu\":{\"mean\":0.0271709596782958},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0.0271709596782958},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0.0271709596782958},\"name\":\"KernelAsyncMessageThread\"},{\"cpu\":{\"mean\":0.0271709596782958},\"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\":0.0979538528515455},\"name\":\"checkinAPIThread\"},{\"cpu\":{\"mean\":0},\"name\":\"actionsAPIThread\"},{\"cpu\":{\"mean\":0.0326530612244898},\"name\":\"stateReportThread\"},{\"cpu\":{\"mean\":0.0166527893422148},\"name\":\"EventsPidMonitorThread\"},{\"cpu\":{\"mean\":0.133222314737719},\"name\":\"EventsDelayEventThread\"}],\"uptime\":{\"endpoint\":18409,\"system\":3198488}},\"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 off 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\":\"a7343ed0-f8cb-11ea-b3d9-2d2b39f3a393\",\"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\":\"2bd3e07d-b528-4cd7-8ca6-2af2236ca446\",\"version\":\"7.9.0\"},\"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-17T15:01:01.968Z", + "type": "fleet-agent-events" + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/endpoint/telemetry/endpoint_uninstalled/mappings.json b/x-pack/test/functional/es_archives/endpoint/telemetry/endpoint_uninstalled/mappings.json new file mode 100644 index 0000000000000..507611b8b4b57 --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/telemetry/endpoint_uninstalled/mappings.json @@ -0,0 +1,2587 @@ +{ + "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_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "43b8830d5d0df85a6823d290885fc9fd", + "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_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "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/fleet/agents/data.json b/x-pack/test/functional/es_archives/fleet/agents/data.json index 12d646de85ec3..e05a2fe010e89 100644 --- a/x-pack/test/functional/es_archives/fleet/agents/data.json +++ b/x-pack/test/functional/es_archives/fleet/agents/data.json @@ -32,6 +32,7 @@ "access_api_key_id": "api-key-2", "active": true, "shared_id": "agent2_filebeat", + "policy_id": "policy1", "type": "PERMANENT", "local_metadata": {}, "user_provided_metadata": {} @@ -54,6 +55,7 @@ "access_api_key_id": "api-key-3", "active": true, "shared_id": "agent3_metricbeat", + "policy_id": "policy1", "type": "PERMANENT", "local_metadata": {}, "user_provided_metadata": {} @@ -76,6 +78,7 @@ "access_api_key_id": "api-key-4", "active": true, "shared_id": "agent4_metricbeat", + "policy_id": "policy1", "type": "PERMANENT", "local_metadata": {}, "user_provided_metadata": {} @@ -246,3 +249,32 @@ } } } + +{ + "type": "doc", + "value": { + "id": "ingest-agent-policies:policy2", + "index": ".kibana", + "source": { + "type": "ingest-agent-policies", + "ingest-agent-policies": { + "name": "Test policy 2", + "namespace": "default", + "description": "Policy 2", + "status": "active", + "package_policies": [], + "is_default": true, + "monitoring_enabled": [ + "logs", + "metrics" + ], + "revision": 2, + "updated_at": "2020-05-07T19:34:42.533Z", + "updated_by": "system" + }, + "migrationVersion": { + "ingest-agent-policies": "7.10.0" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/invalid_scripted_field/data.json.gz b/x-pack/test/functional/es_archives/invalid_scripted_field/data.json.gz new file mode 100644 index 0000000000000..380dd6049179a Binary files /dev/null and b/x-pack/test/functional/es_archives/invalid_scripted_field/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/invalid_scripted_field/mappings.json b/x-pack/test/functional/es_archives/invalid_scripted_field/mappings.json new file mode 100644 index 0000000000000..0024c6943ed1c --- /dev/null +++ b/x-pack/test/functional/es_archives/invalid_scripted_field/mappings.json @@ -0,0 +1,250 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "mappings": { + "dynamic": "strict", + "properties": { + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + }, + "defaultIndex": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "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" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + } + } + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "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" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "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" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "number_of_replicas": "0", + "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 f756d73484198..e3a8743e60897 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/data.json +++ b/x-pack/test/functional/es_archives/maps/kibana/data.json @@ -615,6 +615,37 @@ } } +{ + "type": "doc", + "value": { + "id": "map:78116c8c-fd2a-11ea-adc1-0242ac120002", + "index": ".kibana", + "source": { + "map": { + "bounds": { + "coordinates": [ + [ + -140.62361, + 54.11832 + ], + [ + -55.49169, + 18.17193 + ] + ], + "type": "envelope" + }, + "description": "", + "layerListJSON": "[{\"id\":\"g1xkv\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"sourceDescriptor\":{\"resolution\": \"SUPER_FINE\",\"type\":\"ES_GEO_GRID\",\"id\":\"9305f6ea-4518-4c06-95b9-33321aa38d6a\",\"indexPatternId\":\"c698b940-e149-11e8-a35a-370a8516603a\",\"geoField\":\"geo.coordinates\",\"requestType\":\"grid\",\"metrics\":[{\"type\":\"count\"},{\"type\":\"max\",\"field\":\"bytes\"}]},\"visible\":true,\"temporary\":false,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"max of bytes\",\"name\":\"max_of_bytes\",\"origin\":\"source\"},\"color\":\"Blues\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#cccccc\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"minSize\":4,\"maxSize\":32}}},\"temporary\":true,\"previousStyle\":null},\"type\":\"TILED_VECTOR\"}]", + "mapStateJSON": "{\"zoom\":3.59,\"center\":{\"lon\":-98.05765,\"lat\":38.32288},\"timeFilters\":{\"from\":\"2015-09-20T00:00:00.000Z\",\"to\":\"2015-09-20T01:00:00.000Z\"},\"refreshConfig\":{\"isPaused\":true,\"interval\":1000}}", + "title": "geo grid vector grid example (SUPER_FINE resolution)", + "uiStateJSON": "{\"isDarkMode\":false}" + }, + "type": "map" + } + } +} + { "type": "doc", "value": { diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts index 325ea41ae3977..50da8425e493d 100644 --- a/x-pack/test/functional/services/ml/index.ts +++ b/x-pack/test/functional/services/ml/index.ts @@ -78,8 +78,8 @@ export function MachineLearningProvider(context: FtrProviderContext) { const securityCommon = MachineLearningSecurityCommonProvider(context); const securityUI = MachineLearningSecurityUIProvider(context, securityCommon); const settings = MachineLearningSettingsProvider(context); - const settingsCalendar = MachineLearningSettingsCalendarProvider(context); - const settingsFilterList = MachineLearningSettingsFilterListProvider(context); + const settingsCalendar = MachineLearningSettingsCalendarProvider(context, commonUI); + const settingsFilterList = MachineLearningSettingsFilterListProvider(context, commonUI); const singleMetricViewer = MachineLearningSingleMetricViewerProvider(context); const testExecution = MachineLearningTestExecutionProvider(context); const testResources = MachineLearningTestResourcesProvider(context); diff --git a/x-pack/test/functional/services/ml/security_ui.ts b/x-pack/test/functional/services/ml/security_ui.ts index e09467ff36a34..da4324901d38e 100644 --- a/x-pack/test/functional/services/ml/security_ui.ts +++ b/x-pack/test/functional/services/ml/security_ui.ts @@ -16,7 +16,6 @@ export function MachineLearningSecurityUIProvider( return { async loginAs(user: USER) { const password = mlSecurityCommon.getPasswordForUser(user); - await PageObjects.security.forceLogout(); await PageObjects.security.login(user, password, { diff --git a/x-pack/test/functional/services/ml/settings_calendar.ts b/x-pack/test/functional/services/ml/settings_calendar.ts index 34d18c6e12c47..c269636522923 100644 --- a/x-pack/test/functional/services/ml/settings_calendar.ts +++ b/x-pack/test/functional/services/ml/settings_calendar.ts @@ -7,9 +7,15 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { MlCommonUI } from './common_ui'; -export function MachineLearningSettingsCalendarProvider({ getService }: FtrProviderContext) { +export function MachineLearningSettingsCalendarProvider( + { getService }: FtrProviderContext, + mlCommonUI: MlCommonUI +) { const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const comboBox = getService('comboBox'); return { async parseCalendarTable() { @@ -172,6 +178,11 @@ export function MachineLearningSettingsCalendarProvider({ getService }: FtrProvi ); }, + calendarRowSelector(calendarId: string, subSelector?: string) { + const row = `~mlCalendarTable > ~row-${calendarId}`; + return !subSelector ? row : `${row} > ${subSelector}`; + }, + eventRowSelector(eventDescription: string, subSelector?: string) { const row = `~mlCalendarEventsTable > ~row-${eventDescription}`; return !subSelector ? row : `${row} > ${subSelector}`; @@ -181,6 +192,10 @@ export function MachineLearningSettingsCalendarProvider({ getService }: FtrProvi await testSubjects.existOrFail(this.eventRowSelector(eventDescription)); }, + async assertEventRowMissing(eventDescription: string) { + await testSubjects.missingOrFail(this.eventRowSelector(eventDescription)); + }, + async assertDeleteEventButtonEnabled(eventDescription: string, expectedValue: boolean) { const isEnabled = await testSubjects.isEnabled( this.eventRowSelector(eventDescription, 'mlCalendarEventDeleteButton') @@ -192,5 +207,191 @@ export function MachineLearningSettingsCalendarProvider({ getService }: FtrProvi }' (got '${isEnabled ? 'enabled' : 'disabled'}')` ); }, + + async assertCalendarRowExists(calendarId: string) { + await testSubjects.existOrFail(this.calendarRowSelector(calendarId)); + }, + + async assertCalendarRowNotExists(calendarId: string) { + await testSubjects.missingOrFail(this.calendarRowSelector(calendarId)); + }, + + async assertCalendarIdValue(expectedValue: string) { + const actualValue = await testSubjects.getAttribute('mlCalendarIdInput', 'value'); + expect(actualValue).to.eql( + expectedValue, + `Calendar id should be '${expectedValue}' (got '${actualValue}')` + ); + }, + + async setCalendarId(calendarId: string) { + await mlCommonUI.setValueWithChecks('mlCalendarIdInput', calendarId, { + clearWithKeyboard: true, + }); + await this.assertCalendarIdValue(calendarId); + }, + + async assertCalendarDescriptionValue(expectedValue: string) { + const actualValue = await testSubjects.getAttribute('mlCalendarDescriptionInput', 'value'); + expect(actualValue).to.eql( + expectedValue, + `Calendar description should be '${expectedValue}' (got '${actualValue}')` + ); + }, + + async setCalendarDescription(description: string) { + await mlCommonUI.setValueWithChecks('mlCalendarDescriptionInput', description, { + clearWithKeyboard: true, + }); + await this.assertCalendarDescriptionValue(description); + }, + + async getApplyToAllJobsSwitchCheckedState(): Promise { + const subj = 'mlCalendarApplyToAllJobsSwitch'; + const isSelected = await testSubjects.getAttribute(subj, 'aria-checked'); + return isSelected === 'true'; + }, + + async toggleApplyToAllJobsSwitch(toggle: boolean) { + const subj = 'mlCalendarApplyToAllJobsSwitch'; + if ((await this.getApplyToAllJobsSwitchCheckedState()) !== toggle) { + await retry.tryForTime(5 * 1000, async () => { + await testSubjects.clickWhenNotDisabled(subj); + await this.assertApplyToAllJobsSwitchEnabled(toggle); + }); + } + }, + + async saveCalendar() { + await testSubjects.existOrFail('mlSaveCalendarButton'); + await testSubjects.click('mlSaveCalendarButton'); + await testSubjects.existOrFail('mlPageCalendarManagement'); + }, + + async navigateToCalendarCreationPage() { + await testSubjects.existOrFail('mlCalendarButtonCreate'); + await testSubjects.click('mlCalendarButtonCreate'); + await testSubjects.existOrFail('mlPageCalendarEdit'); + }, + + async openNewCalendarEventForm() { + await testSubjects.existOrFail('mlCalendarNewEventButton'); + await testSubjects.click('mlCalendarNewEventButton'); + await testSubjects.existOrFail('mlPageCalendarEdit'); + }, + + async assertCalendarEventDescriptionValue(expectedValue: string) { + const actualValue = await testSubjects.getAttribute( + 'mlCalendarEventDescriptionInput', + 'value' + ); + expect(actualValue).to.eql( + expectedValue, + `Calendar event description should be '${expectedValue}' (got '${actualValue}')` + ); + }, + + async setCalendarEventDescription(eventDescription: string) { + await testSubjects.existOrFail('mlCalendarEventDescriptionInput'); + await mlCommonUI.setValueWithChecks('mlCalendarEventDescriptionInput', eventDescription, { + clearWithKeyboard: true, + }); + await this.assertCalendarEventDescriptionValue(eventDescription); + }, + + async cancelNewCalendarEvent() { + await testSubjects.existOrFail('mlCalendarCancelEventButton'); + await testSubjects.click('mlCalendarCancelEventButton'); + await testSubjects.missingOrFail('mlCalendarEventForm'); + }, + + async addNewCalendarEvent() { + await testSubjects.existOrFail('mlCalendarAddEventButton'); + await testSubjects.click('mlCalendarAddEventButton'); + await testSubjects.missingOrFail('mlCalendarEventForm'); + }, + + async assertJobSelectionExists() { + await testSubjects.existOrFail('mlCalendarJobSelection'); + }, + + async assertJobSelectionNotExists() { + await testSubjects.missingOrFail('mlCalendarJobSelection'); + }, + + async assertJobSelection(expectedIdentifier: string[]) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'mlCalendarJobSelection > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected job selection to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); + }, + + async assertJobSelectionContain(expectedIdentifier: string) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'mlCalendarJobSelection > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.contain( + expectedIdentifier, + `Expected job selection to contain '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); + }, + + async selectJob(identifier: string) { + await comboBox.set('mlCalendarJobSelection > comboBoxInput', identifier); + await this.assertJobSelectionContain(identifier); + }, + + async assertJobGroupSelectionExists() { + await testSubjects.existOrFail('mlCalendarJobGroupSelection'); + }, + + async assertJobGroupSelectionNotExists() { + await testSubjects.missingOrFail('mlCalendarJobGroupSelection'); + }, + + async assertJobGroupSelection(expectedIdentifier: string[]) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'mlCalendarJobGroupSelection > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected job group selection to be'${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); + }, + + async assertJobGroupSelectionContain(expectedIdentifier: string) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'mlCalendarJobGroupSelection > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.contain( + expectedIdentifier, + `Expected job group selection to contain'${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); + }, + + async selectJobGroup(identifier: string) { + await comboBox.set('mlCalendarJobGroupSelection > comboBoxInput', identifier); + await this.assertJobGroupSelectionContain(identifier); + }, + + async deleteCalendarEventRow(eventDescription: string) { + await this.assertEventRowExists(eventDescription); + await testSubjects.click( + this.eventRowSelector(eventDescription, 'mlCalendarEventDeleteButton') + ); + await this.assertEventRowMissing(eventDescription); + }, + + async deleteCalendar() { + await this.assertDeleteCalendarButtonEnabled(true); + await testSubjects.click('mlCalendarButtonDelete'); + await testSubjects.existOrFail('mlCalendarDeleteConfirmation'); + await testSubjects.existOrFail('confirmModalConfirmButton'); + await testSubjects.click('confirmModalConfirmButton'); + await testSubjects.missingOrFail('mlCalendarDeleteConfirmation'); + }, }; } diff --git a/x-pack/test/functional/services/ml/settings_filter_list.ts b/x-pack/test/functional/services/ml/settings_filter_list.ts index 0afe9f21b03a6..bcac575b65c08 100644 --- a/x-pack/test/functional/services/ml/settings_filter_list.ts +++ b/x-pack/test/functional/services/ml/settings_filter_list.ts @@ -7,9 +7,14 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { MlCommonUI } from './common_ui'; -export function MachineLearningSettingsFilterListProvider({ getService }: FtrProviderContext) { +export function MachineLearningSettingsFilterListProvider( + { getService }: FtrProviderContext, + mlCommonUI: MlCommonUI +) { const testSubjects = getService('testSubjects'); + const browser = getService('browser'); return { async parseFilterListTable() { @@ -17,7 +22,7 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro const $ = await table.parseDomContent(); const rows = []; - for (const tr of $.findTestSubjects('~mlFilterListsRow').toArray()) { + for (const tr of $.findTestSubjects('~mlFilterListRow').toArray()) { const $tr = $(tr); const inUseSubject = $tr @@ -55,6 +60,14 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro return !subSelector ? row : `${row} > ${subSelector}`; }, + async assertFilterListRowExists(filterId: string) { + return await testSubjects.existOrFail(this.rowSelector(filterId)); + }, + + async assertFilterListRowNotExists(filterId: string) { + return await testSubjects.missingOrFail(this.rowSelector(filterId)); + }, + async filterWithSearchString(filter: string, expectedRowCount: number = 1) { const tableListContainer = await testSubjects.find('mlFilterListTableContainer'); const searchBarInput = await tableListContainer.findByClassName('euiFieldSearch'); @@ -101,6 +114,12 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro await this.assertFilterListRowSelected(filterId, false); }, + async selectFilterListRowEditLink(filterId: string) { + await this.assertFilterListRowExists(filterId); + await testSubjects.click(this.rowSelector(filterId, `mlEditFilterListLink`)); + await testSubjects.existOrFail('mlPageFilterListEdit'); + }, + async assertCreateFilterListButtonEnabled(expectedValue: boolean) { const isEnabled = await testSubjects.isEnabled('mlFilterListsButtonCreate'); expect(isEnabled).to.eql( @@ -111,6 +130,10 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro ); }, + async assertDeleteFilterListButtonExists() { + await testSubjects.existOrFail('mlFilterListsDeleteButton'); + }, + async assertDeleteFilterListButtonEnabled(expectedValue: boolean) { const isEnabled = await testSubjects.isEnabled('mlFilterListsDeleteButton'); expect(isEnabled).to.eql( @@ -121,6 +144,16 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro ); }, + async deleteFilterList() { + await this.assertDeleteFilterListButtonExists(); + await this.assertDeleteFilterListButtonEnabled(true); + await testSubjects.click('mlFilterListsDeleteButton'); + await testSubjects.existOrFail('mlFilterListsDeleteButton'); + await testSubjects.existOrFail('mlFilterListDeleteConfirmation'); + await testSubjects.click('confirmModalConfirmButton'); + await testSubjects.missingOrFail('mlFilterListDeleteConfirmation'); + }, + async openFilterListEditForm(filterId: string) { await testSubjects.click(this.rowSelector(filterId, 'mlEditFilterListLink')); await testSubjects.existOrFail('mlPageFilterListEdit'); @@ -136,8 +169,8 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro ); }, - async assertAddItemButtonEnabled(expectedValue: boolean) { - const isEnabled = await testSubjects.isEnabled('mlFilterListAddItemButton'); + async assertOpenNewItemsPopoverButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlFilterListOpenNewItemsPopoverButton'); expect(isEnabled).to.eql( expectedValue, `Expected "add item" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ @@ -146,6 +179,16 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro ); }, + async assertAddItemsButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlFilterListOpenNewItemsPopoverButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "add" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + async assertDeleteItemButtonEnabled(expectedValue: boolean) { const isEnabled = await testSubjects.isEnabled('mlFilterListDeleteItemButton'); expect(isEnabled).to.eql( @@ -156,11 +199,25 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro ); }, + async assertSaveFilterListButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlFilterListSaveButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "save filter list" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + filterItemSelector(filterItem: string, subSelector?: string) { const row = `mlGridItem ${filterItem}`; return !subSelector ? row : `${row} > ${subSelector}`; }, + async assertFilterItemNotExists(filterItem: string) { + await testSubjects.missingOrFail(this.filterItemSelector(filterItem)); + }, + async assertFilterItemExists(filterItem: string) { await testSubjects.existOrFail(this.filterItemSelector(filterItem)); }, @@ -189,6 +246,13 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro await this.assertFilterItemSelected(filterItem, true); }, + async deleteFilterItem(filterItem: string) { + await testSubjects.existOrFail('mlFilterListDeleteItemButton'); + await this.selectFilterItem(filterItem); + await testSubjects.click('mlFilterListDeleteItemButton'); + await this.assertFilterItemNotExists(filterItem); + }, + async deselectFilterItem(filterItem: string) { if ((await this.isFilterItemSelected(filterItem)) === true) { await testSubjects.click(this.filterItemSelector(filterItem)); @@ -196,5 +260,70 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro await this.assertFilterItemSelected(filterItem, false); }, + + async navigateToFilterListCreationPage() { + await this.assertCreateFilterListButtonEnabled(true); + await testSubjects.click('mlFilterListsButtonCreate'); + await testSubjects.existOrFail('mlPageFilterListEdit'); + }, + + async assertFilterListIdValue(expectedValue: string) { + const subj = 'mlNewFilterListIdInput'; + const actualFilterListId = await testSubjects.getAttribute(subj, 'value'); + expect(actualFilterListId).to.eql( + expectedValue, + `Filter list id should be '${expectedValue}' (got '${actualFilterListId}')` + ); + }, + + async setFilterListId(filterId: string) { + const subj = 'mlNewFilterListIdInput'; + await mlCommonUI.setValueWithChecks(subj, filterId, { + clearWithKeyboard: true, + }); + await this.assertFilterListIdValue(filterId); + }, + + async setFilterListDescription(description: string) { + await this.assertEditDescriptionButtonEnabled(true); + await testSubjects.click('mlFilterListEditDescriptionButton'); + await testSubjects.existOrFail('mlFilterListDescriptionInput'); + await mlCommonUI.setValueWithChecks('mlFilterListDescriptionInput', description, { + clearWithKeyboard: true, + }); + await browser.pressKeys(browser.keys.ESCAPE); + await this.assertFilterListDescriptionValue(description); + }, + + async addFilterListKeywords(keywords: string[]) { + await this.assertOpenNewItemsPopoverButtonEnabled(true); + await testSubjects.click('mlFilterListOpenNewItemsPopoverButton'); + await mlCommonUI.setValueWithChecks('mlFilterListAddItemTextArea', keywords.join('\n'), { + clearWithKeyboard: true, + }); + await testSubjects.existOrFail('mlFilterListAddItemsButton'); + await this.assertAddItemsButtonEnabled(true); + await testSubjects.click('mlFilterListAddItemsButton'); + + for (let index = 0; index < keywords.length; index++) { + await this.assertFilterItemExists(keywords[index]); + } + }, + + async assertFilterListDescriptionValue(expectedDescription: string) { + const actualFilterListDescription = await testSubjects.getVisibleText( + 'mlNewFilterListDescriptionText' + ); + expect(actualFilterListDescription).to.eql( + expectedDescription, + `Filter list description should be '${expectedDescription}' (got '${actualFilterListDescription}')` + ); + }, + + async saveFilterList() { + await this.assertSaveFilterListButtonEnabled(true); + await testSubjects.click('mlFilterListSaveButton'); + await testSubjects.existOrFail('mlPageFilterListManagement'); + }, }; } diff --git a/x-pack/test/functional/services/monitoring/elasticsearch_nodes.js b/x-pack/test/functional/services/monitoring/elasticsearch_nodes.js index 0cae469e01697..55c34615373a9 100644 --- a/x-pack/test/functional/services/monitoring/elasticsearch_nodes.js +++ b/x-pack/test/functional/services/monitoring/elasticsearch_nodes.js @@ -6,6 +6,10 @@ import { range } from 'lodash'; +function trimAll(data) { + return data.map((item) => item.trim()); +} + export function MonitoringElasticsearchNodesProvider({ getService, getPageObjects }) { const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['monitoring']); @@ -35,6 +39,9 @@ export function MonitoringElasticsearchNodesProvider({ getService, getPageObject const SUBJ_NODES_DISKS = `${SUBJ_TABLE_BODY} > diskFreeSpace`; const SUBJ_NODES_SHARDS = `${SUBJ_TABLE_BODY} > shards`; + const SUBJ_NODES_ICON_PREFIX = `monitoringCellIcon`; + const SUBJ_NODES_POPOVER_PREFIX = `monitoringCellPopover`; + const SUBJ_NODE_LINK_PREFIX = `${SUBJ_TABLE_BODY} > nodeLink-`; return new (class ElasticsearchIndices { @@ -77,7 +84,6 @@ export function MonitoringElasticsearchNodesProvider({ getService, getPageObject await find.clickByCssSelector(`[data-test-subj="${SUBJ_TABLE_SORT_MEM_COL}"] > button`); await this.waitForTableToFinishLoading(); } - async clickDiskCol() { await find.clickByCssSelector(`[data-test-subj="${SUBJ_TABLE_SORT_DISK_COL}"] > button`); } @@ -108,12 +114,35 @@ export function MonitoringElasticsearchNodesProvider({ getService, getPageObject async getNodesAll() { const names = await testSubjects.getVisibleTextAll(SUBJ_NODES_NAMES); const statuses = await testSubjects.getAttributeAll(SUBJ_NODES_STATUSES, 'alt'); - const cpus = await testSubjects.getVisibleTextAll(SUBJ_NODES_CPUS); - const loads = await testSubjects.getVisibleTextAll(SUBJ_NODES_LOADS); - const memories = await testSubjects.getVisibleTextAll(SUBJ_NODES_MEMS); - const disks = await testSubjects.getVisibleTextAll(SUBJ_NODES_DISKS); + const cpus = trimAll(await testSubjects.getVisibleTextAll(SUBJ_NODES_CPUS)); + const loads = trimAll(await testSubjects.getVisibleTextAll(SUBJ_NODES_LOADS)); + const memories = trimAll(await testSubjects.getVisibleTextAll(SUBJ_NODES_MEMS)); + const disks = trimAll(await testSubjects.getVisibleTextAll(SUBJ_NODES_DISKS)); const shards = await testSubjects.getVisibleTextAll(SUBJ_NODES_SHARDS); + const areasWithText = { + cpuUsage: [], + loadAverage: [], + jvmMemory: [], + diskFreeSpace: [], + }; + + const table = await testSubjects.find(SUBJ_TABLE_BODY); + for (const key of Object.keys(areasWithText)) { + const text = areasWithText[key]; + const icons = await testSubjects.findAllDescendant( + `${SUBJ_NODES_ICON_PREFIX}-${key}`, + table + ); + for (const icon of icons) { + await icon.moveMouseTo(); + await icon.click(); + const _text = await testSubjects.getVisibleTextAll(`${SUBJ_NODES_POPOVER_PREFIX}-${key}`); + text.push(_text[0]); + await icon.click(); + } + } + // tuple-ize the icons and texts together into an array of objects const tableRows = await this.getRows(); const iterator = range(tableRows.length); @@ -124,9 +153,13 @@ export function MonitoringElasticsearchNodesProvider({ getService, getPageObject name: names[current], status: statuses[current], cpu: cpus[current], + cpuText: areasWithText.cpuUsage[current], load: loads[current], + loadText: areasWithText.loadAverage[current], memory: memories[current], + memoryText: areasWithText.jvmMemory[current], disk: disks[current], + diskText: areasWithText.diskFreeSpace[current], shards: shards[current], }, ]; diff --git a/x-pack/test/functional/services/transform/edit_flyout.ts b/x-pack/test/functional/services/transform/edit_flyout.ts index f9504deb39f6a..2ba35bd35d272 100644 --- a/x-pack/test/functional/services/transform/edit_flyout.ts +++ b/x-pack/test/functional/services/transform/edit_flyout.ts @@ -35,6 +35,12 @@ export function TransformEditFlyoutProvider({ getService }: FtrProviderContext) ); }, + // for now we expect this to be used only for opening the accordion + async openTransformEditAccordionAdvancedSettings() { + await testSubjects.click('transformEditAccordionAdvancedSettings'); + await testSubjects.existOrFail('transformEditAccordionAdvancedSettingsContent'); + }, + async setTransformEditFlyoutInputValue(input: string, value: string) { await testSubjects.setValue(`transformEditFlyout${input}Input`, value, { clearWithKeyboard: true, diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts index 67672cb54c21b..359be662b0216 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts.ts @@ -40,8 +40,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { return createdAlert; } - // FLAKY: https://github.com/elastic/kibana/issues/77401 - describe.skip('alerts', function () { + describe('alerts', function () { before(async () => { await pageObjects.common.navigateToApp('triggersActions'); await testSubjects.click('alertsTab'); @@ -385,9 +384,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('deleteAll'); await testSubjects.existOrFail('deleteIdsConfirmation'); await testSubjects.click('deleteIdsConfirmation > confirmModalConfirmButton'); - await testSubjects.missingOrFail('deleteIdsConfirmation', { timeout: 5000 }); + await testSubjects.missingOrFail('deleteIdsConfirmation'); - await pageObjects.common.closeToast(); + await retry.try(async () => { + const toastTitle = await pageObjects.common.closeToast(); + expect(toastTitle).to.eql('Deleted 10 alerts'); + }); await pageObjects.common.navigateToApp('triggersActions'); await pageObjects.triggersActionsUI.searchAlerts(namePrefix); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts index 86e355988da0b..151c837640228 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts @@ -17,6 +17,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const testSubjects = getService('testSubjects'); const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); const find = getService('find'); + const retry = getService('retry'); + const comboBox = getService('comboBox'); describe('Connectors', function () { before(async () => { @@ -76,7 +78,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); - await find.clickByCssSelector('[data-test-subj="saveEditedActionButton"]:not(disabled)'); + await find.clickByCssSelector( + '[data-test-subj="saveAndCloseEditedActionButton"]:not(disabled)' + ); const toastTitle = await pageObjects.common.closeToast(); expect(toastTitle).to.eql(`Updated '${updatedConnectorName}'`); @@ -92,6 +96,64 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ]); }); + it('should test a connector and display a successful result', async () => { + const connectorName = generateUniqueKey(); + const indexName = generateUniqueKey(); + await createIndexConnector(connectorName, indexName); + + await pageObjects.triggersActionsUI.searchConnectors(connectorName); + + const searchResultsBeforeEdit = await pageObjects.triggersActionsUI.getConnectorsList(); + expect(searchResultsBeforeEdit.length).to.eql(1); + + await find.clickByCssSelector('[data-test-subj="connectorsTableCell-name"] button'); + + await find.clickByCssSelector('[data-test-subj="testConnectorTab"]'); + + // test success + await testSubjects.setValue('documentsJsonEditor', '{ "key": "value" }'); + + await find.clickByCssSelector('[data-test-subj="executeActionButton"]:not(disabled)'); + + await retry.try(async () => { + await testSubjects.find('executionSuccessfulResult'); + }); + + await find.clickByCssSelector( + '[data-test-subj="cancelSaveEditedConnectorButton"]:not(disabled)' + ); + }); + + it('should test a connector and display a failure result', async () => { + const connectorName = generateUniqueKey(); + const indexName = generateUniqueKey(); + await createIndexConnector(connectorName, indexName); + + await pageObjects.triggersActionsUI.searchConnectors(connectorName); + + const searchResultsBeforeEdit = await pageObjects.triggersActionsUI.getConnectorsList(); + expect(searchResultsBeforeEdit.length).to.eql(1); + + await find.clickByCssSelector('[data-test-subj="connectorsTableCell-name"] button'); + + await find.clickByCssSelector('[data-test-subj="testConnectorTab"]'); + + await testSubjects.setValue('documentsJsonEditor', '{ "": "value" }'); + + await find.clickByCssSelector('[data-test-subj="executeActionButton"]:not(disabled)'); + + await retry.try(async () => { + const executionFailureResultCallout = await testSubjects.find('executionFailureResult'); + expect(await executionFailureResultCallout.getVisibleText()).to.match( + /error indexing documents/ + ); + }); + + await find.clickByCssSelector( + '[data-test-subj="cancelSaveEditedConnectorButton"]:not(disabled)' + ); + }); + it('should reset connector when canceling an edit', async () => { const connectorName = generateUniqueKey(); await createConnector(connectorName); @@ -193,7 +255,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await find.clickByCssSelector('[data-test-subj="connectorsTableCell-name"] button'); expect(await testSubjects.exists('preconfiguredBadge')).to.be(true); - expect(await testSubjects.exists('saveEditedActionButton')).to.be(false); + expect(await testSubjects.exists('saveAndCloseEditedActionButton')).to.be(false); }); }); @@ -209,4 +271,17 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await find.clickByCssSelector('[data-test-subj="saveNewActionButton"]:not(disabled)'); await pageObjects.common.closeToast(); } + + async function createIndexConnector(connectorName: string, indexName: string) { + await pageObjects.triggersActionsUI.clickCreateConnectorButton(); + + await testSubjects.click('.index-card'); + + await testSubjects.setValue('nameInput', connectorName); + + await comboBox.set('connectorIndexesComboBox', indexName); + + await find.clickByCssSelector('[data-test-subj="saveNewActionButton"]:not(disabled)'); + await pageObjects.common.closeToast(); + } }; diff --git a/x-pack/test/ingest_manager_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/ingest_manager_api_integration/apis/agent_policy/agent_policy.ts index 410b0fe093002..71e2bbc4e4c0a 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/agent_policy/agent_policy.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/agent_policy/agent_policy.ts @@ -82,7 +82,7 @@ export default function ({ getService }: FtrProviderContext) { }); }); - it('should return a 500 with invalid source policy', async () => { + it('should return a 404 with invalid source policy', async () => { await supertest .post(`/api/ingest_manager/agent_policies/INVALID_POLICY_ID/copy`) .set('kbn-xsrf', 'xxxx') @@ -90,7 +90,7 @@ export default function ({ getService }: FtrProviderContext) { name: 'Copied policy', description: '', }) - .expect(500); + .expect(404); }); it('should return a 400 with invalid payload', async () => { diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/bulk_upgrade.ts b/x-pack/test/ingest_manager_api_integration/apis/epm/bulk_upgrade.ts new file mode 100644 index 0000000000000..e377ea5a762f9 --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/bulk_upgrade.ts @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; +import { + BulkInstallPackageInfo, + BulkInstallPackagesResponse, + IBulkInstallPackageError, +} from '../../../../plugins/ingest_manager/common'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + + const deletePackage = async (pkgkey: string) => { + await supertest.delete(`/api/ingest_manager/epm/packages/${pkgkey}`).set('kbn-xsrf', 'xxxx'); + }; + + describe('bulk package upgrade api', async () => { + skipIfNoDockerRegistry(providerContext); + + describe('bulk package upgrade with a package already installed', async () => { + beforeEach(async () => { + await supertest + .post(`/api/ingest_manager/epm/packages/multiple_versions-0.1.0`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + }); + afterEach(async () => { + await deletePackage('multiple_versions-0.1.0'); + await deletePackage('multiple_versions-0.3.0'); + await deletePackage('overrides-0.1.0'); + }); + + it('should return 400 if no packages are requested for upgrade', async function () { + await supertest + .post(`/api/ingest_manager/epm/packages/_bulk`) + .set('kbn-xsrf', 'xxxx') + .expect(400); + }); + it('should return 200 and an array for upgrading a package', async function () { + const { body }: { body: BulkInstallPackagesResponse } = await supertest + .post(`/api/ingest_manager/epm/packages/_bulk`) + .set('kbn-xsrf', 'xxxx') + .send({ packages: ['multiple_versions'] }) + .expect(200); + expect(body.response.length).equal(1); + expect(body.response[0].name).equal('multiple_versions'); + const entry = body.response[0] as BulkInstallPackageInfo; + expect(entry.oldVersion).equal('0.1.0'); + expect(entry.newVersion).equal('0.3.0'); + }); + it('should return an error for packages that do not exist', async function () { + const { body }: { body: BulkInstallPackagesResponse } = await supertest + .post(`/api/ingest_manager/epm/packages/_bulk`) + .set('kbn-xsrf', 'xxxx') + .send({ packages: ['multiple_versions', 'blahblah'] }) + .expect(200); + expect(body.response.length).equal(2); + expect(body.response[0].name).equal('multiple_versions'); + const entry = body.response[0] as BulkInstallPackageInfo; + expect(entry.oldVersion).equal('0.1.0'); + expect(entry.newVersion).equal('0.3.0'); + + const err = body.response[1] as IBulkInstallPackageError; + expect(err.statusCode).equal(404); + expect(body.response[1].name).equal('blahblah'); + }); + it('should upgrade multiple packages', async function () { + const { body }: { body: BulkInstallPackagesResponse } = await supertest + .post(`/api/ingest_manager/epm/packages/_bulk`) + .set('kbn-xsrf', 'xxxx') + .send({ packages: ['multiple_versions', 'overrides'] }) + .expect(200); + expect(body.response.length).equal(2); + expect(body.response[0].name).equal('multiple_versions'); + let entry = body.response[0] as BulkInstallPackageInfo; + expect(entry.oldVersion).equal('0.1.0'); + expect(entry.newVersion).equal('0.3.0'); + + entry = body.response[1] as BulkInstallPackageInfo; + expect(entry.oldVersion).equal(null); + expect(entry.newVersion).equal('0.1.0'); + expect(entry.name).equal('overrides'); + }); + }); + + describe('bulk upgrade without package already installed', async () => { + afterEach(async () => { + await deletePackage('multiple_versions-0.3.0'); + }); + + it('should return 200 and an array for upgrading a package', async function () { + const { body }: { body: BulkInstallPackagesResponse } = await supertest + .post(`/api/ingest_manager/epm/packages/_bulk`) + .set('kbn-xsrf', 'xxxx') + .send({ packages: ['multiple_versions'] }) + .expect(200); + expect(body.response.length).equal(1); + expect(body.response[0].name).equal('multiple_versions'); + const entry = body.response[0] as BulkInstallPackageInfo; + expect(entry.oldVersion).equal(null); + expect(entry.newVersion).equal('0.3.0'); + }); + }); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/epm/index.js b/x-pack/test/ingest_manager_api_integration/apis/epm/index.js index 28743ee5f43c2..e509babc9828b 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/epm/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/epm/index.js @@ -16,6 +16,7 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./install_prerelease')); loadTestFile(require.resolve('./install_remove_assets')); loadTestFile(require.resolve('./install_update')); + loadTestFile(require.resolve('./bulk_upgrade')); loadTestFile(require.resolve('./update_assets')); loadTestFile(require.resolve('./data_stream')); loadTestFile(require.resolve('./package_install_complete')); diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/reassign.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/reassign.ts new file mode 100644 index 0000000000000..f3e24fab1dc2a --- /dev/null +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/reassign.ts @@ -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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../api_integration/ftr_provider_context'; +import { setupIngest } from './services'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + + describe('fleet_reassign_agent', () => { + setupIngest(providerContext); + before(async () => { + await esArchiver.loadIfNeeded('fleet/agents'); + }); + after(async () => { + await esArchiver.unload('fleet/agents'); + }); + + it('should allow to reassign single agent', async () => { + await supertest + .put(`/api/ingest_manager/fleet/agents/agent1/reassign`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: 'policy2', + }) + .expect(200); + const { body } = await supertest + .get(`/api/ingest_manager/fleet/agents/agent1`) + .set('kbn-xsrf', 'xxx'); + expect(body.item.policy_id).to.eql('policy2'); + }); + + it('should throw an error for invalid policy id for single reassign', async () => { + await supertest + .put(`/api/ingest_manager/fleet/agents/agent1/reassign`) + .set('kbn-xsrf', 'xxx') + .send({ + policy_id: 'INVALID_ID', + }) + .expect(404); + }); + + it('should allow to reassign multiple agents by id', async () => { + await supertest + .post(`/api/ingest_manager/fleet/agents/bulk_reassign`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: ['agent2', 'agent3'], + policy_id: 'policy2', + }) + .expect(200); + const [agent2data, agent3data] = await Promise.all([ + supertest.get(`/api/ingest_manager/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), + supertest.get(`/api/ingest_manager/fleet/agents/agent3`).set('kbn-xsrf', 'xxx'), + ]); + expect(agent2data.body.item.policy_id).to.eql('policy2'); + expect(agent3data.body.item.policy_id).to.eql('policy2'); + }); + + it('should allow to reassign multiple agents by kuery', async () => { + await supertest + .post(`/api/ingest_manager/fleet/agents/bulk_reassign`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: 'fleet-agents.active: true', + policy_id: 'policy2', + }) + .expect(200); + const { body } = await supertest + .get(`/api/ingest_manager/fleet/agents`) + .set('kbn-xsrf', 'xxx'); + expect(body.total).to.eql(4); + body.list.forEach((agent: any) => { + expect(agent.policy_id).to.eql('policy2'); + }); + }); + + it('should throw an error for invalid policy id for bulk reassign', async () => { + await supertest + .post(`/api/ingest_manager/fleet/agents/bulk_reassign`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: ['agent2', 'agent3'], + policy_id: 'INVALID_ID', + }) + .expect(404); + }); + }); +} diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/unenroll.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/unenroll.ts index d24e438fa13ea..ce5dfd7714ab2 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/unenroll.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/agents/unenroll.ts @@ -64,7 +64,7 @@ export default function (providerContext: FtrProviderContext) { await esArchiver.unload('fleet/agents'); }); - it('allow to unenroll using a list of ids', async () => { + it('should allow to unenroll single agent', async () => { await supertest .post(`/api/ingest_manager/fleet/agents/agent1/unenroll`) .set('kbn-xsrf', 'xxx') @@ -95,5 +95,39 @@ export default function (providerContext: FtrProviderContext) { expect(outputAPIKeys).length(1); expect(outputAPIKeys[0].invalidated).eql(true); }); + + it('should allow to unenroll multiple agents by id', async () => { + await supertest + .post(`/api/ingest_manager/fleet/agents/bulk_unenroll`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: ['agent2', 'agent3'], + }) + .expect(200); + const [agent2data, agent3data] = await Promise.all([ + supertest.get(`/api/ingest_manager/fleet/agents/agent2`).set('kbn-xsrf', 'xxx'), + supertest.get(`/api/ingest_manager/fleet/agents/agent3`).set('kbn-xsrf', 'xxx'), + ]); + expect(typeof agent2data.body.item.unenrollment_started_at).to.eql('string'); + expect(agent2data.body.item.active).to.eql(true); + expect(typeof agent3data.body.item.unenrollment_started_at).to.be('string'); + expect(agent2data.body.item.active).to.eql(true); + }); + + it('should allow to unenroll multiple agents by kuery', async () => { + await supertest + .post(`/api/ingest_manager/fleet/agents/bulk_unenroll`) + .set('kbn-xsrf', 'xxx') + .send({ + agents: 'fleet-agents.active: true', + force: true, + }) + .expect(200); + + const { body } = await supertest + .get(`/api/ingest_manager/fleet/agents`) + .set('kbn-xsrf', 'xxx'); + expect(body.total).to.eql(0); + }); }); } diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/enrollment_api_keys/crud.ts b/x-pack/test/ingest_manager_api_integration/apis/fleet/enrollment_api_keys/crud.ts index 6c5d552a51eb9..b3519e0ccc2a3 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/enrollment_api_keys/crud.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/enrollment_api_keys/crud.ts @@ -36,7 +36,7 @@ export default function (providerContext: FtrProviderContext) { .get(`/api/ingest_manager/fleet/enrollment-api-keys`) .expect(200); - expect(apiResponse.total).to.be(2); + expect(apiResponse.total).to.be(3); expect(apiResponse.list[0]).to.have.keys('id', 'api_key_id', 'name'); }); }); diff --git a/x-pack/test/ingest_manager_api_integration/apis/fleet/index.js b/x-pack/test/ingest_manager_api_integration/apis/fleet/index.js index 3a72fe6d9f12b..96b9ffd1b04c0 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/fleet/index.js +++ b/x-pack/test/ingest_manager_api_integration/apis/fleet/index.js @@ -18,5 +18,6 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./enrollment_api_keys/crud')); loadTestFile(require.resolve('./install')); loadTestFile(require.resolve('./agents/actions')); + loadTestFile(require.resolve('./agents/reassign')); }); } diff --git a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx index f3d1eb60bf1c0..d70d46fcbc015 100644 --- a/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx +++ b/x-pack/test/plugin_functional/plugins/resolver_test/public/applications/resolver_test/index.tsx @@ -61,12 +61,12 @@ const AppRoot = React.memo( storeFactory, ResolverWithoutProviders, mocks: { - dataAccessLayer: { noAncestorsTwoChildren }, + dataAccessLayer: { noAncestorsTwoChildrenWithRelatedEventsOnOrigin }, }, } = resolverPluginSetup; const dataAccessLayer: DataAccessLayer = useMemo( - () => noAncestorsTwoChildren().dataAccessLayer, - [noAncestorsTwoChildren] + () => noAncestorsTwoChildrenWithRelatedEventsOnOrigin().dataAccessLayer, + [noAncestorsTwoChildrenWithRelatedEventsOnOrigin] ); const store = useMemo(() => { diff --git a/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_cases.ts b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_cases.ts index b32950538f8e5..190b12e038b27 100644 --- a/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_cases.ts +++ b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_cases.ts @@ -4,30 +4,48 @@ * you may not use this file except in compliance with the Elastic License. */ -export const SAVED_OBJECT_TEST_CASES = Object.freeze({ +import { SPACES } from './spaces'; +import { TestCase } from './types'; + +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; +const EACH_SPACE = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]; + +type CommonTestCase = Omit & { originId?: string }; + +export const SAVED_OBJECT_TEST_CASES: Record = Object.freeze({ SINGLE_NAMESPACE_DEFAULT_SPACE: Object.freeze({ type: 'isolatedtype', id: 'defaultspace-isolatedtype-id', + expectedNamespaces: [DEFAULT_SPACE_ID], }), SINGLE_NAMESPACE_SPACE_1: Object.freeze({ type: 'isolatedtype', id: 'space1-isolatedtype-id', + expectedNamespaces: [SPACE_1_ID], }), SINGLE_NAMESPACE_SPACE_2: Object.freeze({ type: 'isolatedtype', id: 'space2-isolatedtype-id', + expectedNamespaces: [SPACE_2_ID], }), MULTI_NAMESPACE_DEFAULT_AND_SPACE_1: Object.freeze({ type: 'sharedtype', id: 'default_and_space_1', + expectedNamespaces: [DEFAULT_SPACE_ID, SPACE_1_ID], }), MULTI_NAMESPACE_ONLY_SPACE_1: Object.freeze({ type: 'sharedtype', id: 'only_space_1', + expectedNamespaces: [SPACE_1_ID], }), MULTI_NAMESPACE_ONLY_SPACE_2: Object.freeze({ type: 'sharedtype', id: 'only_space_2', + expectedNamespaces: [SPACE_2_ID], }), NAMESPACE_AGNOSTIC: Object.freeze({ type: 'globaltype', @@ -38,3 +56,37 @@ export const SAVED_OBJECT_TEST_CASES = Object.freeze({ id: 'any', }), }); + +/** + * These objects exist in the test data for all saved object test suites, but they are only used to test various conflict scenarios. + */ +export const CONFLICT_TEST_CASES: Record = Object.freeze({ + CONFLICT_1_OBJ: Object.freeze({ + type: 'sharedtype', + id: 'conflict_1', + expectedNamespaces: EACH_SPACE, + }), + CONFLICT_2A_OBJ: Object.freeze({ + type: 'sharedtype', + id: 'conflict_2a', + originId: 'conflict_2', + expectedNamespaces: EACH_SPACE, + }), + CONFLICT_2B_OBJ: Object.freeze({ + type: 'sharedtype', + id: 'conflict_2b', + originId: 'conflict_2', + expectedNamespaces: EACH_SPACE, + }), + CONFLICT_3_OBJ: Object.freeze({ + type: 'sharedtype', + id: 'conflict_3', + expectedNamespaces: EACH_SPACE, + }), + CONFLICT_4A_OBJ: Object.freeze({ + type: 'sharedtype', + id: 'conflict_4a', + originId: 'conflict_4', + expectedNamespaces: EACH_SPACE, + }), +}); diff --git a/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts index 595986c08efc1..9d4b5e80e9c3d 100644 --- a/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts +++ b/x-pack/test/saved_object_api_integration/common/lib/saved_object_test_utils.ts @@ -6,7 +6,6 @@ import expect from '@kbn/expect'; import { SavedObjectsErrorHelpers } from '../../../../../src/core/server'; -import { SAVED_OBJECT_TEST_CASES as CASES } from './saved_object_test_cases'; import { SPACES } from './spaces'; import { AUTHENTICATION } from './authentication'; import { TestCase, TestUser, ExpectResponseBody } from './types'; @@ -73,6 +72,28 @@ export const getTestTitle = ( return `${list.join(' and ')}`; }; +export const isUserAuthorizedAtSpace = (user: TestUser | undefined, namespace: string) => + !user || user.authorizedAtSpaces.includes('*') || user.authorizedAtSpaces.includes(namespace); + +export const getRedactedNamespaces = ( + user: TestUser | undefined, + namespaces: string[] | undefined +) => namespaces?.map((x) => (isUserAuthorizedAtSpace(user, x) ? x : '?')).sort(namespaceComparator); +function namespaceComparator(a: string, b: string) { + // namespaces get sorted so that they're all in alphabetical order, and unknown ones appear at the end + // this is to prevent information disclosure + if (a === '?' && b !== '?') { + return 1; + } else if (b === '?' && a !== '?') { + return -1; + } else if (a > b) { + return 1; + } else if (a < b) { + return -1; + } + return 0; +} + export const testCaseFailures = { // test suites need explicit return types for number primitives fail400: (condition?: boolean): { failure?: 400 } => @@ -150,7 +171,7 @@ export const expectResponses = { } }, /** - * Additional assertions that we use in `bulk_create` and `create` to ensure that + * Additional assertions that we use in `import` and `resolve_import_errors` to ensure that * newly-created (or overwritten) objects don't have unexpected properties */ successCreated: async (es: any, spaceId: string, type: string, id: string) => { @@ -161,26 +182,6 @@ export const expectResponses = { id: `${expectedSpacePrefix}${type}:${id}`, index: '.kibana', }); - const { namespace: actualNamespace, namespaces: actualNamespaces } = savedObject._source; - if (isNamespaceUndefined) { - expect(actualNamespace).to.eql(undefined); - } else { - expect(actualNamespace).to.eql(spaceId); - } - if (isMultiNamespace(type)) { - if (['conflict_1', 'conflict_2a', 'conflict_2b', 'conflict_3', 'conflict_4a'].includes(id)) { - expect(actualNamespaces).to.eql([DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]); - } else if (id === CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1.id) { - expect(actualNamespaces).to.eql([DEFAULT_SPACE_ID, SPACE_1_ID]); - } else if (id === CASES.MULTI_NAMESPACE_ONLY_SPACE_1.id) { - expect(actualNamespaces).to.eql([SPACE_1_ID]); - } else if (id === CASES.MULTI_NAMESPACE_ONLY_SPACE_2.id) { - expect(actualNamespaces).to.eql([SPACE_2_ID]); - } else { - // newly created in this space - expect(actualNamespaces).to.eql([spaceId]); - } - } return savedObject; }, }; diff --git a/x-pack/test/saved_object_api_integration/common/lib/types.ts b/x-pack/test/saved_object_api_integration/common/lib/types.ts index 56e6a992b6b62..b52a84f352999 100644 --- a/x-pack/test/saved_object_api_integration/common/lib/types.ts +++ b/x-pack/test/saved_object_api_integration/common/lib/types.ts @@ -21,6 +21,7 @@ export interface TestSuite { export interface TestCase { type: string; id: string; + expectedNamespaces?: string[]; failure?: 400 | 403 | 404 | 409; } diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts index e3163ef77d427..b1608946b8e62 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts @@ -14,8 +14,9 @@ import { expectResponses, getUrlPrefix, getTestTitle, + getRedactedNamespaces, } from '../lib/saved_object_test_utils'; -import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite, TestUser } from '../lib/types'; export interface BulkCreateTestDefinition extends TestDefinition { request: Array<{ type: string; id: string }>; @@ -33,7 +34,7 @@ const NEW_ATTRIBUTE_VAL = `New attribute value ${Date.now()}`; const NEW_SINGLE_NAMESPACE_OBJ = Object.freeze({ type: 'dashboard', id: 'new-dashboard-id' }); const NEW_MULTI_NAMESPACE_OBJ = Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' }); const NEW_NAMESPACE_AGNOSTIC_OBJ = Object.freeze({ type: 'globaltype', id: 'new-globaltype-id' }); -export const TEST_CASES = Object.freeze({ +export const TEST_CASES: Record = Object.freeze({ ...CASES, NEW_SINGLE_NAMESPACE_OBJ, NEW_MULTI_NAMESPACE_OBJ, @@ -45,7 +46,7 @@ export function bulkCreateTestSuiteFactory(es: any, esArchiver: any, supertest: const expectResponseBody = ( testCases: BulkCreateTestCase | BulkCreateTestCase[], statusCode: 200 | 403, - spaceId = SPACES.DEFAULT.spaceId + user?: TestUser ): ExpectResponseBody => async (response: Record) => { const testCaseArray = Array.isArray(testCases) ? testCases : [testCases]; if (statusCode === 403) { @@ -70,7 +71,8 @@ export function bulkCreateTestSuiteFactory(es: any, esArchiver: any, supertest: await expectResponses.permitted(object, testCase); if (!testCase.failure) { expect(object.attributes[NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); - await expectResponses.successCreated(es, spaceId, object.type, object.id); + const redactedNamespaces = getRedactedNamespaces(user, testCase.expectedNamespaces); + expect(object.namespaces).to.eql(redactedNamespaces); } } } @@ -81,6 +83,7 @@ export function bulkCreateTestSuiteFactory(es: any, esArchiver: any, supertest: overwrite: boolean, options?: { spaceId?: string; + user?: TestUser; singleRequest?: boolean; responseBodyOverride?: ExpectResponseBody; } @@ -95,8 +98,7 @@ export function bulkCreateTestSuiteFactory(es: any, esArchiver: any, supertest: request: [createRequest(x)], responseStatusCode, responseBody: - options?.responseBodyOverride || - expectResponseBody(x, responseStatusCode, options?.spaceId), + options?.responseBodyOverride || expectResponseBody(x, responseStatusCode, options?.user), overwrite, })); } @@ -108,7 +110,7 @@ export function bulkCreateTestSuiteFactory(es: any, esArchiver: any, supertest: responseStatusCode, responseBody: options?.responseBodyOverride || - expectResponseBody(cases, responseStatusCode, options?.spaceId), + expectResponseBody(cases, responseStatusCode, options?.user), overwrite, }, ]; diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts index 8de54fe499c07..71ece1265347c 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts @@ -25,7 +25,10 @@ export interface BulkGetTestCase extends TestCase { } const DOES_NOT_EXIST = Object.freeze({ type: 'dashboard', id: 'does-not-exist' }); -export const TEST_CASES = Object.freeze({ ...CASES, DOES_NOT_EXIST }); +export const TEST_CASES: Record = Object.freeze({ + ...CASES, + DOES_NOT_EXIST, +}); export function bulkGetTestSuiteFactory(esArchiver: any, supertest: SuperTest) { const expectForbidden = expectResponses.forbiddenTypes('bulk_get'); diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_update.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_update.ts index 2e3c55f029d29..c3020b2da3219 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/bulk_update.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_update.ts @@ -24,7 +24,10 @@ const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, const NEW_ATTRIBUTE_VAL = `Updated attribute value ${Date.now()}`; const DOES_NOT_EXIST = Object.freeze({ type: 'dashboard', id: 'does-not-exist' }); -export const TEST_CASES = Object.freeze({ ...CASES, DOES_NOT_EXIST }); +export const TEST_CASES: Record = Object.freeze({ + ...CASES, + DOES_NOT_EXIST, +}); const createRequest = ({ type, id, namespace }: BulkUpdateTestCase) => ({ type, diff --git a/x-pack/test/saved_object_api_integration/common/suites/create.ts b/x-pack/test/saved_object_api_integration/common/suites/create.ts index 2a5ab696c4f53..7e28d5ed9ed94 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/create.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/create.ts @@ -13,8 +13,9 @@ import { expectResponses, getUrlPrefix, getTestTitle, + getRedactedNamespaces, } from '../lib/saved_object_test_utils'; -import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; +import { ExpectResponseBody, TestCase, TestDefinition, TestSuite, TestUser } from '../lib/types'; export interface CreateTestDefinition extends TestDefinition { request: { type: string; id: string }; @@ -33,7 +34,7 @@ const NEW_ATTRIBUTE_VAL = `New attribute value ${Date.now()}`; const NEW_SINGLE_NAMESPACE_OBJ = Object.freeze({ type: 'dashboard', id: '' }); const NEW_MULTI_NAMESPACE_OBJ = Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' }); const NEW_NAMESPACE_AGNOSTIC_OBJ = Object.freeze({ type: 'globaltype', id: 'new-globaltype-id' }); -export const TEST_CASES = Object.freeze({ +export const TEST_CASES: Record = Object.freeze({ ...CASES, NEW_SINGLE_NAMESPACE_OBJ, NEW_MULTI_NAMESPACE_OBJ, @@ -44,7 +45,7 @@ export function createTestSuiteFactory(es: any, esArchiver: any, supertest: Supe const expectForbidden = expectResponses.forbiddenTypes('create'); const expectResponseBody = ( testCase: CreateTestCase, - spaceId = SPACES.DEFAULT.spaceId + user?: TestUser ): ExpectResponseBody => async (response: Record) => { if (testCase.failure === 403) { await expectForbidden(testCase.type)(response); @@ -54,7 +55,8 @@ export function createTestSuiteFactory(es: any, esArchiver: any, supertest: Supe await expectResponses.permitted(object, testCase); if (!testCase.failure) { expect(object.attributes[NEW_ATTRIBUTE_KEY]).to.eql(NEW_ATTRIBUTE_VAL); - await expectResponses.successCreated(es, spaceId, object.type, object.id); + const redactedNamespaces = getRedactedNamespaces(user, testCase.expectedNamespaces); + expect(object.namespaces).to.eql(redactedNamespaces); } } }; @@ -64,6 +66,7 @@ export function createTestSuiteFactory(es: any, esArchiver: any, supertest: Supe overwrite: boolean, options?: { spaceId?: string; + user?: TestUser; responseBodyOverride?: ExpectResponseBody; } ): CreateTestDefinition[] => { @@ -76,7 +79,7 @@ export function createTestSuiteFactory(es: any, esArchiver: any, supertest: Supe title: getTestTitle(x), responseStatusCode: x.failure ?? 200, request: createRequest(x), - responseBody: options?.responseBodyOverride || expectResponseBody(x, options?.spaceId), + responseBody: options?.responseBodyOverride || expectResponseBody(x, options?.user), overwrite, })); }; diff --git a/x-pack/test/saved_object_api_integration/common/suites/delete.ts b/x-pack/test/saved_object_api_integration/common/suites/delete.ts index 3179b1b0c9ac5..228e7977f99ac 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/delete.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/delete.ts @@ -25,7 +25,10 @@ export interface DeleteTestCase extends TestCase { } const DOES_NOT_EXIST = Object.freeze({ type: 'dashboard', id: 'does-not-exist' }); -export const TEST_CASES = Object.freeze({ ...CASES, DOES_NOT_EXIST }); +export const TEST_CASES: Record = Object.freeze({ + ...CASES, + DOES_NOT_EXIST, +}); export function deleteTestSuiteFactory(esArchiver: any, supertest: SuperTest) { const expectForbidden = expectResponses.forbiddenTypes('delete'); diff --git a/x-pack/test/saved_object_api_integration/common/suites/export.ts b/x-pack/test/saved_object_api_integration/common/suites/export.ts index 4a8eff1fb380c..4eb967a952c60 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/export.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/export.ts @@ -30,7 +30,10 @@ export interface ExportTestCase { type: string; id?: string; successResult?: SuccessResult | SuccessResult[]; - failure?: 400 | 403; + failure?: { + statusCode: 200 | 400 | 403; // if the user searches for only types they are not authorized for, they will get an empty 200 result + reason: 'unauthorized' | 'bad_request'; + }; } // additional sharedtype objects that exist but do not have common test cases defined @@ -90,41 +93,45 @@ export const getTestCases = (spaceId?: string): { [key: string]: ExportTestCase type: 'globaltype', successResult: CASES.NAMESPACE_AGNOSTIC, }, - hiddenObject: { title: 'hidden object', ...CASES.HIDDEN, failure: 400 }, - hiddenType: { title: 'hidden type', type: 'hiddentype', failure: 400 }, + hiddenObject: { + title: 'hidden object', + ...CASES.HIDDEN, + failure: { statusCode: 400, reason: 'bad_request' }, + }, + hiddenType: { + title: 'hidden type', + type: 'hiddentype', + failure: { statusCode: 400, reason: 'bad_request' }, + }, }); export const createRequest = ({ type, id }: ExportTestCase) => id ? { objects: [{ type, id }] } : { type }; -const getTestTitle = ({ failure, title }: ExportTestCase) => { - let description = 'success'; - if (failure === 400) { - description = 'bad request'; - } else if (failure === 403) { - description = 'forbidden'; - } - return `${description} ["${title}"]`; -}; +const getTestTitle = ({ failure, title }: ExportTestCase) => + `${failure?.reason || 'success'} ["${title}"]`; + +const EMPTY_RESULT = { exportedCount: 0, missingRefCount: 0, missingReferences: [] }; export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest) { const expectForbiddenBulkGet = expectResponses.forbiddenTypes('bulk_get'); - const expectForbiddenFind = expectResponses.forbiddenTypes('find'); const expectResponseBody = (testCase: ExportTestCase): ExpectResponseBody => async ( response: Record ) => { const { type, id, successResult = { type, id } as SuccessResult, failure } = testCase; - if (failure === 403) { - // In export only, the API uses "bulk_get" or "find" depending on the parameters it receives. - // The best that could be done here is to have an if statement to ensure at least one of the - // two errors has been thrown. - if (id) { + if (failure?.reason === 'unauthorized') { + // In export only, the API uses "bulkGet" or "find" depending on the parameters it receives. + if (failure.statusCode === 403) { + // "bulkGet" was unauthorized, which returns a forbidden error await expectForbiddenBulkGet(type)(response); + } else if (failure.statusCode === 200) { + // "find" was unauthorized, which returns an empty result + expect(response.body).not.to.have.property('error'); + expect(response.text).to.equal(JSON.stringify(EMPTY_RESULT)); } else { - await expectForbiddenFind(type)(response); + throw new Error(`Unexpected failure status code: ${failure.statusCode}`); } - } else if (failure === 400) { - // 400 + } else if (failure?.reason === 'bad_request') { expect(response.body.error).to.eql('Bad Request'); - expect(response.body.statusCode).to.eql(failure); + expect(response.body.statusCode).to.eql(failure.statusCode); if (id) { expect(response.body.message).to.eql( `Trying to export object(s) with non-exportable types: ${type}:${id}` @@ -132,6 +139,8 @@ export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest { let cases = Array.isArray(testCases) ? testCases : [testCases]; - if (forbidden) { + if (failure) { // override the expected result in each test case - cases = cases.map((x) => ({ ...x, failure: 403 })); + cases = cases.map((x) => ({ ...x, failure })); } return cases.map((x) => ({ title: getTestTitle(x), - responseStatusCode: x.failure ?? 200, + responseStatusCode: x.failure?.statusCode ?? 200, request: createRequest(x), responseBody: options?.responseBodyOverride || expectResponseBody(x), })); diff --git a/x-pack/test/saved_object_api_integration/common/suites/find.ts b/x-pack/test/saved_object_api_integration/common/suites/find.ts index bab4a4d88534a..381306f810122 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/find.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/find.ts @@ -7,10 +7,13 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; import querystring from 'querystring'; -import { Assign } from '@kbn/utility-types'; -import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; +import { SAVED_OBJECT_TEST_CASES, CONFLICT_TEST_CASES } from '../lib/saved_object_test_cases'; import { SPACES } from '../lib/spaces'; -import { expectResponses, getUrlPrefix } from '../lib/saved_object_test_utils'; +import { + getUrlPrefix, + isUserAuthorizedAtSpace, + getRedactedNamespaces, +} from '../lib/saved_object_test_utils'; import { ExpectResponseBody, TestCase, TestDefinition, TestSuite, TestUser } from '../lib/types'; const { @@ -22,80 +25,34 @@ export interface FindTestDefinition extends TestDefinition { } export type FindTestSuite = TestSuite; -type FindSavedObjectCase = Assign; - export interface FindTestCase { title: string; query: string; successResult?: { - savedObjects?: FindSavedObjectCase | FindSavedObjectCase[]; + savedObjects?: TestCase | TestCase[]; page?: number; perPage?: number; total?: number; }; failure?: { - statusCode: 400 | 403; - reason: - | 'forbidden_types' - | 'forbidden_namespaces' - | 'cross_namespace_not_permitted' - | 'bad_request'; + statusCode: 200 | 400; // if the user searches for types and/or namespaces they are not authorized for, they will get a 200 result with those types/namespaces omitted + reason: 'unauthorized' | 'cross_namespace_not_permitted' | 'bad_request'; }; } -// additional sharedtype objects that exist but do not have common test cases defined -const CONFLICT_1_OBJ = Object.freeze({ - type: 'sharedtype', - id: 'conflict_1', - namespaces: ['default', 'space_1', 'space_2'], -}); -const CONFLICT_2A_OBJ = Object.freeze({ - type: 'sharedtype', - id: 'conflict_2a', - originId: 'conflict_2', - namespaces: ['default', 'space_1', 'space_2'], -}); -const CONFLICT_2B_OBJ = Object.freeze({ - type: 'sharedtype', - id: 'conflict_2b', - originId: 'conflict_2', - namespaces: ['default', 'space_1', 'space_2'], -}); -const CONFLICT_3_OBJ = Object.freeze({ - type: 'sharedtype', - id: 'conflict_3', - namespaces: ['default', 'space_1', 'space_2'], -}); -const CONFLICT_4A_OBJ = Object.freeze({ - type: 'sharedtype', - id: 'conflict_4a', - originId: 'conflict_4', - namespaces: ['default', 'space_1', 'space_2'], -}); - const TEST_CASES = [ - { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, namespaces: ['default'] }, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, namespaces: ['space_1'] }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, namespaces: ['space_2'] }, - { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, namespaces: ['default', 'space_1'] }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, namespaces: ['space_1'] }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, namespaces: ['space_2'] }, - { ...CASES.NAMESPACE_AGNOSTIC, namespaces: undefined }, - { ...CASES.HIDDEN, namespaces: undefined }, + ...Object.values(SAVED_OBJECT_TEST_CASES), + ...Object.values(CONFLICT_TEST_CASES), ]; -expect(TEST_CASES.length).to.eql( - Object.values(CASES).length, - 'Unhandled test cases in `find` suite' -); - export const getTestCases = ( { currentSpace, crossSpaceSearch }: { currentSpace?: string; crossSpaceSearch?: string[] } = { currentSpace: undefined, crossSpaceSearch: undefined, } ) => { - const crossSpaceIds = crossSpaceSearch?.filter((s) => s !== (currentSpace ?? 'default')) ?? []; + const crossSpaceIds = + crossSpaceSearch?.filter((s) => s !== (currentSpace ?? DEFAULT_SPACE_ID)) ?? []; // intentionally exclude the current space const isCrossSpaceSearch = crossSpaceIds.length > 0; const isWildcardSearch = crossSpaceIds.includes('*'); @@ -104,7 +61,7 @@ export const getTestCases = ( : ''; const buildTitle = (title: string) => - crossSpaceSearch ? `${title} (cross-space ${isWildcardSearch ? 'with wildcard' : ''})` : title; + crossSpaceSearch ? `${title} (cross-space${isWildcardSearch ? ' with wildcard' : ''})` : title; type CasePredicate = (testCase: TestCase) => boolean; const getExpectedSavedObjects = (predicate: CasePredicate) => { @@ -117,13 +74,16 @@ export const getTestCases = ( return TEST_CASES.filter((t) => { const hasOtherNamespaces = - Array.isArray(t.namespaces) && - t.namespaces!.some((ns) => ns !== (currentSpace ?? 'default')); + !t.expectedNamespaces || // namespace-agnostic types do not have an expectedNamespaces field + t.expectedNamespaces.some((ns) => ns !== (currentSpace ?? DEFAULT_SPACE_ID)); return hasOtherNamespaces && predicate(t); }); } return TEST_CASES.filter( - (t) => (!t.namespaces || t.namespaces.includes(currentSpace ?? 'default')) && predicate(t) + (t) => + (!t.expectedNamespaces || + t.expectedNamespaces.includes(currentSpace ?? DEFAULT_SPACE_ID)) && + predicate(t) ); }; @@ -140,19 +100,13 @@ export const getTestCases = ( query: `type=sharedtype&fields=title${namespacesQueryParam}`, successResult: { // expected depends on which spaces the user is authorized against... - savedObjects: getExpectedSavedObjects((t) => t.type === 'sharedtype').concat( - CONFLICT_1_OBJ, - CONFLICT_2A_OBJ, - CONFLICT_2B_OBJ, - CONFLICT_3_OBJ, - CONFLICT_4A_OBJ - ), + savedObjects: getExpectedSavedObjects((t) => t.type === 'sharedtype'), }, } as FindTestCase, namespaceAgnosticType: { title: buildTitle('find namespace-agnostic type'), query: `type=globaltype&fields=title${namespacesQueryParam}`, - successResult: { savedObjects: CASES.NAMESPACE_AGNOSTIC }, + successResult: { savedObjects: SAVED_OBJECT_TEST_CASES.NAMESPACE_AGNOSTIC }, } as FindTestCase, hiddenType: { title: buildTitle('find hidden type'), @@ -162,6 +116,15 @@ export const getTestCases = ( title: buildTitle('find unknown type'), query: `type=wigwags${namespacesQueryParam}`, } as FindTestCase, + eachType: { + title: buildTitle('find each type'), + query: `type=isolatedtype&type=sharedtype&type=globaltype&type=hiddentype&type=wigwags${namespacesQueryParam}`, + successResult: { + savedObjects: getExpectedSavedObjects((t) => + ['isolatedtype', 'sharedtype', 'globaltype'].includes(t.type) + ), + }, + } as FindTestCase, pageBeyondTotal: { title: buildTitle('find page beyond total'), query: `type=isolatedtype&page=100&per_page=100${namespacesQueryParam}`, @@ -179,7 +142,7 @@ export const getTestCases = ( filterWithNamespaceAgnosticType: { title: buildTitle('filter with namespace-agnostic type'), query: `type=globaltype&filter=globaltype.attributes.title:*global*${namespacesQueryParam}`, - successResult: { savedObjects: CASES.NAMESPACE_AGNOSTIC }, + successResult: { savedObjects: SAVED_OBJECT_TEST_CASES.NAMESPACE_AGNOSTIC }, } as FindTestCase, filterWithHiddenType: { title: buildTitle('filter with hidden type'), @@ -200,49 +163,48 @@ export const getTestCases = ( }; }; +function objectComparator(a: { id: string }, b: { id: string }) { + return a.id > b.id ? 1 : a.id < b.id ? -1 : 0; +} + export const createRequest = ({ query }: FindTestCase) => ({ query }); -const getTestTitle = ({ failure, title }: FindTestCase) => { - let description = 'success'; - if (failure?.statusCode === 400) { - description = 'bad request'; - } else if (failure?.statusCode === 403) { - description = 'forbidden'; - } - return `${description} ["${title}"]`; -}; +const getTestTitle = ({ failure, title }: FindTestCase) => + `${failure?.reason || 'success'} ["${title}"]`; export function findTestSuiteFactory(esArchiver: any, supertest: SuperTest) { - const expectForbiddenTypes = expectResponses.forbiddenTypes('find'); - const expectForbiddeNamespaces = expectResponses.forbiddenSpaces; const expectResponseBody = ( testCase: FindTestCase, user?: TestUser ): ExpectResponseBody => async (response: Record) => { const { failure, successResult = {}, query } = testCase; const parsedQuery = querystring.parse(query); - if (failure?.statusCode === 403) { - if (failure?.reason === 'forbidden_types') { - const type = parsedQuery.type; - await expectForbiddenTypes(type)(response); - } else if (failure?.reason === 'forbidden_namespaces') { - await expectForbiddeNamespaces(response); + if (failure?.statusCode === 200) { + if (failure?.reason === 'unauthorized') { + // if the user is completely unauthorized, they will receive an empty response body + const expected = { + page: parsedQuery.page || 1, + per_page: parsedQuery.per_page || 20, + total: 0, + saved_objects: [], + }; + expect(response.body).to.eql(expected); } else { - throw new Error(`Unexpected failure reason: ${failure?.reason}`); + throw new Error(`Unexpected failure reason: ${failure.reason}`); } } else if (failure?.statusCode === 400) { - if (failure?.reason === 'bad_request') { + if (failure.reason === 'bad_request') { const type = (parsedQuery.filter as string).split('.')[0]; expect(response.body.error).to.eql('Bad Request'); - expect(response.body.statusCode).to.eql(failure?.statusCode); + expect(response.body.statusCode).to.eql(failure.statusCode); expect(response.body.message).to.eql(`This type ${type} is not allowed: Bad Request`); - } else if (failure?.reason === 'cross_namespace_not_permitted') { + } else if (failure.reason === 'cross_namespace_not_permitted') { expect(response.body.error).to.eql('Bad Request'); - expect(response.body.statusCode).to.eql(failure?.statusCode); + expect(response.body.statusCode).to.eql(failure.statusCode); expect(response.body.message).to.eql( `_find across namespaces is not permitted when the Spaces plugin is disabled.: Bad Request` ); } else { - throw new Error(`Unexpected failure reason: ${failure?.reason}`); + throw new Error(`Unexpected failure reason: ${failure.reason}`); } } else { // 2xx @@ -251,11 +213,8 @@ export function findTestSuiteFactory(esArchiver: any, supertest: SuperTest) const savedObjectsArray = Array.isArray(savedObjects) ? savedObjects : [savedObjects]; const authorizedSavedObjects = savedObjectsArray.filter( (so) => - !user || - !so.namespaces || - so.namespaces.some( - (ns) => user.authorizedAtSpaces.includes(ns) || user.authorizedAtSpaces.includes('*') - ) + !so.expectedNamespaces || + so.expectedNamespaces.some((x) => isUserAuthorizedAtSpace(user, x)) ); expect(response.body.page).to.eql(page); expect(response.body.per_page).to.eql(perPage); @@ -265,16 +224,17 @@ export function findTestSuiteFactory(esArchiver: any, supertest: SuperTest) expect(response.body.total).to.eql(total || authorizedSavedObjects.length); } - authorizedSavedObjects.sort((s1, s2) => (s1.id < s2.id ? -1 : 1)); - response.body.saved_objects.sort((s1: any, s2: any) => (s1.id < s2.id ? -1 : 1)); + authorizedSavedObjects.sort(objectComparator); + response.body.saved_objects.sort(objectComparator); for (let i = 0; i < authorizedSavedObjects.length; i++) { const object = response.body.saved_objects[i]; - const { type: expectedType, id: expectedId } = authorizedSavedObjects[i]; - expect(object.type).to.eql(expectedType); - expect(object.id).to.eql(expectedId); + const expected = authorizedSavedObjects[i]; + const expectedNamespaces = getRedactedNamespaces(user, expected.expectedNamespaces); + expect(object.type).to.eql(expected.type); + expect(object.id).to.eql(expected.id); expect(object.updated_at).to.match(/^[\d-]{10}T[\d:\.]{12}Z$/); - expect(object.namespaces).to.eql(object.namespaces); + expect(object.namespaces).to.eql(expectedNamespaces); // don't test attributes, version, or references } } diff --git a/x-pack/test/saved_object_api_integration/common/suites/get.ts b/x-pack/test/saved_object_api_integration/common/suites/get.ts index fb03cd548d41a..8d8938b5ee79f 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/get.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/get.ts @@ -21,7 +21,7 @@ export type GetTestSuite = TestSuite; export type GetTestCase = TestCase; const DOES_NOT_EXIST = Object.freeze({ type: 'dashboard', id: 'does-not-exist' }); -export const TEST_CASES = Object.freeze({ ...CASES, DOES_NOT_EXIST }); +export const TEST_CASES: Record = Object.freeze({ ...CASES, DOES_NOT_EXIST }); export function getTestSuiteFactory(esArchiver: any, supertest: SuperTest) { const expectForbidden = expectResponses.forbiddenTypes('get'); diff --git a/x-pack/test/saved_object_api_integration/common/suites/import.ts b/x-pack/test/saved_object_api_integration/common/suites/import.ts index 5036d7b200881..b0d0b4f8a815a 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/import.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/import.ts @@ -36,7 +36,7 @@ const NEW_ATTRIBUTE_VAL = `New attribute value ${Date.now()}`; // * id: conflict_4a, originId: conflict_4 // using the seven conflict test case objects below, we can exercise various permutations of exact/inexact/ambiguous conflict scenarios const CID = 'conflict_'; -export const TEST_CASES = Object.freeze({ +export const TEST_CASES: Record = Object.freeze({ ...CASES, CONFLICT_1_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}1` }), CONFLICT_1A_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}1a`, originId: `${CID}1` }), diff --git a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts index 6d294aed9b4de..02fa614ac2b55 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts @@ -37,7 +37,7 @@ const NEW_ATTRIBUTE_VAL = `New attribute value ${Date.now()}`; // * id: conflict_3 // * id: conflict_4a, originId: conflict_4 // using the five conflict test case objects below, we can exercise various permutations of exact/inexact/ambiguous conflict scenarios -export const TEST_CASES = Object.freeze({ +export const TEST_CASES: Record = Object.freeze({ ...CASES, CONFLICT_1A_OBJ: Object.freeze({ type: 'sharedtype', diff --git a/x-pack/test/saved_object_api_integration/common/suites/update.ts b/x-pack/test/saved_object_api_integration/common/suites/update.ts index 82f4699babf46..19921a82b2eb4 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/update.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/update.ts @@ -28,7 +28,10 @@ const NEW_ATTRIBUTE_KEY = 'title'; // all type mappings include this attribute, const NEW_ATTRIBUTE_VAL = `Updated attribute value ${Date.now()}`; const DOES_NOT_EXIST = Object.freeze({ type: 'dashboard', id: 'does-not-exist' }); -export const TEST_CASES = Object.freeze({ ...CASES, DOES_NOT_EXIST }); +export const TEST_CASES: Record = Object.freeze({ + ...CASES, + DOES_NOT_EXIST, +}); export function updateTestSuiteFactory(esArchiver: any, supertest: SuperTest) { const expectForbidden = expectResponses.forbiddenTypes('update'); diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts index 0cc5969e2b7ab..93ae439d01166 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_create.ts @@ -26,13 +26,23 @@ const unresolvableConflict = (condition?: boolean) => const createTestCases = (overwrite: boolean, spaceId: string) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect // to receive an error; otherwise, we expect to receive a success result + const expectedNamespaces = [spaceId]; // newly created objects should have this `namespaces` array in their return value const normalTypes = [ { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), + expectedNamespaces, + }, + { + ...CASES.SINGLE_NAMESPACE_SPACE_1, + ...fail409(!overwrite && spaceId === SPACE_1_ID), + expectedNamespaces, + }, + { + ...CASES.SINGLE_NAMESPACE_SPACE_2, + ...fail409(!overwrite && spaceId === SPACE_2_ID), + expectedNamespaces, }, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), @@ -49,8 +59,8 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { ...unresolvableConflict(spaceId !== SPACE_2_ID), }, { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, - CASES.NEW_SINGLE_NAMESPACE_OBJ, - CASES.NEW_MULTI_NAMESPACE_OBJ, + { ...CASES.NEW_SINGLE_NAMESPACE_OBJ, expectedNamespaces }, + { ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces }, CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, ]; const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; @@ -68,22 +78,28 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = (overwrite: boolean, spaceId: string) => { + const createTests = (overwrite: boolean, spaceId: string, user: TestUser) => { const { normalTypes, hiddenType, allTypes } = createTestCases(overwrite, spaceId); // use singleRequest to reduce execution time and/or test combined cases return { - unauthorized: createTestDefinitions(allTypes, true, overwrite, { spaceId }), + unauthorized: createTestDefinitions(allTypes, true, overwrite, { spaceId, user }), authorized: [ - createTestDefinitions(normalTypes, false, overwrite, { spaceId, singleRequest: true }), - createTestDefinitions(hiddenType, true, overwrite, { spaceId }), + createTestDefinitions(normalTypes, false, overwrite, { + spaceId, + user, + singleRequest: true, + }), + createTestDefinitions(hiddenType, true, overwrite, { spaceId, user }), createTestDefinitions(allTypes, true, overwrite, { spaceId, + user, singleRequest: true, responseBodyOverride: expectForbidden(['hiddentype']), }), ].flat(), superuser: createTestDefinitions(allTypes, false, overwrite, { spaceId, + user, singleRequest: true, }), }; @@ -93,7 +109,6 @@ export default function ({ getService }: FtrProviderContext) { getTestScenarios([false, true]).securityAndSpaces.forEach( ({ spaceId, users, modifier: overwrite }) => { const suffix = ` within the ${spaceId} space${overwrite ? ' with overwrite enabled' : ''}`; - const { unauthorized, authorized, superuser } = createTests(overwrite!, spaceId); const _addTests = (user: TestUser, tests: BulkCreateTestDefinition[]) => { addTests(`${user.description}${suffix}`, { user, spaceId, tests }); }; @@ -106,11 +121,14 @@ export default function ({ getService }: FtrProviderContext) { users.readAtSpace, users.allAtOtherSpace, ].forEach((user) => { + const { unauthorized } = createTests(overwrite!, spaceId, user); _addTests(user, unauthorized); }); [users.dualAll, users.allGlobally, users.allAtSpace].forEach((user) => { + const { authorized } = createTests(overwrite!, spaceId, user); _addTests(user, authorized); }); + const { superuser } = createTests(overwrite!, spaceId, users.superuser); _addTests(users.superuser, superuser); } ); diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts index f81488603dc83..7353dafb5e1b5 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/create.ts @@ -24,13 +24,23 @@ const { fail400, fail409 } = testCaseFailures; const createTestCases = (overwrite: boolean, spaceId: string) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect // to receive an error; otherwise, we expect to receive a success result + const expectedNamespaces = [spaceId]; // newly created objects should have this `namespaces` array in their return value const normalTypes = [ { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), + expectedNamespaces, + }, + { + ...CASES.SINGLE_NAMESPACE_SPACE_1, + ...fail409(!overwrite && spaceId === SPACE_1_ID), + expectedNamespaces, + }, + { + ...CASES.SINGLE_NAMESPACE_SPACE_2, + ...fail409(!overwrite && spaceId === SPACE_2_ID), + expectedNamespaces, }, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), @@ -38,8 +48,8 @@ const createTestCases = (overwrite: boolean, spaceId: string) => { { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(!overwrite || spaceId !== SPACE_1_ID) }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(!overwrite || spaceId !== SPACE_2_ID) }, { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, - CASES.NEW_SINGLE_NAMESPACE_OBJ, - CASES.NEW_MULTI_NAMESPACE_OBJ, + { ...CASES.NEW_SINGLE_NAMESPACE_OBJ, expectedNamespaces }, + { ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces }, CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, ]; const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; @@ -53,15 +63,15 @@ export default function ({ getService }: FtrProviderContext) { const es = getService('legacyEs'); const { addTests, createTestDefinitions } = createTestSuiteFactory(es, esArchiver, supertest); - const createTests = (overwrite: boolean, spaceId: string) => { + const createTests = (overwrite: boolean, spaceId: string, user: TestUser) => { const { normalTypes, hiddenType, allTypes } = createTestCases(overwrite, spaceId); return { - unauthorized: createTestDefinitions(allTypes, true, overwrite, { spaceId }), + unauthorized: createTestDefinitions(allTypes, true, overwrite, { spaceId, user }), authorized: [ - createTestDefinitions(normalTypes, false, overwrite, { spaceId }), - createTestDefinitions(hiddenType, true, overwrite, { spaceId }), + createTestDefinitions(normalTypes, false, overwrite, { spaceId, user }), + createTestDefinitions(hiddenType, true, overwrite, { spaceId, user }), ].flat(), - superuser: createTestDefinitions(allTypes, false, overwrite, { spaceId }), + superuser: createTestDefinitions(allTypes, false, overwrite, { spaceId, user }), }; }; @@ -69,7 +79,6 @@ export default function ({ getService }: FtrProviderContext) { getTestScenarios([false, true]).securityAndSpaces.forEach( ({ spaceId, users, modifier: overwrite }) => { const suffix = ` within the ${spaceId} space${overwrite ? ' with overwrite enabled' : ''}`; - const { unauthorized, authorized, superuser } = createTests(overwrite!, spaceId); const _addTests = (user: TestUser, tests: CreateTestDefinition[]) => { addTests(`${user.description}${suffix}`, { user, spaceId, tests }); }; @@ -82,11 +91,14 @@ export default function ({ getService }: FtrProviderContext) { users.readAtSpace, users.allAtOtherSpace, ].forEach((user) => { + const { unauthorized } = createTests(overwrite!, spaceId, user); _addTests(user, unauthorized); }); [users.dualAll, users.allGlobally, users.allAtSpace].forEach((user) => { + const { authorized } = createTests(overwrite!, spaceId, user); _addTests(user, authorized); }); + const { superuser } = createTests(overwrite!, spaceId, users.superuser); _addTests(users.superuser, superuser); } ); diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts index c581a1757565e..be3906209032f 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/export.ts @@ -15,17 +15,23 @@ import { const createTestCases = (spaceId: string) => { const cases = getTestCases(spaceId); - const exportableTypes = [ + const exportableObjects = [ cases.singleNamespaceObject, - cases.singleNamespaceType, cases.multiNamespaceObject, - cases.multiNamespaceType, cases.namespaceAgnosticObject, + ]; + const exportableTypes = [ + cases.singleNamespaceType, + cases.multiNamespaceType, cases.namespaceAgnosticType, ]; - const nonExportableTypes = [cases.hiddenObject, cases.hiddenType]; - const allTypes = exportableTypes.concat(nonExportableTypes); - return { exportableTypes, nonExportableTypes, allTypes }; + const nonExportableObjectsAndTypes = [cases.hiddenObject, cases.hiddenType]; + const allObjectsAndTypes = [ + exportableObjects, + exportableTypes, + nonExportableObjectsAndTypes, + ].flat(); + return { exportableObjects, exportableTypes, nonExportableObjectsAndTypes, allObjectsAndTypes }; }; export default function ({ getService }: FtrProviderContext) { @@ -34,13 +40,19 @@ export default function ({ getService }: FtrProviderContext) { const { addTests, createTestDefinitions } = exportTestSuiteFactory(esArchiver, supertest); const createTests = (spaceId: string) => { - const { exportableTypes, nonExportableTypes, allTypes } = createTestCases(spaceId); + const { + exportableObjects, + exportableTypes, + nonExportableObjectsAndTypes, + allObjectsAndTypes, + } = createTestCases(spaceId); return { unauthorized: [ - createTestDefinitions(exportableTypes, true), - createTestDefinitions(nonExportableTypes, false), + createTestDefinitions(exportableObjects, { statusCode: 403, reason: 'unauthorized' }), + createTestDefinitions(exportableTypes, { statusCode: 200, reason: 'unauthorized' }), // failure with empty result + createTestDefinitions(nonExportableObjectsAndTypes, false), ].flat(), - authorized: createTestDefinitions(allTypes, false), + authorized: createTestDefinitions(allObjectsAndTypes, false), }; }; diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts index 6ac77507df473..afd4783fab792 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts @@ -4,18 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getTestScenarios } from '../../common/lib/saved_object_test_utils'; +import { SPACES } from '../../common/lib/spaces'; +import { AUTHENTICATION } from '../../common/lib/authentication'; +import { + getTestScenarios, + isUserAuthorizedAtSpace, +} from '../../common/lib/saved_object_test_utils'; import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { findTestSuiteFactory, getTestCases } from '../../common/suites/find'; -const createTestCases = (currentSpace: string, crossSpaceSearch: string[]) => { +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; + +const createTestCases = (currentSpace: string, crossSpaceSearch?: string[]) => { const cases = getTestCases({ currentSpace, crossSpaceSearch }); const normalTypes = [ cases.singleNamespaceType, cases.multiNamespaceType, cases.namespaceAgnosticType, + cases.eachType, cases.pageBeyondTotal, cases.unknownSearchField, cases.filterWithNamespaceAgnosticType, @@ -37,89 +49,72 @@ export default function ({ getService }: FtrProviderContext) { const { addTests, createTestDefinitions } = findTestSuiteFactory(esArchiver, supertest); const createTests = (spaceId: string, user: TestUser) => { - const currentSpaceCases = createTestCases(spaceId, []); + const currentSpaceCases = createTestCases(spaceId); - const explicitCrossSpace = createTestCases(spaceId, ['default', 'space_1', 'space_2']); + const EACH_SPACE = [DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]; + const explicitCrossSpace = createTestCases(spaceId, EACH_SPACE); const wildcardCrossSpace = createTestCases(spaceId, ['*']); - if (user.username === 'elastic') { + if (user.username === AUTHENTICATION.SUPERUSER.username) { return { currentSpace: createTestDefinitions(currentSpaceCases.allTypes, false, { user }), - crossSpace: createTestDefinitions(explicitCrossSpace.allTypes, false, { user }), + crossSpace: [ + createTestDefinitions(explicitCrossSpace.allTypes, false, { user }), + createTestDefinitions(wildcardCrossSpace.allTypes, false, { user }), + ].flat(), }; } - const authorizedAtCurrentSpace = - user.authorizedAtSpaces.includes(spaceId) || user.authorizedAtSpaces.includes('*'); - - const authorizedExplicitCrossSpaces = ['default', 'space_1', 'space_2'].filter( - (s) => - user.authorizedAtSpaces.includes('*') || - (s !== spaceId && user.authorizedAtSpaces.includes(s)) + const isAuthorizedExplicitCrossSpaces = EACH_SPACE.some( + (s) => s !== spaceId && isUserAuthorizedAtSpace(user, s) ); - - const authorizedWildcardCrossSpaces = ['default', 'space_1', 'space_2'].filter( - (s) => user.authorizedAtSpaces.includes('*') || user.authorizedAtSpaces.includes(s) + const isAuthorizedWildcardCrossSpaces = EACH_SPACE.some((s) => + isUserAuthorizedAtSpace(user, s) ); - const explicitCrossSpaceDefinitions = - authorizedExplicitCrossSpaces.length > 0 - ? [ - createTestDefinitions(explicitCrossSpace.normalTypes, false, { user }), - createTestDefinitions( - explicitCrossSpace.hiddenAndUnknownTypes, - { - statusCode: 403, - reason: 'forbidden_types', - }, - { user } - ), - ].flat() - : createTestDefinitions( - explicitCrossSpace.allTypes, - { - statusCode: 403, - reason: 'forbidden_namespaces', - }, + const explicitCrossSpaceDefinitions = isAuthorizedExplicitCrossSpaces + ? [ + createTestDefinitions(explicitCrossSpace.normalTypes, false, { user }), + createTestDefinitions( + explicitCrossSpace.hiddenAndUnknownTypes, + { statusCode: 200, reason: 'unauthorized' }, { user } - ); - - const wildcardCrossSpaceDefinitions = - authorizedWildcardCrossSpaces.length > 0 - ? [ - createTestDefinitions(wildcardCrossSpace.normalTypes, false, { user }), - createTestDefinitions( - wildcardCrossSpace.hiddenAndUnknownTypes, - { - statusCode: 403, - reason: 'forbidden_types', - }, - { user } - ), - ].flat() - : createTestDefinitions( - wildcardCrossSpace.allTypes, - { - statusCode: 403, - reason: 'forbidden_namespaces', - }, + ), + ].flat() + : createTestDefinitions( + explicitCrossSpace.allTypes, + { statusCode: 200, reason: 'unauthorized' }, + { user } + ); + const wildcardCrossSpaceDefinitions = isAuthorizedWildcardCrossSpaces + ? [ + createTestDefinitions(wildcardCrossSpace.normalTypes, false, { user }), + createTestDefinitions( + wildcardCrossSpace.hiddenAndUnknownTypes, + { statusCode: 200, reason: 'unauthorized' }, { user } - ); + ), + ].flat() + : createTestDefinitions( + wildcardCrossSpace.allTypes, + { statusCode: 200, reason: 'unauthorized' }, + { user } + ); return { - currentSpace: authorizedAtCurrentSpace + currentSpace: isUserAuthorizedAtSpace(user, spaceId) ? [ createTestDefinitions(currentSpaceCases.normalTypes, false, { user, }), createTestDefinitions(currentSpaceCases.hiddenAndUnknownTypes, { - statusCode: 403, - reason: 'forbidden_types', + statusCode: 200, + reason: 'unauthorized', }), ].flat() : createTestDefinitions(currentSpaceCases.allTypes, { - statusCode: 403, - reason: 'forbidden_types', + statusCode: 200, + reason: 'unauthorized', }), crossSpace: [...explicitCrossSpaceDefinitions, ...wildcardCrossSpaceDefinitions], }; diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts index 725120687c231..cc2c5e2e7fc00 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_create.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SPACES } from '../../common/lib/spaces'; import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -13,22 +14,26 @@ import { BulkCreateTestDefinition, } from '../../common/suites/bulk_create'; +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, +} = SPACES; const { fail400, fail409 } = testCaseFailures; const unresolvableConflict = () => ({ fail409Param: 'unresolvableConflict' }); const createTestCases = (overwrite: boolean) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect // to receive an error; otherwise, we expect to receive a success result + const expectedNamespaces = [DEFAULT_SPACE_ID]; // newly created objects should have this `namespaces` array in their return value const normalTypes = [ { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite) }, - CASES.SINGLE_NAMESPACE_SPACE_1, - CASES.SINGLE_NAMESPACE_SPACE_2, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, expectedNamespaces }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, expectedNamespaces }, { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(), ...unresolvableConflict() }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(), ...unresolvableConflict() }, { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, - CASES.NEW_SINGLE_NAMESPACE_OBJ, - CASES.NEW_MULTI_NAMESPACE_OBJ, + { ...CASES.NEW_SINGLE_NAMESPACE_OBJ, expectedNamespaces }, + { ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces }, CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, ]; const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; @@ -46,27 +51,27 @@ export default function ({ getService }: FtrProviderContext) { esArchiver, supertest ); - const createTests = (overwrite: boolean) => { + const createTests = (overwrite: boolean, user: TestUser) => { const { normalTypes, hiddenType, allTypes } = createTestCases(overwrite); // use singleRequest to reduce execution time and/or test combined cases return { - unauthorized: createTestDefinitions(allTypes, true, overwrite), + unauthorized: createTestDefinitions(allTypes, true, overwrite, { user }), authorized: [ - createTestDefinitions(normalTypes, false, overwrite, { singleRequest: true }), - createTestDefinitions(hiddenType, true, overwrite), + createTestDefinitions(normalTypes, false, overwrite, { user, singleRequest: true }), + createTestDefinitions(hiddenType, true, overwrite, { user }), createTestDefinitions(allTypes, true, overwrite, { + user, singleRequest: true, responseBodyOverride: expectForbidden(['hiddentype']), }), ].flat(), - superuser: createTestDefinitions(allTypes, false, overwrite, { singleRequest: true }), + superuser: createTestDefinitions(allTypes, false, overwrite, { user, singleRequest: true }), }; }; describe('_bulk_create', () => { getTestScenarios([false, true]).security.forEach(({ users, modifier: overwrite }) => { const suffix = overwrite ? ' with overwrite enabled' : ''; - const { unauthorized, authorized, superuser } = createTests(overwrite!); const _addTests = (user: TestUser, tests: BulkCreateTestDefinition[]) => { addTests(`${user.description}${suffix}`, { user, tests }); }; @@ -81,11 +86,14 @@ export default function ({ getService }: FtrProviderContext) { users.allAtSpace1, users.readAtSpace1, ].forEach((user) => { + const { unauthorized } = createTests(overwrite!, user); _addTests(user, unauthorized); }); [users.dualAll, users.allGlobally].forEach((user) => { + const { authorized } = createTests(overwrite!, user); _addTests(user, authorized); }); + const { superuser } = createTests(overwrite!, users.superuser); _addTests(users.superuser, superuser); }); }); diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/create.ts b/x-pack/test/saved_object_api_integration/security_only/apis/create.ts index 88d096f05d846..b7c6ecef979bd 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/create.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/create.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SPACES } from '../../common/lib/spaces'; import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -13,21 +14,25 @@ import { CreateTestDefinition, } from '../../common/suites/create'; +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, +} = SPACES; const { fail400, fail409 } = testCaseFailures; const createTestCases = (overwrite: boolean) => { // for each permitted (non-403) outcome, if failure !== undefined then we expect // to receive an error; otherwise, we expect to receive a success result + const expectedNamespaces = [DEFAULT_SPACE_ID]; // newly created objects should have this `namespaces` array in their return value const normalTypes = [ { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail409(!overwrite) }, - CASES.SINGLE_NAMESPACE_SPACE_1, - CASES.SINGLE_NAMESPACE_SPACE_2, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, expectedNamespaces }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, expectedNamespaces }, { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, ...fail409(!overwrite) }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409() }, { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409() }, { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, - CASES.NEW_SINGLE_NAMESPACE_OBJ, - CASES.NEW_MULTI_NAMESPACE_OBJ, + { ...CASES.NEW_SINGLE_NAMESPACE_OBJ, expectedNamespaces }, + { ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces }, CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, ]; const hiddenType = [{ ...CASES.HIDDEN, ...fail400() }]; @@ -41,22 +46,21 @@ export default function ({ getService }: FtrProviderContext) { const es = getService('legacyEs'); const { addTests, createTestDefinitions } = createTestSuiteFactory(es, esArchiver, supertest); - const createTests = (overwrite: boolean) => { + const createTests = (overwrite: boolean, user: TestUser) => { const { normalTypes, hiddenType, allTypes } = createTestCases(overwrite); return { - unauthorized: createTestDefinitions(allTypes, true, overwrite), + unauthorized: createTestDefinitions(allTypes, true, overwrite, { user }), authorized: [ - createTestDefinitions(normalTypes, false, overwrite), - createTestDefinitions(hiddenType, true, overwrite), + createTestDefinitions(normalTypes, false, overwrite, { user }), + createTestDefinitions(hiddenType, true, overwrite, { user }), ].flat(), - superuser: createTestDefinitions(allTypes, false, overwrite), + superuser: createTestDefinitions(allTypes, false, overwrite, { user }), }; }; describe('_create', () => { getTestScenarios([false, true]).security.forEach(({ users, modifier: overwrite }) => { const suffix = overwrite ? ' with overwrite enabled' : ''; - const { unauthorized, authorized, superuser } = createTests(overwrite!); const _addTests = (user: TestUser, tests: CreateTestDefinition[]) => { addTests(`${user.description}${suffix}`, { user, tests }); }; @@ -71,11 +75,14 @@ export default function ({ getService }: FtrProviderContext) { users.allAtSpace1, users.readAtSpace1, ].forEach((user) => { + const { unauthorized } = createTests(overwrite!, user); _addTests(user, unauthorized); }); [users.dualAll, users.allGlobally].forEach((user) => { + const { authorized } = createTests(overwrite!, user); _addTests(user, authorized); }); + const { superuser } = createTests(overwrite!, users.superuser); _addTests(users.superuser, superuser); }); }); diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/export.ts b/x-pack/test/saved_object_api_integration/security_only/apis/export.ts index 99babf683ccfa..ea1ed56921d22 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/export.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/export.ts @@ -15,17 +15,23 @@ import { const createTestCases = () => { const cases = getTestCases(); - const exportableTypes = [ + const exportableObjects = [ cases.singleNamespaceObject, - cases.singleNamespaceType, cases.multiNamespaceObject, - cases.multiNamespaceType, cases.namespaceAgnosticObject, + ]; + const exportableTypes = [ + cases.singleNamespaceType, + cases.multiNamespaceType, cases.namespaceAgnosticType, ]; - const nonExportableTypes = [cases.hiddenObject, cases.hiddenType]; - const allTypes = exportableTypes.concat(nonExportableTypes); - return { exportableTypes, nonExportableTypes, allTypes }; + const nonExportableObjectsAndTypes = [cases.hiddenObject, cases.hiddenType]; + const allObjectsAndTypes = [ + exportableObjects, + exportableTypes, + nonExportableObjectsAndTypes, + ].flat(); + return { exportableObjects, exportableTypes, nonExportableObjectsAndTypes, allObjectsAndTypes }; }; export default function ({ getService }: FtrProviderContext) { @@ -34,13 +40,19 @@ export default function ({ getService }: FtrProviderContext) { const { addTests, createTestDefinitions } = exportTestSuiteFactory(esArchiver, supertest); const createTests = () => { - const { exportableTypes, nonExportableTypes, allTypes } = createTestCases(); + const { + exportableObjects, + exportableTypes, + nonExportableObjectsAndTypes, + allObjectsAndTypes, + } = createTestCases(); return { unauthorized: [ - createTestDefinitions(exportableTypes, true), - createTestDefinitions(nonExportableTypes, false), + createTestDefinitions(exportableObjects, { statusCode: 403, reason: 'unauthorized' }), + createTestDefinitions(exportableTypes, { statusCode: 200, reason: 'unauthorized' }), // failure with empty result + createTestDefinitions(nonExportableObjectsAndTypes, false), ].flat(), - authorized: createTestDefinitions(allTypes, false), + authorized: createTestDefinitions(allObjectsAndTypes, false), }; }; diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/find.ts b/x-pack/test/saved_object_api_integration/security_only/apis/find.ts index 3a435119436ca..aa18f32600949 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/find.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/find.ts @@ -4,18 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SPACES } from '../../common/lib/spaces'; +import { AUTHENTICATION } from '../../common/lib/authentication'; import { getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { findTestSuiteFactory, getTestCases } from '../../common/suites/find'; -const createTestCases = (crossSpaceSearch: string[]) => { +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; + +const createTestCases = (crossSpaceSearch?: string[]) => { const cases = getTestCases({ crossSpaceSearch }); const normalTypes = [ cases.singleNamespaceType, cases.multiNamespaceType, cases.namespaceAgnosticType, + cases.eachType, cases.pageBeyondTotal, cases.unknownSearchField, cases.filterWithNamespaceAgnosticType, @@ -37,46 +46,35 @@ export default function ({ getService }: FtrProviderContext) { const { addTests, createTestDefinitions } = findTestSuiteFactory(esArchiver, supertest); const createTests = (user: TestUser) => { - const defaultCases = createTestCases([]); - const crossSpaceCases = createTestCases(['default', 'space_1', 'space_2']); + const defaultCases = createTestCases(); + const crossSpaceCases = createTestCases([DEFAULT_SPACE_ID, SPACE_1_ID, SPACE_2_ID]); - if (user.username === 'elastic') { + if (user.username === AUTHENTICATION.SUPERUSER.username) { return { defaultCases: createTestDefinitions(defaultCases.allTypes, false, { user }), crossSpace: createTestDefinitions( crossSpaceCases.allTypes, - { - statusCode: 400, - reason: 'cross_namespace_not_permitted', - }, + { statusCode: 400, reason: 'cross_namespace_not_permitted' }, { user } ), }; } - const authorizedGlobally = user.authorizedAtSpaces.includes('*'); + const isAuthorizedGlobally = user.authorizedAtSpaces.includes('*'); return { - defaultCases: authorizedGlobally + defaultCases: isAuthorizedGlobally ? [ - createTestDefinitions(defaultCases.normalTypes, false, { - user, - }), + createTestDefinitions(defaultCases.normalTypes, false, { user }), createTestDefinitions(defaultCases.hiddenAndUnknownTypes, { - statusCode: 403, - reason: 'forbidden_types', + statusCode: 200, + reason: 'unauthorized', }), ].flat() - : createTestDefinitions(defaultCases.allTypes, { - statusCode: 403, - reason: 'forbidden_types', - }), + : createTestDefinitions(defaultCases.allTypes, { statusCode: 200, reason: 'unauthorized' }), crossSpace: createTestDefinitions( crossSpaceCases.allTypes, - { - statusCode: 400, - reason: 'cross_namespace_not_permitted', - }, + { statusCode: 400, reason: 'cross_namespace_not_permitted' }, { user } ), }; diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts index 74fade39bf7a5..ef47b09eddbc8 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_create.ts @@ -19,36 +19,48 @@ const { fail400, fail409 } = testCaseFailures; const unresolvableConflict = (condition?: boolean) => condition !== false ? { fail409Param: 'unresolvableConflict' } : {}; -const createTestCases = (overwrite: boolean, spaceId: string) => [ +const createTestCases = (overwrite: boolean, spaceId: string) => { // for each outcome, if failure !== undefined then we expect to receive // an error; otherwise, we expect to receive a success result - { - ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, - ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), - }, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, - { - ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, - ...fail409(!overwrite || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), - ...unresolvableConflict(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), - }, - { - ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, - ...fail409(!overwrite || spaceId !== SPACE_1_ID), - ...unresolvableConflict(spaceId !== SPACE_1_ID), - }, - { - ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, - ...fail409(!overwrite || spaceId !== SPACE_2_ID), - ...unresolvableConflict(spaceId !== SPACE_2_ID), - }, - { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, - { ...CASES.HIDDEN, ...fail400() }, - CASES.NEW_SINGLE_NAMESPACE_OBJ, - CASES.NEW_MULTI_NAMESPACE_OBJ, - CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, -]; + const expectedNamespaces = [spaceId]; // newly created objects should have this `namespaces` array in their return value + return [ + { + ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), + expectedNamespaces, + }, + { + ...CASES.SINGLE_NAMESPACE_SPACE_1, + ...fail409(!overwrite && spaceId === SPACE_1_ID), + expectedNamespaces, + }, + { + ...CASES.SINGLE_NAMESPACE_SPACE_2, + ...fail409(!overwrite && spaceId === SPACE_2_ID), + expectedNamespaces, + }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail409(!overwrite || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), + ...unresolvableConflict(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, + ...fail409(!overwrite || spaceId !== SPACE_1_ID), + ...unresolvableConflict(spaceId !== SPACE_1_ID), + }, + { + ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, + ...fail409(!overwrite || spaceId !== SPACE_2_ID), + ...unresolvableConflict(spaceId !== SPACE_2_ID), + }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, + { ...CASES.HIDDEN, ...fail400() }, + { ...CASES.NEW_SINGLE_NAMESPACE_OBJ, expectedNamespaces }, + { ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces }, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, + ]; +}; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts index 1040f7fd81dde..10e57b4db82dc 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/create.ts @@ -16,27 +16,39 @@ const { } = SPACES; const { fail400, fail409 } = testCaseFailures; -const createTestCases = (overwrite: boolean, spaceId: string) => [ +const createTestCases = (overwrite: boolean, spaceId: string) => { // for each outcome, if failure !== undefined then we expect to receive // an error; otherwise, we expect to receive a success result - { - ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, - ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), - }, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail409(!overwrite && spaceId === SPACE_1_ID) }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail409(!overwrite && spaceId === SPACE_2_ID) }, - { - ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, - ...fail409(!overwrite || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), - }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(!overwrite || spaceId !== SPACE_1_ID) }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(!overwrite || spaceId !== SPACE_2_ID) }, - { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, - { ...CASES.HIDDEN, ...fail400() }, - CASES.NEW_SINGLE_NAMESPACE_OBJ, - CASES.NEW_MULTI_NAMESPACE_OBJ, - CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, -]; + const expectedNamespaces = [spaceId]; // newly created objects should have this `namespaces` array in their return value + return [ + { + ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, + ...fail409(!overwrite && spaceId === DEFAULT_SPACE_ID), + expectedNamespaces, + }, + { + ...CASES.SINGLE_NAMESPACE_SPACE_1, + ...fail409(!overwrite && spaceId === SPACE_1_ID), + expectedNamespaces, + }, + { + ...CASES.SINGLE_NAMESPACE_SPACE_2, + ...fail409(!overwrite && spaceId === SPACE_2_ID), + expectedNamespaces, + }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail409(!overwrite || (spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID)), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail409(!overwrite || spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail409(!overwrite || spaceId !== SPACE_2_ID) }, + { ...CASES.NAMESPACE_AGNOSTIC, ...fail409(!overwrite) }, + { ...CASES.HIDDEN, ...fail400() }, + { ...CASES.NEW_SINGLE_NAMESPACE_OBJ, expectedNamespaces }, + { ...CASES.NEW_MULTI_NAMESPACE_OBJ, expectedNamespaces }, + CASES.NEW_NAMESPACE_AGNOSTIC_OBJ, + ]; +}; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertestWithoutAuth'); diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/find.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/find.ts index 1d46985916cd5..c6779402d3291 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/find.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/find.ts @@ -4,11 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SPACES } from '../../common/lib/spaces'; import { getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { findTestSuiteFactory, getTestCases } from '../../common/suites/find'; -const createTestCases = (spaceId: string, crossSpaceSearch: string[]) => { +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; + +const createTestCases = (spaceId: string, crossSpaceSearch?: string[]) => { const cases = getTestCases({ currentSpace: spaceId, crossSpaceSearch }); return Object.values(cases); }; @@ -18,15 +25,19 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const { addTests, createTestDefinitions } = findTestSuiteFactory(esArchiver, supertest); - const createTests = (spaceId: string, crossSpaceSearch: string[]) => { + const createTests = (spaceId: string, crossSpaceSearch?: string[]) => { const testCases = createTestCases(spaceId, crossSpaceSearch); return createTestDefinitions(testCases, false); }; describe('_find', () => { getTestScenarios().spaces.forEach(({ spaceId }) => { - const currentSpaceTests = createTests(spaceId, []); - const explicitCrossSpaceTests = createTests(spaceId, ['default', 'space_1', 'space_2']); + const currentSpaceTests = createTests(spaceId); + const explicitCrossSpaceTests = createTests(spaceId, [ + DEFAULT_SPACE_ID, + SPACE_1_ID, + SPACE_2_ID, + ]); const wildcardCrossSpaceTests = createTests(spaceId, ['*']); addTests(`within the ${spaceId} space`, { spaceId, diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts index 4beb64affc46b..d46171bbaa49f 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts @@ -86,7 +86,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.exists('emptyPolicyTable'); }); - it('finds data after load and polling', async () => { + it.skip('finds data after load and polling', async () => { await esArchiver.load('endpoint/metadata/destination_index', { useCreate: true }); await pageObjects.endpoint.waitForTableToHaveData('endpointListTable', 1100); const tableData = await pageObjects.endpointPageUtils.tableData('endpointListTable'); @@ -94,7 +94,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - describe('when there is data,', () => { + describe.skip('when there is data,', () => { before(async () => { await esArchiver.load('endpoint/metadata/destination_index', { useCreate: true }); await pageObjects.endpoint.navigateToEndpointList(); @@ -212,7 +212,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - describe('displays the correct table data for the kql queries', () => { + describe.skip('displays the correct table data for the kql queries', () => { before(async () => { await esArchiver.load('endpoint/metadata/destination_index', { useCreate: true }); await pageObjects.endpoint.navigateToEndpointList(); 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 new file mode 100644 index 0000000000000..533ce49b14325 --- /dev/null +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_telemetry.ts @@ -0,0 +1,161 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const telemetryTestResources = getService('telemetryTestResources'); + + describe('security solution endpoint telemetry', () => { + after(async () => { + await esArchiver.load('empty_kibana'); + }); + + describe('when no agents are connected', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + }); + + it('reports no endpoints or 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, + }, + }, + }); + }); + }); + describe('when agents are connected with endpoint integration disabled', () => { + before(async () => { + await esArchiver.load('endpoint/telemetry/agent_only'); + }); + + it('reports no endpoints or 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, + }, + }, + }); + }); + }); + describe('when agents are connected with endpoints seen in past 24 hours', () => { + before(async () => { + await telemetryTestResources.getArchiveSetCheckIn( + 'endpoint_malware_enabled', + 'checkin_now', + 0 + ); + await esArchiver.load('endpoint/telemetry/checkin_now'); + await telemetryTestResources.deleteArchive('checkin_now'); + }); + + it('reports the correct number of endpoints seen total and in past 24 hours', async () => { + const endpointTelemetry = await telemetryTestResources.getEndpointTelemetry(); + expect(endpointTelemetry.total_installed).to.eql(3); + expect(endpointTelemetry.active_within_last_24_hours).to.eql(3); + }); + it('reports the separate OS types of installed endpoints', async () => { + const endpointTelemetry = await telemetryTestResources.getEndpointTelemetry(); + expect(endpointTelemetry.os.length).to.eql(3); + const osPlatforms: string[] = []; + endpointTelemetry.os.forEach((os) => { + expect(os.count).to.eql(1); + osPlatforms.push(os.platform); + }); + expect(osPlatforms).to.contain('windows'); + expect(osPlatforms).to.contain('darwin'); + expect(osPlatforms).to.contain('ubuntu'); + }); + }); + describe('when agents are connected with endpoints not seen in past 24 hours', () => { + before(async () => { + await telemetryTestResources.getArchiveSetCheckIn( + 'endpoint_malware_enabled', + 'checkin_2_days_ago', + 2 + ); + await esArchiver.load('endpoint/telemetry/checkin_2_days_ago'); + await telemetryTestResources.deleteArchive('checkin_2_days_ago'); + }); + + it('reports the correct number of endpoints seen total and in past 24 hours', async () => { + const endpointTelemetry = await telemetryTestResources.getEndpointTelemetry(); + expect(endpointTelemetry.total_installed).to.eql(3); + expect(endpointTelemetry.active_within_last_24_hours).to.eql(0); + }); + it('reports the separate OS types of installed endpoints', async () => { + const endpointTelemetry = await telemetryTestResources.getEndpointTelemetry(); + expect(endpointTelemetry.os.length).to.eql(3); + const osPlatforms: string[] = []; + endpointTelemetry.os.forEach((os) => { + expect(os.count).to.eql(1); + osPlatforms.push(os.platform); + }); + expect(osPlatforms).to.contain('windows'); + expect(osPlatforms).to.contain('darwin'); + expect(osPlatforms).to.contain('ubuntu'); + }); + }); + describe('when agents are connected with endpoints integration malware enabled', () => { + before(async () => { + await esArchiver.load('endpoint/telemetry/endpoint_malware_enabled'); + }); + + it('reports the correct number of windows and macos endpoints under policies malware', async () => { + const endpointTelemetry = await telemetryTestResources.getEndpointTelemetry(); + expect(endpointTelemetry.policies.malware.active).to.eql(2); + expect(endpointTelemetry.policies.malware.inactive).to.eql(0); + }); + }); + describe('when agents are connected with endpoints integration malware disabled', () => { + before(async () => { + await esArchiver.load('endpoint/telemetry/endpoint_malware_disabled'); + }); + it('reports the correct number of windows and macos endpoints under policies malware', async () => { + const endpointTelemetry = await telemetryTestResources.getEndpointTelemetry(); + expect(endpointTelemetry.policies.malware.active).to.eql(0); + expect(endpointTelemetry.policies.malware.inactive).to.eql(2); + }); + }); + describe('when agents are connected with endpoints integration uninstalled', () => { + before(async () => { + await esArchiver.load('endpoint/telemetry/endpoint_uninstalled'); + }); + it('reports no endpoints or 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, + }, + }, + }); + }); + }); + }); +} diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts index c25233a0c82eb..654aa18fba523 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/index.ts @@ -31,5 +31,6 @@ export default function (providerContext: FtrProviderContext) { loadTestFile(require.resolve('./endpoint_list')); loadTestFile(require.resolve('./policy_details')); loadTestFile(require.resolve('./resolver')); + loadTestFile(require.resolve('./endpoint_telemetry')); }); } diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index 325283f5e3440..9610144d3846d 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -29,7 +29,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { port, }); - describe('When on the Endpoint Policy Details Page', function () { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/72102 + describe.skip('When on the Endpoint Policy Details Page', function () { this.tags(['ciGroup7']); describe('with an invalid policy id', () => { diff --git a/x-pack/test/security_solution_endpoint/services/endpoint_telemetry.ts b/x-pack/test/security_solution_endpoint/services/endpoint_telemetry.ts new file mode 100644 index 0000000000000..0f158da5d2f8c --- /dev/null +++ b/x-pack/test/security_solution_endpoint/services/endpoint_telemetry.ts @@ -0,0 +1,98 @@ +/* + * 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 fs from 'fs'; +import Path from 'path'; +import { FtrProviderContext } from '../ftr_provider_context'; + +const TELEMETRY_API_ROOT = '/api/stats?extended=true'; +const TELEMETRY_DATA_ROOT = 'test/functional/es_archives/endpoint/telemetry/'; + +interface EndpointTelemetry { + total_installed: number; + active_within_last_24_hours: number; + os: Array<{ + full_name: string; + platform: string; + version: string; + count: number; + }>; + policies: { + malware: { + active: number; + inactive: number; + failure: number; + }; + }; +} + +export function EndpointTelemetryTestResourcesProvider({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + return { + /** + * Return the endpoint telemetry object from /api/stats?extended=true + */ + async getEndpointTelemetry(): Promise { + const kibanaTelemetry = await supertest.get(TELEMETRY_API_ROOT); + return kibanaTelemetry.body.usage.security_solution.endpoints; + }, + /** + * Create an telemetry archive from a source archive with updated last_checkin field to test + * active_within_last_24_hours + */ + async getArchiveSetCheckIn( + sourceArchive: string, + destinationArchive: string, + checkInOffset: number = 0 + ) { + const sourcePath = Path.join(TELEMETRY_DATA_ROOT, sourceArchive); + const destinationPath = Path.join(TELEMETRY_DATA_ROOT, destinationArchive); + + if (fs.existsSync(destinationPath)) { + this.deleteArchive(destinationArchive); + } + fs.mkdirSync(destinationPath); + + fs.readdirSync(sourcePath).forEach((file) => { + const sourceFilePath = Path.join(sourcePath, file); + const destinationFilePath = Path.join(destinationPath, file); + fs.copyFileSync(sourceFilePath, destinationFilePath); + }); + + fs.readdirSync(destinationPath).forEach((file) => { + if (file !== 'mappings.json') { + const dataFilePath = Path.join(destinationPath, file); + const rawDocuments = fs.readFileSync(dataFilePath).toString().split('\n\n'); + + const dateTime = new Date(); + dateTime.setDate(dateTime.getDate() - checkInOffset); + rawDocuments.forEach((rawDocument, index) => { + const document = JSON.parse(rawDocument); + if (document.value.source.type === 'fleet-agents') { + document.value.source['fleet-agents'].last_checkin = dateTime.toISOString(); + rawDocuments[index] = JSON.stringify(document); + } + }); + + fs.writeFileSync(dataFilePath, rawDocuments.join('\n\n')); + } + }); + }, + /** + * Delete archives created for testing + */ + async deleteArchive(archiveName: string) { + const archivePath = Path.join(TELEMETRY_DATA_ROOT, archiveName); + if (fs.existsSync(archivePath)) { + fs.readdirSync(archivePath).forEach((file) => { + const archiveFile = Path.join(archivePath, file); + fs.unlinkSync(archiveFile); + }); + } + fs.rmdirSync(archivePath); + }, + }; +} diff --git a/x-pack/test/security_solution_endpoint/services/index.ts b/x-pack/test/security_solution_endpoint/services/index.ts index 7eecae41aae4a..a9409998547d5 100644 --- a/x-pack/test/security_solution_endpoint/services/index.ts +++ b/x-pack/test/security_solution_endpoint/services/index.ts @@ -7,9 +7,11 @@ import { services as xPackFunctionalServices } from '../../functional/services'; import { EndpointPolicyTestResourcesProvider } from './endpoint_policy'; import { IngestManagerProvider } from '../../common/services/ingest_manager'; +import { EndpointTelemetryTestResourcesProvider } from './endpoint_telemetry'; export const services = { ...xPackFunctionalServices, policyTestResources: EndpointPolicyTestResourcesProvider, + telemetryTestResources: EndpointTelemetryTestResourcesProvider, ingestManager: IngestManagerProvider, }; diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts b/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts index 17a4182fe9371..6c225dea5430f 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/artifacts/index.ts @@ -18,7 +18,7 @@ export default function (providerContext: FtrProviderContext) { const supertestWithoutAuth = getSupertestWithoutAuth(providerContext); let agentAccessAPIKey: string; - describe('artifact download', () => { + describe.skip('artifact download', () => { before(async () => { await esArchiver.load('endpoint/artifacts/api_feature', { useCreate: true }); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts index b157c3159ccc0..d1e98876596e5 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts @@ -23,7 +23,7 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); - describe('test metadata api', () => { + describe.skip('test metadata api', () => { describe(`POST ${METADATA_REQUEST_ROUTE} when index is empty`, () => { it('metadata api should return empty result when index is empty', async () => { await deleteMetadataStream(getService); diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 44c8449dc5dd0..7978a89231566 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -3,9 +3,6 @@ "include": [ "mocks.ts", "typings/**/*", - "legacy/common/**/*", - "legacy/server/**/*", - "legacy/plugins/**/*", "plugins/**/*", "test_utils/**/*", "tasks/**/*" @@ -21,7 +18,6 @@ "paths": { "kibana/public": ["src/core/public"], "kibana/server": ["src/core/server"], - "plugins/xpack_main/*": ["x-pack/legacy/plugins/xpack_main/public/*"], "test_utils/*": ["x-pack/test_utils/*"], "fixtures/*": ["src/fixtures/*"] }, diff --git a/x-pack/typings/hapi.d.ts b/x-pack/typings/hapi.d.ts index 253b639a52ff2..dd9e0239aeee7 100644 --- a/x-pack/typings/hapi.d.ts +++ b/x-pack/typings/hapi.d.ts @@ -6,7 +6,6 @@ import 'hapi'; -import { XPackMainPlugin } from '../legacy/plugins/xpack_main/server/xpack_main'; import { ActionsPlugin, ActionsClient } from '../plugins/actions/server'; import { AlertingPlugin, AlertsClient } from '../plugins/alerts/server'; import { TaskManager } from '../plugins/task_manager/server'; @@ -17,7 +16,6 @@ declare module 'hapi' { getAlertsClient?: () => AlertsClient; } interface PluginProperties { - xpack_main: XPackMainPlugin; actions?: ActionsPlugin; alerts?: AlertingPlugin; task_manager?: TaskManager; diff --git a/yarn.lock b/yarn.lock index cec2697f6c15c..3549c79970bff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -982,7 +982,7 @@ pirates "^4.0.0" source-map-support "^0.5.16" -"@babel/runtime-corejs2@^7.2.0", "@babel/runtime-corejs2@^7.6.3": +"@babel/runtime-corejs2@^7.2.0": version "7.11.2" resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.11.2.tgz#700a03945ebad0d31ba6690fc8a6bcc9040faa47" integrity sha512-AC/ciV28adSSpEkBglONBWq4/Lvm6GAZuxIoyVtsnUpZMl0bxLtoChEnYAkP+47KyOCayZanojtflUEUJtR/6Q== @@ -1129,12 +1129,21 @@ opentracing "^0.14.3" promise-polyfill "^8.1.3" -"@elastic/apm-rum-react@^1.2.3": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@elastic/apm-rum-react/-/apm-rum-react-1.2.3.tgz#fdf28492daca0ee6aa67c53a457eea1f16739e1e" - integrity sha512-oCjF/L46OYDRLHKt60l7aU+DFE484dwb/kKN12VZCOgueDZm4BCJd7yaosBtWDhnw0tl0Iqc0X3r4U7pQ+g9aA== +"@elastic/apm-rum-core@^5.6.1": + version "5.6.1" + resolved "https://registry.yarnpkg.com/@elastic/apm-rum-core/-/apm-rum-core-5.6.1.tgz#0870e654e84e1f2ffea7c8a247a2da1b72918bcd" + integrity sha512-UtWj8UNN1sfSjav1kQK2NFhHtrH++4FzhtY0g80aSfHrDdBKVXaecWswoGmK3aiGJ9LAVlAXNfF3tPMT6JN23g== + dependencies: + error-stack-parser "^1.3.5" + opentracing "^0.14.3" + promise-polyfill "^8.1.3" + +"@elastic/apm-rum-react@^1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@elastic/apm-rum-react/-/apm-rum-react-1.2.4.tgz#f5b908f69f2696af10d19250226559ceb33dc1e9" + integrity sha512-zjig55n4/maU+kAEePS+DxgD12t4J0X9t3tB9YuO0gUIJhgT7KTL1Nv93ZmJ3u2tCJSpdYVfKQ0GBgSfjt1vVQ== dependencies: - "@elastic/apm-rum" "^5.5.0" + "@elastic/apm-rum" "^5.6.0" hoist-non-react-statics "^3.3.0" "@elastic/apm-rum@^5.5.0": @@ -1144,6 +1153,13 @@ dependencies: "@elastic/apm-rum-core" "^5.6.0" +"@elastic/apm-rum@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@elastic/apm-rum/-/apm-rum-5.6.0.tgz#0af2acb55091b9eb315cf38c6422a83cddfecb6f" + integrity sha512-6CuODbt7dBXoqsKoqhshQQC4GyqsGMPOR1FXZCWbnq55UZq1TWqra6zNCtEEFinz8rPaww7bzmNciXKRvGjIzQ== + dependencies: + "@elastic/apm-rum-core" "^5.6.1" + "@elastic/charts@21.1.2": version "21.1.2" resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-21.1.2.tgz#da7e9c1025bf730a738b6ac6d7024d97dd2b5aa2" @@ -2458,6 +2474,25 @@ jsonwebtoken "^8.3.0" lru-cache "^5.1.1" +"@octokit/auth-token@^2.4.0": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.2.tgz#10d0ae979b100fa6b72fa0e8e63e27e6d0dbff8a" + integrity sha512-jE/lE/IKIz2v1+/P0u4fJqv0kYwXOTujKemJMFr6FeopsxlIK3+wKDCJGnysg81XID5TgZQbIfuJ5J0lnTiuyQ== + dependencies: + "@octokit/types" "^5.0.0" + +"@octokit/core@^3.0.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.1.2.tgz#c937d5f9621b764573068fcd2e5defcc872fd9cc" + integrity sha512-AInOFULmwOa7+NFi9F8DlDkm5qtZVmDQayi7TUgChE3yeIGPq0Y+6cAEXPexQ3Ea+uZy66hKEazR7DJyU+4wfw== + dependencies: + "@octokit/auth-token" "^2.4.0" + "@octokit/graphql" "^4.3.1" + "@octokit/request" "^5.4.0" + "@octokit/types" "^5.0.0" + before-after-hook "^2.1.0" + universal-user-agent "^6.0.0" + "@octokit/endpoint@^3.2.0": version "3.2.3" resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-3.2.3.tgz#bd9aea60cd94ce336656b57a5c9cb7f10be8f4f3" @@ -2468,6 +2503,44 @@ universal-user-agent "^2.0.1" url-template "^2.0.8" +"@octokit/endpoint@^6.0.1": + version "6.0.6" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.6.tgz#4f09f2b468976b444742a1d5069f6fa45826d999" + integrity sha512-7Cc8olaCoL/mtquB7j/HTbPM+sY6Ebr4k2X2y4JoXpVKQ7r5xB4iGQE0IoO58wIPsUk4AzoT65AMEpymSbWTgQ== + dependencies: + "@octokit/types" "^5.0.0" + is-plain-object "^5.0.0" + universal-user-agent "^6.0.0" + +"@octokit/graphql@^4.3.1": + version "4.5.6" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.5.6.tgz#708143ba15cf7c1879ed6188266e7f270be805d4" + integrity sha512-Rry+unqKTa3svswT2ZAuqenpLrzJd+JTv89LTeVa5UM/5OX8o4KTkPL7/1ABq4f/ZkELb0XEK/2IEoYwykcLXg== + dependencies: + "@octokit/request" "^5.3.0" + "@octokit/types" "^5.0.0" + universal-user-agent "^6.0.0" + +"@octokit/plugin-paginate-rest@^2.2.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.4.0.tgz#92f951ddc8a1cd505353fa07650752ca25ed7e93" + integrity sha512-YT6Klz3LLH6/nNgi0pheJnUmTFW4kVnxGft+v8Itc41IIcjl7y1C8TatmKQBbCSuTSNFXO5pCENnqg6sjwpJhg== + dependencies: + "@octokit/types" "^5.5.0" + +"@octokit/plugin-request-log@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.0.tgz#eef87a431300f6148c39a7f75f8cfeb218b2547e" + integrity sha512-ywoxP68aOT3zHCLgWZgwUJatiENeHE7xJzYjfz8WI0goynp96wETBF+d95b8g/uL4QmS6owPVlaxiz3wyMAzcw== + +"@octokit/plugin-rest-endpoint-methods@4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.2.0.tgz#c5a0691b3aba5d8b4ef5dffd6af3649608f167ba" + integrity sha512-1/qn1q1C1hGz6W/iEDm9DoyNoG/xdFDt78E3eZ5hHeUfJTLJgyAMdj9chL/cNBHjcjd+FH5aO1x0VCqR2RE0mw== + dependencies: + "@octokit/types" "^5.5.0" + deprecation "^2.3.1" + "@octokit/plugin-retry@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@octokit/plugin-retry/-/plugin-retry-2.2.0.tgz#11f3957a46ccdb7b7f33caabf8c17e57b25b80b2" @@ -2475,6 +2548,15 @@ dependencies: bottleneck "^2.15.3" +"@octokit/request-error@^2.0.0": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.0.2.tgz#0e76b83f5d8fdda1db99027ea5f617c2e6ba9ed0" + integrity sha512-2BrmnvVSV1MXQvEkrb9zwzP0wXFNbPJij922kYBTLIlIafukrGOb+ABBT2+c6wZiuyWDH1K1zmjGQ0toN/wMWw== + dependencies: + "@octokit/types" "^5.0.1" + deprecation "^2.0.0" + once "^1.4.0" + "@octokit/request@2.4.2", "@octokit/request@^2.1.2", "@octokit/request@^2.4.2": version "2.4.2" resolved "https://registry.yarnpkg.com/@octokit/request/-/request-2.4.2.tgz#87c36e820dd1e43b1629f4f35c95b00cd456320b" @@ -2487,6 +2569,20 @@ once "^1.4.0" universal-user-agent "^2.0.1" +"@octokit/request@^5.3.0", "@octokit/request@^5.4.0": + version "5.4.9" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.4.9.tgz#0a46f11b82351b3416d3157261ad9b1558c43365" + integrity sha512-CzwVvRyimIM1h2n9pLVYfTDmX9m+KHSgCpqPsY8F1NdEK8IaWqXhSBXsdjOBFZSpEcxNEeg4p0UO9cQ8EnOCLA== + dependencies: + "@octokit/endpoint" "^6.0.1" + "@octokit/request-error" "^2.0.0" + "@octokit/types" "^5.0.0" + deprecation "^2.0.0" + is-plain-object "^5.0.0" + node-fetch "^2.6.1" + once "^1.4.0" + universal-user-agent "^6.0.0" + "@octokit/rest@^16.23.2": version "16.23.2" resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-16.23.2.tgz#975e84610427c4ab6c41bec77c24aed9b7563db4" @@ -2505,6 +2601,23 @@ universal-user-agent "^2.0.0" url-template "^2.0.8" +"@octokit/rest@^18.0.6": + version "18.0.6" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.0.6.tgz#76c274f1a68f40741a131768ef483f041e7b98b6" + integrity sha512-ES4lZBKPJMX/yUoQjAZiyFjei9pJ4lTTfb9k7OtYoUzKPDLl/M8jiHqt6qeSauyU4eZGLw0sgP1WiQl9FYeM5w== + dependencies: + "@octokit/core" "^3.0.0" + "@octokit/plugin-paginate-rest" "^2.2.0" + "@octokit/plugin-request-log" "^1.0.0" + "@octokit/plugin-rest-endpoint-methods" "4.2.0" + +"@octokit/types@^5.0.0", "@octokit/types@^5.0.1", "@octokit/types@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-5.5.0.tgz#e5f06e8db21246ca102aa28444cdb13ae17a139b" + integrity sha512-UZ1pErDue6bZNjYOotCNveTXArOMZQFG6hKJfOnGnulVCMcVVi7YIIuuR4WfBhjo7zgpmzn/BkPDnUXtNx+PcQ== + dependencies: + "@types/node" ">= 8" + "@percy/agent@^0.26.0": version "0.26.0" resolved "https://registry.yarnpkg.com/@percy/agent/-/agent-0.26.0.tgz#9f06849d752df7368198835d0b5edc16c2d69a0c" @@ -4112,16 +4225,30 @@ "@types/node" "*" "@types/webpack" "*" -"@types/lodash@^4.14.159": - version "4.14.159" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.159.tgz#61089719dc6fdd9c5cb46efc827f2571d1517065" - integrity sha512-gF7A72f7WQN33DpqOWw9geApQPh4M3PxluMtaHxWHXEGSN12/WbcEk/eNSqWNQcQhF66VSZ06vCF94CrHwXJDg== +"@types/lodash.difference@^4.5.6": + version "4.5.6" + resolved "https://registry.yarnpkg.com/@types/lodash.difference/-/lodash.difference-4.5.6.tgz#41ec5c4e684eeacf543848a9a1b2a4856ccf9853" + integrity sha512-wXH53r+uoUCrKhmh7S5Gf6zo3vpsx/zH2R4pvkmDlmopmMTCROAUXDpPMXATGCWkCjE6ik3VZzZUxBgMjZho9Q== + dependencies: + "@types/lodash" "*" + +"@types/lodash.intersection@^4.4.6": + version "4.4.6" + resolved "https://registry.yarnpkg.com/@types/lodash.intersection/-/lodash.intersection-4.4.6.tgz#0fb241badf6edbb2a7d194a70c50e950e2486e68" + integrity sha512-6ewsKax7+HgT+7mEhzXT6tIyIHc/mjCwZJnarvLbCrtW21qmDQHWbaJj4Ht4DQDBmMdnvZe8APuVlsMpZ5E5mQ== + dependencies: + "@types/lodash" "*" -"@types/lodash@^4.14.160": +"@types/lodash@*", "@types/lodash@^4.14.160": version "4.14.161" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.161.tgz#a21ca0777dabc6e4f44f3d07f37b765f54188b18" integrity sha512-EP6O3Jkr7bXvZZSZYlsgt5DIjiGr0dXP1/jVEwVLTFgg0d+3lWVQkRavYVQszV7dYUwvg0B8R0MBDpcmXg7XIA== +"@types/lodash@^4.14.159": + version "4.14.159" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.159.tgz#61089719dc6fdd9c5cb46efc827f2571d1517065" + integrity sha512-gF7A72f7WQN33DpqOWw9geApQPh4M3PxluMtaHxWHXEGSN12/WbcEk/eNSqWNQcQhF66VSZ06vCF94CrHwXJDg== + "@types/log-symbols@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/log-symbols/-/log-symbols-2.0.0.tgz#7919e2ec3c8d13879bfdcab310dd7a3f7fc9466d" @@ -4269,7 +4396,7 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@8.10.54", "@types/node@>=10.17.17 <10.20.0", "@types/node@>=8.9.0", "@types/node@^12.0.2": +"@types/node@*", "@types/node@8.10.54", "@types/node@>= 8", "@types/node@>=10.17.17 <10.20.0", "@types/node@>=8.9.0", "@types/node@^12.0.2": version "10.17.26" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.26.tgz#a8a119960bff16b823be4c617da028570779bcfd" integrity sha512-myMwkO2Cr82kirHY8uknNRHEVtn0wV3DTQfkrjx17jmkstDRZ24gNUdl8AHXVyVclTYI/bNjgTPTAWvWLqXqkw== @@ -4423,13 +4550,6 @@ "@types/history" "*" "@types/react" "*" -"@types/react-beautiful-dnd@^12.1.1": - version "12.1.1" - resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-12.1.1.tgz#149e638c0f912eee6b74ea419b26bb43d0b1da60" - integrity sha512-CPKynKgGVRK+xmywLMD0qNWamdscxhgf1Um+2oEgN6Qibn1rye3M4p2bdxAMgtOTZ2L81bYl6KGKSzJVboJWeA== - dependencies: - "@types/react" "*" - "@types/react-beautiful-dnd@^13.0.0": version "13.0.0" resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#e60d3d965312fcf1516894af92dc3e9249587db4" @@ -7213,26 +7333,31 @@ bach@^1.0.0: async-settle "^1.0.0" now-and-later "^2.0.0" -backport@5.5.1: - version "5.5.1" - resolved "https://registry.yarnpkg.com/backport/-/backport-5.5.1.tgz#2eeddbdc4cfc0530119bdb2b0c3c30bc7ef574dd" - integrity sha512-vQuGrxxMx9H64ywqsIYUHL8+/xvPeP0nnBa0YQt5S+XqW7etaqOoa5dFW0c77ADdqjfLlGUIvtc2i6UrmqeFUQ== +backport@5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/backport/-/backport-5.6.0.tgz#6dcc0485e5eecf66bb6f950983fd0b018217ec20" + integrity sha512-wz7Ve3uslhGUMtHuctqIEtZFItTGKRRMiNANYso0iw1ar81ILsczDGgxeOlzmmnIQFi1ZvEs6lX3cgypGfef9A== dependencies: - axios "^0.19.2" + "@octokit/rest" "^18.0.6" + "@types/lodash.difference" "^4.5.6" + "@types/lodash.intersection" "^4.4.6" + axios "^0.19.0" dedent "^0.7.0" del "^5.1.0" - find-up "^4.1.0" - inquirer "^7.3.1" + find-up "^5.0.0" + inquirer "^7.3.3" + lodash.difference "^4.5.0" lodash.flatmap "^4.5.0" + lodash.intersection "^4.4.0" lodash.isempty "^4.4.0" lodash.isstring "^4.0.1" lodash.uniq "^4.5.0" make-dir "^3.1.0" - ora "^4.0.4" + ora "^5.1.0" safe-json-stringify "^1.2.0" - strip-json-comments "^3.1.0" + strip-json-comments "^3.1.1" winston "^3.3.3" - yargs "^15.4.0" + yargs "^16.0.3" bail@^1.0.0: version "1.0.2" @@ -7306,6 +7431,11 @@ before-after-hook@^1.4.0: resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-1.4.0.tgz#2b6bf23dca4f32e628fd2747c10a37c74a4b484d" integrity sha512-l5r9ir56nda3qu14nAXIlyq1MmUSs0meCIaFAh8HwkFwP1F8eToOuS3ah2VAHHcY04jaYD7FpJC5JTXHYRbkzg== +before-after-hook@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635" + integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A== + big-integer@^1.6.16: version "1.6.48" resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e" @@ -8622,10 +8752,10 @@ cli-spinners@^2.0.0: resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.1.0.tgz#22c34b4d51f573240885b201efda4e4ec9fff3c7" integrity sha512-8B00fJOEh1HPrx4fo5eW16XmE1PcL1tGpGrxy63CXGP9nHdPBN63X75hA1zhvQuhVztJWLqV58Roj2qlNM7cAA== -cli-spinners@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.2.0.tgz#e8b988d9206c692302d8ee834e7a85c0144d8f77" - integrity sha512-tgU3fKwzYjiLEQgPMD9Jt+JjHVL9kW93FiIMX/l7rivvOD4/LL0Mf7gda3+4U2KJBloybwgj5KEoQgGRioMiKQ== +cli-spinners@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.4.0.tgz#c6256db216b878cfba4720e719cec7cf72685d7f" + integrity sha512-sJAofoarcm76ZGpuooaO0eDy8saEy+YoZBLjC4h8srt4jeBnkYeOgqxgsJQTpyt2LjI5PTfLJHSL+41Yu4fEJA== cli-table3@0.5.1: version "0.5.1" @@ -8744,6 +8874,15 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" +cliui@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.1.tgz#a4cb67aad45cd83d8d05128fc9f4d8fbb887e6b3" + integrity sha512-rcvHOWyGyid6I1WjT/3NatKj2kDt9OdSHSXpyLXaMWFbKpGACNW8pRhhdPUq9MWUOdwn8Rz9AVETjF4105rZZQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + clone-buffer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" @@ -10639,6 +10778,11 @@ deprecation@^1.0.1: resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-1.0.1.tgz#2df79b79005752180816b7b6e079cbd80490d711" integrity sha512-ccVHpE72+tcIKaGMql33x5MAjKQIZrk+3x2GbJ7TeraUCZWHoT+KSZpoC+JQFsUBlSTXUrBaGiF0j6zVTepPLg== +deprecation@^2.0.0, deprecation@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" + integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== + des.js@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" @@ -11755,6 +11899,11 @@ es6-weak-map@^2.0.1, es6-weak-map@^2.0.2: es6-iterator "^2.0.1" es6-symbol "^3.1.1" +escalade@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.0.tgz#e8e2d7c7a8b76f6ee64c2181d6b8151441602d4e" + integrity sha512-mAk+hPSO8fLDkhV7V0dXazH5pDc6MrjBTPyD3VeKzxnVFjH1MIxbCdqGZB9O8+EwWakZs3ZCbDS4IpRt79V1ig== + escape-goat@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" @@ -13059,6 +13208,14 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + find-versions@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/find-versions/-/find-versions-2.0.0.tgz#2ad90d490f6828c1aa40292cf709ac3318210c3c" @@ -13694,7 +13851,7 @@ get-caller-file@^1.0.1: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== -get-caller-file@^2.0.1: +get-caller-file@^2.0.1, get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== @@ -16006,7 +16163,7 @@ inquirer@^6.0.0: strip-ansi "^5.1.0" through "^2.3.6" -inquirer@^7.0.0, inquirer@^7.3.1, inquirer@^7.3.3: +inquirer@^7.0.0, inquirer@^7.3.3: version "7.3.3" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== @@ -16677,6 +16834,11 @@ is-plain-object@3.0.0, is-plain-object@^3.0.0: dependencies: isobject "^4.0.0" +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + is-promise@^2.1, is-promise@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" @@ -18711,6 +18873,13 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + locutus@^2.0.5: version "2.0.10" resolved "https://registry.yarnpkg.com/locutus/-/locutus-2.0.10.tgz#f903619466a98a4ab76e8b87a5854b55a743b917" @@ -19011,7 +19180,7 @@ log-symbols@2.2.0, log-symbols@^2.0.0, log-symbols@^2.1.0, log-symbols@^2.2.0: dependencies: chalk "^2.0.1" -log-symbols@3.0.0, log-symbols@^3.0.0: +log-symbols@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== @@ -21460,16 +21629,16 @@ ora@^3.0.0: strip-ansi "^5.2.0" wcwidth "^1.0.1" -ora@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/ora/-/ora-4.0.4.tgz#e8da697cc5b6a47266655bf68e0fb588d29a545d" - integrity sha512-77iGeVU1cIdRhgFzCK8aw1fbtT1B/iZAvWjS+l/o1x0RShMgxHUZaD2yDpWsNCPwXg9z1ZA78Kbdvr8kBmG/Ww== +ora@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.1.0.tgz#b188cf8cd2d4d9b13fd25383bc3e5cba352c94f8" + integrity sha512-9tXIMPvjZ7hPTbk8DFq1f7Kow/HU/pQYB60JbNq+QnGwcyhWVZaQ4hM9zQDEsPxw/muLpgiHSaumUZxCAmod/w== dependencies: - chalk "^3.0.0" + chalk "^4.1.0" cli-cursor "^3.1.0" - cli-spinners "^2.2.0" + cli-spinners "^2.4.0" is-interactive "^1.0.0" - log-symbols "^3.0.0" + log-symbols "^4.0.0" mute-stream "0.0.8" strip-ansi "^6.0.0" wcwidth "^1.0.1" @@ -21640,6 +21809,13 @@ p-limit@^2.0.0, p-limit@^2.2.0, p-limit@^2.3.0: dependencies: p-try "^2.0.0" +p-limit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.0.2.tgz#1664e010af3cadc681baafd3e2a437be7b0fb5fe" + integrity sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg== + dependencies: + p-try "^2.0.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -21661,6 +21837,13 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + p-map@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" @@ -23280,19 +23463,6 @@ react-apollo@^2.1.4: lodash "^4.17.10" prop-types "^15.6.0" -react-beautiful-dnd@^12.2.0: - version "12.2.0" - resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-12.2.0.tgz#e5f6222f9e7934c6ed4ee09024547f9e353ae423" - integrity sha512-s5UrOXNDgeEC+sx65IgbeFlqKKgK3c0UfbrJLWufP34WBheyu5kJ741DtJbsSgPKyNLkqfswpMYr0P8lRj42cA== - dependencies: - "@babel/runtime-corejs2" "^7.6.3" - css-box-model "^1.2.0" - memoize-one "^5.1.1" - raf-schd "^4.0.2" - react-redux "^7.1.1" - redux "^4.0.4" - use-memo-one "^1.1.1" - react-beautiful-dnd@^13.0.0: version "13.0.0" resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#f70cc8ff82b84bc718f8af157c9f95757a6c3b40" @@ -26947,10 +27117,10 @@ strip-json-comments@^3.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== -strip-json-comments@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.0.tgz#7638d31422129ecf4457440009fba03f9f9ac180" - integrity sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w== +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== strip-json-comments@~1.0.1: version "1.0.4" @@ -28688,6 +28858,11 @@ universal-user-agent@^2.0.0, universal-user-agent@^2.0.1: dependencies: os-name "^3.0.0" +universal-user-agent@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" + integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== + universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -30272,6 +30447,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -30521,6 +30705,11 @@ y18n@^4.0.0: resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== +y18n@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.1.tgz#1ad2a7eddfa8bce7caa2e1f6b5da96c39d99d571" + integrity sha512-/jJ831jEs4vGDbYPQp4yGKDYPSCCEQ45uZWJHE1AoYBzqdZi8+LDWas0z4HrmJXmKdpFsTiowSHXdxyFhpmdMg== + yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" @@ -30582,6 +30771,11 @@ yargs-parser@^18.1.1, yargs-parser@^18.1.2, yargs-parser@^18.1.3: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^20.0.0: + version "20.2.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.0.tgz#944791ca2be2e08ddadd3d87e9de4c6484338605" + integrity sha512-2agPoRFPoIcFzOIp6656gcvsg2ohtscpw2OINr/q46+Sq41xz2OYLqx5HRHabmFU1OARIPAYH5uteICE7mn/5A== + yargs-unparser@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-1.6.0.tgz#ef25c2c769ff6bd09e4b0f9d7c605fb27846ea9f" @@ -30624,7 +30818,7 @@ yargs@13.3.2, yargs@^13.2.2, yargs@^13.3.0, yargs@^13.3.2: y18n "^4.0.0" yargs-parser "^13.1.2" -yargs@^15.0.2, yargs@^15.1.0, yargs@^15.3.1, yargs@^15.4.0, yargs@^15.4.1: +yargs@^15.0.2, yargs@^15.1.0, yargs@^15.3.1, yargs@^15.4.1: version "15.4.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== @@ -30641,6 +30835,19 @@ yargs@^15.0.2, yargs@^15.1.0, yargs@^15.3.1, yargs@^15.4.0, yargs@^15.4.1: y18n "^4.0.0" yargs-parser "^18.1.2" +yargs@^16.0.3: + version "16.0.3" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.0.3.tgz#7a919b9e43c90f80d4a142a89795e85399a7e54c" + integrity sha512-6+nLw8xa9uK1BOEOykaiYAJVh6/CjxWXK/q9b5FpRgNslt8s22F2xMBqVIKgCRjNgGvGPBy8Vog7WN7yh4amtA== + dependencies: + cliui "^7.0.0" + escalade "^3.0.2" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.1" + yargs-parser "^20.0.0" + yargs@^3.15.0: version "3.32.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995"