diff --git a/.eslintrc.js b/.eslintrc.js index b3a1274d1cbeb..dfb4603ba95af 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -185,31 +185,40 @@ module.exports = { zones: [ { target: [ - 'src/legacy/**/*', - 'x-pack/**/*', - '!x-pack/**/*.test.*', - '!x-pack/test/**/*', + '(src|x-pack)/legacy/**/*', '(src|x-pack)/plugins/**/(public|server)/**/*', - 'src/core/(public|server)/**/*', 'examples/**/*', ], from: [ 'src/core/public/**/*', - '!src/core/public/index.ts', - '!src/core/public/mocks.ts', - '!src/core/public/*.test.mocks.ts', + '!src/core/public/index.ts', // relative import + '!src/core/public/mocks{,.ts}', + '!src/core/server/types{,.ts}', '!src/core/public/utils/**/*', + '!src/core/public/*.test.mocks{,.ts}', 'src/core/server/**/*', - '!src/core/server/index.ts', - '!src/core/server/mocks.ts', - '!src/core/server/types.ts', - '!src/core/server/test_utils.ts', + '!src/core/server/index.ts', // relative import + '!src/core/server/mocks{,.ts}', + '!src/core/server/types{,.ts}', + '!src/core/server/test_utils', // for absolute imports until fixed in // https://github.com/elastic/kibana/issues/36096 - '!src/core/server/types', - '!src/core/server/*.test.mocks.ts', - + '!src/core/server/*.test.mocks{,.ts}', + ], + allowSameFolder: true, + errorMessage: + 'Plugins may only import from top-level public and server modules in core.', + }, + { + target: [ + '(src|x-pack)/legacy/**/*', + '(src|x-pack)/plugins/**/(public|server)/**/*', + 'examples/**/*', + '!(src|x-pack)/**/*.test.*', + '!(x-pack/)?test/**/*', + ], + from: [ '(src|x-pack)/plugins/**/(public|server)/**/*', '!(src|x-pack)/plugins/**/(public|server)/(index|mocks).{js,ts,tsx}', ], @@ -730,6 +739,19 @@ module.exports = { }, }, + /** + * Alerting Services overrides + */ + { + // typescript only for front and back end + files: [ + 'x-pack/{,legacy/}plugins/{alerting,alerting_builtins,actions,task_manager,event_log}/**/*.{ts,tsx}', + ], + rules: { + '@typescript-eslint/no-explicit-any': 'error', + }, + }, + /** * Lens overrides */ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c32bf8cbaa1c3..0d86726dca836 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -18,6 +18,7 @@ /src/plugins/vis_type_timelion/ @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/visualize/ @elastic/kibana-app /src/plugins/vis_type_timeseries/ @elastic/kibana-app /src/plugins/vis_type_metric/ @elastic/kibana-app diff --git a/.github/paths-labeller.yml b/.github/paths-labeller.yml index 544dd577313df..89e0af270c54d 100644 --- a/.github/paths-labeller.yml +++ b/.github/paths-labeller.yml @@ -8,6 +8,9 @@ - "Feature:ExpressionLanguage": - "src/plugins/expressions/**/*.*" - "src/plugins/bfetch/**/*.*" + - "Team:apm" + - "x-pack/plugins/apm/**/*.*" + - "x-pack/legacy/plugins/apm/**/*.*" - "Team:uptime": - "x-pack/plugins/uptime/**/*.*" - "x-pack/legacy/plugins/uptime/**/*.*" diff --git a/.i18nrc.json b/.i18nrc.json index d4286a7bd50e0..35ce745234346 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -14,7 +14,7 @@ "esUi": "src/plugins/es_ui_shared", "devTools": "src/plugins/dev_tools", "expressions": "src/plugins/expressions", - "inputControl": "src/legacy/core_plugins/input_control_vis", + "inputControl": "src/plugins/input_control_vis", "inspector": "src/plugins/inspector", "inspectorViews": "src/legacy/core_plugins/inspector_views", "interpreter": "src/legacy/core_plugins/interpreter", @@ -49,9 +49,9 @@ "visTypeMarkdown": "src/plugins/vis_type_markdown", "visTypeMetric": "src/plugins/vis_type_metric", "visTypeTable": "src/plugins/vis_type_table", - "visTypeTagCloud": "src/legacy/core_plugins/vis_type_tagcloud", + "visTypeTagCloud": "src/plugins/vis_type_tagcloud", "visTypeTimeseries": ["src/legacy/core_plugins/vis_type_timeseries", "src/plugins/vis_type_timeseries"], - "visTypeVega": "src/legacy/core_plugins/vis_type_vega", + "visTypeVega": "src/plugins/vis_type_vega", "visTypeVislib": "src/legacy/core_plugins/vis_type_vislib", "visTypeXy": "src/legacy/core_plugins/vis_type_xy", "visualizations": "src/plugins/visualizations", diff --git a/.sass-lint.yml b/.sass-lint.yml index 89735342a2d6f..9b31f3fae6d16 100644 --- a/.sass-lint.yml +++ b/.sass-lint.yml @@ -4,7 +4,6 @@ files: - 'src/legacy/core_plugins/timelion/**/*.s+(a|c)ss' - 'src/legacy/core_plugins/vis_type_vislib/**/*.s+(a|c)ss' - 'src/legacy/core_plugins/vis_type_xy/**/*.s+(a|c)ss' - - 'x-pack/legacy/plugins/rollup/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/security/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/canvas/**/*.s+(a|c)ss' - 'x-pack/plugins/triggers_actions_ui/**/*.s+(a|c)ss' diff --git a/Jenkinsfile b/Jenkinsfile index 6646ee15ba1c2..98cc6994f229b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -11,7 +11,7 @@ kibanaPipeline(timeoutMinutes: 135, checkPrChanges: true) { 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), 'x-pack-intake-agent': workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh'), 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ - // 'oss-firefoxSmoke': kibanaPipeline.functionalTestProcess('kibana-firefoxSmoke', './test/scripts/jenkins_firefox_smoke.sh'), + 'oss-firefoxSmoke': kibanaPipeline.functionalTestProcess('kibana-firefoxSmoke', './test/scripts/jenkins_firefox_smoke.sh'), 'oss-ciGroup1': kibanaPipeline.ossCiGroupProcess(1), 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2), 'oss-ciGroup3': kibanaPipeline.ossCiGroupProcess(3), @@ -28,7 +28,7 @@ kibanaPipeline(timeoutMinutes: 135, checkPrChanges: true) { // 'oss-visualRegression': kibanaPipeline.functionalTestProcess('visualRegression', './test/scripts/jenkins_visual_regression.sh'), ]), 'kibana-xpack-agent': workers.functional('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [ - // 'xpack-firefoxSmoke': kibanaPipeline.functionalTestProcess('xpack-firefoxSmoke', './test/scripts/jenkins_xpack_firefox_smoke.sh'), + 'xpack-firefoxSmoke': kibanaPipeline.functionalTestProcess('xpack-firefoxSmoke', './test/scripts/jenkins_xpack_firefox_smoke.sh'), 'xpack-ciGroup1': kibanaPipeline.xpackCiGroupProcess(1), 'xpack-ciGroup2': kibanaPipeline.xpackCiGroupProcess(2), 'xpack-ciGroup3': kibanaPipeline.xpackCiGroupProcess(3), diff --git a/docs/developer/core/development-functional-tests.asciidoc b/docs/developer/core/development-functional-tests.asciidoc index 51b5273851ce7..2b091d9abb9fc 100644 --- a/docs/developer/core/development-functional-tests.asciidoc +++ b/docs/developer/core/development-functional-tests.asciidoc @@ -154,16 +154,16 @@ A test suite is a collection of tests defined by calling `describe()`, and then Use tags in `describe()` function to group functional tests. Tags include: * `ciGroup{id}` - Assigns test suite to a specific CI worker * `skipCloud` and `skipFirefox` - Excludes test suite from running on Cloud or Firefox -* `smoke` - Groups tests that run on Chrome and Firefox +* `includeFirefox` - Groups tests that run on Chrome and Firefox **Cross-browser testing**::: -On CI, all the functional tests are executed in Chrome by default. To also run a suite against Firefox, assign the `smoke` tag: +On CI, all the functional tests are executed in Chrome by default. To also run a suite against Firefox, assign the `includeFirefox` tag: ["source","js"] ----------- // on CI test suite will be run twice: in Chrome and Firefox describe('My Cross-browser Test Suite', function () { - this.tags('smoke'); + this.tags('includeFirefox'); it('My First Test'); } diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 5450e84417f89..a91a5bec988b7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -263,13 +263,14 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectAttribute](./kibana-plugin-core-server.savedobjectattribute.md) | Type definition for a Saved Object attribute value | | [SavedObjectAttributeSingle](./kibana-plugin-core-server.savedobjectattributesingle.md) | Don't use this type, it's simply a helper type for [SavedObjectAttribute](./kibana-plugin-core-server.savedobjectattribute.md) | | [SavedObjectMigrationFn](./kibana-plugin-core-server.savedobjectmigrationfn.md) | A migration function for a [saved object type](./kibana-plugin-core-server.savedobjectstype.md) used to migrate it to a given version | -| [SavedObjectSanitizedDoc](./kibana-plugin-core-server.savedobjectsanitizeddoc.md) | | -| [SavedObjectsClientContract](./kibana-plugin-core-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.\#\#\# 503s from missing indexUnlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's action.auto_create_index setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated.See [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) | +| [SavedObjectSanitizedDoc](./kibana-plugin-core-server.savedobjectsanitizeddoc.md) | Describes Saved Object documents that have passed through the migration framework and are guaranteed to have a references root property. | +| [SavedObjectsClientContract](./kibana-plugin-core-server.savedobjectsclientcontract.md) | Saved Objects is Kibana's data persisentence mechanism allowing plugins to use Elasticsearch for storing plugin state.\#\# SavedObjectsClient errorsSince the SavedObjectsClient has its hands in everything we are a little paranoid about the way we present errors back to to application code. Ideally, all errors will be either:1. Caused by bad implementation (ie. undefined is not a function) and as such unpredictable 2. An error that has been classified and decorated appropriately by the decorators in [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md)Type 1 errors are inevitable, but since all expected/handle-able errors should be Type 2 the isXYZError() helpers exposed at SavedObjectsErrorHelpers should be used to understand and manage error responses from the SavedObjectsClient.Type 2 errors are decorated versions of the source error, so if the elasticsearch client threw an error it will be decorated based on its type. That means that rather than looking for error.body.error.type or doing substring checks on error.body.error.reason, just use the helpers to understand the meaning of the error:\`\`\`js if (SavedObjectsErrorHelpers.isNotFoundError(error)) { // handle 404 }if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) { // 401 handling should be automatic, but in case you wanted to know }// always rethrow the error unless you handle it throw error; \`\`\`\#\#\# 404s from missing indexFrom the perspective of application code and APIs the SavedObjectsClient is a black box that persists objects. One of the internal details that users have no control over is that we use an elasticsearch index for persistance and that index might be missing.At the time of writing we are in the process of transitioning away from the operating assumption that the SavedObjects index is always available. Part of this transition is handling errors resulting from an index missing. These used to trigger a 500 error in most cases, and in others cause 404s with different error messages.From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing.See [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) | | [SavedObjectsClientFactory](./kibana-plugin-core-server.savedobjectsclientfactory.md) | Describes the factory used to create instances of the Saved Objects Client. | | [SavedObjectsClientFactoryProvider](./kibana-plugin-core-server.savedobjectsclientfactoryprovider.md) | Provider to invoke to retrieve a [SavedObjectsClientFactory](./kibana-plugin-core-server.savedobjectsclientfactory.md). | | [SavedObjectsClientWrapperFactory](./kibana-plugin-core-server.savedobjectsclientwrapperfactory.md) | Describes the factory used to create instances of Saved Objects Client Wrappers. | | [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) | Describe a [saved object type mapping](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) field.Please refer to [elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html) For the mapping documentation | | [SavedObjectsNamespaceType](./kibana-plugin-core-server.savedobjectsnamespacetype.md) | The namespace type dictates how a saved object can be interacted in relation to namespaces. Each type is mutually exclusive: \* single (default): this type of saved object is namespace-isolated, e.g., it exists in only one namespace. \* multiple: this type of saved object is shareable, e.g., it can exist in one or more namespaces. \* agnostic: this type of saved object is global.Note: do not write logic that uses this value directly; instead, use the appropriate accessors in the [type registry](./kibana-plugin-core-server.savedobjecttyperegistry.md). | +| [SavedObjectUnsanitizedDoc](./kibana-plugin-core-server.savedobjectunsanitizeddoc.md) | Describes Saved Object documents from Kibana < 7.0.0 which don't have a references root property defined. This type should only be used in migrations. | | [ScopeableRequest](./kibana-plugin-core-server.scopeablerequest.md) | A user credentials container. It accommodates the necessary auth credentials to impersonate the current user.See [KibanaRequest](./kibana-plugin-core-server.kibanarequest.md). | | [ServiceStatusLevel](./kibana-plugin-core-server.servicestatuslevel.md) | A convenience type that represents the union of each value in [ServiceStatusLevels](./kibana-plugin-core-server.servicestatuslevels.md). | | [SharedGlobalConfig](./kibana-plugin-core-server.sharedglobalconfig.md) | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsanitizeddoc.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsanitizeddoc.md index 47feb50e9a827..6d4e252fe7532 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsanitizeddoc.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsanitizeddoc.md @@ -4,6 +4,7 @@ ## SavedObjectSanitizedDoc type +Describes Saved Object documents that have passed through the migration framework and are guaranteed to have a `references` root property. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclientcontract.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclientcontract.md index c39df0655f1d4..610356a733126 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsclientcontract.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsclientcontract.md @@ -30,10 +30,6 @@ At the time of writing we are in the process of transitioning away from the oper From my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The object the request/call was targeting could not be found. This is why \#14141 takes special care to ensure that 404 errors are generic and don't distinguish between index missing or document missing. -\#\#\# 503s from missing index - -Unlike all other methods, create requests are supposed to succeed even when the Kibana index does not exist because it will be automatically created by elasticsearch. When that is not the case it is because Elasticsearch's `action.auto_create_index` setting prevents it from being created automatically so we throw a special 503 with the intention of informing the user that their Elasticsearch settings need to be updated. - See [SavedObjectsClient](./kibana-plugin-core-server.savedobjectsclient.md) See [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createesautocreateindexerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createesautocreateindexerror.md deleted file mode 100644 index 6350afacee2ba..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.createesautocreateindexerror.md +++ /dev/null @@ -1,15 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [createEsAutoCreateIndexError](./kibana-plugin-core-server.savedobjectserrorhelpers.createesautocreateindexerror.md) - -## SavedObjectsErrorHelpers.createEsAutoCreateIndexError() method - -Signature: - -```typescript -static createEsAutoCreateIndexError(): DecoratedError; -``` -Returns: - -`DecoratedError` - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isesautocreateindexerror.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isesautocreateindexerror.md deleted file mode 100644 index bdffff5c1365b..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.isesautocreateindexerror.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) > [isEsAutoCreateIndexError](./kibana-plugin-core-server.savedobjectserrorhelpers.isesautocreateindexerror.md) - -## SavedObjectsErrorHelpers.isEsAutoCreateIndexError() method - -Signature: - -```typescript -static isEsAutoCreateIndexError(error: Error | DecoratedError): boolean; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| error | Error | DecoratedError | | - -Returns: - -`boolean` - diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md index 250b9d3899670..7874be311d52c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectserrorhelpers.md @@ -17,7 +17,6 @@ export declare class SavedObjectsErrorHelpers | --- | --- | --- | | [createBadRequestError(reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.createbadrequesterror.md) | static | | | [createConflictError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.createconflicterror.md) | static | | -| [createEsAutoCreateIndexError()](./kibana-plugin-core-server.savedobjectserrorhelpers.createesautocreateindexerror.md) | static | | | [createGenericNotFoundError(type, id)](./kibana-plugin-core-server.savedobjectserrorhelpers.creategenericnotfounderror.md) | static | | | [createInvalidVersionError(versionInput)](./kibana-plugin-core-server.savedobjectserrorhelpers.createinvalidversionerror.md) | static | | | [createUnsupportedTypeError(type)](./kibana-plugin-core-server.savedobjectserrorhelpers.createunsupportedtypeerror.md) | static | | @@ -31,7 +30,6 @@ export declare class SavedObjectsErrorHelpers | [decorateRequestEntityTooLargeError(error, reason)](./kibana-plugin-core-server.savedobjectserrorhelpers.decoraterequestentitytoolargeerror.md) | static | | | [isBadRequestError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isbadrequesterror.md) | static | | | [isConflictError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isconflicterror.md) | static | | -| [isEsAutoCreateIndexError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isesautocreateindexerror.md) | static | | | [isEsCannotExecuteScriptError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isescannotexecutescripterror.md) | static | | | [isEsUnavailableError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isesunavailableerror.md) | static | | | [isForbiddenError(error)](./kibana-plugin-core-server.savedobjectserrorhelpers.isforbiddenerror.md) | static | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectunsanitizeddoc.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectunsanitizeddoc.md new file mode 100644 index 0000000000000..be51400addbbc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectunsanitizeddoc.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectUnsanitizedDoc](./kibana-plugin-core-server.savedobjectunsanitizeddoc.md) + +## SavedObjectUnsanitizedDoc type + +Describes Saved Object documents from Kibana < 7.0.0 which don't have a `references` root property defined. This type should only be used in migrations. + +Signature: + +```typescript +export declare type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md deleted file mode 100644 index 2ef8c797f4054..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) > [enabled](./kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md) - -## AggConfigOptions.enabled property - -Signature: - -```typescript -enabled?: boolean; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.id.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.id.md deleted file mode 100644 index 8939854ab19ca..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.id.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) > [id](./kibana-plugin-plugins-data-public.aggconfigoptions.id.md) - -## AggConfigOptions.id property - -Signature: - -```typescript -id?: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.md index b841d9b04d6a7..ff8055b8cf1b1 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.md @@ -2,21 +2,12 @@ [Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) -## AggConfigOptions interface +## AggConfigOptions type Signature: ```typescript -export interface AggConfigOptions +export declare type AggConfigOptions = Assign; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [enabled](./kibana-plugin-plugins-data-public.aggconfigoptions.enabled.md) | boolean | | -| [id](./kibana-plugin-plugins-data-public.aggconfigoptions.id.md) | string | | -| [params](./kibana-plugin-plugins-data-public.aggconfigoptions.params.md) | Record<string, any> | | -| [schema](./kibana-plugin-plugins-data-public.aggconfigoptions.schema.md) | string | | -| [type](./kibana-plugin-plugins-data-public.aggconfigoptions.type.md) | IAggType | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.params.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.params.md deleted file mode 100644 index 45219a837cc33..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.params.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) > [params](./kibana-plugin-plugins-data-public.aggconfigoptions.params.md) - -## AggConfigOptions.params property - -Signature: - -```typescript -params?: Record; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.schema.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.schema.md deleted file mode 100644 index b2b42eb2e5b4d..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.schema.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) > [schema](./kibana-plugin-plugins-data-public.aggconfigoptions.schema.md) - -## AggConfigOptions.schema property - -Signature: - -```typescript -schema?: string; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.type.md deleted file mode 100644 index 866065ce52ba6..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigoptions.type.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) > [type](./kibana-plugin-plugins-data-public.aggconfigoptions.type.md) - -## AggConfigOptions.type property - -Signature: - -```typescript -type: IAggType; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.makeagg.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.makeagg.md index 43f30d73ca6df..a91db7e7aac8b 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.makeagg.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.makeagg.md @@ -7,5 +7,5 @@ Signature: ```typescript -makeAgg: (agg: TAggConfig, state?: any) => TAggConfig; +makeAgg: (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.md index b75065da91abd..f9733529a315d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggparamtype.md @@ -21,5 +21,5 @@ export declare class AggParamType ex | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [allowedAggs](./kibana-plugin-plugins-data-public.aggparamtype.allowedaggs.md) | | string[] | | -| [makeAgg](./kibana-plugin-plugins-data-public.aggparamtype.makeagg.md) | | (agg: TAggConfig, state?: any) => TAggConfig | | +| [makeAgg](./kibana-plugin-plugins-data-public.aggparamtype.makeagg.md) | | (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.baseformatterspublic.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.baseformatterspublic.md index 50e8f2409ac02..ddbf1a8459d1f 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.baseformatterspublic.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.baseformatterspublic.md @@ -7,5 +7,5 @@ Signature: ```typescript -baseFormattersPublic: (import("../../common").IFieldFormatType | typeof DateFormat)[] +baseFormattersPublic: (import("../../common").FieldFormatInstanceType | typeof DateFormat)[] ``` 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 5ef317a57fb0e..604ac5120922b 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 @@ -49,7 +49,6 @@ | Interface | Description | | --- | --- | -| [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) | | | [AggParamOption](./kibana-plugin-plugins-data-public.aggparamoption.md) | | | [DataPublicPluginSetup](./kibana-plugin-plugins-data-public.datapublicpluginsetup.md) | | | [DataPublicPluginStart](./kibana-plugin-plugins-data-public.datapublicpluginstart.md) | | @@ -118,6 +117,7 @@ | Type Alias | Description | | --- | --- | +| [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) | | | [AggParam](./kibana-plugin-plugins-data-public.aggparam.md) | | | [CustomFilter](./kibana-plugin-plugins-data-public.customfilter.md) | | | [EsQuerySortValue](./kibana-plugin-plugins-data-public.esquerysortvalue.md) | | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md index 5c2f542204079..4d7a0b3cfbbca 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.setup.md @@ -9,7 +9,7 @@ ```typescript setup(core: CoreSetup, { usageCollection }: DataPluginSetupDependencies): { fieldFormats: { - register: (customFieldFormat: import("../common").IFieldFormatType) => number; + register: (customFieldFormat: import("../common").FieldFormatInstanceType) => number; }; search: ISearchSetup; }; @@ -26,7 +26,7 @@ setup(core: CoreSetup, { usageCollection }: DataPluginSetupDependencies): { `{ fieldFormats: { - register: (customFieldFormat: import("../common").IFieldFormatType) => number; + register: (customFieldFormat: import("../common").FieldFormatInstanceType) => number; }; search: ISearchSetup; }` diff --git a/docs/images/report-automate-csv.png b/docs/images/report-automate-csv.png new file mode 100644 index 0000000000000..fba77821ae29f Binary files /dev/null and b/docs/images/report-automate-csv.png differ diff --git a/docs/images/report-automate-pdf.png b/docs/images/report-automate-pdf.png new file mode 100644 index 0000000000000..f96eebe6fe02d Binary files /dev/null and b/docs/images/report-automate-pdf.png differ diff --git a/docs/logs/images/logs-action-menu.png b/docs/logs/images/logs-action-menu.png new file mode 100644 index 0000000000000..f1c79b6fa88d1 Binary files /dev/null and b/docs/logs/images/logs-action-menu.png differ diff --git a/docs/logs/images/logs-view-in-context.png b/docs/logs/images/logs-view-in-context.png new file mode 100644 index 0000000000000..09a9e89fc3042 Binary files /dev/null and b/docs/logs/images/logs-view-in-context.png differ diff --git a/docs/logs/using.asciidoc b/docs/logs/using.asciidoc index 96e7c5fa4d312..4f5992f945c7a 100644 --- a/docs/logs/using.asciidoc +++ b/docs/logs/using.asciidoc @@ -69,12 +69,19 @@ To highlight a word or phrase in the logs stream, click *Highlights* and enter y [float] [[logs-event-inspector]] === Inspect a log event -To inspect a log event, hover over it, then click the *View details* icon image:logs/images/logs-view-event.png[View event icon] beside the event. -This opens the *Log event document details* fly-out that shows the fields associated with the log event. +To inspect a log event, hover over it, then click the *View actions for line* icon image:logs/images/logs-action-menu.png[View actions for line icon]. On the menu that opens, select *View details*. This opens the *Log event document details* fly-out that shows the fields associated with the log event. To quickly filter the logs stream by one of the field values, in the log event details, click the *View event with filter* icon image:logs/images/logs-view-event-with-filter.png[View event icon] beside the field. This automatically adds a search filter to the logs stream to filter the entries by this field and value. +[float] +[[log-view-in-context]] +=== View log line in context +To view a certain line in its context (for example, with other log lines from the same file, or the same cloud container), hover over it, then click the *View actions for line* image:logs/images/logs-action-menu.png[View actions for line icon]. On the menu that opens, select *View in context*. This opens the *View log in context* modal, that shows the log line in its context. + +[role="screenshot"] +image::logs/images/logs-view-in-context.png[View a log line in context] + [float] [[view-log-anomalies]] === View log anomalies diff --git a/docs/user/alerting/action-types.asciidoc b/docs/user/alerting/action-types.asciidoc index 49e7bd1d77743..8794c389d72bc 100644 --- a/docs/user/alerting/action-types.asciidoc +++ b/docs/user/alerting/action-types.asciidoc @@ -41,12 +41,14 @@ see https://www.elastic.co/subscriptions[the subscription page]. [float] [[create-connectors]] -=== Connectors +=== Preconfigured connectors and action types You can create connectors for actions in <> or via the action API. For out-of-the-box and standardized connectors, you can <> before {kib} starts. +Action type with only preconfigured connectors could be specified as a <>. + include::action-types/email.asciidoc[] include::action-types/index.asciidoc[] include::action-types/pagerduty.asciidoc[] @@ -54,3 +56,4 @@ include::action-types/server-log.asciidoc[] include::action-types/slack.asciidoc[] include::action-types/webhook.asciidoc[] include::pre-configured-connectors.asciidoc[] +include::pre-configured-action-types.asciidoc[] diff --git a/docs/user/alerting/images/pre-configured-action-type-alert-form.png b/docs/user/alerting/images/pre-configured-action-type-alert-form.png new file mode 100644 index 0000000000000..e12bad468009a Binary files /dev/null and b/docs/user/alerting/images/pre-configured-action-type-alert-form.png differ diff --git a/docs/user/alerting/images/pre-configured-action-type-managing.png b/docs/user/alerting/images/pre-configured-action-type-managing.png new file mode 100644 index 0000000000000..95fe1c6aa0958 Binary files /dev/null and b/docs/user/alerting/images/pre-configured-action-type-managing.png differ diff --git a/docs/user/alerting/images/pre-configured-action-type-select-type.png b/docs/user/alerting/images/pre-configured-action-type-select-type.png new file mode 100644 index 0000000000000..5f555f851cd81 Binary files /dev/null and b/docs/user/alerting/images/pre-configured-action-type-select-type.png differ diff --git a/docs/user/alerting/pre-configured-action-types.asciidoc b/docs/user/alerting/pre-configured-action-types.asciidoc new file mode 100644 index 0000000000000..780a2119037b1 --- /dev/null +++ b/docs/user/alerting/pre-configured-action-types.asciidoc @@ -0,0 +1,61 @@ +[role="xpack"] +[[pre-configured-action-types]] + +== Preconfigured action types + +A preconfigure an action type has all the information it needs prior to startup. +A preconfigured action type offers the following capabilities: + +- Requires no setup. Configuration and credentials needed to execute an +action are predefined. +- Has only <>. +- Connectors of the preconfigured action type cannot be edited or deleted. + +[float] +[[preconfigured-action-type-example]] +=== Creating a preconfigured action + +In the `kibana.yml` file: + +. Exclude the action type from `xpack.actions.enabledActionTypes`. +. Add all its connectors. + +The following example shows a valid configuration of preconfigured action type with one out-of-the box connector. + +```js + xpack.actions.enabledActionTypes: ['.slack', '.email', '.index'] <1> + xpack.actions.preconfigured: <2> + - id: 'my-server-log' + actionTypeId: .server-log + name: 'Server log #xyz' +``` + +<1> `enabledActionTypes` should exclude preconfigured action type to prevent creating and deleting connectors. +<2> `preconfigured` is the setting for defining the list of available connectors for the preconfigured action type. + +[float] +[[pre-configured-action-type-alert-form]] +=== Attaching a preconfigured action to an alert + +To attach an action to an alert, +select from a list of available action types, and +then select the *Server log* type. This action type was configured previously. + +[role="screenshot"] +image::images/pre-configured-action-type-alert-form.png[Create alert with selected Server log action type] + +[float] +[[managing-pre-configured-action-types]] +=== Managing preconfigured actions + +Connectors with preconfigured actions appear in the connector list, regardless of which space the user is in. +They are tagged as “preconfigured” and cannot be deleted. + +[role="screenshot"] +image::images/pre-configured-action-type-managing.png[Connectors managing tab with pre-cofigured] + +Clicking *Create connector* shows the list of available action types. +Preconfigured action types are not included because you can't create a connector with a preconfigured action type. + +[role="screenshot"] +image::images/pre-configured-action-type-select-type.png[Pre-configured connector create menu] diff --git a/docs/user/alerting/pre-configured-connectors.asciidoc b/docs/user/alerting/pre-configured-connectors.asciidoc index 3db13acfb423e..4c408da92f579 100644 --- a/docs/user/alerting/pre-configured-connectors.asciidoc +++ b/docs/user/alerting/pre-configured-connectors.asciidoc @@ -20,8 +20,7 @@ action are predefined, including the connector name and ID. The following example shows a valid configuration 2 out-of-the box connector. -[source,console] ------------------------- +```js xpack.actions.preconfigured: - id: 'my-slack1' <1> actionTypeId: .slack <2> @@ -40,7 +39,7 @@ The following example shows a valid configuration 2 out-of-the box connector. secrets: <5> user: elastic password: changeme ------------------------- +``` <1> `id` is the action connector identifier. <2> `actionTypeId` is the action type identifier. diff --git a/docs/user/reporting/automating-report-generation.asciidoc b/docs/user/reporting/automating-report-generation.asciidoc index 5d35f103ecee0..3e227229ddcc5 100644 --- a/docs/user/reporting/automating-report-generation.asciidoc +++ b/docs/user/reporting/automating-report-generation.asciidoc @@ -1,32 +1,42 @@ [role="xpack"] [[automating-report-generation]] == Automating report generation -You can automatically generate reports with {watcher}, or by submitting -HTTP `POST` requests from a script. +Automatically generate PDF and CSV reports by submitting HTTP `POST` requests using {watcher} or a script. include::report-intervals.asciidoc[] [float] -=== Get the POST URL +=== Create a POST URL -Generating a report either through {watcher} or a script requires capturing the **POST -URL**, which is the URL to queue a report for generation. +Create the POST +URL that triggers a report to generate. -To get the URL for triggering PDF report generation during a given time period: +To create the POST URL for PDF reports: -. Load the saved object in *Visualize* or *Dashboard*. -. To specify a relative or absolute time period, use the time filter. -. In the {kib} toolbar, click *Share*. -. Select *PDF Reports*. -. Click **Copy POST URL**. +. Go to *Visualize* or *Dashboard*, then open the visualization or dashboard. ++ +To specify a relative or absolute time period, use the time filter. -To get the URL for triggering CSV report generation during a given time period: +. From the {kib} toolbar, click *Share*, then select *PDF Reports*. + +. Click *Copy POST URL*. ++ +[role="screenshot"] +image::images/report-automate-pdf.png[Generate Visualize and Dashboard reports] + + +To create the POST URL for CSV reports: . Load the saved search in *Discover*. -. To specify a relative or absolute time period, use the time filter. -. In the {kib} toolbar, click *Share*. -. Select *CSV Reports*. -. Click **Copy POST URL**. ++ +To specify a relative or absolute time period, use the time filter. + +. From the {kib} toolbar, click *Share*, then select *CSV Reports*. + +. Click *Copy POST URL*. ++ +[role="screenshot"] +image::images/report-automate-csv.png[Generate Discover reports] [float] === Use Watcher diff --git a/docs/visualize/color-picker.asciidoc b/docs/visualize/color-picker.asciidoc deleted file mode 100644 index e0f23262068d3..0000000000000 --- a/docs/visualize/color-picker.asciidoc +++ /dev/null @@ -1,4 +0,0 @@ -You can customize the colors of your visualization by clicking the color dot next to each label to display the -_color picker_. - -image::images/color-picker.png[An array of color dots that users can select] diff --git a/docs/visualize/lens.asciidoc b/docs/visualize/lens.asciidoc index b181763c0d0d0..422afbb201183 100644 --- a/docs/visualize/lens.asciidoc +++ b/docs/visualize/lens.asciidoc @@ -14,20 +14,6 @@ beta[] * Save your visualization for use in a dashboard. -[float] -[[lens-aggregation]] -=== Supported aggregations - -Lens supports the following aggregations: - -* <> - -* <> - -* <> - -* <> - [float] [[drag-drop]] === Drag and drop diff --git a/docs/visualize/most-frequent.asciidoc b/docs/visualize/most-frequent.asciidoc index ba291e3cc6859..f716930e7e65c 100644 --- a/docs/visualize/most-frequent.asciidoc +++ b/docs/visualize/most-frequent.asciidoc @@ -13,20 +13,6 @@ The most frequently used visualizations include: [[metric-chart]] -[float] -[[frequently-used-viz-aggregation]] -=== Supported aggregations - -The most frequently used visualizations support the following aggregations: - -* <> - -* <> - -* <> - -* <> - [float] === Configure your visualization diff --git a/docs/visualize/tilemap.asciidoc b/docs/visualize/tilemap.asciidoc index 51342847080e0..c889bd0bb6ca0 100644 --- a/docs/visualize/tilemap.asciidoc +++ b/docs/visualize/tilemap.asciidoc @@ -6,19 +6,6 @@ Display graphical representations of data where the individual values are repres [role="screenshot"] image::images/visualize_heat_map_example.png[] -[float] -[[build-heat-map]] -=== Build a heat map - -To display your data on the heat map, use the supported aggregations. - -Heat maps support the following aggregations: - -* <> -* <> -* <> -* <> - [float] [[navigate-heatmap]] === Change the color ranges diff --git a/docs/visualize/tsvb.asciidoc b/docs/visualize/tsvb.asciidoc index 69d6985acd1e4..36709c2cc6437 100644 --- a/docs/visualize/tsvb.asciidoc +++ b/docs/visualize/tsvb.asciidoc @@ -43,18 +43,6 @@ Table:: Display data from multiple time series by defining the field group to sh [role="screenshot"] image:images/tsvb-table.png["Table visualization"] -[float] -[[tsvb-aggregation]] -=== Supported aggregations - -TSVB supports the following aggregations: - -* <> - -* <> - -* <> - [float] [[create-tsvb-visualization]] === Create TSVB visualizations diff --git a/docs/visualize/visualize_rollup_data.asciidoc b/docs/visualize/visualize_rollup_data.asciidoc deleted file mode 100644 index 481cbc6e39418..0000000000000 --- a/docs/visualize/visualize_rollup_data.asciidoc +++ /dev/null @@ -1,43 +0,0 @@ -[role="xpack"] -[[visualize-rollup-data]] -== Use rolled up data in a visualization - -beta[] - -You can visualize your rolled up data in a variety of charts, tables, maps, and -more. Most visualizations support rolled up data, with the exception of -Timelion and Vega visualizations. - -To get started, go to *Management > Kibana > Index patterns.* -If a rollup index is detected in the cluster, *Create index pattern* -includes an item for creating a rollup index pattern. - -[role="screenshot"] -image::images/management_create_rollup_menu.png[Create index pattern menu] - -You can match an index pattern to only rolled up data, or mix both rolled up -and raw data to visualize all data together. An index pattern can match only one -rolled up index, not multiple. There is no restriction on the number of standard -indices that an index pattern can match. When matching multiple indices, -use a comma to separate the names, with no space after the comma. - -Keep the following in mind when creating a visualization from rolled up data: - -* The data in a rollup index only has summarized metrics for specific fields. -You can’t search any other field from the original raw data. -* Data is summarized into time buckets that might be split into sub buckets for -numeric field values or terms. You can ask for a time aggregation that takes -several time buckets and combines them to lower granularity. For example, -if the rollup job was aggregated by hours, you can ask for buckets of days. - -The following visualization of rolled up data shows the date histogram -interval multiple and the limited metrics aggregations. - -[role="screenshot"] -image::images/management_rollups_visualization.png[][Rollups in visualizations] - -Dashboards can have a mixture of rollup visualizations and regular visualizations, -as shown in the following figure. Note that not all queries and filters support rollups. - -[role="screenshot"] -image::images/management_rolled_dashboard.png[][Rollups in dashboards] diff --git a/package.json b/package.json index a7729b6dab7a1..bfea8c7423f9c 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "**/@types/hapi": "^17.0.18", "**/@types/angular": "^1.6.56", "**/@types/hoist-non-react-statics": "^3.3.1", + "**/@types/chai": "^4.2.11", "**/typescript": "3.7.2", "**/graphql-toolkit/lodash": "^4.17.13", "**/hoist-non-react-statics": "^3.3.2", @@ -120,7 +121,7 @@ "@babel/core": "^7.9.0", "@babel/register": "^7.9.0", "@elastic/apm-rum": "^5.1.1", - "@elastic/charts": "18.3.0", + "@elastic/charts": "18.4.2", "@elastic/datemath": "5.0.3", "@elastic/ems-client": "7.8.0", "@elastic/eui": "22.3.0", @@ -185,7 +186,7 @@ "glob-all": "^3.2.1", "globby": "^8.0.1", "h2o2": "^8.1.2", - "handlebars": "4.5.3", + "handlebars": "4.7.6", "hapi": "^17.5.3", "hapi-auth-cookie": "^9.0.0", "history": "^4.9.0", @@ -441,7 +442,7 @@ "gulp-babel": "^8.0.0", "gulp-sourcemaps": "2.6.5", "has-ansi": "^3.0.0", - "iedriver": "^3.14.1", + "iedriver": "^3.14.2", "intl-messageformat-parser": "^1.4.0", "is-path-inside": "^2.1.0", "istanbul-instrumenter-loader": "3.0.1", @@ -450,13 +451,13 @@ "jest-raw-loader": "^1.0.1", "jimp": "^0.9.6", "json5": "^1.0.1", - "karma": "3.1.4", + "karma": "5.0.2", "karma-chrome-launcher": "2.2.0", "karma-coverage": "1.1.2", "karma-firefox-launcher": "1.1.0", "karma-ie-launcher": "1.0.0", "karma-junit-reporter": "1.2.0", - "karma-mocha": "1.3.0", + "karma-mocha": "2.0.0", "karma-safari-launcher": "1.0.0", "license-checker": "^16.0.0", "listr": "^0.14.1", diff --git a/packages/kbn-babel-preset/package.json b/packages/kbn-babel-preset/package.json index b82c8d0fac897..1a2f6941c2020 100644 --- a/packages/kbn-babel-preset/package.json +++ b/packages/kbn-babel-preset/package.json @@ -15,6 +15,7 @@ "babel-plugin-add-module-exports": "^1.0.2", "babel-plugin-filter-imports": "^3.0.0", "babel-plugin-styled-components": "^1.10.6", - "babel-plugin-transform-define": "^1.3.1" + "babel-plugin-transform-define": "^1.3.1", + "babel-plugin-transform-imports": "^2.0.0" } } diff --git a/packages/kbn-babel-preset/webpack_preset.js b/packages/kbn-babel-preset/webpack_preset.js index d76a3e9714838..2c1129f275bfe 100644 --- a/packages/kbn-babel-preset/webpack_preset.js +++ b/packages/kbn-babel-preset/webpack_preset.js @@ -42,5 +42,24 @@ module.exports = () => { }, ], ], + // NOTE: we can enable this by default for everything as soon as we only have one instance + // of lodash across the entire project. For now we are just enabling it for siem + // as they are extensively using the lodash v4 + overrides: [ + { + test: [/x-pack[\/\\]legacy[\/\\]plugins[\/\\]siem[\/\\]public/], + plugins: [ + [ + require.resolve('babel-plugin-transform-imports'), + { + 'lodash/?(((\\w*)?/?)*)': { + transform: 'lodash/${1}/${member}', + preventFullImport: false, + }, + }, + ], + ], + }, + ], }; }; diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap index 4b4bb1282d939..fe0f75c05c646 100644 --- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap +++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap @@ -57,6 +57,6 @@ OptimizerConfig { } `; -exports[`builds expected bundles, saves bundle counts to metadata: bar bundle 1`] = `"var __kbnBundles__=typeof __kbnBundles__===\\"object\\"?__kbnBundles__:{};__kbnBundles__[\\"plugin/bar\\"]=function(modules){function webpackJsonpCallback(data){var chunkIds=data[0];var moreModules=data[1];var moduleId,chunkId,i=0,resolves=[];for(;i `/${bundle.type}:${bundle.id}/${Path.relative( bundle.sourceRoot, @@ -140,12 +138,22 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { module: { // no parse rules for a few known large packages which have no require() statements + // or which have require() statements that should be ignored because the file is + // already bundled with all its necessary depedencies noParse: [ /[\///]node_modules[\///]elasticsearch-browser[\///]/, - /[\///]node_modules[\///]lodash[\///]index\.js/, + /[\///]node_modules[\///]lodash[\///]index\.js$/, + /[\///]node_modules[\///]vega-lib[\///]build[\///]vega\.js$/, ], rules: [ + { + include: Path.join(bundle.contextDir, bundle.entry), + loader: UiSharedDeps.publicPathLoader, + options: { + key: bundle.id, + }, + }, { test: /\.css$/, include: /node_modules/, @@ -289,6 +297,7 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { resolve: { extensions: ['.js', '.ts', '.tsx', '.json'], + mainFields: ['browser', 'main'], alias: { tinymath: require.resolve('tinymath/lib/tinymath.es5.js'), }, diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 6a2d02ee778dd..b921956642728 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -79178,7 +79178,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(704); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _build_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildProductionProjects"]; }); -/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(927); +/* harmony import */ var _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(939); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "prepareExternalProjectDependencies", function() { return _prepare_project_dependencies__WEBPACK_IMPORTED_MODULE_1__["prepareExternalProjectDependencies"]; }); /* @@ -79363,9 +79363,9 @@ const pAll = __webpack_require__(706); const arrify = __webpack_require__(708); const globby = __webpack_require__(709); const isGlob = __webpack_require__(603); -const cpFile = __webpack_require__(912); -const junk = __webpack_require__(924); -const CpyError = __webpack_require__(925); +const cpFile = __webpack_require__(924); +const junk = __webpack_require__(936); +const CpyError = __webpack_require__(937); const defaultOptions = { ignoreJunk: true @@ -79615,8 +79615,8 @@ const fs = __webpack_require__(23); const arrayUnion = __webpack_require__(710); const glob = __webpack_require__(712); const fastGlob = __webpack_require__(717); -const dirGlob = __webpack_require__(905); -const gitignore = __webpack_require__(908); +const dirGlob = __webpack_require__(917); +const gitignore = __webpack_require__(920); const DEFAULT_FILTER = () => false; @@ -81449,11 +81449,11 @@ module.exports.generateTasks = pkg.generateTasks; Object.defineProperty(exports, "__esModule", { value: true }); var optionsManager = __webpack_require__(719); var taskManager = __webpack_require__(720); -var reader_async_1 = __webpack_require__(876); -var reader_stream_1 = __webpack_require__(900); -var reader_sync_1 = __webpack_require__(901); -var arrayUtils = __webpack_require__(903); -var streamUtils = __webpack_require__(904); +var reader_async_1 = __webpack_require__(888); +var reader_stream_1 = __webpack_require__(912); +var reader_sync_1 = __webpack_require__(913); +var arrayUtils = __webpack_require__(915); +var streamUtils = __webpack_require__(916); /** * Synchronous API. */ @@ -82085,17 +82085,17 @@ module.exports = function isGlob(str, options) { var util = __webpack_require__(29); var braces = __webpack_require__(727); -var toRegex = __webpack_require__(829); -var extend = __webpack_require__(837); +var toRegex = __webpack_require__(840); +var extend = __webpack_require__(848); /** * Local dependencies */ -var compilers = __webpack_require__(840); -var parsers = __webpack_require__(872); -var cache = __webpack_require__(873); -var utils = __webpack_require__(874); +var compilers = __webpack_require__(851); +var parsers = __webpack_require__(884); +var cache = __webpack_require__(885); +var utils = __webpack_require__(886); var MAX_LENGTH = 1024 * 64; /** @@ -82968,17 +82968,17 @@ module.exports = micromatch; */ var toRegex = __webpack_require__(728); -var unique = __webpack_require__(740); +var unique = __webpack_require__(742); var extend = __webpack_require__(737); /** * Local dependencies */ -var compilers = __webpack_require__(741); -var parsers = __webpack_require__(756); -var Braces = __webpack_require__(766); -var utils = __webpack_require__(742); +var compilers = __webpack_require__(743); +var parsers = __webpack_require__(760); +var Braces = __webpack_require__(770); +var utils = __webpack_require__(744); var MAX_LENGTH = 1024 * 64; var cache = {}; @@ -84132,7 +84132,7 @@ module.exports = function isExtendable(val) { "use strict"; -var extend = __webpack_require__(737); +var extend = __webpack_require__(740); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -84202,6 +84202,66 @@ module.exports = toRegex; /* 740 */ /***/ (function(module, exports, __webpack_require__) { +"use strict"; + + +var isObject = __webpack_require__(741); + +module.exports = function extend(o/*, objects*/) { + if (!isObject(o)) { o = {}; } + + var len = arguments.length; + for (var i = 1; i < len; i++) { + var obj = arguments[i]; + + if (isObject(obj)) { + assign(o, obj); + } + } + return o; +}; + +function assign(a, b) { + for (var key in b) { + if (hasOwn(b, key)) { + a[key] = b[key]; + } + } +} + +/** + * Returns true if the given `key` is an own property of `obj`. + */ + +function hasOwn(obj, key) { + return Object.prototype.hasOwnProperty.call(obj, key); +} + + +/***/ }), +/* 741 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/*! + * is-extendable + * + * Copyright (c) 2015, Jon Schlinkert. + * Licensed under the MIT License. + */ + + + +module.exports = function isExtendable(val) { + return typeof val !== 'undefined' && val !== null + && (typeof val === 'object' || typeof val === 'function'); +}; + + +/***/ }), +/* 742 */ +/***/ (function(module, exports, __webpack_require__) { + "use strict"; /*! * array-unique @@ -84249,13 +84309,13 @@ module.exports.immutable = function uniqueImmutable(arr) { /***/ }), -/* 741 */ +/* 743 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(742); +var utils = __webpack_require__(744); module.exports = function(braces, options) { braces.compiler @@ -84538,13 +84598,13 @@ function hasQueue(node) { /***/ }), -/* 742 */ +/* 744 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var splitString = __webpack_require__(743); +var splitString = __webpack_require__(745); var utils = module.exports; /** @@ -84552,11 +84612,11 @@ var utils = module.exports; */ utils.extend = __webpack_require__(737); -utils.flatten = __webpack_require__(749); -utils.isObject = __webpack_require__(747); -utils.fillRange = __webpack_require__(750); -utils.repeat = __webpack_require__(755); -utils.unique = __webpack_require__(740); +utils.flatten = __webpack_require__(751); +utils.isObject = __webpack_require__(749); +utils.fillRange = __webpack_require__(752); +utils.repeat = __webpack_require__(759); +utils.unique = __webpack_require__(742); utils.define = function(obj, key, val) { Object.defineProperty(obj, key, { @@ -84888,7 +84948,7 @@ utils.escapeRegex = function(str) { /***/ }), -/* 743 */ +/* 745 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84901,7 +84961,7 @@ utils.escapeRegex = function(str) { -var extend = __webpack_require__(744); +var extend = __webpack_require__(746); module.exports = function(str, options, fn) { if (typeof str !== 'string') { @@ -85066,14 +85126,14 @@ function keepEscaping(opts, str, idx) { /***/ }), -/* 744 */ +/* 746 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(745); -var assignSymbols = __webpack_require__(748); +var isExtendable = __webpack_require__(747); +var assignSymbols = __webpack_require__(750); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -85133,7 +85193,7 @@ function isEnum(obj, key) { /***/ }), -/* 745 */ +/* 747 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85146,7 +85206,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(746); +var isPlainObject = __webpack_require__(748); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -85154,7 +85214,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 746 */ +/* 748 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85167,7 +85227,7 @@ module.exports = function isExtendable(val) { -var isObject = __webpack_require__(747); +var isObject = __webpack_require__(749); function isObjectObject(o) { return isObject(o) === true @@ -85198,7 +85258,7 @@ module.exports = function isPlainObject(o) { /***/ }), -/* 747 */ +/* 749 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85217,7 +85277,7 @@ module.exports = function isObject(val) { /***/ }), -/* 748 */ +/* 750 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85264,7 +85324,7 @@ module.exports = function(receiver, objects) { /***/ }), -/* 749 */ +/* 751 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85293,7 +85353,7 @@ function flat(arr, res) { /***/ }), -/* 750 */ +/* 752 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85307,10 +85367,10 @@ function flat(arr, res) { var util = __webpack_require__(29); -var isNumber = __webpack_require__(751); -var extend = __webpack_require__(737); -var repeat = __webpack_require__(753); -var toRegex = __webpack_require__(754); +var isNumber = __webpack_require__(753); +var extend = __webpack_require__(755); +var repeat = __webpack_require__(757); +var toRegex = __webpack_require__(758); /** * Return a range of numbers or letters. @@ -85508,7 +85568,7 @@ module.exports = fillRange; /***/ }), -/* 751 */ +/* 753 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85521,7 +85581,7 @@ module.exports = fillRange; -var typeOf = __webpack_require__(752); +var typeOf = __webpack_require__(754); module.exports = function isNumber(num) { var type = typeOf(num); @@ -85537,7 +85597,7 @@ module.exports = function isNumber(num) { /***/ }), -/* 752 */ +/* 754 */ /***/ (function(module, exports, __webpack_require__) { var isBuffer = __webpack_require__(734); @@ -85659,7 +85719,67 @@ module.exports = function kindOf(val) { /***/ }), -/* 753 */ +/* 755 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var isObject = __webpack_require__(756); + +module.exports = function extend(o/*, objects*/) { + if (!isObject(o)) { o = {}; } + + var len = arguments.length; + for (var i = 1; i < len; i++) { + var obj = arguments[i]; + + if (isObject(obj)) { + assign(o, obj); + } + } + return o; +}; + +function assign(a, b) { + for (var key in b) { + if (hasOwn(b, key)) { + a[key] = b[key]; + } + } +} + +/** + * Returns true if the given `key` is an own property of `obj`. + */ + +function hasOwn(obj, key) { + return Object.prototype.hasOwnProperty.call(obj, key); +} + + +/***/ }), +/* 756 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/*! + * is-extendable + * + * Copyright (c) 2015, Jon Schlinkert. + * Licensed under the MIT License. + */ + + + +module.exports = function isExtendable(val) { + return typeof val !== 'undefined' && val !== null + && (typeof val === 'object' || typeof val === 'function'); +}; + + +/***/ }), +/* 757 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85736,7 +85856,7 @@ function repeat(str, num) { /***/ }), -/* 754 */ +/* 758 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85749,8 +85869,8 @@ function repeat(str, num) { -var repeat = __webpack_require__(753); -var isNumber = __webpack_require__(751); +var repeat = __webpack_require__(757); +var isNumber = __webpack_require__(753); var cache = {}; function toRegexRange(min, max, options) { @@ -86037,7 +86157,7 @@ module.exports = toRegexRange; /***/ }), -/* 755 */ +/* 759 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86062,14 +86182,14 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 756 */ +/* 760 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(757); -var utils = __webpack_require__(742); +var Node = __webpack_require__(761); +var utils = __webpack_require__(744); /** * Braces parsers @@ -86429,15 +86549,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 757 */ +/* 761 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(747); -var define = __webpack_require__(758); -var utils = __webpack_require__(765); +var isObject = __webpack_require__(749); +var define = __webpack_require__(762); +var utils = __webpack_require__(769); var ownNames; /** @@ -86928,7 +87048,7 @@ exports = module.exports = Node; /***/ }), -/* 758 */ +/* 762 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86941,7 +87061,7 @@ exports = module.exports = Node; -var isDescriptor = __webpack_require__(759); +var isDescriptor = __webpack_require__(763); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -86966,7 +87086,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 759 */ +/* 763 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86979,9 +87099,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(760); -var isAccessor = __webpack_require__(761); -var isData = __webpack_require__(763); +var typeOf = __webpack_require__(764); +var isAccessor = __webpack_require__(765); +var isData = __webpack_require__(767); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -86995,7 +87115,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 760 */ +/* 764 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87130,7 +87250,7 @@ function isBuffer(val) { /***/ }), -/* 761 */ +/* 765 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87143,7 +87263,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(762); +var typeOf = __webpack_require__(766); // accessor descriptor properties var accessor = { @@ -87206,7 +87326,7 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 762 */ +/* 766 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87341,7 +87461,7 @@ function isBuffer(val) { /***/ }), -/* 763 */ +/* 767 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87354,7 +87474,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(764); +var typeOf = __webpack_require__(768); module.exports = function isDataDescriptor(obj, prop) { // data descriptor properties @@ -87397,7 +87517,7 @@ module.exports = function isDataDescriptor(obj, prop) { /***/ }), -/* 764 */ +/* 768 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -87532,13 +87652,13 @@ function isBuffer(val) { /***/ }), -/* 765 */ +/* 769 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(752); +var typeOf = __webpack_require__(754); var utils = module.exports; /** @@ -88558,17 +88678,17 @@ function assert(val, message) { /***/ }), -/* 766 */ +/* 770 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var extend = __webpack_require__(737); -var Snapdragon = __webpack_require__(767); -var compilers = __webpack_require__(741); -var parsers = __webpack_require__(756); -var utils = __webpack_require__(742); +var Snapdragon = __webpack_require__(771); +var compilers = __webpack_require__(743); +var parsers = __webpack_require__(760); +var utils = __webpack_require__(744); /** * Customize Snapdragon parser and renderer @@ -88669,17 +88789,17 @@ module.exports = Braces; /***/ }), -/* 767 */ +/* 771 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Base = __webpack_require__(768); -var define = __webpack_require__(729); -var Compiler = __webpack_require__(797); -var Parser = __webpack_require__(826); -var utils = __webpack_require__(806); +var Base = __webpack_require__(772); +var define = __webpack_require__(798); +var Compiler = __webpack_require__(808); +var Parser = __webpack_require__(837); +var utils = __webpack_require__(817); var regexCache = {}; var cache = {}; @@ -88850,20 +88970,20 @@ module.exports.Parser = Parser; /***/ }), -/* 768 */ +/* 772 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var define = __webpack_require__(769); -var CacheBase = __webpack_require__(770); -var Emitter = __webpack_require__(771); -var isObject = __webpack_require__(747); -var merge = __webpack_require__(788); -var pascal = __webpack_require__(791); -var cu = __webpack_require__(792); +var define = __webpack_require__(773); +var CacheBase = __webpack_require__(774); +var Emitter = __webpack_require__(775); +var isObject = __webpack_require__(749); +var merge = __webpack_require__(792); +var pascal = __webpack_require__(795); +var cu = __webpack_require__(796); /** * Optionally define a custom `cache` namespace to use. @@ -89292,7 +89412,7 @@ module.exports.namespace = namespace; /***/ }), -/* 769 */ +/* 773 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89305,7 +89425,7 @@ module.exports.namespace = namespace; -var isDescriptor = __webpack_require__(759); +var isDescriptor = __webpack_require__(763); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -89330,21 +89450,21 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 770 */ +/* 774 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(747); -var Emitter = __webpack_require__(771); -var visit = __webpack_require__(772); -var toPath = __webpack_require__(775); -var union = __webpack_require__(776); -var del = __webpack_require__(780); -var get = __webpack_require__(778); -var has = __webpack_require__(785); -var set = __webpack_require__(779); +var isObject = __webpack_require__(749); +var Emitter = __webpack_require__(775); +var visit = __webpack_require__(776); +var toPath = __webpack_require__(779); +var union = __webpack_require__(780); +var del = __webpack_require__(784); +var get = __webpack_require__(782); +var has = __webpack_require__(789); +var set = __webpack_require__(783); /** * Create a `Cache` constructor that when instantiated will @@ -89598,7 +89718,7 @@ module.exports.namespace = namespace; /***/ }), -/* 771 */ +/* 775 */ /***/ (function(module, exports, __webpack_require__) { @@ -89767,7 +89887,7 @@ Emitter.prototype.hasListeners = function(event){ /***/ }), -/* 772 */ +/* 776 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89780,8 +89900,8 @@ Emitter.prototype.hasListeners = function(event){ -var visit = __webpack_require__(773); -var mapVisit = __webpack_require__(774); +var visit = __webpack_require__(777); +var mapVisit = __webpack_require__(778); module.exports = function(collection, method, val) { var result; @@ -89804,7 +89924,7 @@ module.exports = function(collection, method, val) { /***/ }), -/* 773 */ +/* 777 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89817,7 +89937,7 @@ module.exports = function(collection, method, val) { -var isObject = __webpack_require__(747); +var isObject = __webpack_require__(749); module.exports = function visit(thisArg, method, target, val) { if (!isObject(thisArg) && typeof thisArg !== 'function') { @@ -89844,14 +89964,14 @@ module.exports = function visit(thisArg, method, target, val) { /***/ }), -/* 774 */ +/* 778 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var visit = __webpack_require__(773); +var visit = __webpack_require__(777); /** * Map `visit` over an array of objects. @@ -89888,7 +90008,7 @@ function isObject(val) { /***/ }), -/* 775 */ +/* 779 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89901,7 +90021,7 @@ function isObject(val) { -var typeOf = __webpack_require__(752); +var typeOf = __webpack_require__(754); module.exports = function toPath(args) { if (typeOf(args) !== 'arguments') { @@ -89928,16 +90048,16 @@ function filter(arr) { /***/ }), -/* 776 */ +/* 780 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(738); -var union = __webpack_require__(777); -var get = __webpack_require__(778); -var set = __webpack_require__(779); +var isObject = __webpack_require__(741); +var union = __webpack_require__(781); +var get = __webpack_require__(782); +var set = __webpack_require__(783); module.exports = function unionValue(obj, prop, value) { if (!isObject(obj)) { @@ -89965,7 +90085,7 @@ function arrayify(val) { /***/ }), -/* 777 */ +/* 781 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90001,7 +90121,7 @@ module.exports = function union(init) { /***/ }), -/* 778 */ +/* 782 */ /***/ (function(module, exports) { /*! @@ -90057,7 +90177,7 @@ function toString(val) { /***/ }), -/* 779 */ +/* 783 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90070,10 +90190,10 @@ function toString(val) { -var split = __webpack_require__(743); -var extend = __webpack_require__(737); -var isPlainObject = __webpack_require__(746); -var isObject = __webpack_require__(738); +var split = __webpack_require__(745); +var extend = __webpack_require__(740); +var isPlainObject = __webpack_require__(748); +var isObject = __webpack_require__(741); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -90119,7 +90239,7 @@ function isValidKey(key) { /***/ }), -/* 780 */ +/* 784 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90132,8 +90252,8 @@ function isValidKey(key) { -var isObject = __webpack_require__(747); -var has = __webpack_require__(781); +var isObject = __webpack_require__(749); +var has = __webpack_require__(785); module.exports = function unset(obj, prop) { if (!isObject(obj)) { @@ -90158,7 +90278,7 @@ module.exports = function unset(obj, prop) { /***/ }), -/* 781 */ +/* 785 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90171,9 +90291,9 @@ module.exports = function unset(obj, prop) { -var isObject = __webpack_require__(782); -var hasValues = __webpack_require__(784); -var get = __webpack_require__(778); +var isObject = __webpack_require__(786); +var hasValues = __webpack_require__(788); +var get = __webpack_require__(782); module.exports = function(obj, prop, noZero) { if (isObject(obj)) { @@ -90184,7 +90304,7 @@ module.exports = function(obj, prop, noZero) { /***/ }), -/* 782 */ +/* 786 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90197,7 +90317,7 @@ module.exports = function(obj, prop, noZero) { -var isArray = __webpack_require__(783); +var isArray = __webpack_require__(787); module.exports = function isObject(val) { return val != null && typeof val === 'object' && isArray(val) === false; @@ -90205,7 +90325,7 @@ module.exports = function isObject(val) { /***/ }), -/* 783 */ +/* 787 */ /***/ (function(module, exports) { var toString = {}.toString; @@ -90216,7 +90336,7 @@ module.exports = Array.isArray || function (arr) { /***/ }), -/* 784 */ +/* 788 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90259,7 +90379,7 @@ module.exports = function hasValue(o, noZero) { /***/ }), -/* 785 */ +/* 789 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90272,9 +90392,9 @@ module.exports = function hasValue(o, noZero) { -var isObject = __webpack_require__(747); -var hasValues = __webpack_require__(786); -var get = __webpack_require__(778); +var isObject = __webpack_require__(749); +var hasValues = __webpack_require__(790); +var get = __webpack_require__(782); module.exports = function(val, prop) { return hasValues(isObject(val) && prop ? get(val, prop) : val); @@ -90282,7 +90402,7 @@ module.exports = function(val, prop) { /***/ }), -/* 786 */ +/* 790 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90295,8 +90415,8 @@ module.exports = function(val, prop) { -var typeOf = __webpack_require__(787); -var isNumber = __webpack_require__(751); +var typeOf = __webpack_require__(791); +var isNumber = __webpack_require__(753); module.exports = function hasValue(val) { // is-number checks for NaN and other edge cases @@ -90349,7 +90469,7 @@ module.exports = function hasValue(val) { /***/ }), -/* 787 */ +/* 791 */ /***/ (function(module, exports, __webpack_require__) { var isBuffer = __webpack_require__(734); @@ -90474,14 +90594,14 @@ module.exports = function kindOf(val) { /***/ }), -/* 788 */ +/* 792 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(789); -var forIn = __webpack_require__(790); +var isExtendable = __webpack_require__(793); +var forIn = __webpack_require__(794); function mixinDeep(target, objects) { var len = arguments.length, i = 0; @@ -90545,7 +90665,7 @@ module.exports = mixinDeep; /***/ }), -/* 789 */ +/* 793 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90558,7 +90678,7 @@ module.exports = mixinDeep; -var isPlainObject = __webpack_require__(746); +var isPlainObject = __webpack_require__(748); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -90566,7 +90686,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 790 */ +/* 794 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90589,7 +90709,7 @@ module.exports = function forIn(obj, fn, thisArg) { /***/ }), -/* 791 */ +/* 795 */ /***/ (function(module, exports) { /*! @@ -90616,14 +90736,14 @@ module.exports = pascalcase; /***/ }), -/* 792 */ +/* 796 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(29); -var utils = __webpack_require__(793); +var utils = __webpack_require__(797); /** * Expose class utils @@ -90988,7 +91108,7 @@ cu.bubble = function(Parent, events) { /***/ }), -/* 793 */ +/* 797 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91002,10 +91122,10 @@ var utils = {}; * Lazily required module dependencies */ -utils.union = __webpack_require__(777); -utils.define = __webpack_require__(729); -utils.isObj = __webpack_require__(747); -utils.staticExtend = __webpack_require__(794); +utils.union = __webpack_require__(781); +utils.define = __webpack_require__(798); +utils.isObj = __webpack_require__(749); +utils.staticExtend = __webpack_require__(805); /** @@ -91016,36 +91136,638 @@ module.exports = utils; /***/ }), -/* 794 */ +/* 798 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; /*! - * static-extend + * define-property * - * Copyright (c) 2016, Jon Schlinkert. + * Copyright (c) 2015, Jon Schlinkert. * Licensed under the MIT License. */ -var copy = __webpack_require__(795); -var define = __webpack_require__(729); -var util = __webpack_require__(29); +var isDescriptor = __webpack_require__(799); -/** - * Returns a function for extending the static properties, - * prototype properties, and descriptors from the `Parent` - * constructor onto `Child` constructors. - * - * ```js - * var extend = require('static-extend'); - * Parent.extend = extend(Parent); - * - * // optionally pass a custom merge function as the second arg - * Parent.extend = extend(Parent, function(Child) { - * Child.prototype.mixin = function(key, val) { - * Child.prototype[key] = val; +module.exports = function defineProperty(obj, prop, val) { + if (typeof obj !== 'object' && typeof obj !== 'function') { + throw new TypeError('expected an object or function.'); + } + + if (typeof prop !== 'string') { + throw new TypeError('expected `prop` to be a string.'); + } + + if (isDescriptor(val) && ('set' in val || 'get' in val)) { + return Object.defineProperty(obj, prop, val); + } + + return Object.defineProperty(obj, prop, { + configurable: true, + enumerable: false, + writable: true, + value: val + }); +}; + + +/***/ }), +/* 799 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/*! + * is-descriptor + * + * Copyright (c) 2015-2017, Jon Schlinkert. + * Released under the MIT License. + */ + + + +var typeOf = __webpack_require__(800); +var isAccessor = __webpack_require__(801); +var isData = __webpack_require__(803); + +module.exports = function isDescriptor(obj, key) { + if (typeOf(obj) !== 'object') { + return false; + } + if ('get' in obj) { + return isAccessor(obj, key); + } + return isData(obj, key); +}; + + +/***/ }), +/* 800 */ +/***/ (function(module, exports) { + +var toString = Object.prototype.toString; + +/** + * Get the native `typeof` a value. + * + * @param {*} `val` + * @return {*} Native javascript type + */ + +module.exports = function kindOf(val) { + var type = typeof val; + + // primitivies + if (type === 'undefined') { + return 'undefined'; + } + if (val === null) { + return 'null'; + } + if (val === true || val === false || val instanceof Boolean) { + return 'boolean'; + } + if (type === 'string' || val instanceof String) { + return 'string'; + } + if (type === 'number' || val instanceof Number) { + return 'number'; + } + + // functions + if (type === 'function' || val instanceof Function) { + if (typeof val.constructor.name !== 'undefined' && val.constructor.name.slice(0, 9) === 'Generator') { + return 'generatorfunction'; + } + return 'function'; + } + + // array + if (typeof Array.isArray !== 'undefined' && Array.isArray(val)) { + return 'array'; + } + + // check for instances of RegExp and Date before calling `toString` + if (val instanceof RegExp) { + return 'regexp'; + } + if (val instanceof Date) { + return 'date'; + } + + // other objects + type = toString.call(val); + + if (type === '[object RegExp]') { + return 'regexp'; + } + if (type === '[object Date]') { + return 'date'; + } + if (type === '[object Arguments]') { + return 'arguments'; + } + if (type === '[object Error]') { + return 'error'; + } + if (type === '[object Promise]') { + return 'promise'; + } + + // buffer + if (isBuffer(val)) { + return 'buffer'; + } + + // es6: Map, WeakMap, Set, WeakSet + if (type === '[object Set]') { + return 'set'; + } + if (type === '[object WeakSet]') { + return 'weakset'; + } + if (type === '[object Map]') { + return 'map'; + } + if (type === '[object WeakMap]') { + return 'weakmap'; + } + if (type === '[object Symbol]') { + return 'symbol'; + } + + if (type === '[object Map Iterator]') { + return 'mapiterator'; + } + if (type === '[object Set Iterator]') { + return 'setiterator'; + } + if (type === '[object String Iterator]') { + return 'stringiterator'; + } + if (type === '[object Array Iterator]') { + return 'arrayiterator'; + } + + // typed arrays + if (type === '[object Int8Array]') { + return 'int8array'; + } + if (type === '[object Uint8Array]') { + return 'uint8array'; + } + if (type === '[object Uint8ClampedArray]') { + return 'uint8clampedarray'; + } + if (type === '[object Int16Array]') { + return 'int16array'; + } + if (type === '[object Uint16Array]') { + return 'uint16array'; + } + if (type === '[object Int32Array]') { + return 'int32array'; + } + if (type === '[object Uint32Array]') { + return 'uint32array'; + } + if (type === '[object Float32Array]') { + return 'float32array'; + } + if (type === '[object Float64Array]') { + return 'float64array'; + } + + // must be a plain object + return 'object'; +}; + +/** + * If you need to support Safari 5-7 (8-10 yr-old browser), + * take a look at https://github.com/feross/is-buffer + */ + +function isBuffer(val) { + return val.constructor + && typeof val.constructor.isBuffer === 'function' + && val.constructor.isBuffer(val); +} + + +/***/ }), +/* 801 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/*! + * is-accessor-descriptor + * + * Copyright (c) 2015, Jon Schlinkert. + * Licensed under the MIT License. + */ + + + +var typeOf = __webpack_require__(802); + +// accessor descriptor properties +var accessor = { + get: 'function', + set: 'function', + configurable: 'boolean', + enumerable: 'boolean' +}; + +function isAccessorDescriptor(obj, prop) { + if (typeof prop === 'string') { + var val = Object.getOwnPropertyDescriptor(obj, prop); + return typeof val !== 'undefined'; + } + + if (typeOf(obj) !== 'object') { + return false; + } + + if (has(obj, 'value') || has(obj, 'writable')) { + return false; + } + + if (!has(obj, 'get') || typeof obj.get !== 'function') { + return false; + } + + // tldr: it's valid to have "set" be undefined + // "set" might be undefined if `Object.getOwnPropertyDescriptor` + // was used to get the value, and only `get` was defined by the user + if (has(obj, 'set') && typeof obj[key] !== 'function' && typeof obj[key] !== 'undefined') { + return false; + } + + for (var key in obj) { + if (!accessor.hasOwnProperty(key)) { + continue; + } + + if (typeOf(obj[key]) === accessor[key]) { + continue; + } + + if (typeof obj[key] !== 'undefined') { + return false; + } + } + return true; +} + +function has(obj, key) { + return {}.hasOwnProperty.call(obj, key); +} + +/** + * Expose `isAccessorDescriptor` + */ + +module.exports = isAccessorDescriptor; + + +/***/ }), +/* 802 */ +/***/ (function(module, exports, __webpack_require__) { + +var isBuffer = __webpack_require__(734); +var toString = Object.prototype.toString; + +/** + * Get the native `typeof` a value. + * + * @param {*} `val` + * @return {*} Native javascript type + */ + +module.exports = function kindOf(val) { + // primitivies + if (typeof val === 'undefined') { + return 'undefined'; + } + if (val === null) { + return 'null'; + } + if (val === true || val === false || val instanceof Boolean) { + return 'boolean'; + } + if (typeof val === 'string' || val instanceof String) { + return 'string'; + } + if (typeof val === 'number' || val instanceof Number) { + return 'number'; + } + + // functions + if (typeof val === 'function' || val instanceof Function) { + return 'function'; + } + + // array + if (typeof Array.isArray !== 'undefined' && Array.isArray(val)) { + return 'array'; + } + + // check for instances of RegExp and Date before calling `toString` + if (val instanceof RegExp) { + return 'regexp'; + } + if (val instanceof Date) { + return 'date'; + } + + // other objects + var type = toString.call(val); + + if (type === '[object RegExp]') { + return 'regexp'; + } + if (type === '[object Date]') { + return 'date'; + } + if (type === '[object Arguments]') { + return 'arguments'; + } + if (type === '[object Error]') { + return 'error'; + } + + // buffer + if (isBuffer(val)) { + return 'buffer'; + } + + // es6: Map, WeakMap, Set, WeakSet + if (type === '[object Set]') { + return 'set'; + } + if (type === '[object WeakSet]') { + return 'weakset'; + } + if (type === '[object Map]') { + return 'map'; + } + if (type === '[object WeakMap]') { + return 'weakmap'; + } + if (type === '[object Symbol]') { + return 'symbol'; + } + + // typed arrays + if (type === '[object Int8Array]') { + return 'int8array'; + } + if (type === '[object Uint8Array]') { + return 'uint8array'; + } + if (type === '[object Uint8ClampedArray]') { + return 'uint8clampedarray'; + } + if (type === '[object Int16Array]') { + return 'int16array'; + } + if (type === '[object Uint16Array]') { + return 'uint16array'; + } + if (type === '[object Int32Array]') { + return 'int32array'; + } + if (type === '[object Uint32Array]') { + return 'uint32array'; + } + if (type === '[object Float32Array]') { + return 'float32array'; + } + if (type === '[object Float64Array]') { + return 'float64array'; + } + + // must be a plain object + return 'object'; +}; + + +/***/ }), +/* 803 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/*! + * is-data-descriptor + * + * Copyright (c) 2015, Jon Schlinkert. + * Licensed under the MIT License. + */ + + + +var typeOf = __webpack_require__(804); + +// data descriptor properties +var data = { + configurable: 'boolean', + enumerable: 'boolean', + writable: 'boolean' +}; + +function isDataDescriptor(obj, prop) { + if (typeOf(obj) !== 'object') { + return false; + } + + if (typeof prop === 'string') { + var val = Object.getOwnPropertyDescriptor(obj, prop); + return typeof val !== 'undefined'; + } + + if (!('value' in obj) && !('writable' in obj)) { + return false; + } + + for (var key in obj) { + if (key === 'value') continue; + + if (!data.hasOwnProperty(key)) { + continue; + } + + if (typeOf(obj[key]) === data[key]) { + continue; + } + + if (typeof obj[key] !== 'undefined') { + return false; + } + } + return true; +} + +/** + * Expose `isDataDescriptor` + */ + +module.exports = isDataDescriptor; + + +/***/ }), +/* 804 */ +/***/ (function(module, exports, __webpack_require__) { + +var isBuffer = __webpack_require__(734); +var toString = Object.prototype.toString; + +/** + * Get the native `typeof` a value. + * + * @param {*} `val` + * @return {*} Native javascript type + */ + +module.exports = function kindOf(val) { + // primitivies + if (typeof val === 'undefined') { + return 'undefined'; + } + if (val === null) { + return 'null'; + } + if (val === true || val === false || val instanceof Boolean) { + return 'boolean'; + } + if (typeof val === 'string' || val instanceof String) { + return 'string'; + } + if (typeof val === 'number' || val instanceof Number) { + return 'number'; + } + + // functions + if (typeof val === 'function' || val instanceof Function) { + return 'function'; + } + + // array + if (typeof Array.isArray !== 'undefined' && Array.isArray(val)) { + return 'array'; + } + + // check for instances of RegExp and Date before calling `toString` + if (val instanceof RegExp) { + return 'regexp'; + } + if (val instanceof Date) { + return 'date'; + } + + // other objects + var type = toString.call(val); + + if (type === '[object RegExp]') { + return 'regexp'; + } + if (type === '[object Date]') { + return 'date'; + } + if (type === '[object Arguments]') { + return 'arguments'; + } + if (type === '[object Error]') { + return 'error'; + } + + // buffer + if (isBuffer(val)) { + return 'buffer'; + } + + // es6: Map, WeakMap, Set, WeakSet + if (type === '[object Set]') { + return 'set'; + } + if (type === '[object WeakSet]') { + return 'weakset'; + } + if (type === '[object Map]') { + return 'map'; + } + if (type === '[object WeakMap]') { + return 'weakmap'; + } + if (type === '[object Symbol]') { + return 'symbol'; + } + + // typed arrays + if (type === '[object Int8Array]') { + return 'int8array'; + } + if (type === '[object Uint8Array]') { + return 'uint8array'; + } + if (type === '[object Uint8ClampedArray]') { + return 'uint8clampedarray'; + } + if (type === '[object Int16Array]') { + return 'int16array'; + } + if (type === '[object Uint16Array]') { + return 'uint16array'; + } + if (type === '[object Int32Array]') { + return 'int32array'; + } + if (type === '[object Uint32Array]') { + return 'uint32array'; + } + if (type === '[object Float32Array]') { + return 'float32array'; + } + if (type === '[object Float64Array]') { + return 'float64array'; + } + + // must be a plain object + return 'object'; +}; + + +/***/ }), +/* 805 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; +/*! + * static-extend + * + * Copyright (c) 2016, Jon Schlinkert. + * Licensed under the MIT License. + */ + + + +var copy = __webpack_require__(806); +var define = __webpack_require__(798); +var util = __webpack_require__(29); + +/** + * Returns a function for extending the static properties, + * prototype properties, and descriptors from the `Parent` + * constructor onto `Child` constructors. + * + * ```js + * var extend = require('static-extend'); + * Parent.extend = extend(Parent); + * + * // optionally pass a custom merge function as the second arg + * Parent.extend = extend(Parent, function(Child) { + * Child.prototype.mixin = function(key, val) { + * Child.prototype[key] = val; * }; * }); * @@ -91113,15 +91835,15 @@ module.exports = extend; /***/ }), -/* 795 */ +/* 806 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(752); -var copyDescriptor = __webpack_require__(796); -var define = __webpack_require__(729); +var typeOf = __webpack_require__(754); +var copyDescriptor = __webpack_require__(807); +var define = __webpack_require__(798); /** * Copy static properties, prototype properties, and descriptors from one object to another. @@ -91294,7 +92016,7 @@ module.exports.has = has; /***/ }), -/* 796 */ +/* 807 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91382,16 +92104,16 @@ function isObject(val) { /***/ }), -/* 797 */ +/* 808 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(798); -var define = __webpack_require__(729); -var debug = __webpack_require__(800)('snapdragon:compiler'); -var utils = __webpack_require__(806); +var use = __webpack_require__(809); +var define = __webpack_require__(798); +var debug = __webpack_require__(811)('snapdragon:compiler'); +var utils = __webpack_require__(817); /** * Create a new `Compiler` with the given `options`. @@ -91545,7 +92267,7 @@ Compiler.prototype = { // source map support if (opts.sourcemap) { - var sourcemaps = __webpack_require__(825); + var sourcemaps = __webpack_require__(836); sourcemaps(this); this.mapVisit(this.ast.nodes); this.applySourceMaps(); @@ -91566,7 +92288,7 @@ module.exports = Compiler; /***/ }), -/* 798 */ +/* 809 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91579,7 +92301,7 @@ module.exports = Compiler; -var utils = __webpack_require__(799); +var utils = __webpack_require__(810); module.exports = function base(app, opts) { if (!utils.isObject(app) && typeof app !== 'function') { @@ -91694,7 +92416,7 @@ module.exports = function base(app, opts) { /***/ }), -/* 799 */ +/* 810 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91708,8 +92430,8 @@ var utils = {}; * Lazily required module dependencies */ -utils.define = __webpack_require__(729); -utils.isObject = __webpack_require__(747); +utils.define = __webpack_require__(798); +utils.isObject = __webpack_require__(749); utils.isString = function(val) { @@ -91724,7 +92446,7 @@ module.exports = utils; /***/ }), -/* 800 */ +/* 811 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -91733,14 +92455,14 @@ module.exports = utils; */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(801); + module.exports = __webpack_require__(812); } else { - module.exports = __webpack_require__(804); + module.exports = __webpack_require__(815); } /***/ }), -/* 801 */ +/* 812 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -91749,7 +92471,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(802); +exports = module.exports = __webpack_require__(813); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -91931,7 +92653,7 @@ function localstorage() { /***/ }), -/* 802 */ +/* 813 */ /***/ (function(module, exports, __webpack_require__) { @@ -91947,7 +92669,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(803); +exports.humanize = __webpack_require__(814); /** * The currently active debug mode names, and names to skip. @@ -92139,7 +92861,7 @@ function coerce(val) { /***/ }), -/* 803 */ +/* 814 */ /***/ (function(module, exports) { /** @@ -92297,7 +93019,7 @@ function plural(ms, n, name) { /***/ }), -/* 804 */ +/* 815 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -92313,7 +93035,7 @@ var util = __webpack_require__(29); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(802); +exports = module.exports = __webpack_require__(813); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -92492,7 +93214,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(805); + var net = __webpack_require__(816); stream = new net.Socket({ fd: fd, readable: false, @@ -92551,13 +93273,13 @@ exports.enable(load()); /***/ }), -/* 805 */ +/* 816 */ /***/ (function(module, exports) { module.exports = require("net"); /***/ }), -/* 806 */ +/* 817 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92567,9 +93289,9 @@ module.exports = require("net"); * Module dependencies */ -exports.extend = __webpack_require__(737); -exports.SourceMap = __webpack_require__(807); -exports.sourceMapResolve = __webpack_require__(818); +exports.extend = __webpack_require__(740); +exports.SourceMap = __webpack_require__(818); +exports.sourceMapResolve = __webpack_require__(829); /** * Convert backslash in the given string to forward slashes @@ -92612,7 +93334,7 @@ exports.last = function(arr, n) { /***/ }), -/* 807 */ +/* 818 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -92620,13 +93342,13 @@ exports.last = function(arr, n) { * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ -exports.SourceMapGenerator = __webpack_require__(808).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(814).SourceMapConsumer; -exports.SourceNode = __webpack_require__(817).SourceNode; +exports.SourceMapGenerator = __webpack_require__(819).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(825).SourceMapConsumer; +exports.SourceNode = __webpack_require__(828).SourceNode; /***/ }), -/* 808 */ +/* 819 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -92636,10 +93358,10 @@ exports.SourceNode = __webpack_require__(817).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(809); -var util = __webpack_require__(811); -var ArraySet = __webpack_require__(812).ArraySet; -var MappingList = __webpack_require__(813).MappingList; +var base64VLQ = __webpack_require__(820); +var util = __webpack_require__(822); +var ArraySet = __webpack_require__(823).ArraySet; +var MappingList = __webpack_require__(824).MappingList; /** * An instance of the SourceMapGenerator represents a source map which is @@ -93048,7 +93770,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 809 */ +/* 820 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93088,7 +93810,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(810); +var base64 = __webpack_require__(821); // A single base 64 digit can contain 6 bits of data. For the base 64 variable // length quantities we use in the source map spec, the first bit is the sign, @@ -93194,7 +93916,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 810 */ +/* 821 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93267,7 +93989,7 @@ exports.decode = function (charCode) { /***/ }), -/* 811 */ +/* 822 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93690,7 +94412,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 812 */ +/* 823 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93700,7 +94422,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(811); +var util = __webpack_require__(822); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -93817,7 +94539,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 813 */ +/* 824 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93827,7 +94549,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(811); +var util = __webpack_require__(822); /** * Determine whether mappingB is after mappingA with respect to generated @@ -93902,7 +94624,7 @@ exports.MappingList = MappingList; /***/ }), -/* 814 */ +/* 825 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -93912,11 +94634,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(811); -var binarySearch = __webpack_require__(815); -var ArraySet = __webpack_require__(812).ArraySet; -var base64VLQ = __webpack_require__(809); -var quickSort = __webpack_require__(816).quickSort; +var util = __webpack_require__(822); +var binarySearch = __webpack_require__(826); +var ArraySet = __webpack_require__(823).ArraySet; +var base64VLQ = __webpack_require__(820); +var quickSort = __webpack_require__(827).quickSort; function SourceMapConsumer(aSourceMap) { var sourceMap = aSourceMap; @@ -94990,7 +95712,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 815 */ +/* 826 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95107,7 +95829,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 816 */ +/* 827 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95227,7 +95949,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 817 */ +/* 828 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -95237,8 +95959,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(808).SourceMapGenerator; -var util = __webpack_require__(811); +var SourceMapGenerator = __webpack_require__(819).SourceMapGenerator; +var util = __webpack_require__(822); // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). @@ -95646,17 +96368,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 818 */ +/* 829 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var sourceMappingURL = __webpack_require__(819) -var resolveUrl = __webpack_require__(820) -var decodeUriComponent = __webpack_require__(821) -var urix = __webpack_require__(823) -var atob = __webpack_require__(824) +var sourceMappingURL = __webpack_require__(830) +var resolveUrl = __webpack_require__(831) +var decodeUriComponent = __webpack_require__(832) +var urix = __webpack_require__(834) +var atob = __webpack_require__(835) @@ -95954,7 +96676,7 @@ module.exports = { /***/ }), -/* 819 */ +/* 830 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -96017,7 +96739,7 @@ void (function(root, factory) { /***/ }), -/* 820 */ +/* 831 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -96035,13 +96757,13 @@ module.exports = resolveUrl /***/ }), -/* 821 */ +/* 832 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(822) +var decodeUriComponent = __webpack_require__(833) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -96052,7 +96774,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 822 */ +/* 833 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96153,7 +96875,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 823 */ +/* 834 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -96176,7 +96898,7 @@ module.exports = urix /***/ }), -/* 824 */ +/* 835 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96190,7 +96912,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 825 */ +/* 836 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96198,8 +96920,8 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(23); var path = __webpack_require__(16); -var define = __webpack_require__(729); -var utils = __webpack_require__(806); +var define = __webpack_require__(798); +var utils = __webpack_require__(817); /** * Expose `mixin()`. @@ -96342,19 +97064,19 @@ exports.comment = function(node) { /***/ }), -/* 826 */ +/* 837 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(798); +var use = __webpack_require__(809); var util = __webpack_require__(29); -var Cache = __webpack_require__(827); -var define = __webpack_require__(729); -var debug = __webpack_require__(800)('snapdragon:parser'); -var Position = __webpack_require__(828); -var utils = __webpack_require__(806); +var Cache = __webpack_require__(838); +var define = __webpack_require__(798); +var debug = __webpack_require__(811)('snapdragon:parser'); +var Position = __webpack_require__(839); +var utils = __webpack_require__(817); /** * Create a new `Parser` with the given `input` and `options`. @@ -96882,7 +97604,7 @@ module.exports = Parser; /***/ }), -/* 827 */ +/* 838 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -96989,13 +97711,13 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 828 */ +/* 839 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(729); +var define = __webpack_require__(798); /** * Store position for a node @@ -97010,16 +97732,16 @@ module.exports = function Position(start, parser) { /***/ }), -/* 829 */ +/* 840 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var safe = __webpack_require__(830); -var define = __webpack_require__(836); -var extend = __webpack_require__(837); -var not = __webpack_require__(839); +var safe = __webpack_require__(841); +var define = __webpack_require__(847); +var extend = __webpack_require__(848); +var not = __webpack_require__(850); var MAX_LENGTH = 1024 * 64; /** @@ -97172,10 +97894,10 @@ module.exports.makeRe = makeRe; /***/ }), -/* 830 */ +/* 841 */ /***/ (function(module, exports, __webpack_require__) { -var parse = __webpack_require__(831); +var parse = __webpack_require__(842); var types = parse.types; module.exports = function (re, opts) { @@ -97221,13 +97943,13 @@ function isRegExp (x) { /***/ }), -/* 831 */ +/* 842 */ /***/ (function(module, exports, __webpack_require__) { -var util = __webpack_require__(832); -var types = __webpack_require__(833); -var sets = __webpack_require__(834); -var positions = __webpack_require__(835); +var util = __webpack_require__(843); +var types = __webpack_require__(844); +var sets = __webpack_require__(845); +var positions = __webpack_require__(846); module.exports = function(regexpStr) { @@ -97509,11 +98231,11 @@ module.exports.types = types; /***/ }), -/* 832 */ +/* 843 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(833); -var sets = __webpack_require__(834); +var types = __webpack_require__(844); +var sets = __webpack_require__(845); // All of these are private and only used by randexp. @@ -97626,7 +98348,7 @@ exports.error = function(regexp, msg) { /***/ }), -/* 833 */ +/* 844 */ /***/ (function(module, exports) { module.exports = { @@ -97642,10 +98364,10 @@ module.exports = { /***/ }), -/* 834 */ +/* 845 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(833); +var types = __webpack_require__(844); var INTS = function() { return [{ type: types.RANGE , from: 48, to: 57 }]; @@ -97730,10 +98452,10 @@ exports.anyChar = function() { /***/ }), -/* 835 */ +/* 846 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(833); +var types = __webpack_require__(844); exports.wordBoundary = function() { return { type: types.POSITION, value: 'b' }; @@ -97753,7 +98475,7 @@ exports.end = function() { /***/ }), -/* 836 */ +/* 847 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -97766,8 +98488,8 @@ exports.end = function() { -var isobject = __webpack_require__(747); -var isDescriptor = __webpack_require__(759); +var isobject = __webpack_require__(749); +var isDescriptor = __webpack_require__(763); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -97798,14 +98520,14 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 837 */ +/* 848 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(838); -var assignSymbols = __webpack_require__(748); +var isExtendable = __webpack_require__(849); +var assignSymbols = __webpack_require__(750); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -97865,7 +98587,7 @@ function isEnum(obj, key) { /***/ }), -/* 838 */ +/* 849 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -97878,7 +98600,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(746); +var isPlainObject = __webpack_require__(748); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -97886,14 +98608,14 @@ module.exports = function isExtendable(val) { /***/ }), -/* 839 */ +/* 850 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(837); -var safe = __webpack_require__(830); +var extend = __webpack_require__(848); +var safe = __webpack_require__(841); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -97965,14 +98687,14 @@ module.exports = toRegex; /***/ }), -/* 840 */ +/* 851 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(841); -var extglob = __webpack_require__(856); +var nanomatch = __webpack_require__(852); +var extglob = __webpack_require__(868); module.exports = function(snapdragon) { var compilers = snapdragon.compiler.compilers; @@ -98049,7 +98771,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 841 */ +/* 852 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -98060,17 +98782,17 @@ function escapeExtglobs(compiler) { */ var util = __webpack_require__(29); -var toRegex = __webpack_require__(728); -var extend = __webpack_require__(842); +var toRegex = __webpack_require__(853); +var extend = __webpack_require__(854); /** * Local dependencies */ -var compilers = __webpack_require__(844); -var parsers = __webpack_require__(845); -var cache = __webpack_require__(848); -var utils = __webpack_require__(850); +var compilers = __webpack_require__(856); +var parsers = __webpack_require__(857); +var cache = __webpack_require__(860); +var utils = __webpack_require__(862); var MAX_LENGTH = 1024 * 64; /** @@ -98855,53 +99577,208 @@ function compose(patterns, options, matcher) { }); } -/** - * Memoize a generated regex or function. A unique key is generated - * from the `type` (usually method name), the `pattern`, and - * user-defined options. - */ +/** + * Memoize a generated regex or function. A unique key is generated + * from the `type` (usually method name), the `pattern`, and + * user-defined options. + */ + +function memoize(type, pattern, options, fn) { + var key = utils.createKey(type + '=' + pattern, options); + + if (options && options.cache === false) { + return fn(pattern, options); + } + + if (cache.has(type, key)) { + return cache.get(type, key); + } + + var val = fn(pattern, options); + cache.set(type, key, val); + return val; +} + +/** + * Expose compiler, parser and cache on `nanomatch` + */ + +nanomatch.compilers = compilers; +nanomatch.parsers = parsers; +nanomatch.cache = cache; + +/** + * Expose `nanomatch` + * @type {Function} + */ + +module.exports = nanomatch; + + +/***/ }), +/* 853 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +var define = __webpack_require__(798); +var extend = __webpack_require__(740); +var not = __webpack_require__(739); +var MAX_LENGTH = 1024 * 64; + +/** + * Session cache + */ + +var cache = {}; + +/** + * Create a regular expression from the given `pattern` string. + * + * @param {String|RegExp} `pattern` Pattern can be a string or regular expression. + * @param {Object} `options` + * @return {RegExp} + * @api public + */ + +module.exports = function(patterns, options) { + if (!Array.isArray(patterns)) { + return makeRe(patterns, options); + } + return makeRe(patterns.join('|'), options); +}; + +/** + * Create a regular expression from the given `pattern` string. + * + * @param {String|RegExp} `pattern` Pattern can be a string or regular expression. + * @param {Object} `options` + * @return {RegExp} + * @api public + */ + +function makeRe(pattern, options) { + if (pattern instanceof RegExp) { + return pattern; + } + + if (typeof pattern !== 'string') { + throw new TypeError('expected a string'); + } + + if (pattern.length > MAX_LENGTH) { + throw new Error('expected pattern to be less than ' + MAX_LENGTH + ' characters'); + } + + var key = pattern; + // do this before shallow cloning options, it's a lot faster + if (!options || (options && options.cache !== false)) { + key = createKey(pattern, options); + + if (cache.hasOwnProperty(key)) { + return cache[key]; + } + } + + var opts = extend({}, options); + if (opts.contains === true) { + if (opts.negate === true) { + opts.strictNegate = false; + } else { + opts.strict = false; + } + } + + if (opts.strict === false) { + opts.strictOpen = false; + opts.strictClose = false; + } -function memoize(type, pattern, options, fn) { - var key = utils.createKey(type + '=' + pattern, options); + var open = opts.strictOpen !== false ? '^' : ''; + var close = opts.strictClose !== false ? '$' : ''; + var flags = opts.flags || ''; + var regex; - if (options && options.cache === false) { - return fn(pattern, options); + if (opts.nocase === true && !/i/.test(flags)) { + flags += 'i'; } - if (cache.has(type, key)) { - return cache.get(type, key); + try { + if (opts.negate || typeof opts.strictNegate === 'boolean') { + pattern = not.create(pattern, opts); + } + var str = open + '(?:' + pattern + ')' + close; + regex = new RegExp(str, flags); + } catch (err) { + if (opts.strictErrors === true) { + err.key = key; + err.pattern = pattern; + err.originalOptions = options; + err.createdOptions = opts; + throw err; + } + + try { + regex = new RegExp('^' + pattern.replace(/(\W)/g, '\\$1') + '$'); + } catch (err) { + regex = /.^/; //<= match nothing + } } - var val = fn(pattern, options); - cache.set(type, key, val); - return val; + if (opts.cache !== false) { + cacheRegex(regex, key, pattern, opts); + } + return regex; } /** - * Expose compiler, parser and cache on `nanomatch` + * Cache generated regex. This can result in dramatic speed improvements + * and simplify debugging by adding options and pattern to the regex. It can be + * disabled by passing setting `options.cache` to false. */ -nanomatch.compilers = compilers; -nanomatch.parsers = parsers; -nanomatch.cache = cache; +function cacheRegex(regex, key, pattern, options) { + define(regex, 'cached', true); + define(regex, 'pattern', pattern); + define(regex, 'options', options); + define(regex, 'key', key); + cache[key] = regex; +} /** - * Expose `nanomatch` - * @type {Function} + * Create the key to use for memoization. The key is generated + * by iterating over the options and concatenating key-value pairs + * to the pattern string. */ -module.exports = nanomatch; +function createKey(pattern, options) { + if (!options) return pattern; + var key = pattern; + for (var prop in options) { + if (options.hasOwnProperty(prop)) { + key += ';' + prop + '=' + String(options[prop]); + } + } + return key; +} + +/** + * Expose `makeRe` + */ + +module.exports.makeRe = makeRe; /***/ }), -/* 842 */ +/* 854 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(843); -var assignSymbols = __webpack_require__(748); +var isExtendable = __webpack_require__(855); +var assignSymbols = __webpack_require__(750); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -98961,7 +99838,7 @@ function isEnum(obj, key) { /***/ }), -/* 843 */ +/* 855 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -98974,7 +99851,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(746); +var isPlainObject = __webpack_require__(748); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -98982,7 +99859,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 844 */ +/* 856 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99328,15 +100205,15 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 845 */ +/* 857 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var regexNot = __webpack_require__(739); -var toRegex = __webpack_require__(728); -var isOdd = __webpack_require__(846); +var toRegex = __webpack_require__(853); +var isOdd = __webpack_require__(858); /** * Characters to use in negation regex (we want to "not" match @@ -99722,7 +100599,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 846 */ +/* 858 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99735,7 +100612,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(847); +var isNumber = __webpack_require__(859); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -99749,7 +100626,7 @@ module.exports = function isOdd(i) { /***/ }), -/* 847 */ +/* 859 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99777,14 +100654,14 @@ module.exports = function isNumber(num) { /***/ }), -/* 848 */ +/* 860 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(849))(); +module.exports = new (__webpack_require__(861))(); /***/ }), -/* 849 */ +/* 861 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99797,7 +100674,7 @@ module.exports = new (__webpack_require__(849))(); -var MapCache = __webpack_require__(827); +var MapCache = __webpack_require__(838); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -99919,7 +100796,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 850 */ +/* 862 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -99932,14 +100809,14 @@ var path = __webpack_require__(16); * Module dependencies */ -var isWindows = __webpack_require__(851)(); -var Snapdragon = __webpack_require__(767); -utils.define = __webpack_require__(852); -utils.diff = __webpack_require__(853); -utils.extend = __webpack_require__(842); -utils.pick = __webpack_require__(854); -utils.typeOf = __webpack_require__(855); -utils.unique = __webpack_require__(740); +var isWindows = __webpack_require__(863)(); +var Snapdragon = __webpack_require__(771); +utils.define = __webpack_require__(864); +utils.diff = __webpack_require__(865); +utils.extend = __webpack_require__(854); +utils.pick = __webpack_require__(866); +utils.typeOf = __webpack_require__(867); +utils.unique = __webpack_require__(742); /** * Returns true if the given value is effectively an empty string @@ -100305,7 +101182,7 @@ utils.unixify = function(options) { /***/ }), -/* 851 */ +/* 863 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @@ -100333,7 +101210,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 852 */ +/* 864 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100346,8 +101223,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ -var isobject = __webpack_require__(747); -var isDescriptor = __webpack_require__(759); +var isobject = __webpack_require__(749); +var isDescriptor = __webpack_require__(763); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -100378,7 +101255,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 853 */ +/* 865 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100432,7 +101309,7 @@ function diffArray(one, two) { /***/ }), -/* 854 */ +/* 866 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100445,7 +101322,7 @@ function diffArray(one, two) { -var isObject = __webpack_require__(747); +var isObject = __webpack_require__(749); module.exports = function pick(obj, keys) { if (!isObject(obj) && typeof obj !== 'function') { @@ -100474,7 +101351,7 @@ module.exports = function pick(obj, keys) { /***/ }), -/* 855 */ +/* 867 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -100609,7 +101486,7 @@ function isBuffer(val) { /***/ }), -/* 856 */ +/* 868 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -100619,18 +101496,18 @@ function isBuffer(val) { * Module dependencies */ -var extend = __webpack_require__(737); -var unique = __webpack_require__(740); -var toRegex = __webpack_require__(728); +var extend = __webpack_require__(740); +var unique = __webpack_require__(742); +var toRegex = __webpack_require__(853); /** * Local dependencies */ -var compilers = __webpack_require__(857); -var parsers = __webpack_require__(868); -var Extglob = __webpack_require__(871); -var utils = __webpack_require__(870); +var compilers = __webpack_require__(869); +var parsers = __webpack_require__(880); +var Extglob = __webpack_require__(883); +var utils = __webpack_require__(882); var MAX_LENGTH = 1024 * 64; /** @@ -100947,13 +101824,13 @@ module.exports = extglob; /***/ }), -/* 857 */ +/* 869 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(858); +var brackets = __webpack_require__(870); /** * Extglob compilers @@ -101123,7 +102000,7 @@ module.exports = function(extglob) { /***/ }), -/* 858 */ +/* 870 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101133,17 +102010,17 @@ module.exports = function(extglob) { * Local dependencies */ -var compilers = __webpack_require__(859); -var parsers = __webpack_require__(861); +var compilers = __webpack_require__(871); +var parsers = __webpack_require__(873); /** * Module dependencies */ -var debug = __webpack_require__(863)('expand-brackets'); -var extend = __webpack_require__(737); -var Snapdragon = __webpack_require__(767); -var toRegex = __webpack_require__(728); +var debug = __webpack_require__(875)('expand-brackets'); +var extend = __webpack_require__(740); +var Snapdragon = __webpack_require__(771); +var toRegex = __webpack_require__(853); /** * Parses the given POSIX character class `pattern` and returns a @@ -101341,13 +102218,13 @@ module.exports = brackets; /***/ }), -/* 859 */ +/* 871 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var posix = __webpack_require__(860); +var posix = __webpack_require__(872); module.exports = function(brackets) { brackets.compiler @@ -101435,7 +102312,7 @@ module.exports = function(brackets) { /***/ }), -/* 860 */ +/* 872 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -101464,14 +102341,14 @@ module.exports = { /***/ }), -/* 861 */ +/* 873 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(862); -var define = __webpack_require__(729); +var utils = __webpack_require__(874); +var define = __webpack_require__(798); /** * Text regex @@ -101690,13 +102567,13 @@ module.exports.TEXT_REGEX = TEXT_REGEX; /***/ }), -/* 862 */ +/* 874 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var toRegex = __webpack_require__(728); +var toRegex = __webpack_require__(853); var regexNot = __webpack_require__(739); var cached; @@ -101731,7 +102608,7 @@ exports.createRegex = function(pattern, include) { /***/ }), -/* 863 */ +/* 875 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -101740,14 +102617,14 @@ exports.createRegex = function(pattern, include) { */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(864); + module.exports = __webpack_require__(876); } else { - module.exports = __webpack_require__(867); + module.exports = __webpack_require__(879); } /***/ }), -/* 864 */ +/* 876 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -101756,7 +102633,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(865); +exports = module.exports = __webpack_require__(877); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -101938,7 +102815,7 @@ function localstorage() { /***/ }), -/* 865 */ +/* 877 */ /***/ (function(module, exports, __webpack_require__) { @@ -101954,7 +102831,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(866); +exports.humanize = __webpack_require__(878); /** * The currently active debug mode names, and names to skip. @@ -102146,7 +103023,7 @@ function coerce(val) { /***/ }), -/* 866 */ +/* 878 */ /***/ (function(module, exports) { /** @@ -102304,7 +103181,7 @@ function plural(ms, n, name) { /***/ }), -/* 867 */ +/* 879 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -102320,7 +103197,7 @@ var util = __webpack_require__(29); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(865); +exports = module.exports = __webpack_require__(877); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -102499,7 +103376,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(805); + var net = __webpack_require__(816); stream = new net.Socket({ fd: fd, readable: false, @@ -102558,15 +103435,15 @@ exports.enable(load()); /***/ }), -/* 868 */ +/* 880 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(858); -var define = __webpack_require__(869); -var utils = __webpack_require__(870); +var brackets = __webpack_require__(870); +var define = __webpack_require__(881); +var utils = __webpack_require__(882); /** * Characters to use in text regex (we want to "not" match @@ -102721,7 +103598,7 @@ module.exports = parsers; /***/ }), -/* 869 */ +/* 881 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102734,7 +103611,7 @@ module.exports = parsers; -var isDescriptor = __webpack_require__(759); +var isDescriptor = __webpack_require__(763); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -102759,14 +103636,14 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 870 */ +/* 882 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var regex = __webpack_require__(739); -var Cache = __webpack_require__(849); +var Cache = __webpack_require__(861); /** * Utils @@ -102835,7 +103712,7 @@ utils.createRegex = function(str) { /***/ }), -/* 871 */ +/* 883 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -102845,16 +103722,16 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(767); -var define = __webpack_require__(869); -var extend = __webpack_require__(737); +var Snapdragon = __webpack_require__(771); +var define = __webpack_require__(881); +var extend = __webpack_require__(740); /** * Local dependencies */ -var compilers = __webpack_require__(857); -var parsers = __webpack_require__(868); +var compilers = __webpack_require__(869); +var parsers = __webpack_require__(880); /** * Customize Snapdragon parser and renderer @@ -102920,16 +103797,16 @@ module.exports = Extglob; /***/ }), -/* 872 */ +/* 884 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extglob = __webpack_require__(856); -var nanomatch = __webpack_require__(841); +var extglob = __webpack_require__(868); +var nanomatch = __webpack_require__(852); var regexNot = __webpack_require__(739); -var toRegex = __webpack_require__(829); +var toRegex = __webpack_require__(840); var not; /** @@ -103010,14 +103887,14 @@ function textRegex(pattern) { /***/ }), -/* 873 */ +/* 885 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(849))(); +module.exports = new (__webpack_require__(861))(); /***/ }), -/* 874 */ +/* 886 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103030,13 +103907,13 @@ var path = __webpack_require__(16); * Module dependencies */ -var Snapdragon = __webpack_require__(767); -utils.define = __webpack_require__(836); -utils.diff = __webpack_require__(853); -utils.extend = __webpack_require__(837); -utils.pick = __webpack_require__(854); -utils.typeOf = __webpack_require__(875); -utils.unique = __webpack_require__(740); +var Snapdragon = __webpack_require__(771); +utils.define = __webpack_require__(847); +utils.diff = __webpack_require__(865); +utils.extend = __webpack_require__(848); +utils.pick = __webpack_require__(866); +utils.typeOf = __webpack_require__(887); +utils.unique = __webpack_require__(742); /** * Returns true if the platform is windows, or `path.sep` is `\\`. @@ -103333,7 +104210,7 @@ utils.unixify = function(options) { /***/ }), -/* 875 */ +/* 887 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -103468,7 +104345,7 @@ function isBuffer(val) { /***/ }), -/* 876 */ +/* 888 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103487,9 +104364,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(877); -var reader_1 = __webpack_require__(890); -var fs_stream_1 = __webpack_require__(894); +var readdir = __webpack_require__(889); +var reader_1 = __webpack_require__(902); +var fs_stream_1 = __webpack_require__(906); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -103550,15 +104427,15 @@ exports.default = ReaderAsync; /***/ }), -/* 877 */ +/* 889 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(878); -const readdirAsync = __webpack_require__(886); -const readdirStream = __webpack_require__(889); +const readdirSync = __webpack_require__(890); +const readdirAsync = __webpack_require__(898); +const readdirStream = __webpack_require__(901); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -103642,7 +104519,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 878 */ +/* 890 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103650,11 +104527,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(879); +const DirectoryReader = __webpack_require__(891); let syncFacade = { - fs: __webpack_require__(884), - forEach: __webpack_require__(885), + fs: __webpack_require__(896), + forEach: __webpack_require__(897), sync: true }; @@ -103683,7 +104560,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 879 */ +/* 891 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -103692,9 +104569,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(27).Readable; const EventEmitter = __webpack_require__(379).EventEmitter; const path = __webpack_require__(16); -const normalizeOptions = __webpack_require__(880); -const stat = __webpack_require__(882); -const call = __webpack_require__(883); +const normalizeOptions = __webpack_require__(892); +const stat = __webpack_require__(894); +const call = __webpack_require__(895); /** * Asynchronously reads the contents of a directory and streams the results @@ -104070,14 +104947,14 @@ module.exports = DirectoryReader; /***/ }), -/* 880 */ +/* 892 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const globToRegExp = __webpack_require__(881); +const globToRegExp = __webpack_require__(893); module.exports = normalizeOptions; @@ -104254,7 +105131,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 881 */ +/* 893 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -104391,13 +105268,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 882 */ +/* 894 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(883); +const call = __webpack_require__(895); module.exports = stat; @@ -104472,7 +105349,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 883 */ +/* 895 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104533,14 +105410,14 @@ function callOnce (fn) { /***/ }), -/* 884 */ +/* 896 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const call = __webpack_require__(883); +const call = __webpack_require__(895); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -104604,7 +105481,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 885 */ +/* 897 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104633,7 +105510,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 886 */ +/* 898 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104641,12 +105518,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(887); -const DirectoryReader = __webpack_require__(879); +const maybe = __webpack_require__(899); +const DirectoryReader = __webpack_require__(891); let asyncFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(888), + forEach: __webpack_require__(900), async: true }; @@ -104688,7 +105565,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 887 */ +/* 899 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104715,7 +105592,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 888 */ +/* 900 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104751,7 +105628,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 889 */ +/* 901 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104759,11 +105636,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(879); +const DirectoryReader = __webpack_require__(891); let streamFacade = { fs: __webpack_require__(23), - forEach: __webpack_require__(888), + forEach: __webpack_require__(900), async: true }; @@ -104783,16 +105660,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 890 */ +/* 902 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(16); -var deep_1 = __webpack_require__(891); -var entry_1 = __webpack_require__(893); -var pathUtil = __webpack_require__(892); +var deep_1 = __webpack_require__(903); +var entry_1 = __webpack_require__(905); +var pathUtil = __webpack_require__(904); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -104858,13 +105735,13 @@ exports.default = Reader; /***/ }), -/* 891 */ +/* 903 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(892); +var pathUtils = __webpack_require__(904); var patternUtils = __webpack_require__(721); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { @@ -104948,7 +105825,7 @@ exports.default = DeepFilter; /***/ }), -/* 892 */ +/* 904 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -104979,13 +105856,13 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 893 */ +/* 905 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(892); +var pathUtils = __webpack_require__(904); var patternUtils = __webpack_require__(721); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { @@ -105071,7 +105948,7 @@ exports.default = EntryFilter; /***/ }), -/* 894 */ +/* 906 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105091,8 +105968,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var fsStat = __webpack_require__(895); -var fs_1 = __webpack_require__(899); +var fsStat = __webpack_require__(907); +var fs_1 = __webpack_require__(911); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -105142,14 +106019,14 @@ exports.default = FileSystemStream; /***/ }), -/* 895 */ +/* 907 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(896); -const statProvider = __webpack_require__(898); +const optionsManager = __webpack_require__(908); +const statProvider = __webpack_require__(910); /** * Asynchronous API. */ @@ -105180,13 +106057,13 @@ exports.statSync = statSync; /***/ }), -/* 896 */ +/* 908 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(897); +const fsAdapter = __webpack_require__(909); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -105199,7 +106076,7 @@ exports.prepare = prepare; /***/ }), -/* 897 */ +/* 909 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105222,7 +106099,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 898 */ +/* 910 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105274,7 +106151,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 899 */ +/* 911 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105305,7 +106182,7 @@ exports.default = FileSystem; /***/ }), -/* 900 */ +/* 912 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105325,9 +106202,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(27); -var readdir = __webpack_require__(877); -var reader_1 = __webpack_require__(890); -var fs_stream_1 = __webpack_require__(894); +var readdir = __webpack_require__(889); +var reader_1 = __webpack_require__(902); +var fs_stream_1 = __webpack_require__(906); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -105395,7 +106272,7 @@ exports.default = ReaderStream; /***/ }), -/* 901 */ +/* 913 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105414,9 +106291,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(877); -var reader_1 = __webpack_require__(890); -var fs_sync_1 = __webpack_require__(902); +var readdir = __webpack_require__(889); +var reader_1 = __webpack_require__(902); +var fs_sync_1 = __webpack_require__(914); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -105476,7 +106353,7 @@ exports.default = ReaderSync; /***/ }), -/* 902 */ +/* 914 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105495,8 +106372,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(895); -var fs_1 = __webpack_require__(899); +var fsStat = __webpack_require__(907); +var fs_1 = __webpack_require__(911); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -105542,7 +106419,7 @@ exports.default = FileSystemSync; /***/ }), -/* 903 */ +/* 915 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105558,7 +106435,7 @@ exports.flatten = flatten; /***/ }), -/* 904 */ +/* 916 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105579,13 +106456,13 @@ exports.merge = merge; /***/ }), -/* 905 */ +/* 917 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const pathType = __webpack_require__(906); +const pathType = __webpack_require__(918); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -105651,13 +106528,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 906 */ +/* 918 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(23); -const pify = __webpack_require__(907); +const pify = __webpack_require__(919); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -105700,7 +106577,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 907 */ +/* 919 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105791,7 +106668,7 @@ module.exports = (obj, opts) => { /***/ }), -/* 908 */ +/* 920 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -105799,9 +106676,9 @@ module.exports = (obj, opts) => { const fs = __webpack_require__(23); const path = __webpack_require__(16); const fastGlob = __webpack_require__(717); -const gitIgnore = __webpack_require__(909); -const pify = __webpack_require__(910); -const slash = __webpack_require__(911); +const gitIgnore = __webpack_require__(921); +const pify = __webpack_require__(922); +const slash = __webpack_require__(923); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -105899,7 +106776,7 @@ module.exports.sync = options => { /***/ }), -/* 909 */ +/* 921 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -106368,7 +107245,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 910 */ +/* 922 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106443,7 +107320,7 @@ module.exports = (input, options) => { /***/ }), -/* 911 */ +/* 923 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106461,17 +107338,17 @@ module.exports = input => { /***/ }), -/* 912 */ +/* 924 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); const {constants: fsConstants} = __webpack_require__(23); -const pEvent = __webpack_require__(913); -const CpFileError = __webpack_require__(916); -const fs = __webpack_require__(920); -const ProgressEmitter = __webpack_require__(923); +const pEvent = __webpack_require__(925); +const CpFileError = __webpack_require__(928); +const fs = __webpack_require__(932); +const ProgressEmitter = __webpack_require__(935); const cpFileAsync = async (source, destination, options, progressEmitter) => { let readError; @@ -106585,12 +107462,12 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 913 */ +/* 925 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pTimeout = __webpack_require__(914); +const pTimeout = __webpack_require__(926); const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; @@ -106881,12 +107758,12 @@ module.exports.iterator = (emitter, event, options) => { /***/ }), -/* 914 */ +/* 926 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pFinally = __webpack_require__(915); +const pFinally = __webpack_require__(927); class TimeoutError extends Error { constructor(message) { @@ -106932,7 +107809,7 @@ module.exports.TimeoutError = TimeoutError; /***/ }), -/* 915 */ +/* 927 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -106954,12 +107831,12 @@ module.exports = (promise, onFinally) => { /***/ }), -/* 916 */ +/* 928 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(917); +const NestedError = __webpack_require__(929); class CpFileError extends NestedError { constructor(message, nested) { @@ -106973,10 +107850,10 @@ module.exports = CpFileError; /***/ }), -/* 917 */ +/* 929 */ /***/ (function(module, exports, __webpack_require__) { -var inherits = __webpack_require__(918); +var inherits = __webpack_require__(930); var NestedError = function (message, nested) { this.nested = nested; @@ -107027,7 +107904,7 @@ module.exports = NestedError; /***/ }), -/* 918 */ +/* 930 */ /***/ (function(module, exports, __webpack_require__) { try { @@ -107035,12 +107912,12 @@ try { if (typeof util.inherits !== 'function') throw ''; module.exports = util.inherits; } catch (e) { - module.exports = __webpack_require__(919); + module.exports = __webpack_require__(931); } /***/ }), -/* 919 */ +/* 931 */ /***/ (function(module, exports) { if (typeof Object.create === 'function') { @@ -107069,16 +107946,16 @@ if (typeof Object.create === 'function') { /***/ }), -/* 920 */ +/* 932 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(29); const fs = __webpack_require__(22); -const makeDir = __webpack_require__(921); -const pEvent = __webpack_require__(913); -const CpFileError = __webpack_require__(916); +const makeDir = __webpack_require__(933); +const pEvent = __webpack_require__(925); +const CpFileError = __webpack_require__(928); const stat = promisify(fs.stat); const lstat = promisify(fs.lstat); @@ -107175,7 +108052,7 @@ exports.copyFileSync = (source, destination, flags) => { /***/ }), -/* 921 */ +/* 933 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -107183,7 +108060,7 @@ exports.copyFileSync = (source, destination, flags) => { const fs = __webpack_require__(23); const path = __webpack_require__(16); const {promisify} = __webpack_require__(29); -const semver = __webpack_require__(922); +const semver = __webpack_require__(934); const defaults = { mode: 0o777 & (~process.umask()), @@ -107332,7 +108209,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 922 */ +/* 934 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -108934,7 +109811,7 @@ function coerce (version, options) { /***/ }), -/* 923 */ +/* 935 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -108975,7 +109852,7 @@ module.exports = ProgressEmitter; /***/ }), -/* 924 */ +/* 936 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -109021,12 +109898,12 @@ exports.default = module.exports; /***/ }), -/* 925 */ +/* 937 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(926); +const NestedError = __webpack_require__(938); class CpyError extends NestedError { constructor(message, nested) { @@ -109040,7 +109917,7 @@ module.exports = CpyError; /***/ }), -/* 926 */ +/* 938 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(29).inherits; @@ -109096,7 +109973,7 @@ module.exports = NestedError; /***/ }), -/* 927 */ +/* 939 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; diff --git a/packages/kbn-ui-shared-deps/index.d.ts b/packages/kbn-ui-shared-deps/index.d.ts index dec519da69641..b829c87d91c4a 100644 --- a/packages/kbn-ui-shared-deps/index.d.ts +++ b/packages/kbn-ui-shared-deps/index.d.ts @@ -53,3 +53,8 @@ export const lightCssDistFilename: string; export const externals: { [key: string]: string; }; + +/** + * Webpack loader for configuring the public path lookup from `window.__kbnPublicPath__`. + */ +export const publicPathLoader: string; diff --git a/packages/kbn-ui-shared-deps/index.js b/packages/kbn-ui-shared-deps/index.js index 666ec7a46ff06..42ed08259ac8f 100644 --- a/packages/kbn-ui-shared-deps/index.js +++ b/packages/kbn-ui-shared-deps/index.js @@ -64,3 +64,4 @@ exports.externals = { 'elasticsearch-browser': '__kbnSharedDeps__.ElasticsearchBrowser', 'elasticsearch-browser/elasticsearch': '__kbnSharedDeps__.ElasticsearchBrowser', }; +exports.publicPathLoader = require.resolve('./public_path_loader'); diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index 3aa01179c00f3..a60e2b0449d95 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,7 +9,7 @@ "kbn:watch": "node scripts/build --watch" }, "dependencies": { - "@elastic/charts": "18.3.0", + "@elastic/charts": "18.4.2", "@elastic/eui": "22.3.0", "@kbn/i18n": "1.0.0", "abortcontroller-polyfill": "^1.4.0", diff --git a/packages/kbn-ui-shared-deps/public_path_loader.js b/packages/kbn-ui-shared-deps/public_path_loader.js new file mode 100644 index 0000000000000..6b7a27c9ca52b --- /dev/null +++ b/packages/kbn-ui-shared-deps/public_path_loader.js @@ -0,0 +1,23 @@ +/* + * 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(source) { + const options = this.query; + return `__webpack_public_path__ = window.__kbnPublicPath__['${options.key}'];${source}`; +}; diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js index a875274544905..bf63c57765859 100644 --- a/packages/kbn-ui-shared-deps/webpack.config.js +++ b/packages/kbn-ui-shared-deps/webpack.config.js @@ -46,7 +46,6 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({ path: UiSharedDeps.distDir, filename: '[name].js', sourceMapFilename: '[file].map', - publicPath: '__REPLACE_WITH_PUBLIC_PATH__', devtoolModuleFilenameTemplate: info => `kbn-ui-shared-deps/${Path.relative(REPO_ROOT, info.absoluteResourcePath)}`, library: '__kbnSharedDeps__', @@ -55,6 +54,17 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({ module: { noParse: [MOMENT_SRC], rules: [ + { + include: [require.resolve('./entry.js')], + use: [ + { + loader: UiSharedDeps.publicPathLoader, + options: { + key: 'kbn-ui-shared-deps', + }, + }, + ], + }, { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'], diff --git a/src/core/public/http/index.ts b/src/core/public/http/index.ts index d4aced6894526..3cd8ef6169090 100644 --- a/src/core/public/http/index.ts +++ b/src/core/public/http/index.ts @@ -18,4 +18,5 @@ */ export { HttpService } from './http_service'; +export { HttpFetchError } from './http_fetch_error'; export * from './types'; diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 254cac3495599..b4f64125a03ef 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -143,6 +143,7 @@ export { export { HttpHeadersInit, HttpRequestInit, + HttpFetchError, HttpFetchOptions, HttpFetchOptionsWithPath, HttpFetchQuery, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 6d95d1bc7069c..b92bb209d2607 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -593,6 +593,23 @@ export type HandlerFunction = (context: T, ...args: any[]) => // @public export type HandlerParameters> = T extends (context: any, ...args: infer U) => any ? U : never; +// @internal (undocumented) +export class HttpFetchError extends Error implements IHttpFetchError { + constructor(message: string, name: string, request: Request, response?: Response | undefined, body?: any); + // (undocumented) + readonly body?: any; + // (undocumented) + readonly name: string; + // (undocumented) + readonly req: Request; + // (undocumented) + readonly request: Request; + // (undocumented) + readonly res?: Response; + // (undocumented) + readonly response?: Response | undefined; +} + // @public export interface HttpFetchOptions extends HttpRequestInit { asResponse?: boolean; diff --git a/src/core/public/saved_objects/saved_objects_client.ts b/src/core/public/saved_objects/saved_objects_client.ts index d26cadecb184a..7958a4f8134d3 100644 --- a/src/core/public/saved_objects/saved_objects_client.ts +++ b/src/core/public/saved_objects/saved_objects_client.ts @@ -216,17 +216,7 @@ export class SavedObjectsClient { }), }); - return createRequest - .then(resp => this.createSavedObject(resp)) - .catch((error: object) => { - if (isAutoCreateIndexError(error)) { - window.location.assign( - this.http.basePath.prepend('/app/kibana#/error/action.auto_create_index') - ); - } - - throw error; - }); + return createRequest.then(resp => this.createSavedObject(resp)); }; /** @@ -468,9 +458,3 @@ const renameKeys = , U extends Record ...{ [keysMap[key] || key]: obj[key] }, }; }, {}); - -const isAutoCreateIndexError = (error: any) => { - return ( - error?.res?.status === 503 && error?.body?.attributes?.code === 'ES_AUTO_CREATE_INDEX_ERROR' - ); -}; diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 3b9a39db72278..2451b98ffdf29 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -45,6 +45,7 @@ export { elasticsearchServiceMock } from './elasticsearch/elasticsearch_service. export { httpServiceMock } from './http/http_service.mock'; export { loggingServiceMock } from './logging/logging_service.mock'; export { savedObjectsRepositoryMock } from './saved_objects/service/lib/repository.mock'; +export { savedObjectsServiceMock } from './saved_objects/saved_objects_service.mock'; export { typeRegistryMock as savedObjectsTypeRegistryMock } from './saved_objects/saved_objects_type_registry.mock'; export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; export { metricsServiceMock } from './metrics/metrics_service.mock'; diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts index 64270c677ff20..3ec478e3ca28d 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts @@ -149,13 +149,13 @@ describe('DocumentMigrator', () => { expect(_.get(migratedDoc, 'attributes.name')).toBe('Mike'); }); - it('migrates meta properties', () => { + it('migrates root properties', () => { const migrator = new DocumentMigrator({ ...testOpts(), typeRegistry: createRegistry({ name: 'acl', migrations: { - '2.3.5': setAttr('acl', 'admins-only,sucka!'), + '2.3.5': setAttr('acl', 'admins-only, sucka!'), }, }), }); @@ -165,13 +165,13 @@ describe('DocumentMigrator', () => { attributes: { name: 'Tyler' }, acl: 'anyone', migrationVersion: {}, - }); + } as SavedObjectUnsanitizedDoc); expect(actual).toEqual({ id: 'me', type: 'user', attributes: { name: 'Tyler' }, migrationVersion: { acl: '2.3.5' }, - acl: 'admins-only,sucka!', + acl: 'admins-only, sucka!', }); }); @@ -241,7 +241,7 @@ describe('DocumentMigrator', () => { type: 'user', attributes: { name: 'Tyler' }, bbb: 'Shazm', - }); + } as SavedObjectUnsanitizedDoc); expect(actual).toEqual({ id: 'me', type: 'user', @@ -405,7 +405,7 @@ describe('DocumentMigrator', () => { attributes: { name: 'Callie' }, dawg: 'Yo', migrationVersion: {}, - }); + } as SavedObjectUnsanitizedDoc); expect(actual).toEqual({ id: 'smelly', type: 'foo', diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index 0284f513a361c..4ddb2b070d3ac 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -279,7 +279,7 @@ function props(doc: SavedObjectUnsanitizedDoc) { */ function propVersion(doc: SavedObjectUnsanitizedDoc | ActiveMigrations, prop: string) { return ( - (doc[prop] && doc[prop].latestVersion) || + ((doc as any)[prop] && (doc as any)[prop].latestVersion) || (doc.migrationVersion && (doc as any).migrationVersion[prop]) ); } diff --git a/src/core/server/saved_objects/serialization/types.ts b/src/core/server/saved_objects/serialization/types.ts index 7ea61f67e9496..a33e16895078e 100644 --- a/src/core/server/saved_objects/serialization/types.ts +++ b/src/core/server/saved_objects/serialization/types.ts @@ -45,10 +45,7 @@ export interface SavedObjectsRawDocSource { } /** - * A saved object type definition that allows for miscellaneous, unknown - * properties, as current discussions around security, ACLs, etc indicate - * that future props are likely to be added. Migrations support this - * scenario out of the box. + * Saved Object base document */ interface SavedObjectDoc { attributes: any; @@ -59,8 +56,6 @@ interface SavedObjectDoc { migrationVersion?: SavedObjectsMigrationVersion; version?: string; updated_at?: string; - - [rootProp: string]: any; } interface Referencable { @@ -68,14 +63,18 @@ interface Referencable { } /** - * We want to have two types, one that guarantees a "references" attribute - * will exist and one that allows it to be null. Since we're not migrating - * all the saved objects to have a "references" array, we need to support - * the scenarios where it may be missing (ex migrations). + * Describes Saved Object documents from Kibana < 7.0.0 which don't have a + * `references` root property defined. This type should only be used in + * migrations. * * @public */ export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; -/** @public */ +/** + * Describes Saved Object documents that have passed through the migration + * framework and are guaranteed to have a `references` root property. + * + * @public + */ export type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; diff --git a/src/core/server/saved_objects/service/lib/errors.test.ts b/src/core/server/saved_objects/service/lib/errors.test.ts index 4a43835d795d1..324d19e279212 100644 --- a/src/core/server/saved_objects/service/lib/errors.test.ts +++ b/src/core/server/saved_objects/service/lib/errors.test.ts @@ -403,43 +403,4 @@ describe('savedObjectsClient/errorTypes', () => { }); }); }); - - describe('EsAutoCreateIndex error', () => { - describe('createEsAutoCreateIndexError', () => { - it('does not take an error argument', () => { - const error = new Error(); - // @ts-ignore - expect(SavedObjectsErrorHelpers.createEsAutoCreateIndexError(error)).not.toBe(error); - }); - - it('returns a new Error', () => { - expect(SavedObjectsErrorHelpers.createEsAutoCreateIndexError()).toBeInstanceOf(Error); - }); - - it('makes errors identifiable as EsAutoCreateIndex errors', () => { - expect( - SavedObjectsErrorHelpers.isEsAutoCreateIndexError( - SavedObjectsErrorHelpers.createEsAutoCreateIndexError() - ) - ).toBe(true); - }); - - it('returns a boom error', () => { - const error = SavedObjectsErrorHelpers.createEsAutoCreateIndexError(); - expect(error).toHaveProperty('isBoom', true); - }); - - describe('error.output', () => { - it('uses "Automatic index creation failed" message', () => { - const error = SavedObjectsErrorHelpers.createEsAutoCreateIndexError(); - expect(error.output.payload).toHaveProperty('message', 'Automatic index creation failed'); - }); - - it('sets statusCode to 503', () => { - const error = SavedObjectsErrorHelpers.createEsAutoCreateIndexError(); - expect(error.output).toHaveProperty('statusCode', 503); - }); - }); - }); - }); }); diff --git a/src/core/server/saved_objects/service/lib/errors.ts b/src/core/server/saved_objects/service/lib/errors.ts index 478c6b6d26d53..9614d692741e0 100644 --- a/src/core/server/saved_objects/service/lib/errors.ts +++ b/src/core/server/saved_objects/service/lib/errors.ts @@ -37,8 +37,6 @@ const CODE_CONFLICT = 'SavedObjectsClient/conflict'; const CODE_ES_CANNOT_EXECUTE_SCRIPT = 'SavedObjectsClient/esCannotExecuteScript'; // 503 - Es Unavailable const CODE_ES_UNAVAILABLE = 'SavedObjectsClient/esUnavailable'; -// 503 - Unable to automatically create index because of action.auto_create_index setting -const CODE_ES_AUTO_CREATE_INDEX_ERROR = 'SavedObjectsClient/autoCreateIndex'; // 500 - General Error const CODE_GENERAL_ERROR = 'SavedObjectsClient/generalError'; @@ -180,18 +178,6 @@ export class SavedObjectsErrorHelpers { return isSavedObjectsClientError(error) && error[code] === CODE_ES_UNAVAILABLE; } - public static createEsAutoCreateIndexError() { - const error = Boom.serverUnavailable('Automatic index creation failed'); - error.output.payload.attributes = error.output.payload.attributes || {}; - error.output.payload.attributes.code = 'ES_AUTO_CREATE_INDEX_ERROR'; - - return decorate(error, CODE_ES_AUTO_CREATE_INDEX_ERROR, 503); - } - - public static isEsAutoCreateIndexError(error: Error | DecoratedError) { - return isSavedObjectsClientError(error) && error[code] === CODE_ES_AUTO_CREATE_INDEX_ERROR; - } - public static decorateGeneralError(error: Error, reason?: string) { return decorate(error, CODE_GENERAL_ERROR, 500, reason); } diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 5f17c11792763..bc8ad2cdb0058 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -239,40 +239,31 @@ export class SavedObjectsRepository { } } - try { - const migrated = this._migrator.migrateDocument({ - id, - type, - ...(savedObjectNamespace && { namespace: savedObjectNamespace }), - ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), - attributes, - migrationVersion, - updated_at: time, - ...(Array.isArray(references) && { references }), - }); - - const raw = this._serializer.savedObjectToRaw(migrated as SavedObjectSanitizedDoc); + const migrated = this._migrator.migrateDocument({ + id, + type, + ...(savedObjectNamespace && { namespace: savedObjectNamespace }), + ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), + attributes, + migrationVersion, + updated_at: time, + ...(Array.isArray(references) && { references }), + }); - const method = id && overwrite ? 'index' : 'create'; - const response = await this._writeToCluster(method, { - id: raw._id, - index: this.getIndexForType(type), - refresh, - body: raw._source, - }); + const raw = this._serializer.savedObjectToRaw(migrated as SavedObjectSanitizedDoc); - return this._rawToSavedObject({ - ...raw, - ...response, - }); - } catch (error) { - if (SavedObjectsErrorHelpers.isNotFoundError(error)) { - // See "503s from missing index" above - throw SavedObjectsErrorHelpers.createEsAutoCreateIndexError(); - } + const method = id && overwrite ? 'index' : 'create'; + const response = await this._writeToCluster(method, { + id: raw._id, + index: this.getIndexForType(type), + refresh, + body: raw._source, + }); - throw error; - } + return this._rawToSavedObject({ + ...raw, + ...response, + }); } /** diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index b50c6dc9a1abf..43b7663491711 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -156,15 +156,6 @@ export type MutatingOperationRefreshSetting = boolean | 'wait_for'; * takes special care to ensure that 404 errors are generic and don't distinguish * between index missing or document missing. * - * ### 503s from missing index - * - * Unlike all other methods, create requests are supposed to succeed even when - * the Kibana index does not exist because it will be automatically created by - * elasticsearch. When that is not the case it is because Elasticsearch's - * `action.auto_create_index` setting prevents it from being created automatically - * so we throw a special 503 with the intention of informing the user that their - * Elasticsearch settings need to be updated. - * * See {@link SavedObjectsClient} * See {@link SavedObjectsErrorHelpers} * diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 6369720ada2c3..a36e746f6d940 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1707,7 +1707,7 @@ export interface SavedObjectsAddToNamespacesOptions extends SavedObjectsBaseOpti // Warning: (ae-forgotten-export) The symbol "SavedObjectDoc" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "Referencable" needs to be exported by the entry point index.d.ts // -// @public (undocumented) +// @public export type SavedObjectSanitizedDoc = SavedObjectDoc & Referencable; // @public (undocumented) @@ -1875,8 +1875,6 @@ export class SavedObjectsErrorHelpers { // (undocumented) static createConflictError(type: string, id: string): DecoratedError; // (undocumented) - static createEsAutoCreateIndexError(): DecoratedError; - // (undocumented) static createGenericNotFoundError(type?: string | null, id?: string | null): DecoratedError; // (undocumented) static createInvalidVersionError(versionInput?: string): DecoratedError; @@ -1903,8 +1901,6 @@ export class SavedObjectsErrorHelpers { // (undocumented) static isConflictError(error: Error | DecoratedError): boolean; // (undocumented) - static isEsAutoCreateIndexError(error: Error | DecoratedError): boolean; - // (undocumented) static isEsCannotExecuteScriptError(error: Error | DecoratedError): boolean; // (undocumented) static isEsUnavailableError(error: Error | DecoratedError): boolean; diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 7da14e0dfe51b..43a2cbd78c502 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -63,6 +63,7 @@ export default { '/src/dev/jest/mocks/file_mock.js', '\\.(css|less|scss)$': '/src/dev/jest/mocks/style_mock.js', '\\.ace\\.worker.js$': '/src/dev/jest/mocks/ace_worker_module_mock.js', + '^(!!)?file-loader!': '/src/dev/jest/mocks/file_mock.js', }, setupFiles: [ '/src/dev/jest/setup/babel_polyfill.js', diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index a75a6997a8cb2..fc95288eabed8 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -159,8 +159,6 @@ export const TEMPORARILY_IGNORED_PATHS = [ 'webpackShims/elasticsearch-browser.js', 'webpackShims/moment-timezone.js', 'webpackShims/ui-bootstrap.js', - 'x-pack/legacy/plugins/graph/public/graphClientWorkspace.js', - 'x-pack/legacy/plugins/graph/public/angular-venn-simple.js', 'x-pack/legacy/plugins/index_management/public/lib/editSettings.js', 'x-pack/legacy/plugins/license_management/public/store/reducers/licenseManagement.js', 'x-pack/legacy/plugins/monitoring/public/components/sparkline/__mocks__/plugins/xpack_main/jquery_flot.js', diff --git a/src/dev/run_check_published_api_changes.ts b/src/dev/run_check_published_api_changes.ts index ba3cd1280f34b..6d5fa04a93951 100644 --- a/src/dev/run_check_published_api_changes.ts +++ b/src/dev/run_check_published_api_changes.ts @@ -250,7 +250,7 @@ async function run( Options: --accept {dim Accepts all changes by updating the API Review files and documentation} --docs {dim Updates the Core API documentation} - --only {dim RegExp that folder names must match, folders: [${folders.join(', ')}]} + --filter {dim RegExp that folder names must match, folders: [${folders.join(', ')}]} --help {dim Show this message} `) ); diff --git a/src/es_archiver/actions/rebuild_all.ts b/src/es_archiver/actions/rebuild_all.ts index 1467a1d0430b7..f35b2ca49c666 100644 --- a/src/es_archiver/actions/rebuild_all.ts +++ b/src/es_archiver/actions/rebuild_all.ts @@ -18,7 +18,7 @@ */ import { resolve, dirname, relative } from 'path'; -import { stat, rename, createReadStream, createWriteStream } from 'fs'; +import { stat, Stats, rename, createReadStream, createWriteStream } from 'fs'; import { Readable, Writable } from 'stream'; import { fromNode } from 'bluebird'; import { ToolingLog } from '@kbn/dev-utils'; @@ -33,7 +33,7 @@ import { } from '../lib'; async function isDirectory(path: string): Promise { - const stats = await fromNode(cb => stat(path, cb)); + const stats: Stats = await fromNode(cb => stat(path, cb)); return stats.isDirectory(); } diff --git a/src/legacy/core_plugins/input_control_vis/index.ts b/src/legacy/core_plugins/input_control_vis/index.ts deleted file mode 100644 index 0529aa24dffd7..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/index.ts +++ /dev/null @@ -1,44 +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 { Legacy } from 'kibana'; - -import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; - -const inputControlVisPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'input_control_vis', - require: ['kibana', 'elasticsearch', 'interpreter'], - publicDir: resolve(__dirname, 'public'), - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars: server => ({}), - }, - init: (server: Legacy.Server) => ({}), - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - } as Legacy.PluginSpecOptions); - -// eslint-disable-next-line import/no-default-export -export default inputControlVisPluginInitializer; diff --git a/src/legacy/core_plugins/input_control_vis/package.json b/src/legacy/core_plugins/input_control_vis/package.json deleted file mode 100644 index 0d52be412f2fd..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "input_control_vis", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/input_control_vis/public/legacy.ts b/src/legacy/core_plugins/input_control_vis/public/legacy.ts deleted file mode 100644 index 67299068819e8..0000000000000 --- a/src/legacy/core_plugins/input_control_vis/public/legacy.ts +++ /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. - */ - -import { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart } from 'ui/new_platform'; - -import { plugin } from '.'; - -import { - InputControlVisPluginSetupDependencies, - InputControlVisPluginStartDependencies, -} from './plugin'; - -const setupPlugins: Readonly = { - expressions: npSetup.plugins.expressions, - data: npSetup.plugins.data, - visualizations: npSetup.plugins.visualizations, -}; - -const startPlugins: Readonly = { - expressions: npStart.plugins.expressions, - data: npStart.plugins.data, - visualizations: npStart.plugins.visualizations, -}; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, setupPlugins); -export const start = pluginInstance.start(npStart.core, startPlugins); diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 8465d71e1e998..48d86e3628e49 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -118,22 +118,6 @@ export default function(kibana) { }, ], - savedObjectsManagement: { - url: { - defaultSearchField: 'url', - isImportableAndExportable: true, - getTitle(obj) { - return `/goto/${encodeURIComponent(obj.id)}`; - }, - }, - }, - - savedObjectSchemas: { - 'kql-telemetry': { - isNamespaceAgnostic: true, - }, - }, - injectDefaultVars(server, options) { const mapConfig = server.config().get('map'); const tilemap = mapConfig.tilemap; @@ -158,61 +142,6 @@ export default function(kibana) { uiSettingDefaults: getUiSettingDefaults(), }, - uiCapabilities: async function() { - return { - discover: { - show: true, - createShortUrl: true, - save: true, - saveQuery: true, - }, - visualize: { - show: true, - createShortUrl: true, - delete: true, - save: true, - saveQuery: true, - }, - dashboard: { - createNew: true, - show: true, - showWriteControls: true, - saveQuery: true, - }, - catalogue: { - discover: true, - dashboard: true, - visualize: true, - console: true, - advanced_settings: true, - index_patterns: true, - }, - advancedSettings: { - show: true, - save: true, - }, - indexPatterns: { - save: true, - }, - savedObjectsManagement: { - delete: true, - edit: true, - read: true, - }, - management: { - /* - * Management settings correspond to management section/link ids, and should not be changed - * without also updating those definitions. - */ - kibana: { - settings: true, - index_patterns: true, - objects: true, - }, - }, - }; - }, - preInit: async function(server) { try { // Create the data directory (recursively, if the a parent dir doesn't exist). diff --git a/src/legacy/core_plugins/kibana/mappings.json b/src/legacy/core_plugins/kibana/mappings.json index febdf2cc3d649..e2cbd584dbe1f 100644 --- a/src/legacy/core_plugins/kibana/mappings.json +++ b/src/legacy/core_plugins/kibana/mappings.json @@ -1,41 +1,9 @@ { - "url": { - "properties": { - "accessCount": { - "type": "long" - }, - "accessDate": { - "type": "date" - }, - "createDate": { - "type": "date" - }, - "url": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } - } - } - }, "server": { "properties": { "uuid": { "type": "keyword" } } - }, - "kql-telemetry": { - "properties": { - "optInCount": { - "type": "long" - }, - "optOutCount": { - "type": "long" - } - } } } diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/afterparamchange.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/afterparamchange.png similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/afterparamchange.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/afterparamchange.png diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/afterresize.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/afterresize.png similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/afterresize.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/afterresize.png diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/basicdraw.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/basicdraw.png similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/basicdraw.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/basicdraw.png diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/simpleload.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/simpleload.png similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/simpleload.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/simpleload.png diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud.js similarity index 98% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud.js index 152efe5667f18..8f08f6a1f37e6 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud.js @@ -21,7 +21,6 @@ import expect from '@kbn/expect'; import _ from 'lodash'; import d3 from 'd3'; -import { TagCloud } from '../tag_cloud'; import { fromNode, delay } from 'bluebird'; import { ImageComparator } from 'test_utils/image_comparator'; import simpleloadPng from './simpleload.png'; @@ -29,6 +28,9 @@ import simpleloadPng from './simpleload.png'; // Replace with mock when converting to jest tests // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { seedColors } from '../../../../../../plugins/charts/public/services/colors/seed_colors'; +// Will be replaced with new path when tests are moved +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { TagCloud } from '../../../../../../plugins/vis_type_tagcloud/public/components/tag_cloud'; describe('tag cloud tests', function() { const minValue = 1; diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud_visualization.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud_visualization.js similarity index 89% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud_visualization.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud_visualization.js index 9e611861417cd..040ee18916fa2 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/__tests__/tag_cloud_visualization.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_tagcloud/tag_cloud_visualization.js @@ -20,7 +20,6 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import { ImageComparator } from 'test_utils/image_comparator'; -import { createTagCloudVisualization } from '../tag_cloud_visualization'; import basicdrawPng from './basicdraw.png'; import afterresizePng from './afterresize.png'; import afterparamChange from './afterparamchange.png'; @@ -32,7 +31,14 @@ import { ExprVis } from '../../../../../../plugins/visualizations/public/express import { seedColors } from '../../../../../../plugins/charts/public/services/colors/seed_colors'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { BaseVisType } from '../../../../../../plugins/visualizations/public/vis_types/base_vis_type'; -import { createTagCloudVisTypeDefinition } from '../../tag_cloud_type'; +// Will be replaced with new path when tests are moved +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createTagCloudVisTypeDefinition } from '../../../../../../plugins/vis_type_tagcloud/public/tag_cloud_type'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createTagCloudVisualization } from '../../../../../../plugins/vis_type_tagcloud/public/components/tag_cloud_visualization'; +import { npStart } from 'ui/new_platform'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { setFormatService } from '../../../../../../plugins/vis_type_tagcloud/public/services'; const THRESHOLD = 0.65; const PIXEL_DIFF = 64; @@ -66,6 +72,8 @@ describe('TagCloudVisualizationTest', function() { }, }); + before(() => setFormatService(npStart.plugins.data.fieldFormats)); + beforeEach(ngMock.module('kibana')); describe('TagCloudVisualization - basics', function() { diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_graph.hjson b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_graph.hjson similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_graph.hjson rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_graph.hjson diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_image_512.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_image_512.png similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_image_512.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_image_512.png diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_map_image_256.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_image_256.png similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_map_image_256.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_image_256.png diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_map_test.hjson b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_test.hjson similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_map_test.hjson rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_map_test.hjson diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_tooltip_test.hjson b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_tooltip_test.hjson similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_tooltip_test.hjson rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_tooltip_test.hjson diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js similarity index 86% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js index 6412d8a569b2a..21b7ea7dbf4c3 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vega_visualization.js +++ b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vega_visualization.js @@ -21,7 +21,9 @@ import Bluebird from 'bluebird'; import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import $ from 'jquery'; -import { createVegaVisualization } from '../vega_visualization'; +// Will be replaced with new path when tests are moved +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createVegaVisualization } from '../../../../../../plugins/vis_type_vega/public/vega_visualization'; import { ImageComparator } from 'test_utils/image_comparator'; import vegaliteGraph from '!!raw-loader!./vegalite_graph.hjson'; @@ -35,24 +37,34 @@ import vegaTooltipGraph from '!!raw-loader!./vega_tooltip_test.hjson'; import vegaMapGraph from '!!raw-loader!./vega_map_test.hjson'; import vegaMapImage256 from './vega_map_image_256.png'; +// Will be replaced with new path when tests are moved +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { VegaParser } from '../../../../../../plugins/vis_type_vega/public/data_model/vega_parser'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { SearchCache } from '../../../../../../plugins/vis_type_vega/public/data_model/search_cache'; -import { VegaParser } from '../data_model/vega_parser'; -import { SearchCache } from '../data_model/search_cache'; - -import { createVegaTypeDefinition } from '../vega_type'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createVegaTypeDefinition } from '../../../../../../plugins/vis_type_vega/public/vega_type'; // TODO This is an integration test and thus requires a running platform. When moving to the new platform, // this test has to be migrated to the newly created integration test environment. // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { npStart } from 'ui/new_platform'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { BaseVisType } from '../../../../../plugins/visualizations/public/vis_types/base_vis_type'; +import { BaseVisType } from '../../../../../../plugins/visualizations/public/vis_types/base_vis_type'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ExprVis } from '../../../../../plugins/visualizations/public/expressions/vis'; -import { setInjectedVars } from '../services'; +import { ExprVis } from '../../../../../../plugins/visualizations/public/expressions/vis'; + +import { + setInjectedVars, + setData, + setSavedObjects, + setNotifications, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../plugins/vis_type_vega/public/services'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { setInjectedVarFunc } from '../../../../../plugins/maps_legacy/public/kibana_services'; +import { setInjectedVarFunc } from '../../../../../../plugins/maps_legacy/public/kibana_services'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ServiceSettings } from '../../../../../plugins/maps_legacy/public/map/service_settings'; +import { ServiceSettings } from '../../../../../../plugins/maps_legacy/public/map/service_settings'; const THRESHOLD = 0.1; const PIXEL_DIFF = 30; @@ -70,6 +82,9 @@ describe('VegaVisualizations', () => { enableExternalUrls: true, esShardTimeout: 10000, }); + setData(npStart.plugins.data); + setSavedObjects(npStart.core.savedObjects); + setNotifications(npStart.core.notifications); beforeEach(ngMock.module('kibana')); beforeEach( @@ -111,9 +126,6 @@ describe('VegaVisualizations', () => { timefilter: {}, }, }, - __LEGACY: { - esClient: npStart.plugins.data.search.__LEGACY.esClient, - }, }, }, }; diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_graph.hjson b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_graph.hjson similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_graph.hjson rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_graph.hjson diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_256.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_image_256.png similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_256.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_image_256.png diff --git a/src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_512.png b/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_image_512.png similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/__tests__/vegalite_image_512.png rename to src/legacy/core_plugins/kibana/public/__tests__/vis_type_vega/vegalite_image_512.png diff --git a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts index 3aa552b1da07d..77664e87a3279 100644 --- a/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/discover/kibana_services.ts @@ -59,11 +59,7 @@ export const [getUrlTracker, setUrlTracker] = createGetterSetter<{ export const getHistory = _.once(() => createHashHistory()); export const { getRequestInspectorStats, getResponseInspectorStats, tabifyAggResponse } = search; -export { - unhashUrl, - redirectWhenMissing, - ensureDefaultIndexPattern, -} from '../../../../../plugins/kibana_utils/public'; +export { unhashUrl, redirectWhenMissing } from '../../../../../plugins/kibana_utils/public'; export { formatMsg, formatStack, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js index 567cfda45cc0d..c1de704d1c00a 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js @@ -49,7 +49,6 @@ import { subscribeWithScope, tabifyAggResponse, getAngularModule, - ensureDefaultIndexPattern, redirectWhenMissing, } from '../../kibana_services'; @@ -118,7 +117,7 @@ app.config($routeProvider => { savedObjects: function($route, Promise) { const history = getHistory(); const savedSearchId = $route.current.params.id; - return ensureDefaultIndexPattern(core, data, history).then(() => { + return data.indexPatterns.ensureDefaultIndexPattern(history).then(() => { const { appStateContainer } = getState({ history }); const { index } = appStateContainer.getState(); return Promise.props({ diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap index 09a06bd8827ce..ed65db10e0acb 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap @@ -149,6 +149,7 @@ exports[`CreateIndexPatternWizard renders time field step when step is set to 2 indexPatternsService={ Object { "clearCache": [MockFunction], + "ensureDefaultIndexPattern": [MockFunction], "get": [MockFunction], "make": [Function], } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html index 0376df6bbdc58..0bf7c7f0bdfbe 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html @@ -6,156 +6,7 @@ role="region" aria-label="{{::'kbn.management.editIndexPattern.detailsAria' | i18n: { defaultMessage: 'Index pattern details' } }}" > - -
- -
-

- - - - - - - - - - - - - - - {{tag.name}} - - - - -

- -
- -
-

- - - - - -

-
- -
- - -
-
- - -
- -
-
- -
-
-
- - -
- -
- - -
-
-
- - -
-
- -
- -
- -
- -
-
- - -
-
- -
- -
-
+
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js deleted file mode 100644 index 3239a17f109e4..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.js +++ /dev/null @@ -1,511 +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 _ from 'lodash'; -import { HashRouter } from 'react-router-dom'; -import { IndexHeader } from './index_header'; -import { CreateEditField } from './create_edit_field'; -import { docTitle } from 'ui/doc_title'; -import { KbnUrlProvider } from 'ui/url'; -import { IndicesEditSectionsProvider } from './edit_sections'; -import { fatalError, toastNotifications } from 'ui/notify'; -import { RegistryFieldFormatEditorsProvider } from 'ui/registry/field_format_editors'; -import uiRoutes from 'ui/routes'; -import { uiModules } from 'ui/modules'; -import template from './edit_index_pattern.html'; -import createEditFieldtemplate from './create_edit_field.html'; -import { fieldWildcardMatcher } from '../../../../../../../../plugins/kibana_utils/public'; -import { subscribeWithScope } from '../../../../../../../../plugins/kibana_legacy/public'; -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { SourceFiltersTable } from './source_filters_table'; -import { IndexedFieldsTable } from './indexed_fields_table'; -import { ScriptedFieldsTable } from './scripted_fields_table'; -import { i18n } from '@kbn/i18n'; -import { I18nContext } from 'ui/i18n'; -import { npStart } from 'ui/new_platform'; -import { - getEditBreadcrumbs, - getEditFieldBreadcrumbs, - getCreateFieldBreadcrumbs, -} from '../breadcrumbs'; -import { TAB_INDEXED_FIELDS, TAB_SCRIPTED_FIELDS, TAB_SOURCE_FILTERS } from './constants'; -import { createEditIndexPatternPageStateContainer } from './edit_index_pattern_state_container'; - -const REACT_SOURCE_FILTERS_DOM_ELEMENT_ID = 'reactSourceFiltersTable'; -const REACT_INDEXED_FIELDS_DOM_ELEMENT_ID = 'reactIndexedFieldsTable'; -const REACT_SCRIPTED_FIELDS_DOM_ELEMENT_ID = 'reactScriptedFieldsTable'; -const REACT_INDEX_HEADER_DOM_ELEMENT_ID = 'reactIndexHeader'; - -const EDIT_FIELD_PATH = '/management/kibana/index_patterns/{{indexPattern.id}}/field/{{name}}'; - -function updateSourceFiltersTable($scope) { - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_SOURCE_FILTERS_DOM_ELEMENT_ID); - if (!node) { - return; - } - - render( - - { - $scope.editSections = $scope.editSectionsProvider( - $scope.indexPattern, - $scope.fieldFilter, - $scope.indexPatternListProvider - ); - $scope.refreshFilters(); - $scope.$apply(); - }} - /> - , - node - ); - }); -} - -function destroySourceFiltersTable() { - const node = document.getElementById(REACT_SOURCE_FILTERS_DOM_ELEMENT_ID); - node && unmountComponentAtNode(node); -} - -function updateScriptedFieldsTable($scope) { - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_SCRIPTED_FIELDS_DOM_ELEMENT_ID); - if (!node) { - return; - } - - render( - - { - $scope.kbnUrl.changePath(EDIT_FIELD_PATH, field); - $scope.$apply(); - }, - getRouteHref: (obj, route) => $scope.kbnUrl.getRouteHref(obj, route), - }} - onRemoveField={() => { - $scope.editSections = $scope.editSectionsProvider( - $scope.indexPattern, - $scope.fieldFilter, - $scope.indexPatternListProvider - ); - $scope.refreshFilters(); - $scope.$apply(); - }} - /> - , - node - ); - }); -} - -function destroyScriptedFieldsTable() { - const node = document.getElementById(REACT_SCRIPTED_FIELDS_DOM_ELEMENT_ID); - node && unmountComponentAtNode(node); -} - -function updateIndexedFieldsTable($scope) { - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_INDEXED_FIELDS_DOM_ELEMENT_ID); - if (!node) { - return; - } - - render( - - { - $scope.kbnUrl.changePath(EDIT_FIELD_PATH, field); - $scope.$apply(); - }, - getFieldInfo: $scope.getFieldInfo, - }} - /> - , - node - ); - }); -} - -function destroyIndexedFieldsTable() { - const node = document.getElementById(REACT_INDEXED_FIELDS_DOM_ELEMENT_ID); - node && unmountComponentAtNode(node); -} - -function destroyIndexHeader() { - const node = document.getElementById(REACT_INDEX_HEADER_DOM_ELEMENT_ID); - node && unmountComponentAtNode(node); -} - -function renderIndexHeader($scope, config) { - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_INDEX_HEADER_DOM_ELEMENT_ID); - if (!node) { - return; - } - - render( - - - , - node - ); - }); -} - -function handleTabChange($scope, newTab) { - destroyIndexedFieldsTable(); - destroySourceFiltersTable(); - destroyScriptedFieldsTable(); - updateTables($scope, newTab); -} - -function updateTables($scope, currentTab) { - switch (currentTab) { - case TAB_SCRIPTED_FIELDS: - return updateScriptedFieldsTable($scope); - case TAB_INDEXED_FIELDS: - return updateIndexedFieldsTable($scope); - case TAB_SOURCE_FILTERS: - return updateSourceFiltersTable($scope); - } -} - -uiRoutes.when('/management/kibana/index_patterns/:indexPatternId', { - template, - k7Breadcrumbs: getEditBreadcrumbs, - resolve: { - indexPattern: function($route, Promise, redirectWhenMissing) { - const { indexPatterns } = npStart.plugins.data; - return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId)).catch( - redirectWhenMissing('/management/kibana/index_patterns') - ); - }, - }, -}); - -uiModules - .get('apps/management') - .controller('managementIndexPatternsEdit', function( - $scope, - $location, - $route, - Promise, - config, - Private - ) { - const { - startSyncingState, - stopSyncingState, - setCurrentTab, - getCurrentTab, - state$, - } = createEditIndexPatternPageStateContainer({ - useHashedUrl: config.get('state:storeInSessionStorage'), - defaultTab: TAB_INDEXED_FIELDS, - }); - - $scope.getCurrentTab = getCurrentTab; - $scope.setCurrentTab = setCurrentTab; - - const stateChangedSub = subscribeWithScope( - $scope, - state$, - { - next: ({ tab }) => { - handleTabChange($scope, tab); - }, - }, - fatalError - ); - - handleTabChange($scope, getCurrentTab()); // setup initial tab depending on initial tab state - - startSyncingState(); // starts syncing state between state container and url - - const destroyState = () => { - stateChangedSub.unsubscribe(); - stopSyncingState(); - }; - - $scope.fieldWildcardMatcher = (...args) => - fieldWildcardMatcher(...args, config.get('metaFields')); - $scope.editSectionsProvider = Private(IndicesEditSectionsProvider); - $scope.kbnUrl = Private(KbnUrlProvider); - $scope.indexPattern = $route.current.locals.indexPattern; - $scope.indexPatternListProvider = npStart.plugins.indexPatternManagement.list; - $scope.indexPattern.tags = npStart.plugins.indexPatternManagement.list.getIndexPatternTags( - $scope.indexPattern, - $scope.indexPattern.id === config.get('defaultIndex') - ); - $scope.getFieldInfo = npStart.plugins.indexPatternManagement.list.getFieldInfo; - docTitle.change($scope.indexPattern.title); - - const otherPatterns = _.filter($route.current.locals.indexPatterns, pattern => { - return pattern.id !== $scope.indexPattern.id; - }); - - $scope.$watch('indexPattern.fields', function() { - $scope.editSections = $scope.editSectionsProvider( - $scope.indexPattern, - $scope.fieldFilter, - npStart.plugins.indexPatternManagement.list - ); - $scope.refreshFilters(); - $scope.fields = $scope.indexPattern.getNonScriptedFields(); - }); - - $scope.refreshFilters = function() { - const indexedFieldTypes = []; - const scriptedFieldLanguages = []; - $scope.indexPattern.fields.forEach(field => { - if (field.scripted) { - scriptedFieldLanguages.push(field.lang); - } else { - indexedFieldTypes.push(field.type); - } - }); - - $scope.indexedFieldTypes = _.unique(indexedFieldTypes); - $scope.scriptedFieldLanguages = _.unique(scriptedFieldLanguages); - }; - - $scope.changeFilter = function(filter, val) { - $scope[filter] = val || ''; // null causes filter to check for null explicitly - }; - - $scope.$watchCollection('indexPattern.fields', function() { - $scope.conflictFields = $scope.indexPattern.fields.filter(field => field.type === 'conflict'); - }); - - $scope.refreshFields = function() { - const confirmMessage = i18n.translate('kbn.management.editIndexPattern.refreshLabel', { - defaultMessage: 'This action resets the popularity counter of each field.', - }); - const confirmModalOptions = { - confirmButtonText: i18n.translate('kbn.management.editIndexPattern.refreshButton', { - defaultMessage: 'Refresh', - }), - title: i18n.translate('kbn.management.editIndexPattern.refreshHeader', { - defaultMessage: 'Refresh field list?', - }), - }; - - npStart.core.overlays - .openConfirm(confirmMessage, confirmModalOptions) - .then(async isConfirmed => { - if (isConfirmed) { - await $scope.indexPattern.init(true); - $scope.fields = $scope.indexPattern.getNonScriptedFields(); - } - }); - }; - - $scope.removePattern = function() { - function doRemove() { - if ($scope.indexPattern.id === config.get('defaultIndex')) { - config.remove('defaultIndex'); - - if (otherPatterns.length) { - config.set('defaultIndex', otherPatterns[0].id); - } - } - - Promise.resolve($scope.indexPattern.destroy()) - .then(function() { - $location.url('/management/kibana/index_patterns'); - }) - .catch(fatalError); - } - - const confirmModalOptions = { - confirmButtonText: i18n.translate('kbn.management.editIndexPattern.deleteButton', { - defaultMessage: 'Delete', - }), - title: i18n.translate('kbn.management.editIndexPattern.deleteHeader', { - defaultMessage: 'Delete index pattern?', - }), - }; - - npStart.core.overlays.openConfirm('', confirmModalOptions).then(isConfirmed => { - if (isConfirmed) { - doRemove(); - } - }); - }; - - $scope.setDefaultPattern = function() { - config.set('defaultIndex', $scope.indexPattern.id); - }; - - $scope.setIndexPatternsTimeField = function(field) { - if (field.type !== 'date') { - const errorMessage = i18n.translate('kbn.management.editIndexPattern.notDateErrorMessage', { - defaultMessage: 'That field is a {fieldType} not a date.', - values: { fieldType: field.type }, - }); - toastNotifications.addDanger(errorMessage); - return; - } - $scope.indexPattern.timeFieldName = field.name; - return $scope.indexPattern.save(); - }; - - $scope.$watch('fieldFilter', () => { - $scope.editSections = $scope.editSectionsProvider( - $scope.indexPattern, - $scope.fieldFilter, - npStart.plugins.indexPatternManagement.list - ); - - if ($scope.fieldFilter === undefined) { - return; - } - - updateTables($scope, getCurrentTab()); - }); - - $scope.$watch('indexedFieldTypeFilter', () => { - if ($scope.indexedFieldTypeFilter !== undefined && getCurrentTab() === TAB_INDEXED_FIELDS) { - updateIndexedFieldsTable($scope); - } - }); - - $scope.$watch('scriptedFieldLanguageFilter', () => { - if ( - $scope.scriptedFieldLanguageFilter !== undefined && - getCurrentTab() === TAB_SCRIPTED_FIELDS - ) { - updateScriptedFieldsTable($scope); - } - }); - - $scope.$on('$destroy', () => { - destroyIndexedFieldsTable(); - destroyScriptedFieldsTable(); - destroySourceFiltersTable(); - destroyIndexHeader(); - destroyState(); - }); - - renderIndexHeader($scope, config); - }); - -// routes for create edit field. Will be removed after migartion all component to react. -const REACT_FIELD_EDITOR_ID = 'reactFieldEditor'; -const renderCreateEditField = ($scope, $route, getConfig, $http, fieldFormatEditors) => { - $scope.$$postDigest(() => { - const node = document.getElementById(REACT_FIELD_EDITOR_ID); - if (!node) { - return; - } - - render( - - - - - , - node - ); - }); -}; - -const destroyCreateEditField = () => { - const node = document.getElementById(REACT_FIELD_EDITOR_ID); - node && unmountComponentAtNode(node); -}; - -uiRoutes - .when('/management/kibana/index_patterns/:indexPatternId/field/:fieldName*', { - mode: 'edit', - k7Breadcrumbs: getEditFieldBreadcrumbs, - }) - .when('/management/kibana/index_patterns/:indexPatternId/create-field/', { - mode: 'create', - k7Breadcrumbs: getCreateFieldBreadcrumbs, - }) - .defaults(/management\/kibana\/index_patterns\/[^\/]+\/(field|create-field)(\/|$)/, { - template: createEditFieldtemplate, - mapBreadcrumbs($route, breadcrumbs) { - const { indexPattern } = $route.current.locals; - return breadcrumbs.map(crumb => { - if (crumb.id !== indexPattern.id) { - return crumb; - } - - return { - ...crumb, - display: indexPattern.title, - }; - }); - }, - resolve: { - indexPattern: function($route, Promise, redirectWhenMissing) { - const { indexPatterns } = npStart.plugins.data; - return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId)).catch( - redirectWhenMissing('/management/kibana/index_patterns') - ); - }, - }, - controllerAs: 'fieldSettings', - controller: function FieldEditorPageController($scope, $route, $http, Private, config) { - const getConfig = (...args) => config.get(...args); - const fieldFormatEditors = Private(RegistryFieldFormatEditorsProvider); - - renderCreateEditField($scope, $route, getConfig, $http, fieldFormatEditors); - - $scope.$on('$destroy', () => { - destroyCreateEditField(); - }); - }, - }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.tsx new file mode 100644 index 0000000000000..e869ac84c2db2 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.tsx @@ -0,0 +1,238 @@ +/* + * 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 { filter } from 'lodash'; +import React, { useEffect, useState, useCallback } from 'react'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiBadge, + EuiText, + EuiLink, + EuiIcon, + EuiCallOut, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { IndexPattern, IndexPatternField } from '../../../../../../../../plugins/data/public'; +import { + ChromeDocTitle, + NotificationsStart, + OverlayStart, +} from '../../../../../../../../core/public'; +import { IndexPatternManagementStart } from '../../../../../../../../plugins/index_pattern_management/public'; +import { Tabs } from './tabs'; +import { IndexHeader } from './index_header'; + +interface EditIndexPatternProps extends RouteComponentProps { + indexPattern: IndexPattern; + indexPatterns: IndexPattern[]; + config: Record; + services: { + notifications: NotificationsStart; + docTitle: ChromeDocTitle; + overlays: OverlayStart; + indexPatternManagement: IndexPatternManagementStart; + }; +} + +const mappingAPILink = i18n.translate( + 'kbn.management.editIndexPattern.timeFilterLabel.mappingAPILink', + { + defaultMessage: 'Mapping API', + } +); + +const mappingConflictHeader = i18n.translate( + 'kbn.management.editIndexPattern.mappingConflictHeader', + { + defaultMessage: 'Mapping conflict', + } +); + +const confirmMessage = i18n.translate('kbn.management.editIndexPattern.refreshLabel', { + defaultMessage: 'This action resets the popularity counter of each field.', +}); + +const confirmModalOptionsRefresh = { + confirmButtonText: i18n.translate('kbn.management.editIndexPattern.refreshButton', { + defaultMessage: 'Refresh', + }), + title: i18n.translate('kbn.management.editIndexPattern.refreshHeader', { + defaultMessage: 'Refresh field list?', + }), +}; + +const confirmModalOptionsDelete = { + confirmButtonText: i18n.translate('kbn.management.editIndexPattern.deleteButton', { + defaultMessage: 'Delete', + }), + title: i18n.translate('kbn.management.editIndexPattern.deleteHeader', { + defaultMessage: 'Delete index pattern?', + }), +}; + +export const EditIndexPattern = withRouter( + ({ indexPattern, indexPatterns, config, services, history, location }: EditIndexPatternProps) => { + const [fields, setFields] = useState(indexPattern.getNonScriptedFields()); + const [conflictedFields, setConflictedFields] = useState( + indexPattern.fields.filter(field => field.type === 'conflict') + ); + const [defaultIndex, setDefaultIndex] = useState(config.get('defaultIndex')); + const [tags, setTags] = useState([]); + + useEffect(() => { + setFields(indexPattern.getNonScriptedFields()); + setConflictedFields(indexPattern.fields.filter(field => field.type === 'conflict')); + }, [indexPattern, indexPattern.fields]); + + useEffect(() => { + const indexPatternTags = + services.indexPatternManagement.list.getIndexPatternTags( + indexPattern, + indexPattern.id === defaultIndex + ) || []; + setTags(indexPatternTags); + }, [defaultIndex, indexPattern, services.indexPatternManagement.list]); + + const setDefaultPattern = useCallback(() => { + config.set('defaultIndex', indexPattern.id); + setDefaultIndex(indexPattern.id || ''); + }, [config, indexPattern.id]); + + const refreshFields = () => { + services.overlays + .openConfirm(confirmMessage, confirmModalOptionsRefresh) + .then(async isConfirmed => { + if (isConfirmed) { + await indexPattern.init(true); + setFields(indexPattern.getNonScriptedFields()); + } + }); + }; + + const removePattern = () => { + function doRemove() { + if (indexPattern.id === defaultIndex) { + config.remove('defaultIndex'); + const otherPatterns = filter(indexPatterns, pattern => { + return pattern.id !== indexPattern.id; + }); + + if (otherPatterns.length) { + config.set('defaultIndex', otherPatterns[0].id); + } + } + + Promise.resolve(indexPattern.destroy()).then(function() { + history.push('/management/kibana/index_patterns'); + }); + } + + services.overlays.openConfirm('', confirmModalOptionsDelete).then(isConfirmed => { + if (isConfirmed) { + doRemove(); + } + }); + }; + + const timeFilterHeader = i18n.translate('kbn.management.editIndexPattern.timeFilterHeader', { + defaultMessage: "Time Filter field name: '{timeFieldName}'", + values: { timeFieldName: indexPattern.timeFieldName }, + }); + + const mappingConflictLabel = i18n.translate( + 'kbn.management.editIndexPattern.mappingConflictLabel', + { + defaultMessage: + '{conflictFieldsLength, plural, one {A field is} other {# fields are}} defined as several types (string, integer, etc) across the indices that match this pattern. You may still be able to use these conflict fields in parts of Kibana, but they will be unavailable for functions that require Kibana to know their type. Correcting this issue will require reindexing your data.', + values: { conflictFieldsLength: conflictedFields.length }, + } + ); + + services.docTitle.change(indexPattern.title); + + const showTagsSection = Boolean(indexPattern.timeFieldName || (tags && tags.length > 0)); + + return ( + + + + + {showTagsSection && ( + + {Boolean(indexPattern.timeFieldName) && ( + + {timeFilterHeader} + + )} + {tags.map((tag: any) => ( + + {tag.name} + + ))} + + )} + + +

+ {indexPattern.title} }} + />{' '} + + {mappingAPILink} + + +

+
+ {conflictedFields.length > 0 && ( + +

{mappingConflictLabel}

+
+ )} +
+ + + +
+ ); + } +); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern_state_container.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern_state_container.ts index 473417a7aabd6..5723a596f95d5 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern_state_container.ts +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern_state_container.ts @@ -25,7 +25,7 @@ import { } from '../../../../../../../../plugins/kibana_utils/public'; interface IEditIndexPatternState { - tab: string; // TODO: type those 3 tabs with enum, when edit_index_pattern.js migrated to ts + tab: string; } /** @@ -38,7 +38,6 @@ export function createEditIndexPatternPageStateContainer({ defaultTab: string; useHashedUrl: boolean; }) { - // until angular is used as shell - use hash history const history = createHashHistory(); // query param to store app state at const stateStorageKey = '_a'; @@ -78,12 +77,10 @@ export function createEditIndexPatternPageStateContainer({ // makes sure initial url is the same as initial state (this is not really required) kbnUrlStateStorage.set(stateStorageKey, stateContainer.getState(), { replace: true }); - // expose api needed for Controller return { startSyncingState: start, stopSyncingState: stop, setCurrentTab: (newTab: string) => stateContainer.transitions.setTab(newTab), getCurrentTab: () => stateContainer.selectors.tab(), - state$: stateContainer.state$, }; } diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_sections.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_sections.js deleted file mode 100644 index f0220b2f798e5..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_sections.js +++ /dev/null @@ -1,80 +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 _ from 'lodash'; -import { i18n } from '@kbn/i18n'; - -function filterBy(items, key, filter) { - const lowercaseFilter = (filter || '').toLowerCase(); - return items.filter(item => item[key].toLowerCase().includes(lowercaseFilter)); -} - -function getCounts(fields, sourceFilters, fieldFilter = '') { - const fieldCount = _.countBy(filterBy(fields, 'name', fieldFilter), function(field) { - return field.scripted ? 'scripted' : 'indexed'; - }); - - _.defaults(fieldCount, { - indexed: 0, - scripted: 0, - sourceFilters: sourceFilters ? filterBy(sourceFilters, 'value', fieldFilter).length : 0, - }); - - return fieldCount; -} - -export function IndicesEditSectionsProvider() { - return function(indexPattern, fieldFilter, indexPatternListProvider) { - const totalCount = getCounts(indexPattern.fields, indexPattern.sourceFilters); - const filteredCount = getCounts(indexPattern.fields, indexPattern.sourceFilters, fieldFilter); - - const editSections = []; - - editSections.push({ - title: i18n.translate('kbn.management.editIndexPattern.tabs.fieldsHeader', { - defaultMessage: 'Fields', - }), - index: 'indexedFields', - count: filteredCount.indexed, - totalCount: totalCount.indexed, - }); - - if (indexPatternListProvider.areScriptedFieldsEnabled(indexPattern)) { - editSections.push({ - title: i18n.translate('kbn.management.editIndexPattern.tabs.scriptedHeader', { - defaultMessage: 'Scripted fields', - }), - index: 'scriptedFields', - count: filteredCount.scripted, - totalCount: totalCount.scripted, - }); - } - - editSections.push({ - title: i18n.translate('kbn.management.editIndexPattern.tabs.sourceHeader', { - defaultMessage: 'Source filters', - }), - index: 'sourceFilters', - count: filteredCount.sourceFilters, - totalCount: totalCount.sourceFilters, - }); - - return editSections; - }; -} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/field_controls.html b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/field_controls.html deleted file mode 100644 index c14dcd3f3a8d5..0000000000000 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/field_controls.html +++ /dev/null @@ -1,19 +0,0 @@ -
- - - - - -
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index.js index 6beaee60b3788..e05aea3678fe2 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index.js @@ -17,4 +17,159 @@ * under the License. */ -import './edit_index_pattern'; +import React from 'react'; +import { HashRouter } from 'react-router-dom'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { RegistryFieldFormatEditorsProvider } from 'ui/registry/field_format_editors'; +import uiRoutes from 'ui/routes'; +import { uiModules } from 'ui/modules'; +import { I18nContext } from 'ui/i18n'; +import { npStart } from 'ui/new_platform'; +import template from './edit_index_pattern.html'; +import createEditFieldtemplate from './create_edit_field.html'; +import { + getEditBreadcrumbs, + getEditFieldBreadcrumbs, + getCreateFieldBreadcrumbs, +} from '../breadcrumbs'; +import { EditIndexPattern } from './edit_index_pattern'; +import { CreateEditField } from './create_edit_field'; + +const REACT_EDIT_INDEX_PATTERN_DOM_ELEMENT_ID = 'reactEditIndexPattern'; + +function destroyEditIndexPattern() { + const node = document.getElementById(REACT_EDIT_INDEX_PATTERN_DOM_ELEMENT_ID); + node && unmountComponentAtNode(node); +} + +function renderEditIndexPattern($scope, config, $route) { + $scope.$$postDigest(() => { + const node = document.getElementById(REACT_EDIT_INDEX_PATTERN_DOM_ELEMENT_ID); + if (!node) { + return; + } + + render( + + + + + , + node + ); + }); +} + +uiRoutes.when('/management/kibana/index_patterns/:indexPatternId', { + template, + k7Breadcrumbs: getEditBreadcrumbs, + resolve: { + indexPattern: function($route, Promise, redirectWhenMissing) { + const { indexPatterns } = npStart.plugins.data; + return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId)).catch( + redirectWhenMissing('/management/kibana/index_patterns') + ); + }, + }, +}); + +uiModules + .get('apps/management') + .controller('managementIndexPatternsEdit', function($scope, $route, config) { + $scope.$on('$destroy', () => { + destroyEditIndexPattern(); + }); + + renderEditIndexPattern($scope, config, $route); + }); + +// routes for create edit field. Will be removed after migartion all component to react. +const REACT_FIELD_EDITOR_ID = 'reactFieldEditor'; +const renderCreateEditField = ($scope, $route, getConfig, $http, fieldFormatEditors) => { + $scope.$$postDigest(() => { + const node = document.getElementById(REACT_FIELD_EDITOR_ID); + if (!node) { + return; + } + + render( + + + + + , + node + ); + }); +}; + +const destroyCreateEditField = () => { + const node = document.getElementById(REACT_FIELD_EDITOR_ID); + node && unmountComponentAtNode(node); +}; + +uiRoutes + .when('/management/kibana/index_patterns/:indexPatternId/field/:fieldName*', { + mode: 'edit', + k7Breadcrumbs: getEditFieldBreadcrumbs, + }) + .when('/management/kibana/index_patterns/:indexPatternId/create-field/', { + mode: 'create', + k7Breadcrumbs: getCreateFieldBreadcrumbs, + }) + .defaults(/management\/kibana\/index_patterns\/[^\/]+\/(field|create-field)(\/|$)/, { + template: createEditFieldtemplate, + mapBreadcrumbs($route, breadcrumbs) { + const { indexPattern } = $route.current.locals; + return breadcrumbs.map(crumb => { + if (crumb.id !== indexPattern.id) { + return crumb; + } + + return { + ...crumb, + display: indexPattern.title, + }; + }); + }, + resolve: { + indexPattern: function($route, Promise, redirectWhenMissing) { + const { indexPatterns } = npStart.plugins.data; + return Promise.resolve(indexPatterns.get($route.current.params.indexPatternId)).catch( + redirectWhenMissing('/management/kibana/index_patterns') + ); + }, + }, + controllerAs: 'fieldSettings', + controller: function FieldEditorPageController($scope, $route, $http, Private, config) { + const getConfig = (...args) => config.get(...args); + const fieldFormatEditors = Private(RegistryFieldFormatEditorsProvider); + + renderCreateEditField($scope, $route, getConfig, $http, fieldFormatEditors); + + $scope.$on('$destroy', () => { + destroyCreateEditField(); + }); + }, + }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.tsx index deac85d9a32e9..a06671ef6a470 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/index_header/index_header.tsx @@ -92,7 +92,7 @@ export function IndexHeader({ diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx index 7c2bb565615d7..c69063967b1e2 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx @@ -19,7 +19,11 @@ import React, { Component } from 'react'; import { createSelector } from 'reselect'; -import { IndexPatternField, IIndexPattern } from '../../../../../../../../../plugins/data/public'; +import { + IndexPatternField, + IIndexPattern, + IFieldType, +} from '../../../../../../../../../plugins/data/public'; import { Table } from './components/table'; import { getFieldFormat } from './lib'; import { IndexedFieldItem } from './types'; @@ -31,7 +35,7 @@ interface IndexedFieldsTableProps { indexedFieldTypeFilter?: string; helpers: { redirectToRoute: (obj: any) => void; - getFieldInfo: (indexPattern: IIndexPattern, field: string) => string[]; + getFieldInfo: (indexPattern: IIndexPattern, field: IFieldType) => string[]; }; fieldWildcardMatcher: (filters: any[]) => (val: any) => boolean; } @@ -76,7 +80,7 @@ export class IndexedFieldsTable extends Component< indexPattern: field.indexPattern, format: getFieldFormat(indexPattern, field.name), excluded: fieldWildcardMatch ? fieldWildcardMatch(field.name) : false, - info: helpers.getFieldInfo && helpers.getFieldInfo(indexPattern, field.name), + info: helpers.getFieldInfo && helpers.getFieldInfo(indexPattern, field), }; })) || [] diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__snapshots__/scripted_field_table.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__snapshots__/scripted_field_table.test.tsx.snap index 569b75c848c52..202b09ddc6066 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__snapshots__/scripted_field_table.test.tsx.snap +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/scripted_fields_table/__snapshots__/scripted_field_table.test.tsx.snap @@ -3,7 +3,7 @@ exports[`ScriptedFieldsTable should filter based on the lang filter 1`] = `
void; } @@ -136,14 +136,19 @@ export class ScriptedFieldsTable extends Component< }; render() { - const { helpers, indexPattern } = this.props; + const { indexPattern } = this.props; const { fieldToDelete, deprecatedLangsInUse } = this.state; const items = this.getFilteredItems(); return ( <> -
+
{ + indexPattern: IndexPattern; + config: Record; + fields: IndexPatternField[]; + services: { + indexPatternManagement: IndexPatternManagementStart; + }; +} + +const filterAriaLabel = i18n.translate('kbn.management.editIndexPattern.fields.filterAria', { + defaultMessage: 'Filter', +}); + +const filterPlaceholder = i18n.translate( + 'kbn.management.editIndexPattern.fields.filterPlaceholder', + { + defaultMessage: 'Filter', + } +); + +export function Tabs({ config, indexPattern, fields, services, history, location }: TabsProps) { + const [fieldFilter, setFieldFilter] = useState(''); + const [indexedFieldTypeFilter, setIndexedFieldTypeFilter] = useState(''); + const [scriptedFieldLanguageFilter, setScriptedFieldLanguageFilter] = useState(''); + const [indexedFieldTypes, setIndexedFieldType] = useState([]); + const [scriptedFieldLanguages, setScriptedFieldLanguages] = useState([]); + const [syncingStateFunc, setSyncingStateFunc] = useState({ + getCurrentTab: () => TAB_INDEXED_FIELDS, + }); + + const refreshFilters = useCallback(() => { + const tempIndexedFieldTypes: string[] = []; + const tempScriptedFieldLanguages: string[] = []; + indexPattern.fields.forEach(field => { + if (field.scripted) { + if (field.lang) { + tempScriptedFieldLanguages.push(field.lang); + } + } else { + tempIndexedFieldTypes.push(field.type); + } + }); + + setIndexedFieldType(convertToEuiSelectOption(tempIndexedFieldTypes, 'indexedFiledTypes')); + setScriptedFieldLanguages( + convertToEuiSelectOption(tempScriptedFieldLanguages, 'scriptedFieldLanguages') + ); + }, [indexPattern]); + + useEffect(() => { + refreshFilters(); + }, [indexPattern, indexPattern.fields, refreshFilters]); + + const fieldWildcardMatcherDecorated = useCallback( + (filters: string[]) => fieldWildcardMatcher(filters, config.get('metaFields')), + [config] + ); + + const getFilterSection = useCallback( + (type: string) => { + return ( + + + setFieldFilter(e.target.value)} + data-test-subj="indexPatternFieldFilter" + aria-label={filterAriaLabel} + /> + + {type === TAB_INDEXED_FIELDS && indexedFieldTypes.length > 0 && ( + + setIndexedFieldTypeFilter(e.target.value)} + data-test-subj="indexedFieldTypeFilterDropdown" + /> + + )} + {type === TAB_SCRIPTED_FIELDS && scriptedFieldLanguages.length > 0 && ( + + setScriptedFieldLanguageFilter(e.target.value)} + data-test-subj="scriptedFieldLanguageFilterDropdown" + /> + + )} + + ); + }, + [ + fieldFilter, + indexedFieldTypeFilter, + indexedFieldTypes, + scriptedFieldLanguageFilter, + scriptedFieldLanguages, + ] + ); + + const getContent = useCallback( + (type: string) => { + switch (type) { + case TAB_INDEXED_FIELDS: + return ( + + + {getFilterSection(type)} + + { + history.push(getPath(field)); + }, + getFieldInfo: services.indexPatternManagement.list.getFieldInfo, + }} + /> + + ); + case TAB_SCRIPTED_FIELDS: + return ( + + + {getFilterSection(type)} + + { + history.push(getPath(field)); + }, + }} + onRemoveField={refreshFilters} + /> + + ); + case TAB_SOURCE_FILTERS: + return ( + + + {getFilterSection(type)} + + + + ); + } + }, + [ + fieldFilter, + fieldWildcardMatcherDecorated, + fields, + getFilterSection, + history, + indexPattern, + indexedFieldTypeFilter, + refreshFilters, + scriptedFieldLanguageFilter, + services.indexPatternManagement.list.getFieldInfo, + ] + ); + + const euiTabs: EuiTabbedContentTab[] = useMemo( + () => + getTabs(indexPattern, fieldFilter, services.indexPatternManagement.list).map( + (tab: Pick) => { + return { + ...tab, + content: getContent(tab.id), + }; + } + ), + [fieldFilter, getContent, indexPattern, services.indexPatternManagement.list] + ); + + const [selectedTabId, setSelectedTabId] = useState(euiTabs[0].id); + + useEffect(() => { + const { + startSyncingState, + stopSyncingState, + setCurrentTab, + getCurrentTab, + } = createEditIndexPatternPageStateContainer({ + useHashedUrl: config.get('state:storeInSessionStorage'), + defaultTab: TAB_INDEXED_FIELDS, + }); + + startSyncingState(); + setSyncingStateFunc({ + setCurrentTab, + getCurrentTab, + }); + setSelectedTabId(getCurrentTab()); + + return () => { + stopSyncingState(); + }; + }, [config]); + + return ( + tab.id === selectedTabId)} + onTabClick={tab => { + setSelectedTabId(tab.id); + syncingStateFunc.setCurrentTab(tab.id); + }} + /> + ); +} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/utils.ts b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/utils.ts new file mode 100644 index 0000000000000..bdb1436c37efb --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/utils.ts @@ -0,0 +1,146 @@ +/* + * 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 { Dictionary, countBy, defaults, unique } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { IndexPattern, IndexPatternField } from '../../../../../../../../../plugins/data/public'; +import { IndexPatternManagementStart } from '../../../../../../../../../plugins/index_pattern_management/public'; +import { TAB_INDEXED_FIELDS, TAB_SCRIPTED_FIELDS, TAB_SOURCE_FILTERS } from '../constants'; + +function filterByName(items: IndexPatternField[], filter: string) { + const lowercaseFilter = (filter || '').toLowerCase(); + return items.filter(item => item.name.toLowerCase().includes(lowercaseFilter)); +} + +function getCounts( + fields: IndexPatternField[], + sourceFilters: { + excludes: string[]; + }, + fieldFilter = '' +) { + const fieldCount = countBy(filterByName(fields, fieldFilter), function(field) { + return field.scripted ? 'scripted' : 'indexed'; + }); + + defaults(fieldCount, { + indexed: 0, + scripted: 0, + sourceFilters: sourceFilters.excludes + ? sourceFilters.excludes.filter(value => + value.toLowerCase().includes(fieldFilter.toLowerCase()) + ).length + : 0, + }); + + return fieldCount; +} + +function getTitle(type: string, filteredCount: Dictionary, totalCount: Dictionary) { + let title = ''; + switch (type) { + case 'indexed': + title = i18n.translate('kbn.management.editIndexPattern.tabs.fieldsHeader', { + defaultMessage: 'Fields', + }); + break; + case 'scripted': + title = i18n.translate('kbn.management.editIndexPattern.tabs.scriptedHeader', { + defaultMessage: 'Scripted fields', + }); + break; + case 'sourceFilters': + title = i18n.translate('kbn.management.editIndexPattern.tabs.sourceHeader', { + defaultMessage: 'Source filters', + }); + break; + } + const count = ` (${ + filteredCount[type] === totalCount[type] + ? filteredCount[type] + : filteredCount[type] + ' / ' + totalCount[type] + })`; + return title + count; +} + +export function getTabs( + indexPattern: IndexPattern, + fieldFilter: string, + indexPatternListProvider: IndexPatternManagementStart['list'] +) { + const totalCount = getCounts(indexPattern.fields, indexPattern.getSourceFiltering()); + const filteredCount = getCounts( + indexPattern.fields, + indexPattern.getSourceFiltering(), + fieldFilter + ); + + const tabs = []; + + tabs.push({ + name: getTitle('indexed', filteredCount, totalCount), + id: TAB_INDEXED_FIELDS, + }); + + if (indexPatternListProvider.areScriptedFieldsEnabled(indexPattern)) { + tabs.push({ + name: getTitle('scripted', filteredCount, totalCount), + id: TAB_SCRIPTED_FIELDS, + }); + } + + tabs.push({ + name: getTitle('sourceFilters', filteredCount, totalCount), + id: TAB_SOURCE_FILTERS, + }); + + return tabs; +} + +export function getPath(field: IndexPatternField) { + return `/management/kibana/index_patterns/${field.indexPattern?.id}/field/${field.name}`; +} + +const allTypesDropDown = i18n.translate('kbn.management.editIndexPattern.fields.allTypesDropDown', { + defaultMessage: 'All field types', +}); + +const allLangsDropDown = i18n.translate('kbn.management.editIndexPattern.fields.allLangsDropDown', { + defaultMessage: 'All languages', +}); + +export function convertToEuiSelectOption(options: string[], type: string) { + const euiOptions = + options.length > 0 + ? [ + { + value: '', + text: type === 'scriptedFieldLanguages' ? allLangsDropDown : allTypesDropDown, + }, + ] + : []; + return euiOptions.concat( + unique(options).map(option => { + return { + value: option, + text: option, + }; + }) + ); +} diff --git a/src/legacy/core_plugins/tests_bundle/index.js b/src/legacy/core_plugins/tests_bundle/index.js index 5e78047088d2a..e1966a9e8b266 100644 --- a/src/legacy/core_plugins/tests_bundle/index.js +++ b/src/legacy/core_plugins/tests_bundle/index.js @@ -148,6 +148,19 @@ export default kibana => { .type('text/css'); }, }); + + // Sets global variables normally set by the bootstrap.js script + kbnServer.server.route({ + path: '/test_bundle/karma/globals.js', + method: 'GET', + async handler(req, h) { + const basePath = config.get('server.basePath'); + + const file = `window.__kbnPublicPath__ = { 'kbn-ui-shared-deps': "${basePath}/bundles/kbn-ui-shared-deps/" };`; + + return h.response(file).header('content-type', 'application/json'); + }, + }); }, __globalImportAliases__: { diff --git a/src/legacy/core_plugins/vis_type_tagcloud/index.ts b/src/legacy/core_plugins/vis_type_tagcloud/index.ts deleted file mode 100644 index 6f768131f2190..0000000000000 --- a/src/legacy/core_plugins/vis_type_tagcloud/index.ts +++ /dev/null @@ -1,44 +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 { Legacy } from 'kibana'; - -import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; - -const tagCloudPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'tagcloud', - require: ['kibana', 'elasticsearch'], - publicDir: resolve(__dirname, 'public'), - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars: server => ({}), - }, - init: (server: Legacy.Server) => ({}), - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - } as Legacy.PluginSpecOptions); - -// eslint-disable-next-line import/no-default-export -export default tagCloudPluginInitializer; diff --git a/src/legacy/core_plugins/vis_type_tagcloud/package.json b/src/legacy/core_plugins/vis_type_tagcloud/package.json deleted file mode 100644 index 4200ef264fece..0000000000000 --- a/src/legacy/core_plugins/vis_type_tagcloud/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "tagcloud", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/vis_type_vega/index.ts b/src/legacy/core_plugins/vis_type_vega/index.ts deleted file mode 100644 index ac7e407ca9e4d..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/index.ts +++ /dev/null @@ -1,55 +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 { Legacy } from 'kibana'; - -import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types'; - -const vegaPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) => - new Plugin({ - id: 'vis_type_vega', - deprecations: ({ rename }: { rename: any }) => [ - rename('vega.enabled', 'vis_type_vega.enabled'), - ], - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - enableExternalUrls: Joi.boolean().default(false), - }).default(); - }, - require: ['kibana', 'elasticsearch'], - publicDir: resolve(__dirname, 'public'), - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - hacks: [resolve(__dirname, 'public/legacy')], - injectDefaultVars: server => { - const serverConfig = server.config(); - const mapConfig: Record = serverConfig.get('map'); - - return { - emsTileLayerId: mapConfig.emsTileLayerId, - }; - }, - }, - init: (server: Legacy.Server) => ({}), - } as Legacy.PluginSpecOptions); - -// eslint-disable-next-line import/no-default-export -export default vegaPluginInitializer; diff --git a/src/legacy/core_plugins/vis_type_vega/package.json b/src/legacy/core_plugins/vis_type_vega/package.json deleted file mode 100644 index acd6f3da128ab..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "author": "Yuri Astrakhan", - "name": "vega", - "version": "kibana" -} - diff --git a/src/legacy/core_plugins/vis_type_vega/public/legacy.ts b/src/legacy/core_plugins/vis_type_vega/public/legacy.ts deleted file mode 100644 index 450af4a6f253e..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/public/legacy.ts +++ /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. - */ - -import { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart } from 'ui/new_platform'; -import { VegaPluginSetupDependencies, VegaPluginStartDependencies } from './plugin'; -import { plugin } from '.'; - -const setupPlugins: Readonly = { - ...npSetup.plugins, - visualizations: npSetup.plugins.visualizations, - mapsLegacy: npSetup.plugins.mapsLegacy, -}; - -const startPlugins: Readonly = { - ...npStart.plugins, -}; - -const pluginInstance = plugin({} as PluginInitializerContext); - -export const setup = pluginInstance.setup(npSetup.core, setupPlugins); -export const start = pluginInstance.start(npStart.core, startPlugins); diff --git a/src/legacy/ui/public/_index.scss b/src/legacy/ui/public/_index.scss index b36e62297cc23..f10718ba58c2c 100644 --- a/src/legacy/ui/public/_index.scss +++ b/src/legacy/ui/public/_index.scss @@ -10,7 +10,6 @@ @import './accessibility/index'; @import './directives/index'; -@import './error_auto_create_index/index'; @import './error_url_overflow/index'; @import './exit_full_screen/index'; @import './field_editor/index'; diff --git a/src/legacy/ui/public/error_auto_create_index/_error_auto_create_index.scss b/src/legacy/ui/public/error_auto_create_index/_error_auto_create_index.scss deleted file mode 100644 index ad31aabfc66cd..0000000000000 --- a/src/legacy/ui/public/error_auto_create_index/_error_auto_create_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -.kbnError--auto-create-index { - padding: $euiSizeL; -} diff --git a/src/legacy/ui/public/error_auto_create_index/_index.scss b/src/legacy/ui/public/error_auto_create_index/_index.scss deleted file mode 100644 index 42e672ab322dc..0000000000000 --- a/src/legacy/ui/public/error_auto_create_index/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './error_auto_create_index' diff --git a/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.html b/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.html deleted file mode 100644 index 2af31dda6c345..0000000000000 --- a/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.html +++ /dev/null @@ -1,69 +0,0 @@ -
-

- - -

- -

- -

- -

-
    -
  1. -
  2. -
  3. -
- -
-
- - -
- -
-
-
-
-
diff --git a/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.js b/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.js deleted file mode 100644 index a8f6318090b1d..0000000000000 --- a/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.js +++ /dev/null @@ -1,95 +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. - */ - -// @ts-ignore -import './error_auto_create_index.test.mocks'; -import fetchMock from 'fetch-mock/es5/client'; -import { kfetch } from '../kfetch'; - -import { isAutoCreateIndexError } from './error_auto_create_index'; - -describe('isAutoCreateIndexError correctly handles KFetchError thrown by kfetch', () => { - describe('404', () => { - beforeEach(() => { - fetchMock.post({ - matcher: '*', - response: { - status: 404, - }, - }); - }); - afterEach(() => fetchMock.restore()); - - test('should return false', async () => { - expect.assertions(1); - try { - await kfetch({ method: 'POST', pathname: '/my/path' }); - } catch (kfetchError) { - expect(isAutoCreateIndexError(kfetchError)).toBe(false); - } - }); - }); - - describe('503 error that is not ES_AUTO_CREATE_INDEX_ERROR', () => { - beforeEach(() => { - fetchMock.post({ - matcher: '*', - response: { - status: 503, - }, - }); - }); - afterEach(() => fetchMock.restore()); - - test('should return false', async () => { - expect.assertions(1); - try { - await kfetch({ method: 'POST', pathname: '/my/path' }); - } catch (kfetchError) { - expect(isAutoCreateIndexError(kfetchError)).toBe(false); - } - }); - }); - - describe('503 error that is ES_AUTO_CREATE_INDEX_ERROR', () => { - beforeEach(() => { - fetchMock.post({ - matcher: '*', - response: { - body: { - attributes: { - code: 'ES_AUTO_CREATE_INDEX_ERROR', - }, - }, - status: 503, - }, - }); - }); - afterEach(() => fetchMock.restore()); - - test('should return true', async () => { - expect.assertions(1); - try { - await kfetch({ method: 'POST', pathname: '/my/path' }); - } catch (kfetchError) { - expect(isAutoCreateIndexError(kfetchError)).toBe(true); - } - }); - }); -}); diff --git a/src/legacy/ui/public/new_platform/new_platform.ts b/src/legacy/ui/public/new_platform/new_platform.ts index 5ae2e2348aaa1..a15c7cce5511d 100644 --- a/src/legacy/ui/public/new_platform/new_platform.ts +++ b/src/legacy/ui/public/new_platform/new_platform.ts @@ -59,7 +59,6 @@ import { NavigationPublicPluginSetup, NavigationPublicPluginStart, } from '../../../../plugins/navigation/public'; -import { VisTypeVegaSetup } from '../../../../plugins/vis_type_vega/public'; import { DiscoverSetup, DiscoverStart } from '../../../../plugins/discover/public'; import { SavedObjectsManagementPluginSetup, @@ -88,7 +87,6 @@ export interface PluginsSetup { usageCollection: UsageCollectionSetup; advancedSettings: AdvancedSettingsSetup; management: ManagementSetup; - visTypeVega: VisTypeVegaSetup; discover: DiscoverSetup; visualizations: VisualizationsSetup; telemetry?: TelemetryPluginSetup; diff --git a/src/legacy/ui/ui_exports/ui_export_defaults.js b/src/legacy/ui/ui_exports/ui_export_defaults.js index bb246d97bfe4e..35e1f8b7d2127 100644 --- a/src/legacy/ui/ui_exports/ui_export_defaults.js +++ b/src/legacy/ui/ui_exports/ui_export_defaults.js @@ -24,6 +24,7 @@ export const UI_EXPORT_DEFAULTS = { webpackNoParseRules: [ /node_modules[\/\\](angular|elasticsearch-browser)[\/\\]/, /node_modules[\/\\](mocha|moment)[\/\\]/, + /node_modules[\/\\]vega-lib[\/\\]build[\/\\]vega\.js$/, ], webpackAliases: { diff --git a/src/legacy/ui/ui_render/bootstrap/template.js.hbs b/src/legacy/ui/ui_render/bootstrap/template.js.hbs index 7250fa4fc9eca..8a71c6ccb1506 100644 --- a/src/legacy/ui/ui_render/bootstrap/template.js.hbs +++ b/src/legacy/ui/ui_render/bootstrap/template.js.hbs @@ -1,6 +1,7 @@ var kbnCsp = JSON.parse(document.querySelector('kbn-csp').getAttribute('data')); window.__kbnStrictCsp__ = kbnCsp.strictCsp; window.__kbnDarkMode__ = {{darkMode}}; +window.__kbnPublicPath__ = {{publicPathMap}}; if (window.__kbnStrictCsp__ && window.__kbnCspNotEnforced__) { var legacyBrowserError = document.getElementById('kbn_legacy_browser_error'); diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index 1e84405dd5153..801eecf5b608b 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -153,11 +153,25 @@ export function uiRenderMixin(kbnServer, server, config) { `${regularBundlePath}/plugin/kibanaReact/kibanaReact.plugin.js`, ]; + const uiPluginIds = [...kbnServer.newPlatform.__internals.uiPlugins.public.keys()]; + + // These paths should align with the bundle routes configured in + // src/optimize/bundles_route/bundles_route.js + const publicPathMap = JSON.stringify({ + core: `${regularBundlePath}/core/`, + 'kbn-ui-shared-deps': `${regularBundlePath}/kbn-ui-shared-deps/`, + ...uiPluginIds.reduce( + (acc, pluginId) => ({ ...acc, [pluginId]: `${regularBundlePath}/plugin/${pluginId}/` }), + {} + ), + }); + const bootstrap = new AppBootstrap({ templateData: { darkMode, jsDependencyPaths, styleSheetPaths, + publicPathMap, entryBundlePath: isCore ? `${regularBundlePath}/core/core.entry.js` : `${regularBundlePath}/${app.getId()}.bundle.js`, diff --git a/src/optimize/bundles_route/bundles_route.js b/src/optimize/bundles_route/bundles_route.js index 530dabb9a46a3..4030988c8552c 100644 --- a/src/optimize/bundles_route/bundles_route.js +++ b/src/optimize/bundles_route/bundles_route.js @@ -72,43 +72,57 @@ export function createBundlesRoute({ } return [ - buildRouteForBundles( - `${basePublicPath}/bundles/kbn-ui-shared-deps/`, - '/bundles/kbn-ui-shared-deps/', - UiSharedDeps.distDir, - fileHashCache - ), + buildRouteForBundles({ + publicPath: `${basePublicPath}/bundles/kbn-ui-shared-deps/`, + routePath: '/bundles/kbn-ui-shared-deps/', + bundlesPath: UiSharedDeps.distDir, + fileHashCache, + replacePublicPath: false, + }), ...npUiPluginPublicDirs.map(({ id, path }) => - buildRouteForBundles( - `${basePublicPath}/bundles/plugin/${id}/`, - `/bundles/plugin/${id}/`, - path, - fileHashCache - ) - ), - buildRouteForBundles( - `${basePublicPath}/bundles/core/`, - `/bundles/core/`, - fromRoot(join('src', 'core', 'target', 'public')), - fileHashCache - ), - buildRouteForBundles( - `${basePublicPath}/bundles/`, - '/bundles/', - regularBundlesPath, - fileHashCache + buildRouteForBundles({ + publicPath: `${basePublicPath}/bundles/plugin/${id}/`, + routePath: `/bundles/plugin/${id}/`, + bundlesPath: path, + fileHashCache, + replacePublicPath: false, + }) ), - buildRouteForBundles( - `${basePublicPath}/built_assets/dlls/`, - '/built_assets/dlls/', - dllBundlesPath, - fileHashCache - ), - buildRouteForBundles(`${basePublicPath}/`, '/built_assets/css/', builtCssPath, fileHashCache), + buildRouteForBundles({ + publicPath: `${basePublicPath}/bundles/core/`, + routePath: `/bundles/core/`, + bundlesPath: fromRoot(join('src', 'core', 'target', 'public')), + fileHashCache, + replacePublicPath: false, + }), + buildRouteForBundles({ + publicPath: `${basePublicPath}/bundles/`, + routePath: '/bundles/', + bundlesPath: regularBundlesPath, + fileHashCache, + }), + buildRouteForBundles({ + publicPath: `${basePublicPath}/built_assets/dlls/`, + routePath: '/built_assets/dlls/', + bundlesPath: dllBundlesPath, + fileHashCache, + }), + buildRouteForBundles({ + publicPath: `${basePublicPath}/`, + routePath: '/built_assets/css/', + bundlesPath: builtCssPath, + fileHashCache, + }), ]; } -function buildRouteForBundles(publicPath, routePath, bundlesPath, fileHashCache) { +function buildRouteForBundles({ + publicPath, + routePath, + bundlesPath, + fileHashCache, + replacePublicPath = true, +}) { return { method: 'GET', path: `${routePath}{path*}`, @@ -129,6 +143,7 @@ function buildRouteForBundles(publicPath, routePath, bundlesPath, fileHashCache) bundlesPath, fileHashCache, publicPath, + replacePublicPath, }); }, }, diff --git a/src/optimize/bundles_route/dynamic_asset_response.js b/src/optimize/bundles_route/dynamic_asset_response.js index 7af780a79e430..80c49a26270fd 100644 --- a/src/optimize/bundles_route/dynamic_asset_response.js +++ b/src/optimize/bundles_route/dynamic_asset_response.js @@ -52,7 +52,7 @@ import { replacePlaceholder } from '../public_path_placeholder'; * @property {LruCache} options.fileHashCache */ export async function createDynamicAssetResponse(options) { - const { request, h, bundlesPath, publicPath, fileHashCache } = options; + const { request, h, bundlesPath, publicPath, fileHashCache, replacePublicPath } = options; let fd; try { @@ -78,11 +78,14 @@ export async function createDynamicAssetResponse(options) { }); fd = null; // read stream is now responsible for fd + const content = replacePublicPath ? replacePlaceholder(read, publicPath) : read; + const etag = replacePublicPath ? `${hash}-${publicPath}` : hash; + return h - .response(replacePlaceholder(read, publicPath)) + .response(content) .takeover() .code(200) - .etag(`${hash}-${publicPath}`) + .etag(etag) .header('cache-control', 'must-revalidate') .type(request.server.mime.path(path).type); } catch (error) { diff --git a/src/plugins/advanced_settings/kibana.json b/src/plugins/advanced_settings/kibana.json index cac9a6daa8df8..e6ca6e797ba45 100644 --- a/src/plugins/advanced_settings/kibana.json +++ b/src/plugins/advanced_settings/kibana.json @@ -1,7 +1,7 @@ { "id": "advancedSettings", "version": "kibana", - "server": false, + "server": true, "ui": true, "requiredPlugins": ["management"] } diff --git a/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.mocks.js b/src/plugins/advanced_settings/server/capabilities_provider.ts similarity index 84% rename from src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.mocks.js rename to src/plugins/advanced_settings/server/capabilities_provider.ts index 1ac30b85c5a85..083d5f3ffced4 100644 --- a/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.test.mocks.js +++ b/src/plugins/advanced_settings/server/capabilities_provider.ts @@ -17,6 +17,9 @@ * under the License. */ -import { setup } from '../../../../test_utils/public/http_test_setup'; - -jest.doMock('ui/new_platform', () => ({ npSetup: { core: setup() } })); +export const capabilitiesProvider = () => ({ + advancedSettings: { + show: true, + save: true, + }, +}); diff --git a/src/legacy/core_plugins/vis_type_vega/public/index.ts b/src/plugins/advanced_settings/server/index.ts similarity index 77% rename from src/legacy/core_plugins/vis_type_vega/public/index.ts rename to src/plugins/advanced_settings/server/index.ts index 34ca0e72190e4..ffcf7cd49a8c3 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/index.ts +++ b/src/plugins/advanced_settings/server/index.ts @@ -17,9 +17,8 @@ * under the License. */ -import { PluginInitializerContext } from '../../../../core/public'; -import { VegaPlugin as Plugin } from './plugin'; +import { PluginInitializerContext } from 'kibana/server'; +import { AdvancedSettingsServerPlugin } from './plugin'; -export function plugin(initializerContext: PluginInitializerContext) { - return new Plugin(initializerContext); -} +export const plugin = (initContext: PluginInitializerContext) => + new AdvancedSettingsServerPlugin(initContext); diff --git a/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.ts b/src/plugins/advanced_settings/server/plugin.ts similarity index 54% rename from src/legacy/ui/public/error_auto_create_index/error_auto_create_index.ts rename to src/plugins/advanced_settings/server/plugin.ts index 09c6bfd93148f..4d7bd34259819 100644 --- a/src/legacy/ui/public/error_auto_create_index/error_auto_create_index.ts +++ b/src/plugins/advanced_settings/server/plugin.ts @@ -17,31 +17,28 @@ * under the License. */ -import { i18n } from '@kbn/i18n'; -import { get } from 'lodash'; - -import uiRoutes from '../routes'; - -import template from './error_auto_create_index.html'; - -uiRoutes.when('/error/action.auto_create_index', { - template, - k7Breadcrumbs: () => [ - { - text: i18n.translate('common.ui.errorAutoCreateIndex.breadcrumbs.errorText', { - defaultMessage: 'Error', - }), - }, - ], -}); - -export function isAutoCreateIndexError(error: object) { - return ( - get(error, 'res.status') === 503 && - get(error, 'body.attributes.code') === 'ES_AUTO_CREATE_INDEX_ERROR' - ); -} +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from 'kibana/server'; +import { capabilitiesProvider } from './capabilities_provider'; + +export class AdvancedSettingsServerPlugin implements Plugin { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + this.logger.debug('advancedSettings: Setup'); + + core.capabilities.registerProvider(capabilitiesProvider); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('advancedSettings: Started'); + return {}; + } -export function showAutoCreateIndexErrorPage() { - window.location.hash = '/error/action.auto_create_index'; + public stop() {} } diff --git a/src/plugins/dashboard/public/application/legacy_app.js b/src/plugins/dashboard/public/application/legacy_app.js index 10243dbf2f979..31225530b10b9 100644 --- a/src/plugins/dashboard/public/application/legacy_app.js +++ b/src/plugins/dashboard/public/application/legacy_app.js @@ -28,7 +28,6 @@ import { initDashboardAppDirective } from './dashboard_app'; import { createDashboardEditUrl, DashboardConstants } from '../dashboard_constants'; import { createKbnUrlStateStorage, - ensureDefaultIndexPattern, redirectWhenMissing, InvalidJSONProperty, SavedObjectNotFound, @@ -138,7 +137,7 @@ export function initDashboardApp(app, deps) { }, resolve: { dash: function($route, history) { - return ensureDefaultIndexPattern(deps.core, deps.data, history).then(() => { + return deps.data.indexPatterns.ensureDefaultIndexPattern(history).then(() => { const savedObjectsClient = deps.savedObjectsClient; const title = $route.current.params.title; if (title) { @@ -173,7 +172,8 @@ export function initDashboardApp(app, deps) { requireUICapability: 'dashboard.createNew', resolve: { dash: history => - ensureDefaultIndexPattern(deps.core, deps.data, history) + deps.data.indexPatterns + .ensureDefaultIndexPattern(history) .then(() => deps.savedDashboards.get()) .catch( redirectWhenMissing({ @@ -194,7 +194,8 @@ export function initDashboardApp(app, deps) { dash: function($route, history) { const id = $route.current.params.id; - return ensureDefaultIndexPattern(deps.core, deps.data, history) + return deps.data.indexPatterns + .ensureDefaultIndexPattern(history) .then(() => deps.savedDashboards.get(id)) .then(savedDashboard => { deps.chrome.recentlyAccessed.add( diff --git a/src/plugins/dashboard/server/capabilities_provider.ts b/src/plugins/dashboard/server/capabilities_provider.ts new file mode 100644 index 0000000000000..0bb53d60c38a5 --- /dev/null +++ b/src/plugins/dashboard/server/capabilities_provider.ts @@ -0,0 +1,27 @@ +/* + * 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 capabilitiesProvider = () => ({ + dashboard: { + createNew: true, + show: true, + showWriteControls: true, + saveQuery: true, + }, +}); diff --git a/src/plugins/dashboard/server/plugin.ts b/src/plugins/dashboard/server/plugin.ts index 5d1b66002e749..ba7bdeeda0133 100644 --- a/src/plugins/dashboard/server/plugin.ts +++ b/src/plugins/dashboard/server/plugin.ts @@ -26,6 +26,7 @@ import { } from '../../../core/server'; import { dashboardSavedObjectType } from './saved_objects'; +import { capabilitiesProvider } from './capabilities_provider'; import { DashboardPluginSetup, DashboardPluginStart } from './types'; @@ -40,6 +41,7 @@ export class DashboardPlugin implements Plugin { expect(fromLiteralExpression('true')).toEqual(booleanTrueLiteral); expect(fromLiteralExpression('false')).toEqual(booleanFalseLiteral); expect(fromLiteralExpression('42')).toEqual(numberLiteral); + + expect(fromLiteralExpression('.3').value).toEqual(0.3); + expect(fromLiteralExpression('.36').value).toEqual(0.36); + expect(fromLiteralExpression('.00001').value).toEqual(0.00001); + expect(fromLiteralExpression('3').value).toEqual(3); + expect(fromLiteralExpression('-4').value).toEqual(-4); + expect(fromLiteralExpression('0').value).toEqual(0); + expect(fromLiteralExpression('0.0').value).toEqual(0); + expect(fromLiteralExpression('2.0').value).toEqual(2.0); + expect(fromLiteralExpression('0.8').value).toEqual(0.8); + expect(fromLiteralExpression('790.9').value).toEqual(790.9); + expect(fromLiteralExpression('0.0001').value).toEqual(0.0001); + expect(fromLiteralExpression('96565646732345').value).toEqual(96565646732345); + + expect(fromLiteralExpression('..4').value).toEqual('..4'); + expect(fromLiteralExpression('.3text').value).toEqual('.3text'); + expect(fromLiteralExpression('text').value).toEqual('text'); + expect(fromLiteralExpression('.').value).toEqual('.'); + expect(fromLiteralExpression('-').value).toEqual('-'); + expect(fromLiteralExpression('001').value).toEqual('001'); + expect(fromLiteralExpression('00.2').value).toEqual('00.2'); + expect(fromLiteralExpression('0.0.1').value).toEqual('0.0.1'); + expect(fromLiteralExpression('3.').value).toEqual('3.'); + expect(fromLiteralExpression('--4').value).toEqual('--4'); + expect(fromLiteralExpression('-.4').value).toEqual('-.4'); + expect(fromLiteralExpression('-0').value).toEqual('-0'); + expect(fromLiteralExpression('00949').value).toEqual('00949'); }); test('should allow escaping of special characters with a backslash', () => { diff --git a/src/plugins/data/common/es_query/kuery/ast/kuery.peg b/src/plugins/data/common/es_query/kuery/ast/kuery.peg index 389b9a82d2c76..625c5069f936a 100644 --- a/src/plugins/data/common/es_query/kuery/ast/kuery.peg +++ b/src/plugins/data/common/es_query/kuery/ast/kuery.peg @@ -247,9 +247,8 @@ UnquotedLiteral if (sequence === 'true') return buildLiteralNode(true); if (sequence === 'false') return buildLiteralNode(false); if (chars.includes(wildcardSymbol)) return buildWildcardNode(sequence); - const number = Number(sequence); - const value = isNaN(number) ? sequence : number; - return buildLiteralNode(value); + const isNumberPattern = /^(-?[1-9]+\d*([.]\d+)?)$|^(-?0[.]\d*[1-9]+)$|^0$|^0.0$|^[.]\d{1,}$/ + return buildLiteralNode(isNumberPattern.test(sequence) ? Number(sequence) : sequence); } UnquotedCharacter diff --git a/src/plugins/data/common/field_formats/constants/base_formatters.ts b/src/plugins/data/common/field_formats/constants/base_formatters.ts index 6befe8cea71f5..921c50571f727 100644 --- a/src/plugins/data/common/field_formats/constants/base_formatters.ts +++ b/src/plugins/data/common/field_formats/constants/base_formatters.ts @@ -17,7 +17,7 @@ * under the License. */ -import { IFieldFormatType } from '../types'; +import { FieldFormatInstanceType } from '../types'; import { BoolFormat, @@ -36,7 +36,7 @@ import { UrlFormat, } from '../converters'; -export const baseFormatters: IFieldFormatType[] = [ +export const baseFormatters: FieldFormatInstanceType[] = [ BoolFormat, BytesFormat, ColorFormat, diff --git a/src/plugins/data/common/field_formats/converters/custom.ts b/src/plugins/data/common/field_formats/converters/custom.ts index a1ce0cf3e7b54..4dd011a7feff3 100644 --- a/src/plugins/data/common/field_formats/converters/custom.ts +++ b/src/plugins/data/common/field_formats/converters/custom.ts @@ -18,9 +18,9 @@ */ import { FieldFormat } from '../field_format'; -import { TextContextTypeConvert, FIELD_FORMAT_IDS, IFieldFormatType } from '../types'; +import { TextContextTypeConvert, FIELD_FORMAT_IDS, FieldFormatInstanceType } from '../types'; -export const createCustomFieldFormat = (convert: TextContextTypeConvert): IFieldFormatType => +export const createCustomFieldFormat = (convert: TextContextTypeConvert): FieldFormatInstanceType => class CustomFieldFormat extends FieldFormat { static id = FIELD_FORMAT_IDS.CUSTOM; diff --git a/src/plugins/data/common/field_formats/field_format.ts b/src/plugins/data/common/field_formats/field_format.ts index 49baa8c074da8..96d0024dff2a2 100644 --- a/src/plugins/data/common/field_formats/field_format.ts +++ b/src/plugins/data/common/field_formats/field_format.ts @@ -22,7 +22,7 @@ import { createCustomFieldFormat } from './converters/custom'; import { FieldFormatsGetConfigFn, FieldFormatsContentType, - IFieldFormatType, + FieldFormatInstanceType, FieldFormatConvert, FieldFormatConvertFunction, HtmlContextTypeOptions, @@ -199,7 +199,7 @@ export abstract class FieldFormat { }; } - static from(convertFn: FieldFormatConvertFunction): IFieldFormatType { + static from(convertFn: FieldFormatConvertFunction): FieldFormatInstanceType { return createCustomFieldFormat(convertFn); } diff --git a/src/plugins/data/common/field_formats/field_formats_registry.test.ts b/src/plugins/data/common/field_formats/field_formats_registry.test.ts index 0b32a62744fb1..f04524505a711 100644 --- a/src/plugins/data/common/field_formats/field_formats_registry.test.ts +++ b/src/plugins/data/common/field_formats/field_formats_registry.test.ts @@ -18,7 +18,7 @@ */ import { FieldFormatsRegistry } from './field_formats_registry'; import { BoolFormat, PercentFormat, StringFormat } from './converters'; -import { FieldFormatsGetConfigFn, IFieldFormatType } from './types'; +import { FieldFormatsGetConfigFn, FieldFormatInstanceType } from './types'; import { KBN_FIELD_TYPES } from '../../common'; const getValueOfPrivateField = (instance: any, field: string) => instance[field]; @@ -75,10 +75,10 @@ describe('FieldFormatsRegistry', () => { test('should register field formats', () => { fieldFormatsRegistry.register([StringFormat, BoolFormat]); - const registeredFieldFormatters: Map = getValueOfPrivateField( - fieldFormatsRegistry, - 'fieldFormats' - ); + const registeredFieldFormatters: Map< + string, + FieldFormatInstanceType + > = getValueOfPrivateField(fieldFormatsRegistry, 'fieldFormats'); expect(registeredFieldFormatters.size).toBe(2); diff --git a/src/plugins/data/common/field_formats/field_formats_registry.ts b/src/plugins/data/common/field_formats/field_formats_registry.ts index 15b1687e22312..b0a57ad6912a7 100644 --- a/src/plugins/data/common/field_formats/field_formats_registry.ts +++ b/src/plugins/data/common/field_formats/field_formats_registry.ts @@ -24,7 +24,7 @@ import { FieldFormatsGetConfigFn, FieldFormatConfig, FIELD_FORMAT_IDS, - IFieldFormatType, + FieldFormatInstanceType, FieldFormatId, IFieldFormatMetaParams, IFieldFormat, @@ -35,7 +35,7 @@ import { SerializedFieldFormat } from '../../../expressions/common/types'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../types'; export class FieldFormatsRegistry { - protected fieldFormats: Map = new Map(); + protected fieldFormats: Map = new Map(); protected defaultMap: Record = {}; protected metaParamsOptions: Record = {}; protected getConfig?: FieldFormatsGetConfigFn; @@ -47,7 +47,7 @@ export class FieldFormatsRegistry { init( getConfig: FieldFormatsGetConfigFn, metaParamsOptions: Record = {}, - defaultFieldConverters: IFieldFormatType[] = baseFormatters + defaultFieldConverters: FieldFormatInstanceType[] = baseFormatters ) { const defaultTypeMap = getConfig('format:defaultTypeMap'); this.register(defaultFieldConverters); @@ -79,23 +79,23 @@ export class FieldFormatsRegistry { * Get a derived FieldFormat class by its id. * * @param {FieldFormatId} formatId - the format id - * @return {IFieldFormatType | undefined} + * @return {FieldFormatInstanceType | undefined} */ - getType = (formatId: FieldFormatId): IFieldFormatType | undefined => { + getType = (formatId: FieldFormatId): FieldFormatInstanceType | undefined => { const fieldFormat = this.fieldFormats.get(formatId); if (fieldFormat) { const decoratedFieldFormat: any = this.fieldFormatMetaParamsDecorator(fieldFormat); if (decoratedFieldFormat) { - return decoratedFieldFormat as IFieldFormatType; + return decoratedFieldFormat as FieldFormatInstanceType; } } return undefined; }; - getTypeWithoutMetaParams = (formatId: FieldFormatId): IFieldFormatType | undefined => { + getTypeWithoutMetaParams = (formatId: FieldFormatId): FieldFormatInstanceType | undefined => { return this.fieldFormats.get(formatId); }; @@ -106,12 +106,12 @@ export class FieldFormatsRegistry { * * @param {KBN_FIELD_TYPES} fieldType * @param {ES_FIELD_TYPES[]} esTypes - Array of ES data types - * @return {IFieldFormatType | undefined} + * @return {FieldFormatInstanceType | undefined} */ getDefaultType = ( fieldType: KBN_FIELD_TYPES, esTypes: ES_FIELD_TYPES[] - ): IFieldFormatType | undefined => { + ): FieldFormatInstanceType | undefined => { const config = this.getDefaultConfig(fieldType, esTypes); return this.getType(config.id); @@ -206,14 +206,16 @@ export class FieldFormatsRegistry { * Get filtered list of field formats by format type * * @param {KBN_FIELD_TYPES} fieldType - * @return {IFieldFormatType[]} + * @return {FieldFormatInstanceType[]} */ - getByFieldType(fieldType: KBN_FIELD_TYPES): IFieldFormatType[] { + getByFieldType(fieldType: KBN_FIELD_TYPES): FieldFormatInstanceType[] { return [...this.fieldFormats.values()] - .filter((format: IFieldFormatType) => format && format.fieldType.indexOf(fieldType) !== -1) + .filter( + (format: FieldFormatInstanceType) => format && format.fieldType.indexOf(fieldType) !== -1 + ) .map( - (format: IFieldFormatType) => - this.fieldFormatMetaParamsDecorator(format) as IFieldFormatType + (format: FieldFormatInstanceType) => + this.fieldFormatMetaParamsDecorator(format) as FieldFormatInstanceType ); } @@ -238,7 +240,7 @@ export class FieldFormatsRegistry { }); } - register(fieldFormats: IFieldFormatType[]) { + register(fieldFormats: FieldFormatInstanceType[]) { fieldFormats.forEach(fieldFormat => this.fieldFormats.set(fieldFormat.id, fieldFormat)); } @@ -246,12 +248,12 @@ export class FieldFormatsRegistry { * FieldFormat decorator - provide a one way to add meta-params for all field formatters * * @private - * @param {IFieldFormatType} fieldFormat - field format type - * @return {IFieldFormatType | undefined} + * @param {FieldFormatInstanceType} fieldFormat - field format type + * @return {FieldFormatInstanceType | undefined} */ private fieldFormatMetaParamsDecorator = ( - fieldFormat: IFieldFormatType - ): IFieldFormatType | undefined => { + fieldFormat: FieldFormatInstanceType + ): FieldFormatInstanceType | undefined => { const getMetaParams = (customParams: Record) => this.buildMetaParams(customParams); if (fieldFormat) { diff --git a/src/plugins/data/common/field_formats/index.ts b/src/plugins/data/common/field_formats/index.ts index 13d3d9d73d43a..b64e115fd55ff 100644 --- a/src/plugins/data/common/field_formats/index.ts +++ b/src/plugins/data/common/field_formats/index.ts @@ -52,6 +52,6 @@ export { FieldFormatConfig, FieldFormatId, // Used in data plugin only - IFieldFormatType, + FieldFormatInstanceType, IFieldFormat, } from './types'; diff --git a/src/plugins/data/common/field_formats/types.ts b/src/plugins/data/common/field_formats/types.ts index 7c1d6a8522e52..5f11c7fe094bc 100644 --- a/src/plugins/data/common/field_formats/types.ts +++ b/src/plugins/data/common/field_formats/types.ts @@ -16,9 +16,7 @@ * specific language governing permissions and limitations * under the License. */ - import { FieldFormat } from './field_format'; -export { FieldFormat }; /** @public **/ export type FieldFormatsContentType = 'html' | 'text'; @@ -82,10 +80,12 @@ export type IFieldFormat = PublicMethodsOf; */ export type FieldFormatId = FIELD_FORMAT_IDS | string; -export type IFieldFormatType = (new ( +/** @internal **/ +export type FieldFormatInstanceType = (new ( params?: any, getConfig?: FieldFormatsGetConfigFn ) => FieldFormat) & { + // Static properties: id: FieldFormatId; title: string; fieldType: string | string[]; diff --git a/src/plugins/data/public/index_patterns/index_patterns/ensure_default_index_pattern.tsx b/src/plugins/data/public/index_patterns/index_patterns/ensure_default_index_pattern.tsx new file mode 100644 index 0000000000000..6b71739862f62 --- /dev/null +++ b/src/plugins/data/public/index_patterns/index_patterns/ensure_default_index_pattern.tsx @@ -0,0 +1,98 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { contains } from 'lodash'; +import React from 'react'; +import { History } from 'history'; +import { i18n } from '@kbn/i18n'; +import { EuiCallOut } from '@elastic/eui'; +import { CoreStart } from 'kibana/public'; +import { toMountPoint } from '../../../../kibana_react/public'; +import { IndexPatternsContract } from './index_patterns'; + +export type EnsureDefaultIndexPattern = (history: History) => Promise | undefined; + +export const createEnsureDefaultIndexPattern = (core: CoreStart) => { + let bannerId: string; + let timeoutId: NodeJS.Timeout | undefined; + + /** + * Checks whether a default index pattern is set and exists and defines + * one otherwise. + * + * If there are no index patterns, redirect to management page and show + * banner. In this case the promise returned from this function will never + * resolve to wait for the URL change to happen. + */ + return async function ensureDefaultIndexPattern(this: IndexPatternsContract, history: History) { + const patterns = await this.getIds(); + let defaultId = core.uiSettings.get('defaultIndex'); + let defined = !!defaultId; + const exists = contains(patterns, defaultId); + + if (defined && !exists) { + core.uiSettings.remove('defaultIndex'); + defaultId = defined = false; + } + + if (defined) { + return; + } + + // If there is any index pattern created, set the first as default + if (patterns.length >= 1) { + defaultId = patterns[0]; + core.uiSettings.set('defaultIndex', defaultId); + } else { + const canManageIndexPatterns = core.application.capabilities.management.kibana.index_patterns; + const redirectTarget = canManageIndexPatterns ? '/management/kibana/index_pattern' : '/home'; + + if (timeoutId) { + clearTimeout(timeoutId); + } + + // Avoid being hostile to new users who don't have an index pattern setup yet + // give them a friendly info message instead of a terse error message + bannerId = core.overlays.banners.replace( + bannerId, + toMountPoint( + + ) + ); + + // hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around + timeoutId = setTimeout(() => { + core.overlays.banners.remove(bannerId); + timeoutId = undefined; + }, 15000); + + history.push(redirectTarget); + + // return never-resolving promise to stop resolving and wait for the url change + return new Promise(() => {}); + } + }; +}; diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts index c429431b632bd..cf1f83d0e28cb 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.test.ts @@ -21,9 +21,9 @@ import { IndexPatternsService } from './index_patterns'; import { SavedObjectsClientContract, - IUiSettingsClient, HttpSetup, SavedObjectsFindResponsePublic, + CoreStart, } from 'kibana/public'; jest.mock('./index_pattern', () => { @@ -61,10 +61,10 @@ describe('IndexPatterns', () => { }) as Promise> ); - const uiSettings = {} as IUiSettingsClient; + const core = {} as CoreStart; const http = {} as HttpSetup; - indexPatterns = new IndexPatternsService(uiSettings, savedObjectsClient, http); + indexPatterns = new IndexPatternsService(core, savedObjectsClient, http); }); test('does cache gets for the same id', async () => { diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts index acce5ed57683c..b5d66a6aab60a 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns.ts @@ -22,11 +22,16 @@ import { SimpleSavedObject, IUiSettingsClient, HttpStart, + CoreStart, } from 'src/core/public'; import { createIndexPatternCache } from './_pattern_cache'; import { IndexPattern } from './index_pattern'; import { IndexPatternsApiClient, GetFieldsOptions } from './index_patterns_api_client'; +import { + createEnsureDefaultIndexPattern, + EnsureDefaultIndexPattern, +} from './ensure_default_index_pattern'; const indexPatternCache = createIndexPatternCache(); @@ -37,15 +42,13 @@ export class IndexPatternsService { private savedObjectsClient: SavedObjectsClientContract; private savedObjectsCache?: Array>> | null; private apiClient: IndexPatternsApiClient; + ensureDefaultIndexPattern: EnsureDefaultIndexPattern; - constructor( - config: IUiSettingsClient, - savedObjectsClient: SavedObjectsClientContract, - http: HttpStart - ) { + constructor(core: CoreStart, savedObjectsClient: SavedObjectsClientContract, http: HttpStart) { this.apiClient = new IndexPatternsApiClient(http); - this.config = config; + this.config = core.uiSettings; this.savedObjectsClient = savedObjectsClient; + this.ensureDefaultIndexPattern = createEnsureDefaultIndexPattern(core); } private async refreshSavedObjectsCache() { diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index a2df754786a68..ba1df89c41358 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -57,6 +57,7 @@ const createStartContract = (): Start => { SearchBar: jest.fn(), }, indexPatterns: ({ + ensureDefaultIndexPattern: jest.fn(), make: () => ({ fieldsFetcher: { fetchForWildcard: jest.fn(), diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 49ef1000d7993..f3a88287313a0 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -139,6 +139,7 @@ export class DataPublicPlugin implements Plugin; - // (undocumented) - schema?: string; - // (undocumented) +export type AggConfigOptions = Assign; // Warning: (ae-missing-release-tag) "AggGroupNames" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -112,7 +105,7 @@ export class AggParamType extends Ba // (undocumented) allowedAggs: string[]; // (undocumented) - makeAgg: (agg: TAggConfig, state?: any) => TAggConfig; + makeAgg: (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig; } // Warning: (ae-missing-release-tag) "AggTypeFieldFilters" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -145,7 +138,7 @@ export class AggTypeFilters { // Warning: (ae-missing-release-tag) "baseFormattersPublic" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const baseFormattersPublic: (import("../../common").IFieldFormatType | typeof DateFormat)[]; +export const baseFormattersPublic: (import("../../common").FieldFormatInstanceType | typeof DateFormat)[]; // Warning: (ae-missing-release-tag) "BUCKET_TYPES" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/src/plugins/data/public/search/aggs/agg_config.test.ts b/src/plugins/data/public/search/aggs/agg_config.test.ts index 2813e3b9c5373..b5df90313230c 100644 --- a/src/plugins/data/public/search/aggs/agg_config.test.ts +++ b/src/plugins/data/public/search/aggs/agg_config.test.ts @@ -24,18 +24,21 @@ import { AggConfigs, CreateAggConfigParams } from './agg_configs'; import { AggType } from './agg_type'; import { AggTypesRegistryStart } from './agg_types_registry'; import { mockDataServices, mockAggTypesRegistry } from './test_helpers'; +import { MetricAggType } from './metrics/metric_agg_type'; import { Field as IndexPatternField, IndexPattern } from '../../index_patterns'; import { stubIndexPatternWithFields } from '../../../public/stubs'; +import { FieldFormatsStart } from '../../field_formats'; import { fieldFormatsServiceMock } from '../../field_formats/mocks'; describe('AggConfig', () => { let indexPattern: IndexPattern; let typesRegistry: AggTypesRegistryStart; - const fieldFormats = fieldFormatsServiceMock.createStartContract(); + let fieldFormats: FieldFormatsStart; beforeEach(() => { jest.restoreAllMocks(); mockDataServices(); + fieldFormats = fieldFormatsServiceMock.createStartContract(); indexPattern = stubIndexPatternWithFields as IndexPattern; typesRegistry = mockAggTypesRegistry(); }); @@ -325,7 +328,7 @@ describe('AggConfig', () => { }); }); - describe('#toJSON', () => { + describe('#serialize', () => { it('includes the aggs id, params, type and schema', () => { const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); const configStates = { @@ -342,7 +345,7 @@ describe('AggConfig', () => { expect(aggConfig.type).toHaveProperty('name', 'date_histogram'); expect(typeof aggConfig.schema).toBe('string'); - const state = aggConfig.toJSON(); + const state = aggConfig.serialize(); expect(state).toHaveProperty('id', '1'); expect(typeof state.params).toBe('object'); expect(state).toHaveProperty('type', 'date_histogram'); @@ -367,6 +370,201 @@ describe('AggConfig', () => { }); }); + describe('#toExpressionAst', () => { + beforeEach(() => { + fieldFormats.getDefaultInstance = (() => ({ + getConverterFor: (t?: string) => t || identity, + })) as any; + indexPattern.fields.getByName = name => + ({ + format: { + getConverterFor: (t?: string) => t || identity, + }, + } as IndexPatternField); + }); + + it('works with primitive param types', () => { + const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); + const configStates = { + enabled: true, + type: 'terms', + schema: 'segment', + params: { + field: 'machine.os.keyword', + order: 'asc', + }, + }; + const aggConfig = ac.createAggConfig(configStates); + expect(aggConfig.toExpressionAst()).toMatchInlineSnapshot(` + Object { + "arguments": Object { + "enabled": Array [ + true, + ], + "id": Array [ + "1", + ], + "missingBucket": Array [ + false, + ], + "missingBucketLabel": Array [ + "Missing", + ], + "order": Array [ + "asc", + ], + "otherBucket": Array [ + false, + ], + "otherBucketLabel": Array [ + "Other", + ], + "schema": Array [ + "segment", + ], + "size": Array [ + 5, + ], + }, + "function": "aggTerms", + "type": "function", + } + `); + }); + + it('creates a subexpression for params of type "agg"', () => { + const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); + const configStates = { + type: 'terms', + params: { + field: 'machine.os.keyword', + order: 'asc', + orderAgg: { + enabled: true, + type: 'terms', + params: { + field: 'bytes', + order: 'asc', + size: 5, + }, + }, + }, + }; + const aggConfig = ac.createAggConfig(configStates); + const aggArg = aggConfig.toExpressionAst()?.arguments.orderAgg; + expect(aggArg).toMatchInlineSnapshot(` + Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "enabled": Array [ + true, + ], + "id": Array [ + "1-orderAgg", + ], + "missingBucket": Array [ + false, + ], + "missingBucketLabel": Array [ + "Missing", + ], + "order": Array [ + "asc", + ], + "otherBucket": Array [ + false, + ], + "otherBucketLabel": Array [ + "Other", + ], + "schema": Array [ + "orderAgg", + ], + "size": Array [ + 5, + ], + }, + "function": "aggTerms", + "type": "function", + }, + ], + "type": "expression", + }, + ] + `); + }); + + it('creates a subexpression for param types other than "agg" which have specified toExpressionAst', () => { + // Overwrite the `ranges` param in the `range` agg with a mock toExpressionAst function + const range: MetricAggType = typesRegistry.get('range'); + range.expressionName = 'aggRange'; + const rangesParam = range.params.find(p => p.name === 'ranges'); + rangesParam!.toExpressionAst = (val: any) => ({ + type: 'function', + function: 'aggRanges', + arguments: { + ranges: ['oh hi there!'], + }, + }); + + const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); + const configStates = { + type: 'range', + params: { + field: 'bytes', + }, + }; + + const aggConfig = ac.createAggConfig(configStates); + const ranges = aggConfig.toExpressionAst()!.arguments.ranges; + expect(ranges).toMatchInlineSnapshot(` + Array [ + Object { + "chain": Array [ + Object { + "arguments": Object { + "ranges": Array [ + "oh hi there!", + ], + }, + "function": "aggRanges", + "type": "function", + }, + ], + "type": "expression", + }, + ] + `); + }); + + it('stringifies any other params which are an object', () => { + const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); + const configStates = { + type: 'terms', + params: { + field: 'machine.os.keyword', + order: 'asc', + json: { foo: 'bar' }, + }, + }; + const aggConfig = ac.createAggConfig(configStates); + const json = aggConfig.toExpressionAst()?.arguments.json; + expect(json).toEqual([JSON.stringify(configStates.params.json)]); + }); + + it(`returns undefined if an expressionName doesn't exist on the agg type`, () => { + const ac = new AggConfigs(indexPattern, [], { typesRegistry, fieldFormats }); + const configStates = { + type: 'unknown type', + params: {}, + }; + const aggConfig = ac.createAggConfig(configStates); + expect(aggConfig.toExpressionAst()).toBe(undefined); + }); + }); + describe('#makeLabel', () => { let aggConfig: AggConfig; @@ -422,6 +620,9 @@ describe('AggConfig', () => { let aggConfig: AggConfig; beforeEach(() => { + fieldFormats.getDefaultInstance = (() => ({ + getConverterFor: (t?: string) => t || identity, + })) as any; indexPattern.fields.getByName = name => ({ format: { @@ -434,11 +635,7 @@ describe('AggConfig', () => { type: 'histogram', schema: 'bucket', params: { - field: { - format: { - getConverterFor: (t?: string) => t || identity, - }, - }, + field: 'bytes', }, }; const ac = new AggConfigs(indexPattern, [configStates], { typesRegistry, fieldFormats }); @@ -446,6 +643,11 @@ describe('AggConfig', () => { }); it("returns the field's formatter", () => { + aggConfig.params.field = { + format: { + getConverterFor: (t?: string) => t || identity, + }, + }; expect(aggConfig.fieldFormatter().toString()).toBe( aggConfig .getField() diff --git a/src/plugins/data/public/search/aggs/agg_config.ts b/src/plugins/data/public/search/aggs/agg_config.ts index 6188849e0e6d4..973c69e3d4f5f 100644 --- a/src/plugins/data/public/search/aggs/agg_config.ts +++ b/src/plugins/data/public/search/aggs/agg_config.ts @@ -19,6 +19,8 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; +import { ExpressionAstFunction, ExpressionAstArgument } from 'src/plugins/expressions/public'; import { IAggType } from './agg_type'; import { writeParams } from './agg_params'; import { IAggConfigs } from './agg_configs'; @@ -27,11 +29,17 @@ import { ISearchSource } from '../search_source'; import { FieldFormatsContentType, KBN_FIELD_TYPES } from '../../../common'; import { FieldFormatsStart } from '../../field_formats'; -export interface AggConfigOptions { - type: IAggType; +type State = string | number | boolean | null | undefined | SerializableState; + +interface SerializableState { + [key: string]: State | State[]; +} + +export interface AggConfigSerialized { + type: string; enabled?: boolean; id?: string; - params?: Record; + params?: SerializableState; schema?: string; } @@ -39,6 +47,8 @@ export interface AggConfigDependencies { fieldFormats: FieldFormatsStart; } +export type AggConfigOptions = Assign; + /** * @name AggConfig * @@ -257,7 +267,10 @@ export class AggConfig { return configDsl; } - toJSON() { + /** + * @returns Returns a serialized representation of an AggConfig. + */ + serialize(): AggConfigSerialized { const params = this.params; const outParams = _.transform( @@ -281,7 +294,64 @@ export class AggConfig { enabled: this.enabled, type: this.type && this.type.name, schema: this.schema, - params: outParams, + params: outParams as SerializableState, + }; + } + + /** + * @deprecated - Use serialize() instead. + */ + toJSON(): AggConfigSerialized { + return this.serialize(); + } + + /** + * @returns Returns an ExpressionAst representing the function for this agg type. + */ + toExpressionAst(): ExpressionAstFunction | undefined { + const functionName = this.type && this.type.expressionName; + const { type, ...rest } = this.serialize(); + if (!functionName || !rest.params) { + // Return undefined - there is no matching expression function for this agg + return; + } + + // Go through each of the params and convert to an array of expression args. + const params = Object.entries(rest.params).reduce((acc, [key, value]) => { + const deserializedParam = this.getAggParams().find(p => p.name === key); + + if (deserializedParam && deserializedParam.toExpressionAst) { + // If the param provides `toExpressionAst`, we call it with the value + const paramExpressionAst = deserializedParam.toExpressionAst(this.getParam(key)); + if (paramExpressionAst) { + acc[key] = [ + { + type: 'expression', + chain: [paramExpressionAst], + }, + ]; + } + } else if (typeof value === 'object') { + // For object params which don't provide `toExpressionAst`, we stringify + acc[key] = [JSON.stringify(value)]; + } else if (typeof value !== 'undefined') { + // Everything else just gets stored in an array if it is defined + acc[key] = [value]; + } + + return acc; + }, {} as Record); + + return { + type: 'function', + function: functionName, + arguments: { + ...params, + // Expression args which are provided to all functions + id: [this.id], + enabled: [this.enabled], + ...(this.schema ? { schema: [this.schema] } : {}), // schema may be undefined + }, }; } diff --git a/src/plugins/data/public/search/aggs/agg_configs.ts b/src/plugins/data/public/search/aggs/agg_configs.ts index 5ad09f824d3e4..d2151a2c5ed4d 100644 --- a/src/plugins/data/public/search/aggs/agg_configs.ts +++ b/src/plugins/data/public/search/aggs/agg_configs.ts @@ -20,7 +20,7 @@ import _ from 'lodash'; import { Assign } from '@kbn/utility-types'; -import { AggConfig, AggConfigOptions, IAggConfig } from './agg_config'; +import { AggConfig, AggConfigSerialized, IAggConfig } from './agg_config'; import { IAggType } from './agg_type'; import { AggTypesRegistryStart } from './agg_types_registry'; import { AggGroupNames } from './agg_groups'; @@ -51,7 +51,7 @@ export interface AggConfigsOptions { fieldFormats: FieldFormatsStart; } -export type CreateAggConfigParams = Assign; +export type CreateAggConfigParams = Assign; /** * @name AggConfigs diff --git a/src/plugins/data/public/search/aggs/agg_type.ts b/src/plugins/data/public/search/aggs/agg_type.ts index 70c116d560c6f..fb0cb609a08cf 100644 --- a/src/plugins/data/public/search/aggs/agg_type.ts +++ b/src/plugins/data/public/search/aggs/agg_type.ts @@ -39,6 +39,7 @@ export interface AggTypeConfig< createFilter?: (aggConfig: TAggConfig, key: any, params?: any) => any; type?: string; dslName?: string; + expressionName?: string; makeLabel?: ((aggConfig: TAggConfig) => string) | (() => string); ordered?: any; hasNoDsl?: boolean; @@ -88,6 +89,14 @@ export class AggType< * @type {string} */ dslName: string; + /** + * the name of the expression function that this aggType represents. + * TODO: this should probably be a required field. + * + * @property name + * @type {string} + */ + expressionName?: string; /** * the user friendly name that will be shown in the ui for this aggType * @@ -219,6 +228,7 @@ export class AggType< this.name = config.name; this.type = config.type || 'metrics'; this.dslName = config.dslName || config.name; + this.expressionName = config.expressionName; this.title = config.title; this.makeLabel = config.makeLabel || constant(this.name); this.ordered = config.ordered; diff --git a/src/plugins/data/public/search/aggs/agg_types.ts b/src/plugins/data/public/search/aggs/agg_types.ts index 4b154c338d48c..da07f581c9274 100644 --- a/src/plugins/data/public/search/aggs/agg_types.ts +++ b/src/plugins/data/public/search/aggs/agg_types.ts @@ -37,6 +37,7 @@ import { getDerivativeMetricAgg } from './metrics/derivative'; import { getCumulativeSumMetricAgg } from './metrics/cumulative_sum'; import { getMovingAvgMetricAgg } from './metrics/moving_avg'; import { getSerialDiffMetricAgg } from './metrics/serial_diff'; + import { getDateHistogramBucketAgg } from './buckets/date_histogram'; import { getHistogramBucketAgg } from './buckets/histogram'; import { getRangeBucketAgg } from './buckets/range'; @@ -103,3 +104,7 @@ export const getAggTypes = ({ getGeoTitleBucketAgg({ getInternalStartServices }), ], }); + +import { aggTerms } from './buckets/terms_fn'; + +export const getAggTypesFunctions = () => [aggTerms]; diff --git a/src/plugins/data/public/search/aggs/buckets/terms.ts b/src/plugins/data/public/search/aggs/buckets/terms.ts index 698e0dfb1d340..a12a1d7ac2d3d 100644 --- a/src/plugins/data/public/search/aggs/buckets/terms.ts +++ b/src/plugins/data/public/search/aggs/buckets/terms.ts @@ -26,7 +26,7 @@ import { isStringOrNumberType, migrateIncludeExcludeFormat, } from './migrate_include_exclude_format'; -import { IAggConfigs } from '../agg_configs'; +import { AggConfigSerialized, IAggConfigs } from '../types'; import { Adapters } from '../../../../../inspector/public'; import { ISearchSource } from '../../search_source'; @@ -63,10 +63,27 @@ export interface TermsBucketAggDependencies { getInternalStartServices: GetInternalStartServicesFn; } +export interface AggParamsTerms { + field: string; + order: 'asc' | 'desc'; + orderBy: string; + orderAgg?: AggConfigSerialized; + size?: number; + missingBucket?: boolean; + missingBucketLabel?: string; + otherBucket?: boolean; + otherBucketLabel?: string; + // advanced + exclude?: string; + include?: string; + json?: string; +} + export const getTermsBucketAgg = ({ getInternalStartServices }: TermsBucketAggDependencies) => new BucketAggType( { name: BUCKET_TYPES.TERMS, + expressionName: 'aggTerms', title: termsTitle, makeLabel(agg) { const params = agg.params; @@ -154,8 +171,7 @@ export const getTermsBucketAgg = ({ getInternalStartServices }: TermsBucketAggDe type: 'agg', allowedAggs: termsAggFilter, default: null, - makeAgg(termsAgg, state) { - state = state || {}; + makeAgg(termsAgg, state = { type: 'count' }) { state.schema = 'orderAgg'; const orderAgg = termsAgg.aggConfigs.createAggConfig(state, { addToAggConfigs: false, diff --git a/src/plugins/data/public/search/aggs/buckets/terms_fn.test.ts b/src/plugins/data/public/search/aggs/buckets/terms_fn.test.ts new file mode 100644 index 0000000000000..f55f1de796013 --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/terms_fn.test.ts @@ -0,0 +1,164 @@ +/* + * 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 { functionWrapper } from '../test_helpers'; +import { aggTerms } from './terms_fn'; + +describe('agg_expression_functions', () => { + describe('aggTerms', () => { + const fn = functionWrapper(aggTerms()); + + test('fills in defaults when only required args are provided', () => { + const actual = fn({ + field: 'machine.os.keyword', + order: 'asc', + orderBy: '1', + }); + expect(actual).toMatchInlineSnapshot(` + Object { + "type": "agg_type", + "value": Object { + "enabled": true, + "id": undefined, + "params": Object { + "exclude": undefined, + "field": "machine.os.keyword", + "include": undefined, + "json": undefined, + "missingBucket": false, + "missingBucketLabel": "Missing", + "order": "asc", + "orderAgg": undefined, + "orderBy": "1", + "otherBucket": false, + "otherBucketLabel": "Other", + "size": 5, + }, + "schema": undefined, + "type": "terms", + }, + } + `); + }); + + test('includes optional params when they are provided', () => { + const actual = fn({ + id: '1', + enabled: false, + schema: 'whatever', + field: 'machine.os.keyword', + order: 'desc', + orderBy: '2', + size: 6, + missingBucket: true, + missingBucketLabel: 'missing', + otherBucket: true, + otherBucketLabel: 'other', + exclude: 'ios', + }); + + expect(actual.value).toMatchInlineSnapshot(` + Object { + "enabled": false, + "id": "1", + "params": Object { + "exclude": "ios", + "field": "machine.os.keyword", + "include": undefined, + "json": undefined, + "missingBucket": true, + "missingBucketLabel": "missing", + "order": "desc", + "orderAgg": undefined, + "orderBy": "2", + "otherBucket": true, + "otherBucketLabel": "other", + "size": 6, + }, + "schema": "whatever", + "type": "terms", + } + `); + }); + + test('handles orderAgg as a subexpression', () => { + const actual = fn({ + field: 'machine.os.keyword', + order: 'asc', + orderBy: '1', + orderAgg: fn({ field: 'name', order: 'asc', orderBy: '1' }), + }); + + expect(actual.value.params).toMatchInlineSnapshot(` + Object { + "exclude": undefined, + "field": "machine.os.keyword", + "include": undefined, + "json": undefined, + "missingBucket": false, + "missingBucketLabel": "Missing", + "order": "asc", + "orderAgg": Object { + "enabled": true, + "id": undefined, + "params": Object { + "exclude": undefined, + "field": "name", + "include": undefined, + "json": undefined, + "missingBucket": false, + "missingBucketLabel": "Missing", + "order": "asc", + "orderAgg": undefined, + "orderBy": "1", + "otherBucket": false, + "otherBucketLabel": "Other", + "size": 5, + }, + "schema": undefined, + "type": "terms", + }, + "orderBy": "1", + "otherBucket": false, + "otherBucketLabel": "Other", + "size": 5, + } + `); + }); + + test('correctly parses json string argument', () => { + const actual = fn({ + field: 'machine.os.keyword', + order: 'asc', + orderBy: '1', + json: '{ "foo": true }', + }); + + expect(actual.value.params.json).toEqual({ foo: true }); + expect(() => { + fn({ + field: 'machine.os.keyword', + order: 'asc', + orderBy: '1', + json: '/// intentionally malformed json ///', + }); + }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + }); + }); +}); diff --git a/src/plugins/data/public/search/aggs/buckets/terms_fn.ts b/src/plugins/data/public/search/aggs/buckets/terms_fn.ts new file mode 100644 index 0000000000000..7980bfabe79fb --- /dev/null +++ b/src/plugins/data/public/search/aggs/buckets/terms_fn.ts @@ -0,0 +1,181 @@ +/* + * 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 { Assign } from '@kbn/utility-types'; +import { ExpressionFunctionDefinition } from '../../../../../expressions/public'; +import { AggExpressionType, AggExpressionFunctionArgs } from '../'; + +const aggName = 'terms'; +const fnName = 'aggTerms'; + +type Input = any; +type AggArgs = AggExpressionFunctionArgs; +// Since the orderAgg param is an agg nested in a subexpression, we need to +// overwrite the param type to expect a value of type AggExpressionType. +type Arguments = AggArgs & + Assign< + AggArgs, + { orderAgg?: AggArgs['orderAgg'] extends undefined ? undefined : AggExpressionType } + >; +type Output = AggExpressionType; +type FunctionDefinition = ExpressionFunctionDefinition; + +export const aggTerms = (): FunctionDefinition => ({ + name: fnName, + help: i18n.translate('data.search.aggs.function.buckets.terms.help', { + defaultMessage: 'Generates a serialized agg config for a terms agg', + }), + type: 'agg_type', + args: { + id: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.terms.id.help', { + defaultMessage: 'ID for this aggregation', + }), + }, + enabled: { + types: ['boolean'], + default: true, + help: i18n.translate('data.search.aggs.buckets.terms.enabled.help', { + defaultMessage: 'Specifies whether this aggregation should be enabled', + }), + }, + schema: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.terms.schema.help', { + defaultMessage: 'Schema to use for this aggregation', + }), + }, + field: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.buckets.terms.field.help', { + defaultMessage: 'Field to use for this aggregation', + }), + }, + order: { + types: ['string'], + required: true, + help: i18n.translate('data.search.aggs.buckets.terms.order.help', { + defaultMessage: 'Order in which to return the results: asc or desc', + }), + }, + orderBy: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.terms.orderBy.help', { + defaultMessage: 'Field to order results by', + }), + }, + orderAgg: { + types: ['agg_type'], + help: i18n.translate('data.search.aggs.buckets.terms.orderAgg.help', { + defaultMessage: 'Agg config to use for ordering results', + }), + }, + size: { + types: ['number'], + default: 5, + help: i18n.translate('data.search.aggs.buckets.terms.size.help', { + defaultMessage: 'Max number of buckets to retrieve', + }), + }, + missingBucket: { + types: ['boolean'], + default: false, + help: i18n.translate('data.search.aggs.buckets.terms.missingBucket.help', { + defaultMessage: 'When set to true, groups together any buckets with missing fields', + }), + }, + missingBucketLabel: { + types: ['string'], + default: i18n.translate('data.search.aggs.buckets.terms.missingBucketLabel', { + defaultMessage: 'Missing', + description: `Default label used in charts when documents are missing a field. + Visible when you create a chart with a terms aggregation and enable "Show missing values"`, + }), + help: i18n.translate('data.search.aggs.buckets.terms.missingBucketLabel.help', { + defaultMessage: 'Default label used in charts when documents are missing a field.', + }), + }, + otherBucket: { + types: ['boolean'], + default: false, + help: i18n.translate('data.search.aggs.buckets.terms.otherBucket.help', { + defaultMessage: 'When set to true, groups together any buckets beyond the allowed size', + }), + }, + otherBucketLabel: { + types: ['string'], + default: i18n.translate('data.search.aggs.buckets.terms.otherBucketLabel', { + defaultMessage: 'Other', + }), + help: i18n.translate('data.search.aggs.buckets.terms.otherBucketLabel.help', { + defaultMessage: 'Default label used in charts for documents in the Other bucket', + }), + }, + exclude: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.terms.exclude.help', { + defaultMessage: 'Specific bucket values to exclude from results', + }), + }, + include: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.terms.include.help', { + defaultMessage: 'Specific bucket values to include in results', + }), + }, + json: { + types: ['string'], + help: i18n.translate('data.search.aggs.buckets.terms.json.help', { + defaultMessage: 'Advanced json to include when the agg is sent to Elasticsearch', + }), + }, + }, + fn: (input, args) => { + const { id, enabled, schema, ...rest } = args; + + let json; + try { + json = args.json ? JSON.parse(args.json) : undefined; + } catch (e) { + throw new Error('Unable to parse json argument string'); + } + + // Need to spread this object to work around TS bug: + // https://github.com/microsoft/TypeScript/issues/15300#issuecomment-436793742 + const orderAgg = args.orderAgg?.value ? { ...args.orderAgg.value } : undefined; + + return { + type: 'agg_type', + value: { + id, + enabled, + schema, + type: aggName, + params: { + ...rest, + orderAgg, + json, + }, + }, + }; + }, +}); diff --git a/src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts b/src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts index 3868d8f1bcd16..947394c97bdcd 100644 --- a/src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts +++ b/src/plugins/data/public/search/aggs/metrics/lib/parent_pipeline_agg_helper.ts @@ -36,14 +36,14 @@ const metricAggFilter = [ '!geo_centroid', ]; -const parentPipelineType = i18n.translate( +export const parentPipelineType = i18n.translate( 'data.search.aggs.metrics.parentPipelineAggregationsSubtypeTitle', { defaultMessage: 'Parent Pipeline Aggregations', } ); -const parentPipelineAggHelper = { +export const parentPipelineAggHelper = { subtype: parentPipelineType, params() { return [ @@ -56,13 +56,9 @@ const parentPipelineAggHelper = { name: 'customMetric', type: 'agg', allowedAggs: metricAggFilter, - makeAgg(termsAgg, state: any) { - state = state || { type: 'count' }; - + makeAgg(termsAgg, state = { type: 'count' }) { const metricAgg = termsAgg.aggConfigs.createAggConfig(state, { addToAggConfigs: false }); - metricAgg.id = termsAgg.id + '-metric'; - return metricAgg; }, modifyAggConfigOnSearchRequestStart: forwardModifyAggConfigOnSearchRequestStart( @@ -89,5 +85,3 @@ const parentPipelineAggHelper = { return subAgg ? subAgg.type.getFormat(subAgg) : new (FieldFormat.from(identity))(); }, }; - -export { parentPipelineAggHelper, parentPipelineType }; diff --git a/src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts b/src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts index c1d05a39285b7..cee7841a8c3b9 100644 --- a/src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts +++ b/src/plugins/data/public/search/aggs/metrics/lib/sibling_pipeline_agg_helper.ts @@ -43,14 +43,14 @@ const metricAggFilter: string[] = [ ]; const bucketAggFilter: string[] = []; -const siblingPipelineType = i18n.translate( +export const siblingPipelineType = i18n.translate( 'data.search.aggs.metrics.siblingPipelineAggregationsSubtypeTitle', { defaultMessage: 'Sibling pipeline aggregations', } ); -const siblingPipelineAggHelper = { +export const siblingPipelineAggHelper = { subtype: siblingPipelineType, params() { return [ @@ -59,11 +59,9 @@ const siblingPipelineAggHelper = { type: 'agg', allowedAggs: bucketAggFilter, default: null, - makeAgg(agg: IMetricAggConfig, state: any) { - state = state || { type: 'date_histogram' }; + makeAgg(agg: IMetricAggConfig, state = { type: 'date_histogram' }) { const orderAgg = agg.aggConfigs.createAggConfig(state, { addToAggConfigs: false }); orderAgg.id = agg.id + '-bucket'; - return orderAgg; }, modifyAggConfigOnSearchRequestStart: forwardModifyAggConfigOnSearchRequestStart( @@ -76,11 +74,9 @@ const siblingPipelineAggHelper = { type: 'agg', allowedAggs: metricAggFilter, default: null, - makeAgg(agg: IMetricAggConfig, state: any) { - state = state || { type: 'count' }; + makeAgg(agg: IMetricAggConfig, state = { type: 'count' }) { const orderAgg = agg.aggConfigs.createAggConfig(state, { addToAggConfigs: false }); orderAgg.id = agg.id + '-metric'; - return orderAgg; }, modifyAggConfigOnSearchRequestStart: forwardModifyAggConfigOnSearchRequestStart( @@ -98,5 +94,3 @@ const siblingPipelineAggHelper = { : new (FieldFormat.from(identity))(); }, }; - -export { siblingPipelineAggHelper, siblingPipelineType }; diff --git a/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts b/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts index d20530a17ca65..7491f15aa3002 100644 --- a/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/percentile_ranks.test.ts @@ -25,21 +25,28 @@ import { import { AggConfigs, IAggConfigs } from '../agg_configs'; import { mockAggTypesRegistry } from '../test_helpers'; import { METRIC_TYPES } from './metric_agg_types'; +import { FieldFormatsStart } from '../../../field_formats'; import { fieldFormatsServiceMock } from '../../../field_formats/mocks'; import { notificationServiceMock } from '../../../../../../../src/core/public/mocks'; import { InternalStartServices } from '../../../types'; describe('AggTypesMetricsPercentileRanksProvider class', function() { let aggConfigs: IAggConfigs; - const aggTypesDependencies: PercentileRanksMetricAggDependencies = { - getInternalStartServices: () => - (({ - fieldFormats: fieldFormatsServiceMock.createStartContract(), - notifications: notificationServiceMock.createStartContract(), - } as unknown) as InternalStartServices), - }; + let fieldFormats: FieldFormatsStart; + let aggTypesDependencies: PercentileRanksMetricAggDependencies; beforeEach(() => { + fieldFormats = fieldFormatsServiceMock.createStartContract(); + fieldFormats.getDefaultInstance = (() => ({ + convert: (t?: string) => t, + })) as any; + aggTypesDependencies = { + getInternalStartServices: () => + (({ + fieldFormats, + notifications: notificationServiceMock.createStartContract(), + } as unknown) as InternalStartServices), + }; const typesRegistry = mockAggTypesRegistry([getPercentileRanksMetricAgg(aggTypesDependencies)]); const field = { name: 'bytes', @@ -61,12 +68,7 @@ describe('AggTypesMetricsPercentileRanksProvider class', function() { type: METRIC_TYPES.PERCENTILE_RANKS, schema: 'metric', params: { - field: { - displayName: 'bytes', - format: { - convert: jest.fn(x => x), - }, - }, + field: 'bytes', customLabel: 'my custom field label', values: [5000, 10000], }, diff --git a/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts b/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts index 0ac1e8417514c..76382c01bcc10 100644 --- a/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts +++ b/src/plugins/data/public/search/aggs/metrics/percentiles.test.ts @@ -61,12 +61,7 @@ describe('AggTypesMetricsPercentilesProvider class', () => { type: METRIC_TYPES.PERCENTILES, schema: 'metric', params: { - field: { - displayName: 'bytes', - format: { - convert: jest.fn(x => `${x}th`), - }, - }, + field: 'bytes', customLabel: 'prince', percents: [95], }, diff --git a/src/plugins/data/public/search/aggs/param_types/agg.ts b/src/plugins/data/public/search/aggs/param_types/agg.ts index e5b53020c3159..e3f8c7c922170 100644 --- a/src/plugins/data/public/search/aggs/param_types/agg.ts +++ b/src/plugins/data/public/search/aggs/param_types/agg.ts @@ -17,13 +17,13 @@ * under the License. */ -import { AggConfig, IAggConfig } from '../agg_config'; +import { AggConfig, IAggConfig, AggConfigSerialized } from '../agg_config'; import { BaseParamType } from './base'; export class AggParamType extends BaseParamType< TAggConfig > { - makeAgg: (agg: TAggConfig, state?: any) => TAggConfig; + makeAgg: (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig; allowedAggs: string[] = []; constructor(config: Record) { @@ -42,17 +42,25 @@ export class AggParamType extends Ba } if (!config.serialize) { this.serialize = (agg: TAggConfig) => { - return agg.toJSON(); + return agg.serialize(); }; } if (!config.deserialize) { - this.deserialize = (state: unknown, agg?: TAggConfig): TAggConfig => { + this.deserialize = (state: AggConfigSerialized, agg?: TAggConfig): TAggConfig => { if (!agg) { throw new Error('aggConfig was not provided to AggParamType deserialize function'); } return this.makeAgg(agg, state); }; } + if (!config.toExpressionAst) { + this.toExpressionAst = (agg: TAggConfig) => { + if (!agg || !agg.toExpressionAst) { + throw new Error('aggConfig was not provided to AggParamType toExpressionAst function'); + } + return agg.toExpressionAst(); + }; + } this.makeAgg = config.makeAgg; this.valueType = AggConfig; diff --git a/src/plugins/data/public/search/aggs/param_types/base.ts b/src/plugins/data/public/search/aggs/param_types/base.ts index 2cbc5866e284d..a6f7e5adea043 100644 --- a/src/plugins/data/public/search/aggs/param_types/base.ts +++ b/src/plugins/data/public/search/aggs/param_types/base.ts @@ -17,6 +17,7 @@ * under the License. */ +import { ExpressionAstFunction } from 'src/plugins/expressions/public'; import { IAggConfigs } from '../agg_configs'; import { IAggConfig } from '../agg_config'; import { FetchOptions } from '../../fetch'; @@ -37,6 +38,7 @@ export class BaseParamType { ) => void; serialize: (value: any, aggConfig?: TAggConfig) => any; deserialize: (value: any, aggConfig?: TAggConfig) => any; + toExpressionAst?: (value: any) => ExpressionAstFunction | undefined; options: any[]; valueType?: any; @@ -77,6 +79,7 @@ export class BaseParamType { this.write = config.write || defaultWrite; this.serialize = config.serialize; this.deserialize = config.deserialize; + this.toExpressionAst = config.toExpressionAst; this.options = config.options; this.modifyAggConfigOnSearchRequestStart = config.modifyAggConfigOnSearchRequestStart || function() {}; diff --git a/src/plugins/data/public/search/aggs/test_helpers/function_wrapper.ts b/src/plugins/data/public/search/aggs/test_helpers/function_wrapper.ts new file mode 100644 index 0000000000000..cb0e37c0296d7 --- /dev/null +++ b/src/plugins/data/public/search/aggs/test_helpers/function_wrapper.ts @@ -0,0 +1,49 @@ +/* + * 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 { mapValues } from 'lodash'; +import { + AnyExpressionFunctionDefinition, + ExpressionFunctionDefinition, + ExecutionContext, +} from '../../../../../../plugins/expressions/public'; + +/** + * Takes a function spec and passes in default args, + * overriding with any provided args. + * + * Similar to the test helper used in Expressions & Canvas, + * however in this case we are ignoring the input & execution + * context, as they are not applicable to the agg type + * expression functions. + */ +export const functionWrapper = (spec: T) => { + const defaultArgs = mapValues(spec.args, argSpec => argSpec.default); + return ( + args: T extends ExpressionFunctionDefinition< + infer Name, + infer Input, + infer Arguments, + infer Output, + infer Context + > + ? Arguments + : never + ) => spec.fn(null, { ...defaultArgs, ...args }, {} as ExecutionContext); +}; diff --git a/src/plugins/data/public/search/aggs/test_helpers/index.ts b/src/plugins/data/public/search/aggs/test_helpers/index.ts index 131f921586144..63f8ae0ce5f58 100644 --- a/src/plugins/data/public/search/aggs/test_helpers/index.ts +++ b/src/plugins/data/public/search/aggs/test_helpers/index.ts @@ -17,5 +17,6 @@ * under the License. */ +export { functionWrapper } from './function_wrapper'; export { mockAggTypesRegistry } from './mock_agg_types_registry'; export { mockDataServices } from './mock_data_services'; diff --git a/src/plugins/data/public/search/aggs/types.ts b/src/plugins/data/public/search/aggs/types.ts index 4b2b1620ad1d3..95a7a45013567 100644 --- a/src/plugins/data/public/search/aggs/types.ts +++ b/src/plugins/data/public/search/aggs/types.ts @@ -19,21 +19,23 @@ import { IndexPattern } from '../../index_patterns'; import { + AggConfig, + AggConfigSerialized, + AggConfigs, + AggParamsTerms, AggType, + aggTypeFieldFilters, AggTypesRegistrySetup, AggTypesRegistryStart, - AggConfig, - AggConfigs, CreateAggConfigParams, FieldParamType, getCalculateAutoTimeExpression, MetricAggType, - aggTypeFieldFilters, parentPipelineAggHelper, siblingPipelineAggHelper, } from './'; -export { IAggConfig } from './agg_config'; +export { IAggConfig, AggConfigSerialized } from './agg_config'; export { CreateAggConfigParams, IAggConfigs } from './agg_configs'; export { IAggType } from './agg_type'; export { AggParam, AggParamOption } from './agg_params'; @@ -70,3 +72,25 @@ export interface SearchAggsStart { ) => InstanceType; types: AggTypesRegistryStart; } + +/** @internal */ +export interface AggExpressionType { + type: 'agg_type'; + value: AggConfigSerialized; +} + +/** @internal */ +export type AggExpressionFunctionArgs< + Name extends keyof AggParamsMapping +> = AggParamsMapping[Name] & Pick; + +/** + * A global list of the param interfaces for each agg type. + * For now this is internal, but eventually we will probably + * want to make it public. + * + * @internal + */ +export interface AggParamsMapping { + terms: AggParamsTerms; +} diff --git a/src/plugins/data/public/search/expressions/utils/serialize_agg_config.ts b/src/plugins/data/public/search/expressions/utils/serialize_agg_config.ts index 4ca976d328c91..78b4935077d10 100644 --- a/src/plugins/data/public/search/expressions/utils/serialize_agg_config.ts +++ b/src/plugins/data/public/search/expressions/utils/serialize_agg_config.ts @@ -27,7 +27,7 @@ export const serializeAggConfig = (aggConfig: IAggConfig): KibanaDatatableColumn return { type: aggConfig.type.name, indexPatternId: aggConfig.getIndexPattern().id, - aggConfigParams: aggConfig.toJSON().params, + aggConfigParams: aggConfig.serialize().params, }; }; diff --git a/src/plugins/data/public/search/search_service.test.ts b/src/plugins/data/public/search/search_service.test.ts index 19308dd387d3a..b1f7925bec4bb 100644 --- a/src/plugins/data/public/search/search_service.test.ts +++ b/src/plugins/data/public/search/search_service.test.ts @@ -18,9 +18,10 @@ */ import { coreMock } from '../../../../core/public/mocks'; +import { CoreSetup } from '../../../../core/public'; +import { expressionsPluginMock } from '../../../../plugins/expressions/public/mocks'; import { SearchService } from './search_service'; -import { CoreSetup } from '../../../../core/public'; describe('Search service', () => { let searchService: SearchService; @@ -35,6 +36,7 @@ describe('Search service', () => { it('exposes proper contract', async () => { const setup = searchService.setup(mockCoreSetup, { packageInfo: { version: '8' }, + expressions: expressionsPluginMock.createSetupContract(), } as any); expect(setup).toHaveProperty('registerSearchStrategyProvider'); }); diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 3f3266b5fe90f..b59524baa9fa7 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -18,6 +18,7 @@ */ import { Plugin, CoreSetup, CoreStart, PackageInfo } from '../../../../core/public'; +import { ExpressionsSetup } from '../../../../plugins/expressions/public'; import { SYNC_SEARCH_STRATEGY, syncSearchStrategyProvider } from './sync_search_strategy'; import { @@ -37,6 +38,7 @@ import { GetInternalStartServicesFn } from '../types'; import { SearchInterceptor } from './search_interceptor'; import { getAggTypes, + getAggTypesFunctions, AggType, AggTypesRegistry, AggConfig, @@ -52,9 +54,10 @@ import { FieldFormatsStart } from '../field_formats'; import { ISearchGeneric } from './i_search'; interface SearchServiceSetupDependencies { + expressions: ExpressionsSetup; + getInternalStartServices: GetInternalStartServicesFn; packageInfo: PackageInfo; query: QuerySetup; - getInternalStartServices: GetInternalStartServicesFn; } interface SearchServiceStartDependencies { @@ -97,22 +100,27 @@ export class SearchService implements Plugin { public setup( core: CoreSetup, - { packageInfo, query, getInternalStartServices }: SearchServiceSetupDependencies + { expressions, packageInfo, query, getInternalStartServices }: SearchServiceSetupDependencies ): ISearchSetup { this.esClient = getEsClient(core.injectedMetadata, core.http, packageInfo); this.registerSearchStrategyProvider(SYNC_SEARCH_STRATEGY, syncSearchStrategyProvider); this.registerSearchStrategyProvider(ES_SEARCH_STRATEGY, esSearchStrategyProvider); const aggTypesSetup = this.aggTypesRegistry.setup(); + + // register each agg type const aggTypes = getAggTypes({ query, uiSettings: core.uiSettings, getInternalStartServices, }); - aggTypes.buckets.forEach(b => aggTypesSetup.registerBucket(b)); aggTypes.metrics.forEach(m => aggTypesSetup.registerMetric(m)); + // register expression functions for each agg type + const aggFunctions = getAggTypesFunctions(); + aggFunctions.forEach(fn => expressions.registerFunction(fn)); + return { aggs: { calculateAutoTimeExpression: getCalculateAutoTimeExpression(core.uiSettings), diff --git a/src/plugins/data/server/field_formats/field_formats_service.ts b/src/plugins/data/server/field_formats/field_formats_service.ts index 0dac64fb5dc1d..3404fe8cee9fd 100644 --- a/src/plugins/data/server/field_formats/field_formats_service.ts +++ b/src/plugins/data/server/field_formats/field_formats_service.ts @@ -17,16 +17,20 @@ * under the License. */ import { has } from 'lodash'; -import { FieldFormatsRegistry, IFieldFormatType, baseFormatters } from '../../common/field_formats'; +import { + FieldFormatsRegistry, + FieldFormatInstanceType, + baseFormatters, +} from '../../common/field_formats'; import { IUiSettingsClient } from '../../../../core/server'; import { DateFormat } from './converters'; export class FieldFormatsService { - private readonly fieldFormatClasses: IFieldFormatType[] = [DateFormat, ...baseFormatters]; + private readonly fieldFormatClasses: FieldFormatInstanceType[] = [DateFormat, ...baseFormatters]; public setup() { return { - register: (customFieldFormat: IFieldFormatType) => + register: (customFieldFormat: FieldFormatInstanceType) => this.fieldFormatClasses.push(customFieldFormat), }; } diff --git a/src/plugins/data/server/index_patterns/capabilities_provider.ts b/src/plugins/data/server/index_patterns/capabilities_provider.ts new file mode 100644 index 0000000000000..d603830e44a53 --- /dev/null +++ b/src/plugins/data/server/index_patterns/capabilities_provider.ts @@ -0,0 +1,24 @@ +/* + * 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 capabilitiesProvider = () => ({ + indexPatterns: { + save: true, + }, +}); diff --git a/src/plugins/data/server/index_patterns/index_patterns_service.ts b/src/plugins/data/server/index_patterns/index_patterns_service.ts index 58e8fbae9f9e2..3e31f8e8a566d 100644 --- a/src/plugins/data/server/index_patterns/index_patterns_service.ts +++ b/src/plugins/data/server/index_patterns/index_patterns_service.ts @@ -20,10 +20,12 @@ import { CoreSetup, Plugin } from 'kibana/server'; import { registerRoutes } from './routes'; import { indexPatternSavedObjectType } from '../saved_objects'; +import { capabilitiesProvider } from './capabilities_provider'; export class IndexPatternsService implements Plugin { public setup(core: CoreSetup) { core.savedObjects.registerType(indexPatternSavedObjectType); + core.capabilities.registerProvider(capabilitiesProvider); registerRoutes(core.http); } diff --git a/src/plugins/data/server/kql_telemetry/kql_telemetry_service.ts b/src/plugins/data/server/kql_telemetry/kql_telemetry_service.ts index e45ad796bd9d4..3dfaa9c6d0a98 100644 --- a/src/plugins/data/server/kql_telemetry/kql_telemetry_service.ts +++ b/src/plugins/data/server/kql_telemetry/kql_telemetry_service.ts @@ -22,14 +22,16 @@ import { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; import { registerKqlTelemetryRoute } from './route'; import { UsageCollectionSetup } from '../../../usage_collection/server'; import { makeKQLUsageCollector } from './usage_collector'; +import { kqlTelemetry } from '../saved_objects'; export class KqlTelemetryService implements Plugin { constructor(private initializerContext: PluginInitializerContext) {} public setup( - { http, getStartServices }: CoreSetup, + { http, getStartServices, savedObjects }: CoreSetup, { usageCollection }: { usageCollection?: UsageCollectionSetup } ) { + savedObjects.registerType(kqlTelemetry); registerKqlTelemetryRoute( http.createRouter(), getStartServices, diff --git a/src/plugins/data/server/saved_objects/index.ts b/src/plugins/data/server/saved_objects/index.ts index 5d980974474de..4326200141179 100644 --- a/src/plugins/data/server/saved_objects/index.ts +++ b/src/plugins/data/server/saved_objects/index.ts @@ -20,3 +20,4 @@ export { searchSavedObjectType } from './search'; export { querySavedObjectType } from './query'; export { indexPatternSavedObjectType } from './index_patterns'; +export { kqlTelemetry } from './kql_telementry'; diff --git a/src/plugins/data/server/saved_objects/kql_telementry.ts b/src/plugins/data/server/saved_objects/kql_telementry.ts new file mode 100644 index 0000000000000..6539d5eacfde2 --- /dev/null +++ b/src/plugins/data/server/saved_objects/kql_telementry.ts @@ -0,0 +1,35 @@ +/* + * 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 { SavedObjectsType } from 'kibana/server'; + +export const kqlTelemetry: SavedObjectsType = { + name: 'kql-telemetry', + namespaceType: 'agnostic', + hidden: false, + mappings: { + properties: { + optInCount: { + type: 'long', + }, + optOutCount: { + type: 'long', + }, + }, + }, +}; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index f8a9a7792c492..9b673de60ca65 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -609,7 +609,7 @@ export class Plugin implements Plugin_2 { // (undocumented) setup(core: CoreSetup, { usageCollection }: DataPluginSetupDependencies): { fieldFormats: { - register: (customFieldFormat: import("../common").IFieldFormatType) => number; + register: (customFieldFormat: import("../common").FieldFormatInstanceType) => number; }; search: ISearchSetup; }; diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index 91d6358d44c18..2d41293f26369 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -1,6 +1,6 @@ { "id": "discover", "version": "kibana", - "server": false, + "server": true, "ui": true } diff --git a/src/plugins/discover/server/capabilities_provider.ts b/src/plugins/discover/server/capabilities_provider.ts new file mode 100644 index 0000000000000..2e03631894aee --- /dev/null +++ b/src/plugins/discover/server/capabilities_provider.ts @@ -0,0 +1,27 @@ +/* + * 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 capabilitiesProvider = () => ({ + discover: { + show: true, + createShortUrl: true, + save: true, + saveQuery: true, + }, +}); diff --git a/src/plugins/discover/server/index.ts b/src/plugins/discover/server/index.ts new file mode 100644 index 0000000000000..15a948c56148e --- /dev/null +++ b/src/plugins/discover/server/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { PluginInitializerContext } from 'kibana/server'; +import { DiscoverServerPlugin } from './plugin'; + +export const plugin = (initContext: PluginInitializerContext) => + new DiscoverServerPlugin(initContext); diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/legacy.ts b/src/plugins/discover/server/plugin.ts similarity index 54% rename from src/legacy/core_plugins/vis_type_tagcloud/public/legacy.ts rename to src/plugins/discover/server/plugin.ts index f70789edc66ba..04502f5fc14e6 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/legacy.ts +++ b/src/plugins/discover/server/plugin.ts @@ -17,18 +17,28 @@ * under the License. */ -import { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart } from 'ui/new_platform'; -import { TagCloudPluginSetupDependencies } from './plugin'; -import { plugin } from '.'; +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from 'kibana/server'; +import { capabilitiesProvider } from './capabilities_provider'; -const plugins: Readonly = { - expressions: npSetup.plugins.expressions, - visualizations: npSetup.plugins.visualizations, - charts: npSetup.plugins.charts, -}; +export class DiscoverServerPlugin implements Plugin { + private readonly logger: Logger; -const pluginInstance = plugin({} as PluginInitializerContext); + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } -export const setup = pluginInstance.setup(npSetup.core, plugins); -export const start = pluginInstance.start(npStart.core, { data: npStart.plugins.data }); + public setup(core: CoreSetup) { + this.logger.debug('discover: Setup'); + + core.capabilities.registerProvider(capabilitiesProvider); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('discover: Started'); + return {}; + } + + public stop() {} +} diff --git a/src/plugins/home/kibana.json b/src/plugins/home/kibana.json index d5b047924f599..1c4b44a946e62 100644 --- a/src/plugins/home/kibana.json +++ b/src/plugins/home/kibana.json @@ -4,5 +4,5 @@ "server": true, "ui": true, "requiredPlugins": ["data", "kibanaLegacy"], - "optionalPlugins": ["usage_collection", "telemetry"] + "optionalPlugins": ["usageCollection", "telemetry"] } diff --git a/src/plugins/home/server/capabilities_provider.ts b/src/plugins/home/server/capabilities_provider.ts new file mode 100644 index 0000000000000..1c662e2301e16 --- /dev/null +++ b/src/plugins/home/server/capabilities_provider.ts @@ -0,0 +1,29 @@ +/* + * 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 capabilitiesProvider = () => ({ + catalogue: { + discover: true, + dashboard: true, + visualize: true, + console: true, + advanced_settings: true, + index_patterns: true, + }, +}); diff --git a/src/plugins/home/server/plugin.ts b/src/plugins/home/server/plugin.ts index 6ea6784cd5adb..1050c19362ae1 100644 --- a/src/plugins/home/server/plugin.ts +++ b/src/plugins/home/server/plugin.ts @@ -26,10 +26,11 @@ import { SampleDataRegistryStart, } from './services'; import { UsageCollectionSetup } from '../../usage_collection/server'; +import { capabilitiesProvider } from './capabilities_provider'; import { sampleDataTelemetry } from './saved_objects'; interface HomeServerPluginSetupDependencies { - usage_collection?: UsageCollectionSetup; + usageCollection?: UsageCollectionSetup; } export class HomeServerPlugin implements Plugin { @@ -38,10 +39,11 @@ export class HomeServerPlugin implements Plugin { diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.tsx b/src/plugins/input_control_vis/public/components/editor/options_tab.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/options_tab.tsx rename to src/plugins/input_control_vis/public/components/editor/options_tab.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx b/src/plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx rename to src/plugins/input_control_vis/public/components/editor/range_control_editor.test.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx b/src/plugins/input_control_vis/public/components/editor/range_control_editor.tsx similarity index 97% rename from src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx rename to src/plugins/input_control_vis/public/components/editor/range_control_editor.tsx index 97850879a2d38..b6b852bcfa707 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/editor/range_control_editor.tsx +++ b/src/plugins/input_control_vis/public/components/editor/range_control_editor.tsx @@ -24,11 +24,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { IndexPatternSelectFormRow } from './index_pattern_select_form_row'; import { FieldSelect } from './field_select'; import { ControlParams, ControlParamsOptions } from '../../editor_utils'; -import { - IIndexPattern, - IFieldType, - IndexPatternSelect, -} from '../../../../../../plugins/data/public'; +import { IIndexPattern, IFieldType, IndexPatternSelect } from '../../../../data/public'; import { InputControlVisDependencies } from '../../plugin'; interface RangeControlEditorProps { diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.tsx.snap b/src/plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.tsx.snap rename to src/plugins/input_control_vis/public/components/vis/__snapshots__/form_row.test.tsx.snap diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap b/src/plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap rename to src/plugins/input_control_vis/public/components/vis/__snapshots__/input_control_vis.test.tsx.snap diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap b/src/plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap rename to src/plugins/input_control_vis/public/components/vis/__snapshots__/list_control.test.tsx.snap diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.tsx.snap b/src/plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.tsx.snap similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.tsx.snap rename to src/plugins/input_control_vis/public/components/vis/__snapshots__/range_control.test.tsx.snap diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/_index.scss b/src/plugins/input_control_vis/public/components/vis/_index.scss similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/_index.scss rename to src/plugins/input_control_vis/public/components/vis/_index.scss diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/_vis.scss b/src/plugins/input_control_vis/public/components/vis/_vis.scss similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/_vis.scss rename to src/plugins/input_control_vis/public/components/vis/_vis.scss diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.test.tsx b/src/plugins/input_control_vis/public/components/vis/form_row.test.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.test.tsx rename to src/plugins/input_control_vis/public/components/vis/form_row.test.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.tsx b/src/plugins/input_control_vis/public/components/vis/form_row.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/form_row.tsx rename to src/plugins/input_control_vis/public/components/vis/form_row.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx b/src/plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx similarity index 99% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx rename to src/plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx index 1712f024f5b7b..b0b674ad7b6ee 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx +++ b/src/plugins/input_control_vis/public/components/vis/input_control_vis.test.tsx @@ -28,8 +28,6 @@ import { InputControlVis } from './input_control_vis'; import { ListControl } from '../../control/list_control_factory'; import { RangeControl } from '../../control/range_control_factory'; -jest.mock('ui/new_platform'); - const mockListControl: ListControl = { id: 'mock-list-control', isEnabled: () => { diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.tsx b/src/plugins/input_control_vis/public/components/vis/input_control_vis.tsx similarity index 97% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.tsx rename to src/plugins/input_control_vis/public/components/vis/input_control_vis.tsx index e2497287f35d0..c0ef99664fdf8 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/input_control_vis.tsx +++ b/src/plugins/input_control_vis/public/components/vis/input_control_vis.tsx @@ -23,8 +23,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { CONTROL_TYPES } from '../../editor_utils'; import { ListControl } from '../../control/list_control_factory'; import { RangeControl } from '../../control/range_control_factory'; -import { ListControl as ListControlComponent } from '../vis/list_control'; -import { RangeControl as RangeControlComponent } from '../vis/range_control'; +import { ListControl as ListControlComponent } from './list_control'; +import { RangeControl as RangeControlComponent } from './range_control'; function isListControl(control: RangeControl | ListControl): control is ListControl { return control.type === CONTROL_TYPES.LIST; diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.test.tsx b/src/plugins/input_control_vis/public/components/vis/list_control.test.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.test.tsx rename to src/plugins/input_control_vis/public/components/vis/list_control.test.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx b/src/plugins/input_control_vis/public/components/vis/list_control.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/list_control.tsx rename to src/plugins/input_control_vis/public/components/vis/list_control.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.test.tsx b/src/plugins/input_control_vis/public/components/vis/range_control.test.tsx similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.test.tsx rename to src/plugins/input_control_vis/public/components/vis/range_control.test.tsx index 639616151a395..ff5d572fa21c4 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.test.tsx +++ b/src/plugins/input_control_vis/public/components/vis/range_control.test.tsx @@ -23,8 +23,6 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers'; import { RangeControl, ceilWithPrecision, floorWithPrecision } from './range_control'; import { RangeControl as RangeControlClass } from '../../control/range_control_factory'; -jest.mock('ui/new_platform'); - const control: RangeControlClass = { id: 'mock-range-control', isEnabled: () => { diff --git a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx b/src/plugins/input_control_vis/public/components/vis/range_control.tsx similarity index 97% rename from src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx rename to src/plugins/input_control_vis/public/components/vis/range_control.tsx index 0cd2a2b331980..f028feaf5f84f 100644 --- a/src/legacy/core_plugins/input_control_vis/public/components/vis/range_control.tsx +++ b/src/plugins/input_control_vis/public/components/vis/range_control.tsx @@ -19,7 +19,7 @@ import _ from 'lodash'; import React, { PureComponent } from 'react'; -import { ValidatedDualRange } from '../../../../../../../src/plugins/kibana_react/public'; +import { ValidatedDualRange } from '../../../../kibana_react/public'; import { FormRow } from './form_row'; import { RangeControl as RangeControlClass } from '../../control/range_control_factory'; diff --git a/src/legacy/core_plugins/input_control_vis/public/control/control.test.ts b/src/plugins/input_control_vis/public/control/control.test.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/control/control.test.ts rename to src/plugins/input_control_vis/public/control/control.test.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/control/control.ts b/src/plugins/input_control_vis/public/control/control.ts similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/control/control.ts rename to src/plugins/input_control_vis/public/control/control.ts index 62e0090e466c0..c57b09a19ebc8 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/control.ts +++ b/src/plugins/input_control_vis/public/control/control.ts @@ -22,7 +22,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { Filter } from '../../../../../plugins/data/public'; +import { Filter } from 'src/plugins/data/public'; import { ControlParams, ControlParamsOptions, CONTROL_TYPES } from '../editor_utils'; import { RangeFilterManager } from './filter_manager/range_filter_manager'; import { PhraseFilterManager } from './filter_manager/phrase_filter_manager'; diff --git a/src/legacy/core_plugins/input_control_vis/public/control/control_factory.ts b/src/plugins/input_control_vis/public/control/control_factory.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/control/control_factory.ts rename to src/plugins/input_control_vis/public/control/control_factory.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts b/src/plugins/input_control_vis/public/control/create_search_source.ts similarity index 97% rename from src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts rename to src/plugins/input_control_vis/public/control/create_search_source.ts index 8f86232f63be7..d6772a7cba5b8 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/create_search_source.ts +++ b/src/plugins/input_control_vis/public/control/create_search_source.ts @@ -23,7 +23,7 @@ import { IndexPattern, TimefilterContract, DataPublicPluginStart, -} from '../../../../../plugins/data/public'; +} from 'src/plugins/data/public'; export function createSearchSource( { create }: DataPublicPluginStart['search']['searchSource'], diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts b/src/plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts similarity index 93% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts rename to src/plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts index 39c9d843e6bce..a9b7550be44ae 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/filter_manager.test.ts @@ -20,12 +20,8 @@ import expect from '@kbn/expect'; import { FilterManager } from './filter_manager'; -import { coreMock } from '../../../../../../core/public/mocks'; -import { - Filter, - IndexPattern, - FilterManager as QueryFilterManager, -} from '../../../../../../plugins/data/public'; +import { coreMock } from '../../../../../core/public/mocks'; +import { Filter, IndexPattern, FilterManager as QueryFilterManager } from '../../../../data/public'; const setupMock = coreMock.createSetup(); diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.ts b/src/plugins/input_control_vis/public/control/filter_manager/filter_manager.ts similarity index 93% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.ts rename to src/plugins/input_control_vis/public/control/filter_manager/filter_manager.ts index 90b88a56950e2..bb806b336c3e0 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/filter_manager.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/filter_manager.ts @@ -19,11 +19,7 @@ import _ from 'lodash'; -import { - FilterManager as QueryFilterManager, - IndexPattern, - Filter, -} from '../../../../../../plugins/data/public'; +import { FilterManager as QueryFilterManager, IndexPattern, Filter } from '../../../../data/public'; export abstract class FilterManager { constructor( diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts b/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts rename to src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts index 5be5d0157541e..6398c10b63a8c 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.test.ts @@ -19,11 +19,7 @@ import expect from '@kbn/expect'; -import { - Filter, - IndexPattern, - FilterManager as QueryFilterManager, -} from '../../../../../../plugins/data/public'; +import { Filter, IndexPattern, FilterManager as QueryFilterManager } from '../../../../data/public'; import { PhraseFilterManager } from './phrase_filter_manager'; describe('PhraseFilterManager', function() { diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts b/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts rename to src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts index 6f4a95b491907..bf167afa69bcf 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/phrase_filter_manager.ts @@ -25,7 +25,7 @@ import { esFilters, IndexPattern, FilterManager as QueryFilterManager, -} from '../../../../../../plugins/data/public'; +} from '../../../../data/public'; export class PhraseFilterManager extends FilterManager { constructor( diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts b/src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts rename to src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts index c776042ea4ba6..6e66b6942e5d3 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.test.ts @@ -25,7 +25,7 @@ import { RangeFilterMeta, IndexPattern, FilterManager as QueryFilterManager, -} from '../../../../../../plugins/data/public'; +} from '../../../../data/public'; describe('RangeFilterManager', function() { const controlId = 'control1'; diff --git a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts b/src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts similarity index 95% rename from src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts rename to src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts index 7a6719e85961b..1a884cf267c41 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts +++ b/src/plugins/input_control_vis/public/control/filter_manager/range_filter_manager.ts @@ -20,12 +20,7 @@ import _ from 'lodash'; import { FilterManager } from './filter_manager'; -import { - esFilters, - RangeFilter, - RangeFilterParams, - IFieldType, -} from '../../../../../../plugins/data/public'; +import { esFilters, RangeFilter, RangeFilterParams, IFieldType } from '../../../../data/public'; interface SliderValue { min?: string | number; diff --git a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.test.ts b/src/plugins/input_control_vis/public/control/list_control_factory.test.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.test.ts rename to src/plugins/input_control_vis/public/control/list_control_factory.test.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts b/src/plugins/input_control_vis/public/control/list_control_factory.ts similarity index 99% rename from src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts rename to src/plugins/input_control_vis/public/control/list_control_factory.ts index 4b2b1d751ffc7..123ef83277e0b 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.ts +++ b/src/plugins/input_control_vis/public/control/list_control_factory.ts @@ -19,17 +19,17 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import { Control, noValuesDisableMsg, noIndexPatternMsg } from './control'; -import { PhraseFilterManager } from './filter_manager/phrase_filter_manager'; -import { createSearchSource } from './create_search_source'; -import { ControlParams } from '../editor_utils'; -import { InputControlVisDependencies } from '../plugin'; import { IFieldType, TimefilterContract, SearchSourceFields, DataPublicPluginStart, -} from '../../../../../plugins/data/public'; +} from 'src/plugins/data/public'; +import { Control, noValuesDisableMsg, noIndexPatternMsg } from './control'; +import { PhraseFilterManager } from './filter_manager/phrase_filter_manager'; +import { createSearchSource } from './create_search_source'; +import { ControlParams } from '../editor_utils'; +import { InputControlVisDependencies } from '../plugin'; function getEscapedQuery(query = '') { // https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html#_standard_operators diff --git a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.test.ts b/src/plugins/input_control_vis/public/control/range_control_factory.test.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.test.ts rename to src/plugins/input_control_vis/public/control/range_control_factory.test.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts b/src/plugins/input_control_vis/public/control/range_control_factory.ts similarity index 97% rename from src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts rename to src/plugins/input_control_vis/public/control/range_control_factory.ts index 5f3c9994ef353..326756ad5ffc6 100644 --- a/src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.ts +++ b/src/plugins/input_control_vis/public/control/range_control_factory.ts @@ -20,16 +20,12 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; +import { IFieldType, TimefilterContract, DataPublicPluginStart } from 'src/plugins/data/public'; import { Control, noValuesDisableMsg, noIndexPatternMsg } from './control'; import { RangeFilterManager } from './filter_manager/range_filter_manager'; import { createSearchSource } from './create_search_source'; import { ControlParams } from '../editor_utils'; import { InputControlVisDependencies } from '../plugin'; -import { - IFieldType, - TimefilterContract, - DataPublicPluginStart, -} from '../.../../../../../../plugins/data/public'; const minMaxAgg = (field?: IFieldType) => { const aggBody: any = {}; diff --git a/src/legacy/core_plugins/input_control_vis/public/editor_utils.ts b/src/plugins/input_control_vis/public/editor_utils.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/editor_utils.ts rename to src/plugins/input_control_vis/public/editor_utils.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/index.scss b/src/plugins/input_control_vis/public/index.scss similarity index 80% rename from src/legacy/core_plugins/input_control_vis/public/index.scss rename to src/plugins/input_control_vis/public/index.scss index ac4692494b923..42fded23d7761 100644 --- a/src/legacy/core_plugins/input_control_vis/public/index.scss +++ b/src/plugins/input_control_vis/public/index.scss @@ -1,5 +1,3 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - // Prefix all styles with "icv" to avoid conflicts. // Examples // icvChart diff --git a/src/legacy/core_plugins/input_control_vis/public/index.ts b/src/plugins/input_control_vis/public/index.ts similarity index 91% rename from src/legacy/core_plugins/input_control_vis/public/index.ts rename to src/plugins/input_control_vis/public/index.ts index e14c2cc4b69b6..8edd3fd9996c3 100644 --- a/src/legacy/core_plugins/input_control_vis/public/index.ts +++ b/src/plugins/input_control_vis/public/index.ts @@ -17,7 +17,9 @@ * under the License. */ -import { PluginInitializerContext } from '../../../../core/public'; +import './index.scss'; + +import { PluginInitializerContext } from '../../../core/public'; import { InputControlVisPlugin as Plugin } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { diff --git a/src/legacy/core_plugins/input_control_vis/public/input_control_fn.test.ts b/src/plugins/input_control_vis/public/input_control_fn.test.ts similarity index 92% rename from src/legacy/core_plugins/input_control_vis/public/input_control_fn.test.ts rename to src/plugins/input_control_vis/public/input_control_fn.test.ts index d654acefd0550..f3ea2d2d6f0ba 100644 --- a/src/legacy/core_plugins/input_control_vis/public/input_control_fn.test.ts +++ b/src/plugins/input_control_vis/public/input_control_fn.test.ts @@ -18,9 +18,7 @@ */ import { createInputControlVisFn } from './input_control_fn'; - -// eslint-disable-next-line -import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils'; +import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils'; describe('interpreter/functions#input_control_vis', () => { const fn = functionWrapper(createInputControlVisFn()); diff --git a/src/legacy/core_plugins/input_control_vis/public/input_control_fn.ts b/src/plugins/input_control_vis/public/input_control_fn.ts similarity index 93% rename from src/legacy/core_plugins/input_control_vis/public/input_control_fn.ts rename to src/plugins/input_control_vis/public/input_control_fn.ts index e779c6d344ab5..59c0e03505bb7 100644 --- a/src/legacy/core_plugins/input_control_vis/public/input_control_fn.ts +++ b/src/plugins/input_control_vis/public/input_control_fn.ts @@ -19,11 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { - ExpressionFunctionDefinition, - KibanaDatatable, - Render, -} from '../../../../plugins/expressions/public'; +import { ExpressionFunctionDefinition, KibanaDatatable, Render } from '../../expressions/public'; interface Arguments { visConfig: string; diff --git a/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts b/src/plugins/input_control_vis/public/input_control_vis_type.ts similarity index 96% rename from src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts rename to src/plugins/input_control_vis/public/input_control_vis_type.ts index badea68eec19f..8114dbf110f8b 100644 --- a/src/legacy/core_plugins/input_control_vis/public/input_control_vis_type.ts +++ b/src/plugins/input_control_vis/public/input_control_vis_type.ts @@ -23,7 +23,7 @@ import { createInputControlVisController } from './vis_controller'; import { getControlsTab } from './components/editor/controls_tab'; import { OptionsTab } from './components/editor/options_tab'; import { InputControlVisDependencies } from './plugin'; -import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/public'; +import { defaultFeedbackMessage } from '../../kibana_utils/public'; export function createInputControlVisTypeDefinition(deps: InputControlVisDependencies) { const InputControlVisController = createInputControlVisController(deps); diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/index.ts b/src/plugins/input_control_vis/public/lineage/index.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/lineage/index.ts rename to src/plugins/input_control_vis/public/lineage/index.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.test.ts b/src/plugins/input_control_vis/public/lineage/lineage_map.test.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.test.ts rename to src/plugins/input_control_vis/public/lineage/lineage_map.test.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.ts b/src/plugins/input_control_vis/public/lineage/lineage_map.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/lineage/lineage_map.ts rename to src/plugins/input_control_vis/public/lineage/lineage_map.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.test.ts b/src/plugins/input_control_vis/public/lineage/parent_candidates.test.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.test.ts rename to src/plugins/input_control_vis/public/lineage/parent_candidates.test.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.ts b/src/plugins/input_control_vis/public/lineage/parent_candidates.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/lineage/parent_candidates.ts rename to src/plugins/input_control_vis/public/lineage/parent_candidates.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/plugin.ts b/src/plugins/input_control_vis/public/plugin.ts similarity index 92% rename from src/legacy/core_plugins/input_control_vis/public/plugin.ts rename to src/plugins/input_control_vis/public/plugin.ts index b743468065430..9fc7df24c2dcc 100644 --- a/src/legacy/core_plugins/input_control_vis/public/plugin.ts +++ b/src/plugins/input_control_vis/public/plugin.ts @@ -19,11 +19,8 @@ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; -import { - VisualizationsSetup, - VisualizationsStart, -} from '../../../../plugins/visualizations/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; +import { VisualizationsSetup, VisualizationsStart } from '../../visualizations/public'; import { createInputControlVisFn } from './input_control_fn'; import { createInputControlVisTypeDefinition } from './input_control_vis_type'; diff --git a/src/legacy/core_plugins/input_control_vis/public/test_utils/get_deps_mock.tsx b/src/plugins/input_control_vis/public/test_utils/get_deps_mock.tsx similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/test_utils/get_deps_mock.tsx rename to src/plugins/input_control_vis/public/test_utils/get_deps_mock.tsx diff --git a/src/legacy/core_plugins/input_control_vis/public/test_utils/get_index_pattern_mock.ts b/src/plugins/input_control_vis/public/test_utils/get_index_pattern_mock.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/test_utils/get_index_pattern_mock.ts rename to src/plugins/input_control_vis/public/test_utils/get_index_pattern_mock.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/test_utils/get_index_patterns_mock.ts b/src/plugins/input_control_vis/public/test_utils/get_index_patterns_mock.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/test_utils/get_index_patterns_mock.ts rename to src/plugins/input_control_vis/public/test_utils/get_index_patterns_mock.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/test_utils/get_search_service_mock.ts b/src/plugins/input_control_vis/public/test_utils/get_search_service_mock.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/test_utils/get_search_service_mock.ts rename to src/plugins/input_control_vis/public/test_utils/get_search_service_mock.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/test_utils/index.ts b/src/plugins/input_control_vis/public/test_utils/index.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/test_utils/index.ts rename to src/plugins/input_control_vis/public/test_utils/index.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/test_utils/update_component.ts b/src/plugins/input_control_vis/public/test_utils/update_component.ts similarity index 100% rename from src/legacy/core_plugins/input_control_vis/public/test_utils/update_component.ts rename to src/plugins/input_control_vis/public/test_utils/update_component.ts diff --git a/src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx b/src/plugins/input_control_vis/public/vis_controller.tsx similarity index 98% rename from src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx rename to src/plugins/input_control_vis/public/vis_controller.tsx index 818221353afbc..97506556d7e0a 100644 --- a/src/legacy/core_plugins/input_control_vis/public/vis_controller.tsx +++ b/src/plugins/input_control_vis/public/vis_controller.tsx @@ -28,8 +28,8 @@ import { ControlParams } from './editor_utils'; import { RangeControl } from './control/range_control_factory'; import { ListControl } from './control/list_control_factory'; import { InputControlVisDependencies } from './plugin'; -import { FilterManager, Filter } from '../../../../plugins/data/public'; -import { VisParams, Vis } from '../../../../plugins/visualizations/public'; +import { FilterManager, Filter } from '../../data/public'; +import { VisParams, Vis } from '../../visualizations/public'; export const createInputControlVisController = (deps: InputControlVisDependencies) => { return class InputControlVisController { diff --git a/src/plugins/input_control_vis/server/index.ts b/src/plugins/input_control_vis/server/index.ts new file mode 100644 index 0000000000000..043657ba98a3c --- /dev/null +++ b/src/plugins/input_control_vis/server/index.ts @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginConfigDescriptor } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; + +export const config: PluginConfigDescriptor = { + schema: schema.object({ enabled: schema.boolean({ defaultValue: true }) }), +}; + +export const plugin = () => ({ + setup() {}, + start() {}, +}); diff --git a/src/plugins/kibana_utils/public/history/ensure_default_index_pattern.tsx b/src/plugins/kibana_utils/public/history/ensure_default_index_pattern.tsx deleted file mode 100644 index 7992f650cb372..0000000000000 --- a/src/plugins/kibana_utils/public/history/ensure_default_index_pattern.tsx +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { contains } from 'lodash'; -import React from 'react'; -import { History } from 'history'; -import { i18n } from '@kbn/i18n'; -import { EuiCallOut } from '@elastic/eui'; -import { CoreStart } from 'kibana/public'; -import { DataPublicPluginStart } from 'src/plugins/data/public'; -import { toMountPoint } from '../../../kibana_react/public'; - -let bannerId: string; -let timeoutId: NodeJS.Timeout | undefined; - -/** - * Checks whether a default index pattern is set and exists and defines - * one otherwise. - * - * If there are no index patterns, redirect to management page and show - * banner. In this case the promise returned from this function will never - * resolve to wait for the URL change to happen. - */ -export async function ensureDefaultIndexPattern( - core: CoreStart, - data: DataPublicPluginStart, - history: History -) { - const patterns = await data.indexPatterns.getIds(); - let defaultId = core.uiSettings.get('defaultIndex'); - let defined = !!defaultId; - const exists = contains(patterns, defaultId); - - if (defined && !exists) { - core.uiSettings.remove('defaultIndex'); - defaultId = defined = false; - } - - if (defined) { - return; - } - - // If there is any index pattern created, set the first as default - if (patterns.length >= 1) { - defaultId = patterns[0]; - core.uiSettings.set('defaultIndex', defaultId); - } else { - const canManageIndexPatterns = core.application.capabilities.management.kibana.index_patterns; - const redirectTarget = canManageIndexPatterns ? '/management/kibana/index_pattern' : '/home'; - - if (timeoutId) { - clearTimeout(timeoutId); - } - - // Avoid being hostile to new users who don't have an index pattern setup yet - // give them a friendly info message instead of a terse error message - bannerId = core.overlays.banners.replace( - bannerId, - toMountPoint( - - ) - ); - - // hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around - timeoutId = setTimeout(() => { - core.overlays.banners.remove(bannerId); - timeoutId = undefined; - }, 15000); - - history.push(redirectTarget); - - // return never-resolving promise to stop resolving and wait for the url change - return new Promise(() => {}); - } -} diff --git a/src/plugins/kibana_utils/public/history/index.ts b/src/plugins/kibana_utils/public/history/index.ts index 1a73bbb6b04a1..bb13ea09f928a 100644 --- a/src/plugins/kibana_utils/public/history/index.ts +++ b/src/plugins/kibana_utils/public/history/index.ts @@ -19,4 +19,3 @@ export { removeQueryParam } from './remove_query_param'; export { redirectWhenMissing } from './redirect_when_missing'; -export { ensureDefaultIndexPattern } from './ensure_default_index_pattern'; diff --git a/src/plugins/kibana_utils/public/index.ts b/src/plugins/kibana_utils/public/index.ts index 2f139050e994a..c634322b23d0b 100644 --- a/src/plugins/kibana_utils/public/index.ts +++ b/src/plugins/kibana_utils/public/index.ts @@ -74,7 +74,7 @@ export { StartSyncStateFnType, StopSyncStateFnType, } from './state_sync'; -export { removeQueryParam, redirectWhenMissing, ensureDefaultIndexPattern } from './history'; +export { removeQueryParam, redirectWhenMissing } from './history'; export { applyDiff } from './state_management/utils/diff_object'; /** dummy plugin, we just want kibanaUtils to have its own bundle */ diff --git a/src/plugins/management/kibana.json b/src/plugins/management/kibana.json index 1789b7cd5ddba..cc411a8c6a25c 100644 --- a/src/plugins/management/kibana.json +++ b/src/plugins/management/kibana.json @@ -1,7 +1,7 @@ { "id": "management", "version": "kibana", - "server": false, + "server": true, "ui": true, "requiredPlugins": ["kibanaLegacy", "home"] } diff --git a/src/plugins/management/server/capabilities_provider.ts b/src/plugins/management/server/capabilities_provider.ts new file mode 100644 index 0000000000000..9a69749c8233b --- /dev/null +++ b/src/plugins/management/server/capabilities_provider.ts @@ -0,0 +1,32 @@ +/* + * 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 capabilitiesProvider = () => ({ + management: { + /* + * Management settings correspond to management section/link ids, and should not be changed + * without also updating those definitions. + */ + kibana: { + settings: true, + index_patterns: true, + objects: true, + }, + }, +}); diff --git a/src/plugins/management/server/index.ts b/src/plugins/management/server/index.ts new file mode 100644 index 0000000000000..afc7adf8832e1 --- /dev/null +++ b/src/plugins/management/server/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { PluginInitializerContext } from 'kibana/server'; +import { ManagementServerPlugin } from './plugin'; + +export const plugin = (initContext: PluginInitializerContext) => + new ManagementServerPlugin(initContext); diff --git a/src/plugins/management/server/plugin.ts b/src/plugins/management/server/plugin.ts new file mode 100644 index 0000000000000..f8fda7da9b95a --- /dev/null +++ b/src/plugins/management/server/plugin.ts @@ -0,0 +1,44 @@ +/* + * 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 { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from 'kibana/server'; +import { capabilitiesProvider } from './capabilities_provider'; + +export class ManagementServerPlugin implements Plugin { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + this.logger.debug('management: Setup'); + + core.capabilities.registerProvider(capabilitiesProvider); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('management: Started'); + return {}; + } + + public stop() {} +} diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap index 563ce87b82ae5..fb3d6efa63826 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap @@ -238,6 +238,7 @@ exports[`SavedObjectsTable import should show the flyout 1`] = ` indexPatterns={ Object { "clearCache": [MockFunction], + "ensureDefaultIndexPattern": [MockFunction], "get": [MockFunction], "make": [Function], } diff --git a/src/plugins/saved_objects_management/server/capabilities_provider.ts b/src/plugins/saved_objects_management/server/capabilities_provider.ts new file mode 100644 index 0000000000000..bd621de4a6195 --- /dev/null +++ b/src/plugins/saved_objects_management/server/capabilities_provider.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const capabilitiesProvider = () => ({ + savedObjectsManagement: { + delete: true, + edit: true, + read: true, + }, +}); diff --git a/src/plugins/saved_objects_management/server/plugin.ts b/src/plugins/saved_objects_management/server/plugin.ts index b72644b500967..4e39b08f5c62c 100644 --- a/src/plugins/saved_objects_management/server/plugin.ts +++ b/src/plugins/saved_objects_management/server/plugin.ts @@ -23,6 +23,7 @@ import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from ' import { SavedObjectsManagementPluginSetup, SavedObjectsManagementPluginStart } from './types'; import { SavedObjectsManagement } from './services'; import { registerRoutes } from './routes'; +import { capabilitiesProvider } from './capabilities_provider'; export class SavedObjectsManagementPlugin implements Plugin { @@ -33,13 +34,15 @@ export class SavedObjectsManagementPlugin this.logger = this.context.logger.get(); } - public async setup({ http }: CoreSetup) { + public async setup({ http, capabilities }: CoreSetup) { this.logger.debug('Setting up SavedObjectsManagement plugin'); registerRoutes({ http, managementServicePromise: this.managementService$.pipe(first()).toPromise(), }); + capabilities.registerProvider(capabilitiesProvider); + return {}; } diff --git a/src/plugins/share/server/plugin.ts b/src/plugins/share/server/plugin.ts index bcb681a50652a..0d9f183d13404 100644 --- a/src/plugins/share/server/plugin.ts +++ b/src/plugins/share/server/plugin.ts @@ -19,12 +19,14 @@ import { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; import { createRoutes } from './routes/create_routes'; +import { url } from './saved_objects'; export class SharePlugin implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} public async setup(core: CoreSetup) { createRoutes(core, this.initializerContext.logger.get()); + core.savedObjects.registerType(url); } public start() { diff --git a/src/plugins/share/server/saved_objects/index.ts b/src/plugins/share/server/saved_objects/index.ts new file mode 100644 index 0000000000000..956f031d2f1a5 --- /dev/null +++ b/src/plugins/share/server/saved_objects/index.ts @@ -0,0 +1,19 @@ +/* + * 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 { url } from './url'; diff --git a/src/plugins/share/server/saved_objects/url.ts b/src/plugins/share/server/saved_objects/url.ts new file mode 100644 index 0000000000000..c76c21993a13f --- /dev/null +++ b/src/plugins/share/server/saved_objects/url.ts @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { SavedObjectsType } from 'kibana/server'; + +export const url: SavedObjectsType = { + name: 'url', + namespaceType: 'single', + hidden: false, + management: { + icon: 'link', + defaultSearchField: 'url', + importableAndExportable: true, + getTitle(obj) { + return `/goto/${encodeURIComponent(obj.id)}`; + }, + }, + mappings: { + properties: { + accessCount: { + type: 'long', + }, + accessDate: { + type: 'date', + }, + createDate: { + type: 'date', + }, + url: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + }, + }, + }, + }, + }, +}; diff --git a/src/plugins/telemetry/public/mocks.ts b/src/plugins/telemetry/public/mocks.ts index 4e0f02242961a..fd88f520205f5 100644 --- a/src/plugins/telemetry/public/mocks.ts +++ b/src/plugins/telemetry/public/mocks.ts @@ -25,11 +25,20 @@ import { httpServiceMock } from '../../../core/public/http/http_service.mock'; import { notificationServiceMock } from '../../../core/public/notifications/notifications_service.mock'; import { TelemetryService } from './services/telemetry_service'; import { TelemetryNotifications } from './services/telemetry_notifications/telemetry_notifications'; -import { TelemetryPluginStart } from './plugin'; +import { TelemetryPluginStart, TelemetryPluginConfig } from './plugin'; + +// The following is to be able to access private methods +/* eslint-disable dot-notation */ + +export interface TelemetryServiceMockOptions { + reportOptInStatusChange?: boolean; + config?: Partial; +} export function mockTelemetryService({ reportOptInStatusChange, -}: { reportOptInStatusChange?: boolean } = {}) { + config: configOverride = {}, +}: TelemetryServiceMockOptions = {}) { const config = { enabled: true, url: 'http://localhost', @@ -39,14 +48,22 @@ export function mockTelemetryService({ banner: true, allowChangingOptInStatus: true, telemetryNotifyUserAboutOptInDefault: true, + ...configOverride, }; - return new TelemetryService({ + const telemetryService = new TelemetryService({ config, http: httpServiceMock.createStartContract(), notifications: notificationServiceMock.createStartContract(), reportOptInStatusChange, }); + + const originalReportOptInStatus = telemetryService['reportOptInStatus']; + telemetryService['reportOptInStatus'] = jest.fn().mockImplementation(optInPayload => { + return originalReportOptInStatus(optInPayload); // Actually calling the original method + }); + + return telemetryService; } export function mockTelemetryNotifications({ diff --git a/src/plugins/telemetry/public/services/telemetry_service.test.ts b/src/plugins/telemetry/public/services/telemetry_service.test.ts index 0a49b0ae3084e..16faa0cfc7536 100644 --- a/src/plugins/telemetry/public/services/telemetry_service.test.ts +++ b/src/plugins/telemetry/public/services/telemetry_service.test.ts @@ -67,17 +67,31 @@ describe('TelemetryService', () => { }); describe('setOptIn', () => { + it('does not call the api if canChangeOptInStatus==false', async () => { + const telemetryService = mockTelemetryService({ + reportOptInStatusChange: false, + config: { allowChangingOptInStatus: false }, + }); + expect(await telemetryService.setOptIn(true)).toBe(false); + + expect(telemetryService['http'].post).toBeCalledTimes(0); + }); + it('calls api if canChangeOptInStatus', async () => { - const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); - telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + const telemetryService = mockTelemetryService({ + reportOptInStatusChange: false, + config: { allowChangingOptInStatus: true }, + }); await telemetryService.setOptIn(true); expect(telemetryService['http'].post).toBeCalledTimes(1); }); it('sends enabled true if optedIn: true', async () => { - const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); - telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + const telemetryService = mockTelemetryService({ + reportOptInStatusChange: false, + config: { allowChangingOptInStatus: true }, + }); const optedIn = true; await telemetryService.setOptIn(optedIn); @@ -87,8 +101,10 @@ describe('TelemetryService', () => { }); it('sends enabled false if optedIn: false', async () => { - const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); - telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + const telemetryService = mockTelemetryService({ + reportOptInStatusChange: false, + config: { allowChangingOptInStatus: true }, + }); const optedIn = false; await telemetryService.setOptIn(optedIn); @@ -98,9 +114,10 @@ describe('TelemetryService', () => { }); it('does not call reportOptInStatus if reportOptInStatusChange is false', async () => { - const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); - telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); - telemetryService['reportOptInStatus'] = jest.fn(); + const telemetryService = mockTelemetryService({ + reportOptInStatusChange: false, + config: { allowChangingOptInStatus: true }, + }); await telemetryService.setOptIn(true); expect(telemetryService['reportOptInStatus']).toBeCalledTimes(0); @@ -108,9 +125,10 @@ describe('TelemetryService', () => { }); it('calls reportOptInStatus if reportOptInStatusChange is true', async () => { - const telemetryService = mockTelemetryService({ reportOptInStatusChange: true }); - telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); - telemetryService['reportOptInStatus'] = jest.fn(); + const telemetryService = mockTelemetryService({ + reportOptInStatusChange: true, + config: { allowChangingOptInStatus: true }, + }); await telemetryService.setOptIn(true); expect(telemetryService['reportOptInStatus']).toBeCalledTimes(1); @@ -118,9 +136,10 @@ describe('TelemetryService', () => { }); it('adds an error toast on api error', async () => { - const telemetryService = mockTelemetryService({ reportOptInStatusChange: false }); - telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); - telemetryService['reportOptInStatus'] = jest.fn(); + const telemetryService = mockTelemetryService({ + reportOptInStatusChange: false, + config: { allowChangingOptInStatus: true }, + }); telemetryService['http'].post = jest.fn().mockImplementation((url: string) => { if (url === '/api/telemetry/v2/optIn') { throw Error('failed to update opt in.'); @@ -133,9 +152,13 @@ describe('TelemetryService', () => { expect(telemetryService['notifications'].toasts.addError).toBeCalledTimes(1); }); + // This one should not happen because the entire method is fully caught but hey! :) it('adds an error toast on reportOptInStatus error', async () => { - const telemetryService = mockTelemetryService({ reportOptInStatusChange: true }); - telemetryService.getCanChangeOptInStatus = jest.fn().mockReturnValue(true); + const telemetryService = mockTelemetryService({ + reportOptInStatusChange: true, + config: { allowChangingOptInStatus: true }, + }); + telemetryService['reportOptInStatus'] = jest.fn().mockImplementation(() => { throw Error('failed to report OptIn Status.'); }); @@ -146,4 +169,50 @@ describe('TelemetryService', () => { expect(telemetryService['notifications'].toasts.addError).toBeCalledTimes(1); }); }); + + describe('getTelemetryUrl', () => { + it('should return the config.url parameter', async () => { + const url = 'http://test.com'; + const telemetryService = mockTelemetryService({ + config: { url }, + }); + + expect(telemetryService.getTelemetryUrl()).toBe(url); + }); + }); + + describe('setUserHasSeenNotice', () => { + it('should hit the API and change the config', async () => { + const telemetryService = mockTelemetryService({ + config: { telemetryNotifyUserAboutOptInDefault: undefined }, + }); + + expect(telemetryService.userHasSeenOptedInNotice).toBe(undefined); + expect(telemetryService.getUserHasSeenOptedInNotice()).toBe(false); + await telemetryService.setUserHasSeenNotice(); + expect(telemetryService['http'].put).toBeCalledTimes(1); + expect(telemetryService.userHasSeenOptedInNotice).toBe(true); + expect(telemetryService.getUserHasSeenOptedInNotice()).toBe(true); + }); + + it('should show a toast notification if the request fail', async () => { + const telemetryService = mockTelemetryService({ + config: { telemetryNotifyUserAboutOptInDefault: undefined }, + }); + + telemetryService['http'].put = jest.fn().mockImplementation((url: string) => { + if (url === '/api/telemetry/v2/userHasSeenNotice') { + throw Error('failed to update opt in.'); + } + }); + + expect(telemetryService.userHasSeenOptedInNotice).toBe(undefined); + expect(telemetryService.getUserHasSeenOptedInNotice()).toBe(false); + await telemetryService.setUserHasSeenNotice(); + expect(telemetryService['http'].put).toBeCalledTimes(1); + expect(telemetryService['notifications'].toasts.addError).toBeCalledTimes(1); + expect(telemetryService.userHasSeenOptedInNotice).toBe(false); + expect(telemetryService.getUserHasSeenOptedInNotice()).toBe(false); + }); + }); }); diff --git a/src/plugins/telemetry/public/services/telemetry_service.ts b/src/plugins/telemetry/public/services/telemetry_service.ts index cac4e3fdf5f50..6d87a74197fe5 100644 --- a/src/plugins/telemetry/public/services/telemetry_service.ts +++ b/src/plugins/telemetry/public/services/telemetry_service.ts @@ -122,11 +122,15 @@ export class TelemetryService { } try { - await this.http.post('/api/telemetry/v2/optIn', { + // Report the option to the Kibana server to store the settings. + // It returns the encrypted update to send to the telemetry cluster [{cluster_uuid, opt_in_status}] + const optInPayload = await this.http.post('/api/telemetry/v2/optIn', { body: JSON.stringify({ enabled: optedIn }), }); if (this.reportOptInStatusChange) { - await this.reportOptInStatus(optedIn); + // Use the response to report about the change to the remote telemetry cluster. + // If it's opt-out, this will be the last communication to the remote service. + await this.reportOptInStatus(optInPayload); } this.isOptedIn = optedIn; } catch (err) { @@ -162,7 +166,11 @@ export class TelemetryService { } }; - private reportOptInStatus = async (OptInStatus: boolean): Promise => { + /** + * Pushes the encrypted payload [{cluster_uuid, opt_in_status}] to the remote telemetry service + * @param optInPayload [{cluster_uuid, opt_in_status}] encrypted by the server into an array of strings + */ + private reportOptInStatus = async (optInPayload: string[]): Promise => { const telemetryOptInStatusUrl = this.getOptInStatusUrl(); try { @@ -171,7 +179,7 @@ export class TelemetryService { headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ enabled: OptInStatus }), + body: JSON.stringify(optInPayload), }); } catch (err) { // Sending the ping is best-effort. Telemetry tries to send the ping once and discards it immediately if sending fails. diff --git a/src/plugins/telemetry/server/routes/telemetry_opt_in.ts b/src/plugins/telemetry/server/routes/telemetry_opt_in.ts index e65ade0ab8aaa..4ed5dbf251275 100644 --- a/src/plugins/telemetry/server/routes/telemetry_opt_in.ts +++ b/src/plugins/telemetry/server/routes/telemetry_opt_in.ts @@ -22,7 +22,10 @@ import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; import { schema } from '@kbn/config-schema'; import { IRouter } from 'kibana/server'; -import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; +import { + StatsGetterConfig, + TelemetryCollectionManagerPluginSetup, +} from 'src/plugins/telemetry_collection_manager/server'; import { getTelemetryAllowChangingOptInStatus } from '../../common/telemetry_config'; import { sendTelemetryOptInStatus } from './telemetry_opt_in_stats'; @@ -79,23 +82,30 @@ export function registerTelemetryOptInRoutes({ }); } + const statsGetterConfig: StatsGetterConfig = { + start: moment() + .subtract(20, 'minutes') + .toISOString(), + end: moment().toISOString(), + unencrypted: false, + }; + + const optInStatus = await telemetryCollectionManager.getOptInStats( + newOptInStatus, + statsGetterConfig + ); + if (config.sendUsageFrom === 'server') { const optInStatusUrl = config.optInStatusUrl; await sendTelemetryOptInStatus( telemetryCollectionManager, { optInStatusUrl, newOptInStatus }, - { - start: moment() - .subtract(20, 'minutes') - .toISOString(), - end: moment().toISOString(), - unencrypted: false, - } + statsGetterConfig ); } await updateTelemetrySavedObject(context.core.savedObjects.client, attributes); - return res.ok({}); + return res.ok({ body: optInStatus }); } ); } diff --git a/src/plugins/vis_type_tagcloud/config.ts b/src/plugins/vis_type_tagcloud/config.ts new file mode 100644 index 0000000000000..6749bd83de39f --- /dev/null +++ b/src/plugins/vis_type_tagcloud/config.ts @@ -0,0 +1,26 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/vis_type_tagcloud/kibana.json b/src/plugins/vis_type_tagcloud/kibana.json new file mode 100644 index 0000000000000..dbc9a1b9ef692 --- /dev/null +++ b/src/plugins/vis_type_tagcloud/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "visTypeTagcloud", + "version": "kibana", + "ui": true, + "server": true, + "requiredPlugins": ["data", "expressions", "visualizations", "charts"] +} diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap b/src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap rename to src/plugins/vis_type_tagcloud/public/__snapshots__/tag_cloud_fn.test.ts.snap diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/_tag_cloud.scss b/src/plugins/vis_type_tagcloud/public/_tag_cloud.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/_tag_cloud.scss rename to src/plugins/vis_type_tagcloud/public/_tag_cloud.scss diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/feedback_message.js b/src/plugins/vis_type_tagcloud/public/components/feedback_message.js similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/feedback_message.js rename to src/plugins/vis_type_tagcloud/public/components/feedback_message.js diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/label.js b/src/plugins/vis_type_tagcloud/public/components/label.js similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/label.js rename to src/plugins/vis_type_tagcloud/public/components/label.js diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud.js similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud.js rename to src/plugins/vis_type_tagcloud/public/components/tag_cloud.js diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx similarity index 91% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx rename to src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx index 7a64549edd747..d33576e4e5529 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx +++ b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_options.tsx @@ -20,9 +20,9 @@ import React from 'react'; import { EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { ValidatedDualRange } from '../../../../../../src/plugins/kibana_react/public'; -import { SelectOption, SwitchOption } from '../../../../../plugins/charts/public'; +import { VisOptionsProps } from '../../../vis_default_editor/public'; +import { ValidatedDualRange } from '../../../kibana_react/public'; +import { SelectOption, SwitchOption } from '../../../charts/public'; import { TagCloudVisParams } from '../types'; function TagCloudOptions({ stateParams, setValue, vis }: VisOptionsProps) { diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js b/src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js rename to src/plugins/vis_type_tagcloud/public/components/tag_cloud_visualization.js diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/index.scss b/src/plugins/vis_type_tagcloud/public/index.scss similarity index 76% rename from src/legacy/core_plugins/vis_type_tagcloud/public/index.scss rename to src/plugins/vis_type_tagcloud/public/index.scss index a4fcf8418ce1c..e6893b9a2474c 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/index.scss +++ b/src/plugins/vis_type_tagcloud/public/index.scss @@ -1,5 +1,3 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - // Prefix all styles with "tgc" to avoid conflicts. // Examples // tgcChart diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/index.ts b/src/plugins/vis_type_tagcloud/public/index.ts similarity index 93% rename from src/legacy/core_plugins/vis_type_tagcloud/public/index.ts rename to src/plugins/vis_type_tagcloud/public/index.ts index 90e6305262caa..ff27d96b710fa 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/index.ts +++ b/src/plugins/vis_type_tagcloud/public/index.ts @@ -17,7 +17,7 @@ * under the License. */ -import { PluginInitializerContext } from '../../../../core/public'; +import { PluginInitializerContext } from 'kibana/public'; import { TagCloudPlugin as Plugin } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts b/src/plugins/vis_type_tagcloud/public/plugin.ts similarity index 81% rename from src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts rename to src/plugins/vis_type_tagcloud/public/plugin.ts index 1061271aa315b..6978186058b1d 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/plugin.ts +++ b/src/plugins/vis_type_tagcloud/public/plugin.ts @@ -17,15 +17,18 @@ * under the License. */ -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../../core/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; -import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; -import { ChartsPluginSetup } from '../../../../plugins/charts/public'; +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; +import { VisualizationsSetup } from '../../visualizations/public'; +import { ChartsPluginSetup } from '../../charts/public'; import { createTagCloudFn } from './tag_cloud_fn'; import { createTagCloudVisTypeDefinition } from './tag_cloud_type'; -import { DataPublicPluginStart } from '../../../../plugins/data/public'; +import { DataPublicPluginStart } from '../../data/public'; import { setFormatService } from './services'; +import { ConfigSchema } from '../config'; + +import './index.scss'; /** @internal */ export interface TagCloudPluginSetupDependencies { @@ -46,9 +49,9 @@ export interface TagCloudVisPluginStartDependencies { /** @internal */ export class TagCloudPlugin implements Plugin { - initializerContext: PluginInitializerContext; + initializerContext: PluginInitializerContext; - constructor(initializerContext: PluginInitializerContext) { + constructor(initializerContext: PluginInitializerContext) { this.initializerContext = initializerContext; } diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/services.ts b/src/plugins/vis_type_tagcloud/public/services.ts similarity index 82% rename from src/legacy/core_plugins/vis_type_tagcloud/public/services.ts rename to src/plugins/vis_type_tagcloud/public/services.ts index 272bed3e91a08..f6002afc66493 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/services.ts +++ b/src/plugins/vis_type_tagcloud/public/services.ts @@ -17,11 +17,9 @@ * under the License. */ -import { createGetterSetter } from '../../../../plugins/kibana_utils/public'; -import { DataPublicPluginStart } from '../../../../plugins/data/public'; +import { createGetterSetter } from '../../kibana_utils/public'; +import { DataPublicPluginStart } from '../../data/public'; export const [getFormatService, setFormatService] = createGetterSetter< DataPublicPluginStart['fieldFormats'] >('data.fieldFormats'); - -export { npStart } from 'ui/new_platform'; diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts similarity index 91% rename from src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts rename to src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts index 65c54766133d1..eb16b0855a138 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts +++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.test.ts @@ -19,8 +19,7 @@ import { createTagCloudFn } from './tag_cloud_fn'; -// eslint-disable-next-line -import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils'; +import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils'; describe('interpreter/functions#tagcloud', () => { const fn = functionWrapper(createTagCloudFn()); diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts similarity index 96% rename from src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.ts rename to src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts index 31c7fd118cefd..05cf05ab00b75 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_fn.ts +++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_fn.ts @@ -19,11 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { - ExpressionFunctionDefinition, - KibanaDatatable, - Render, -} from '../../../../plugins/expressions/public'; +import { ExpressionFunctionDefinition, KibanaDatatable, Render } from '../../expressions/public'; import { TagCloudVisParams } from './types'; const name = 'tagcloud'; diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts similarity index 98% rename from src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts rename to src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts index b7dfa62c93fb9..5a8cc3004a315 100644 --- a/src/legacy/core_plugins/vis_type_tagcloud/public/tag_cloud_type.ts +++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_type.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { Schemas } from '../../../../plugins/vis_default_editor/public'; +import { Schemas } from '../../vis_default_editor/public'; import { TagCloudOptions } from './components/tag_cloud_options'; diff --git a/src/legacy/core_plugins/vis_type_tagcloud/public/types.ts b/src/plugins/vis_type_tagcloud/public/types.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_tagcloud/public/types.ts rename to src/plugins/vis_type_tagcloud/public/types.ts diff --git a/src/plugins/vis_type_tagcloud/server/index.ts b/src/plugins/vis_type_tagcloud/server/index.ts new file mode 100644 index 0000000000000..bd9656b29c524 --- /dev/null +++ b/src/plugins/vis_type_tagcloud/server/index.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginConfigDescriptor } from 'kibana/server'; + +import { configSchema, ConfigSchema } from '../config'; + +export const config: PluginConfigDescriptor = { + schema: configSchema, + deprecations: ({ renameFromRoot }) => [ + renameFromRoot('tagcloud.enabled', 'vis_type_tagcloud.enabled'), + ], +}; + +export const plugin = () => ({ + setup() {}, + start() {}, +}); diff --git a/src/plugins/vis_type_vega/kibana.json b/src/plugins/vis_type_vega/kibana.json index 6bfd6c9536df4..f1f82e7f5b7ad 100644 --- a/src/plugins/vis_type_vega/kibana.json +++ b/src/plugins/vis_type_vega/kibana.json @@ -2,5 +2,6 @@ "id": "visTypeVega", "version": "kibana", "server": true, - "ui": true + "ui": true, + "requiredPlugins": ["data", "visualizations", "mapsLegacy", "expressions"] } diff --git a/src/legacy/core_plugins/vis_type_vega/public/__mocks__/services.ts b/src/plugins/vis_type_vega/public/__mocks__/services.ts similarity index 87% rename from src/legacy/core_plugins/vis_type_vega/public/__mocks__/services.ts rename to src/plugins/vis_type_vega/public/__mocks__/services.ts index b2f3e5b2241e6..1bf051232e4c9 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/__mocks__/services.ts +++ b/src/plugins/vis_type_vega/public/__mocks__/services.ts @@ -17,11 +17,11 @@ * under the License. */ -import { createGetterSetter } from '../../../../../plugins/kibana_utils/public'; -import { DataPublicPluginStart } from '../../../../../plugins/data/public'; +import { createGetterSetter } from '../../../kibana_utils/public'; +import { DataPublicPluginStart } from '../../../data/public'; import { IUiSettingsClient, NotificationsStart, SavedObjectsStart } from 'kibana/public'; -import { dataPluginMock } from '../../../../../plugins/data/public/mocks'; -import { coreMock } from '../../../../../core/public/mocks'; +import { dataPluginMock } from '../../../data/public/mocks'; +import { coreMock } from '../../../../core/public/mocks'; export const [getData, setData] = createGetterSetter('Data'); setData(dataPluginMock.createStartContract()); diff --git a/src/legacy/core_plugins/vis_type_vega/public/_vega_editor.scss b/src/plugins/vis_type_vega/public/_vega_editor.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/_vega_editor.scss rename to src/plugins/vis_type_vega/public/_vega_editor.scss diff --git a/src/legacy/core_plugins/vis_type_vega/public/_vega_vis.scss b/src/plugins/vis_type_vega/public/_vega_vis.scss similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/_vega_vis.scss rename to src/plugins/vis_type_vega/public/_vega_vis.scss diff --git a/src/legacy/core_plugins/vis_type_vega/public/components/index.ts b/src/plugins/vis_type_vega/public/components/index.ts similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/components/index.ts rename to src/plugins/vis_type_vega/public/components/index.ts diff --git a/src/legacy/core_plugins/vis_type_vega/public/components/vega_actions_menu.tsx b/src/plugins/vis_type_vega/public/components/vega_actions_menu.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/components/vega_actions_menu.tsx rename to src/plugins/vis_type_vega/public/components/vega_actions_menu.tsx diff --git a/src/legacy/core_plugins/vis_type_vega/public/components/vega_help_menu.tsx b/src/plugins/vis_type_vega/public/components/vega_help_menu.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/components/vega_help_menu.tsx rename to src/plugins/vis_type_vega/public/components/vega_help_menu.tsx diff --git a/src/legacy/core_plugins/vis_type_vega/public/components/vega_vis_editor.tsx b/src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/components/vega_vis_editor.tsx rename to src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/ems_file_parser.js b/src/plugins/vis_type_vega/public/data_model/ems_file_parser.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/ems_file_parser.js rename to src/plugins/vis_type_vega/public/data_model/ems_file_parser.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.js b/src/plugins/vis_type_vega/public/data_model/es_query_parser.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.js rename to src/plugins/vis_type_vega/public/data_model/es_query_parser.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.test.js b/src/plugins/vis_type_vega/public/data_model/es_query_parser.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.test.js rename to src/plugins/vis_type_vega/public/data_model/es_query_parser.test.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/search_cache.js b/src/plugins/vis_type_vega/public/data_model/search_cache.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/search_cache.js rename to src/plugins/vis_type_vega/public/data_model/search_cache.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/search_cache.test.js b/src/plugins/vis_type_vega/public/data_model/search_cache.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/search_cache.test.js rename to src/plugins/vis_type_vega/public/data_model/search_cache.test.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/time_cache.js b/src/plugins/vis_type_vega/public/data_model/time_cache.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/time_cache.js rename to src/plugins/vis_type_vega/public/data_model/time_cache.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/time_cache.test.js b/src/plugins/vis_type_vega/public/data_model/time_cache.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/time_cache.test.js rename to src/plugins/vis_type_vega/public/data_model/time_cache.test.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/url_parser.js b/src/plugins/vis_type_vega/public/data_model/url_parser.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/url_parser.js rename to src/plugins/vis_type_vega/public/data_model/url_parser.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/utils.js b/src/plugins/vis_type_vega/public/data_model/utils.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/utils.js rename to src/plugins/vis_type_vega/public/data_model/utils.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.js rename to src/plugins/vis_type_vega/public/data_model/vega_parser.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.test.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.test.js rename to src/plugins/vis_type_vega/public/data_model/vega_parser.test.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/default.spec.hjson b/src/plugins/vis_type_vega/public/default.spec.hjson similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/default.spec.hjson rename to src/plugins/vis_type_vega/public/default.spec.hjson diff --git a/src/legacy/core_plugins/vis_type_vega/public/index.scss b/src/plugins/vis_type_vega/public/index.scss similarity index 78% rename from src/legacy/core_plugins/vis_type_vega/public/index.scss rename to src/plugins/vis_type_vega/public/index.scss index 1ab2119d481a0..78d9eb61999f7 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/index.scss +++ b/src/plugins/vis_type_vega/public/index.scss @@ -1,5 +1,3 @@ -@import 'src/legacy/ui/public/styles/styling_constants'; - // Prefix all styles with "vga" to avoid conflicts. // Examples // vgaChart diff --git a/src/plugins/vis_type_vega/public/index.ts b/src/plugins/vis_type_vega/public/index.ts index 71f3474f8217e..78878d38e1674 100644 --- a/src/plugins/vis_type_vega/public/index.ts +++ b/src/plugins/vis_type_vega/public/index.ts @@ -19,20 +19,8 @@ import { PluginInitializerContext } from 'kibana/public'; import { ConfigSchema } from '../config'; +import { VegaPlugin as Plugin } from './plugin'; -export const plugin = (initializerContext: PluginInitializerContext) => ({ - setup() { - return { - /** - * The configuration is temporarily exposed to allow the legacy vega plugin to consume - * the setting. Once the vega plugin is migrated completely, this will become an implementation - * detail. - * @deprecated - */ - config: initializerContext.config.get(), - }; - }, - start() {}, -}); - -export type VisTypeVegaSetup = ReturnType['setup']>; +export function plugin(initializerContext: PluginInitializerContext) { + return new Plugin(initializerContext); +} diff --git a/src/legacy/core_plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts similarity index 77% rename from src/legacy/core_plugins/vis_type_vega/public/plugin.ts rename to src/plugins/vis_type_vega/public/plugin.ts index 9fa77d28fbbfa..c312705194cde 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -16,10 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../../core/public'; -import { Plugin as ExpressionsPublicPlugin } from '../../../../plugins/expressions/public'; -import { Plugin as DataPublicPlugin } from '../../../../plugins/data/public'; -import { VisualizationsSetup } from '../../../../plugins/visualizations/public'; +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; +import { Plugin as DataPublicPlugin } from '../../data/public'; +import { VisualizationsSetup } from '../../visualizations/public'; import { setNotifications, setData, @@ -30,8 +30,10 @@ import { import { createVegaFn } from './vega_fn'; import { createVegaTypeDefinition } from './vega_type'; -import { VisTypeVegaSetup } from '../../../../plugins/vis_type_vega/public'; -import { IServiceSettings } from '../../../../plugins/maps_legacy/public'; +import { IServiceSettings } from '../../maps_legacy/public'; +import { ConfigSchema } from '../config'; + +import './index.scss'; /** @internal */ export interface VegaVisualizationDependencies { @@ -47,7 +49,6 @@ export interface VegaPluginSetupDependencies { expressions: ReturnType; visualizations: VisualizationsSetup; data: ReturnType; - visTypeVega: VisTypeVegaSetup; mapsLegacy: any; } @@ -58,18 +59,18 @@ export interface VegaPluginStartDependencies { /** @internal */ export class VegaPlugin implements Plugin, void> { - initializerContext: PluginInitializerContext; + initializerContext: PluginInitializerContext; - constructor(initializerContext: PluginInitializerContext) { + constructor(initializerContext: PluginInitializerContext) { this.initializerContext = initializerContext; } public async setup( core: CoreSetup, - { data, expressions, visualizations, visTypeVega, mapsLegacy }: VegaPluginSetupDependencies + { data, expressions, visualizations, mapsLegacy }: VegaPluginSetupDependencies ) { setInjectedVars({ - enableExternalUrls: visTypeVega.config.enableExternalUrls, + enableExternalUrls: this.initializerContext.config.get().enableExternalUrls, esShardTimeout: core.injectedMetadata.getInjectedVar('esShardTimeout') as number, emsTileLayerId: core.injectedMetadata.getInjectedVar('emsTileLayerId', true), }); diff --git a/src/legacy/core_plugins/vis_type_vega/public/services.ts b/src/plugins/vis_type_vega/public/services.ts similarity index 86% rename from src/legacy/core_plugins/vis_type_vega/public/services.ts rename to src/plugins/vis_type_vega/public/services.ts index 88e0e0098bf8c..e349cfbdc0024 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/services.ts +++ b/src/plugins/vis_type_vega/public/services.ts @@ -18,10 +18,9 @@ */ import { SavedObjectsStart } from 'kibana/public'; -import { NotificationsStart } from 'src/core/public'; -import { DataPublicPluginStart } from '../../../../plugins/data/public'; -import { createGetterSetter } from '../../../../plugins/kibana_utils/public'; -import { IUiSettingsClient } from '../../../../core/public'; +import { NotificationsStart, IUiSettingsClient } from 'src/core/public'; +import { DataPublicPluginStart } from '../../data/public'; +import { createGetterSetter } from '../../kibana_utils/public'; export const [getData, setData] = createGetterSetter('Data'); diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_fn.ts b/src/plugins/vis_type_vega/public/vega_fn.ts similarity index 94% rename from src/legacy/core_plugins/vis_type_vega/public/vega_fn.ts rename to src/plugins/vis_type_vega/public/vega_fn.ts index 2a0da81a31a96..6d45e043f7cee 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_fn.ts +++ b/src/plugins/vis_type_vega/public/vega_fn.ts @@ -19,11 +19,7 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { - ExpressionFunctionDefinition, - KibanaContext, - Render, -} from '../../../../plugins/expressions/public'; +import { ExpressionFunctionDefinition, KibanaContext, Render } from '../../expressions/public'; import { VegaVisualizationDependencies } from './plugin'; import { createVegaRequestHandler } from './vega_request_handler'; diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts b/src/plugins/vis_type_vega/public/vega_request_handler.ts similarity index 84% rename from src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts rename to src/plugins/vis_type_vega/public/vega_request_handler.ts index f63efc0007c3b..196e8fdcbafda 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_request_handler.ts +++ b/src/plugins/vis_type_vega/public/vega_request_handler.ts @@ -17,9 +17,7 @@ * under the License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getSearchService } from '../../../../plugins/data/public/services'; -import { Filter, esQuery, TimeRange, Query } from '../../../../plugins/data/public'; +import { Filter, esQuery, TimeRange, Query } from '../../data/public'; // @ts-ignore import { VegaParser } from './data_model/vega_parser'; @@ -30,6 +28,7 @@ import { TimeCache } from './data_model/time_cache'; import { VegaVisualizationDependencies } from './plugin'; import { VisParams } from './vega_fn'; +import { getData } from './services'; interface VegaRequestHandlerParams { query: Query; @@ -43,12 +42,18 @@ export function createVegaRequestHandler({ core: { uiSettings }, serviceSettings, }: VegaVisualizationDependencies) { - const { esClient } = getSearchService().__LEGACY; - const searchCache = new SearchCache(esClient, { max: 10, maxAge: 4 * 1000 }); + let searchCache: SearchCache | undefined; const { timefilter } = data.query.timefilter; const timeCache = new TimeCache(timefilter, 3 * 1000); return ({ timeRange, filters, query, visParams }: VegaRequestHandlerParams) => { + if (!searchCache) { + searchCache = new SearchCache(getData().search.__LEGACY.esClient, { + max: 10, + maxAge: 4 * 1000, + }); + } + timeCache.setTimeRange(timeRange); const esQueryConfigs = esQuery.getEsQueryConfig(uiSettings); diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts b/src/plugins/vis_type_vega/public/vega_type.ts similarity index 92% rename from src/legacy/core_plugins/vis_type_vega/public/vega_type.ts rename to src/plugins/vis_type_vega/public/vega_type.ts index f56d7682efc6f..c864553c118b9 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_type.ts +++ b/src/plugins/vis_type_vega/public/vega_type.ts @@ -18,10 +18,10 @@ */ import { i18n } from '@kbn/i18n'; -import { DefaultEditorSize } from '../../../../plugins/vis_default_editor/public'; +import { DefaultEditorSize } from '../../vis_default_editor/public'; import { VegaVisualizationDependencies } from './plugin'; import { VegaVisEditor } from './components'; -import { defaultFeedbackMessage } from '../../../../plugins/kibana_utils/public'; +import { defaultFeedbackMessage } from '../../kibana_utils/public'; import { createVegaRequestHandler } from './vega_request_handler'; // @ts-ignore diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js similarity index 99% rename from src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_base_view.js rename to src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index c90f059ff7c94..be98d2b69ad87 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -26,7 +26,7 @@ import { Utils } from '../data_model/utils'; import { VISUALIZATION_COLORS } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { TooltipHandler } from './vega_tooltip'; -import { esFilters } from '../../../../../plugins/data/public'; +import { esFilters } from '../../../data/public'; import { getEnableExternalUrls } from '../services'; diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_layer.js b/src/plugins/vis_type_vega/public/vega_view/vega_map_layer.js similarity index 94% rename from src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_layer.js rename to src/plugins/vis_type_vega/public/vega_view/vega_map_layer.js index d43eb9c3351ea..8e4009eab8488 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_layer.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_layer.js @@ -19,7 +19,7 @@ import L from 'leaflet'; import 'leaflet-vega'; -import { KibanaMapLayer } from '../../../../../plugins/maps_legacy/public'; +import { KibanaMapLayer } from '../../../maps_legacy/public'; export class VegaMapLayer extends KibanaMapLayer { constructor(spec, options) { diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js similarity index 98% rename from src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_view.js rename to src/plugins/vis_type_vega/public/vega_view/vega_map_view.js index 03aef29dc5739..bd6652a597203 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_map_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js @@ -21,7 +21,7 @@ import * as vega from 'vega-lib'; import { i18n } from '@kbn/i18n'; import { VegaBaseView } from './vega_base_view'; import { VegaMapLayer } from './vega_map_layer'; -import { KibanaMap } from '../../../../../plugins/maps_legacy/public'; +import { KibanaMap } from '../../../maps_legacy/public'; import { getEmsTileLayerId, getUISettings } from '../services'; export class VegaMapView extends VegaBaseView { diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_tooltip.js b/src/plugins/vis_type_vega/public/vega_view/vega_tooltip.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_tooltip.js rename to src/plugins/vis_type_vega/public/vega_view/vega_tooltip.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_view.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/vega_view/vega_view.js rename to src/plugins/vis_type_vega/public/vega_view/vega_view.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/vega_visualization.js b/src/plugins/vis_type_vega/public/vega_visualization.js similarity index 100% rename from src/legacy/core_plugins/vis_type_vega/public/vega_visualization.js rename to src/plugins/vis_type_vega/public/vega_visualization.js diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index 7df420e7ba585..e475684ed5934 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -43,6 +43,8 @@ export type VisualizeEmbeddableContract = PublicContract; export { VisualizeInput } from './embeddable'; export type ExprVis = ExprVisClass; export { SchemaConfig } from './legacy/build_pipeline'; +// @ts-ignore +export { updateOldState } from './legacy/vis_update_state'; export { PersistedState } from './persisted_state'; export { VisualizationController, diff --git a/src/plugins/visualizations/public/legacy/vis_update.js b/src/plugins/visualizations/public/legacy/vis_update.js deleted file mode 100644 index 338a322e6aa57..0000000000000 --- a/src/plugins/visualizations/public/legacy/vis_update.js +++ /dev/null @@ -1,57 +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. - */ - -// TODO: this should be moved to vis_update_state -// Currently the migration takes place in Vis when calling setCurrentState. -// It should rather convert the raw saved object before starting to instantiate -// any JavaScript classes from it. -const updateVisualizationConfig = (stateConfig, config) => { - if (!stateConfig || stateConfig.seriesParams) return; - if (!['line', 'area', 'histogram'].includes(config.type)) return; - - // update value axis options - const isUserDefinedYAxis = config.setYExtents; - const defaultYExtents = config.defaultYExtents; - const mode = ['stacked', 'overlap'].includes(config.mode) ? 'normal' : config.mode || 'normal'; - config.valueAxes[0].scale = { - ...config.valueAxes[0].scale, - type: config.scale || 'linear', - setYExtents: config.setYExtents || false, - defaultYExtents: config.defaultYExtents || false, - boundsMargin: defaultYExtents ? config.boundsMargin : 0, - min: isUserDefinedYAxis ? config.yAxis.min : undefined, - max: isUserDefinedYAxis ? config.yAxis.max : undefined, - mode: mode, - }; - - // update series options - const interpolate = config.smoothLines ? 'cardinal' : config.interpolate; - const stacked = ['stacked', 'percentage', 'wiggle', 'silhouette'].includes(config.mode); - config.seriesParams[0] = { - ...config.seriesParams[0], - type: config.type || 'line', - mode: stacked ? 'stacked' : 'normal', - interpolate: interpolate, - drawLinesBetweenPoints: config.drawLinesBetweenPoints, - showCircles: config.showCircles, - radiusRatio: config.radiusRatio, - }; -}; - -export { updateVisualizationConfig }; diff --git a/src/plugins/visualizations/public/legacy/vis_update_state.js b/src/plugins/visualizations/public/legacy/vis_update_state.js index 45610701e08c8..e345b9e5b8c9a 100644 --- a/src/plugins/visualizations/public/legacy/vis_update_state.js +++ b/src/plugins/visualizations/public/legacy/vis_update_state.js @@ -75,6 +75,77 @@ function convertDateHistogramScaleMetrics(visState) { } } +function convertSeriesParams(visState) { + if (visState.params.seriesParams) { + return; + } + + // update value axis options + const isUserDefinedYAxis = visState.params.setYExtents; + const defaultYExtents = visState.params.defaultYExtents; + const mode = ['stacked', 'overlap'].includes(visState.params.mode) + ? 'normal' + : visState.params.mode || 'normal'; + + if (!visState.params.valueAxes || !visState.params.valueAxes.length) { + visState.params.valueAxes = [ + { + id: 'ValueAxis-1', + name: 'LeftAxis-1', + type: 'value', + position: 'left', + show: true, + style: {}, + scale: { + type: 'linear', + mode: 'normal', + }, + labels: { + show: true, + rotate: 0, + filter: false, + truncate: 100, + }, + title: { + text: 'Count', + }, + }, + ]; + } + + visState.params.valueAxes[0].scale = { + ...visState.params.valueAxes[0].scale, + type: visState.params.scale || 'linear', + setYExtents: visState.params.setYExtents || false, + defaultYExtents: visState.params.defaultYExtents || false, + boundsMargin: defaultYExtents ? visState.params.boundsMargin : 0, + min: isUserDefinedYAxis ? visState.params.yAxis.min : undefined, + max: isUserDefinedYAxis ? visState.params.yAxis.max : undefined, + mode: mode, + }; + + // update series options + const interpolate = visState.params.smoothLines ? 'cardinal' : visState.params.interpolate; + const stacked = ['stacked', 'percentage', 'wiggle', 'silhouette'].includes(visState.params.mode); + visState.params.seriesParams = [ + { + show: true, + type: visState.params.type || 'line', + mode: stacked ? 'stacked' : 'normal', + interpolate: interpolate, + drawLinesBetweenPoints: visState.params.drawLinesBetweenPoints, + showCircles: visState.params.showCircles, + radiusRatio: visState.params.radiusRatio, + data: { + label: 'Count', + id: '1', + }, + lineWidth: 2, + valueAxis: 'ValueAxis-1', + }, + ]; +} + /** * This function is responsible for updating old visStates - the actual saved object * object - into the format, that will be required by the current Kibana version. @@ -90,6 +161,10 @@ export const updateOldState = visState => { convertPropertyNames(newState); convertDateHistogramScaleMetrics(newState); + if (visState.params && ['line', 'area', 'histogram'].includes(visState.params.type)) { + convertSeriesParams(newState); + } + if (visState.type === 'gauge' && visState.fontSize) { delete newState.fontSize; _.set(newState, 'gauge.style.fontSize', visState.fontSize); diff --git a/src/plugins/visualizations/public/vis.ts b/src/plugins/visualizations/public/vis.ts index 3cab4faf2a27f..009dd71b9a912 100644 --- a/src/plugins/visualizations/public/vis.ts +++ b/src/plugins/visualizations/public/vis.ts @@ -29,8 +29,6 @@ import { isFunction, defaults, cloneDeep } from 'lodash'; import { PersistedState } from './persisted_state'; -// @ts-ignore -import { updateVisualizationConfig } from './legacy/vis_update'; import { getTypes, getAggs } from './services'; import { VisType } from './vis_types'; import { @@ -121,9 +119,6 @@ export class Vis { this.params = this.getParams(state.params); } - // move to migration script - updateVisualizationConfig(state.params, this.params); - if (state.data && state.data.searchSource) { this.data.searchSource = state.data.searchSource!; this.data.indexPattern = this.data.searchSource.getField('index'); diff --git a/src/plugins/visualize/kibana.json b/src/plugins/visualize/kibana.json index a7afa0697a5eb..d536d2f246a6b 100644 --- a/src/plugins/visualize/kibana.json +++ b/src/plugins/visualize/kibana.json @@ -1,7 +1,7 @@ { "id": "visualize", "version": "kibana", - "server": false, + "server": true, "ui": true, "requiredPlugins": [ "data", diff --git a/src/plugins/visualize/public/application/legacy_app.js b/src/plugins/visualize/public/application/legacy_app.js index 7c5e3ce9408f0..c7cc11c1f3ff5 100644 --- a/src/plugins/visualize/public/application/legacy_app.js +++ b/src/plugins/visualize/public/application/legacy_app.js @@ -21,11 +21,7 @@ import { find } from 'lodash'; import { i18n } from '@kbn/i18n'; import { createHashHistory } from 'history'; -import { - createKbnUrlStateStorage, - redirectWhenMissing, - ensureDefaultIndexPattern, -} from '../../../kibana_utils/public'; +import { createKbnUrlStateStorage, redirectWhenMissing } from '../../../kibana_utils/public'; import { createSavedSearchesLoader } from '../../../discover/public'; import editorTemplate from './editor/editor.html'; @@ -127,7 +123,7 @@ export function initVisualizeApp(app, deps) { controllerAs: 'listingController', resolve: { createNewVis: () => false, - hasDefaultIndex: history => ensureDefaultIndexPattern(deps.core, deps.data, history), + hasDefaultIndex: history => deps.data.indexPatterns.ensureDefaultIndexPattern(history), }, }) .when(VisualizeConstants.WIZARD_STEP_1_PAGE_PATH, { @@ -138,7 +134,7 @@ export function initVisualizeApp(app, deps) { controllerAs: 'listingController', resolve: { createNewVis: () => true, - hasDefaultIndex: history => ensureDefaultIndexPattern(deps.core, deps.data, history), + hasDefaultIndex: history => deps.data.indexPatterns.ensureDefaultIndexPattern(history), }, }) .when(VisualizeConstants.CREATE_PATH, { @@ -147,7 +143,7 @@ export function initVisualizeApp(app, deps) { k7Breadcrumbs: getCreateBreadcrumbs, resolve: { resolved: function($route, history) { - const { core, data, savedVisualizations, visualizations, toastNotifications } = deps; + const { data, savedVisualizations, visualizations, toastNotifications } = deps; const visTypes = visualizations.all(); const visType = find(visTypes, { name: $route.current.params.type }); const shouldHaveIndex = visType.requiresSearch && visType.options.showIndexSelection; @@ -164,7 +160,8 @@ export function initVisualizeApp(app, deps) { ); } - return ensureDefaultIndexPattern(core, data, history) + return data.indexPatterns + .ensureDefaultIndexPattern(history) .then(() => savedVisualizations.get($route.current.params)) .then(getResolvedResults(deps)) .catch( @@ -183,9 +180,10 @@ export function initVisualizeApp(app, deps) { k7Breadcrumbs: getEditBreadcrumbs, resolve: { resolved: function($route, history) { - const { chrome, core, data, savedVisualizations, toastNotifications } = deps; + const { chrome, data, savedVisualizations, toastNotifications } = deps; - return ensureDefaultIndexPattern(core, data, history) + return data.indexPatterns + .ensureDefaultIndexPattern(history) .then(() => savedVisualizations.get($route.current.params.id)) .then(savedVis => { chrome.recentlyAccessed.add(savedVis.getFullPath(), savedVis.title, savedVis.id); diff --git a/src/plugins/visualize/server/capabilities_provider.ts b/src/plugins/visualize/server/capabilities_provider.ts new file mode 100644 index 0000000000000..3b09b28f53c77 --- /dev/null +++ b/src/plugins/visualize/server/capabilities_provider.ts @@ -0,0 +1,28 @@ +/* + * 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 capabilitiesProvider = () => ({ + visualize: { + show: true, + createShortUrl: true, + delete: true, + save: true, + saveQuery: true, + }, +}); diff --git a/src/plugins/visualize/server/index.ts b/src/plugins/visualize/server/index.ts new file mode 100644 index 0000000000000..5cebef71d8d22 --- /dev/null +++ b/src/plugins/visualize/server/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { PluginInitializerContext } from 'kibana/server'; +import { VisualizeServerPlugin } from './plugin'; + +export const plugin = (initContext: PluginInitializerContext) => + new VisualizeServerPlugin(initContext); diff --git a/src/plugins/visualize/server/plugin.ts b/src/plugins/visualize/server/plugin.ts new file mode 100644 index 0000000000000..7cc57c25f3229 --- /dev/null +++ b/src/plugins/visualize/server/plugin.ts @@ -0,0 +1,44 @@ +/* + * 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 { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from 'kibana/server'; +import { capabilitiesProvider } from './capabilities_provider'; + +export class VisualizeServerPlugin implements Plugin { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + this.logger.debug('visualize: Setup'); + + core.capabilities.registerProvider(capabilitiesProvider); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('visualize: Started'); + return {}; + } + + public stop() {} +} diff --git a/tasks/config/karma.js b/tasks/config/karma.js index 4e106ef3e039a..1ec7c831b4864 100644 --- a/tasks/config/karma.js +++ b/tasks/config/karma.js @@ -53,6 +53,8 @@ module.exports = function(grunt) { function getKarmaFiles(shardNum) { return [ 'http://localhost:5610/test_bundle/built_css.css', + // Sets global variables normally set by the bootstrap.js script + 'http://localhost:5610/test_bundle/karma/globals.js', ...UiSharedDeps.jsDepFilenames.map( chunkFilename => `http://localhost:5610/bundles/kbn-ui-shared-deps/${chunkFilename}` diff --git a/test/accessibility/apps/discover.ts b/test/accessibility/apps/discover.ts index 086b13ecee2b3..0168626d4a1a9 100644 --- a/test/accessibility/apps/discover.ts +++ b/test/accessibility/apps/discover.ts @@ -33,7 +33,8 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { ['geo.src', 'IN'], ]; - describe('Discover', () => { + // FLAKY: https://github.com/elastic/kibana/issues/62497 + describe.skip('Discover', () => { before(async () => { await esArchiver.load('discover'); await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/test/functional/apps/console/_console.ts b/test/functional/apps/console/_console.ts index 456752c0cd6eb..47b8a4a2e9f70 100644 --- a/test/functional/apps/console/_console.ts +++ b/test/functional/apps/console/_console.ts @@ -40,7 +40,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'console']); describe('console app', function describeIndexTests() { - this.tags('smoke'); + this.tags('includeFirefox'); before(async () => { log.debug('navigateTo console'); await PageObjects.common.navigateToApp('console'); diff --git a/test/functional/apps/context/_discover_navigation.js b/test/functional/apps/context/_discover_navigation.js index a56a85546bbcd..9a0130d39bc2f 100644 --- a/test/functional/apps/context/_discover_navigation.js +++ b/test/functional/apps/context/_discover_navigation.js @@ -33,7 +33,6 @@ export default function({ getService, getPageObjects }) { // FLAKY: https://github.com/elastic/kibana/issues/53308 describe.skip('context link in discover', function contextSize() { - this.tags('smoke'); before(async function() { await PageObjects.common.navigateToApp('discover'); await PageObjects.timePicker.setDefaultAbsoluteRange(); diff --git a/test/functional/apps/dashboard/dashboard_filtering.js b/test/functional/apps/dashboard/dashboard_filtering.js index f388993dcaf7d..8846a753f3794 100644 --- a/test/functional/apps/dashboard/dashboard_filtering.js +++ b/test/functional/apps/dashboard/dashboard_filtering.js @@ -38,7 +38,7 @@ export default function({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'visualize', 'timePicker']); describe('dashboard filtering', function() { - this.tags('smoke'); + this.tags('includeFirefox'); before(async () => { await esArchiver.load('dashboard/current/kibana'); diff --git a/test/functional/apps/dashboard/dashboard_save.js b/test/functional/apps/dashboard/dashboard_save.js index 2ea1389b89ad4..7ffe951faa398 100644 --- a/test/functional/apps/dashboard/dashboard_save.js +++ b/test/functional/apps/dashboard/dashboard_save.js @@ -24,7 +24,7 @@ export default function({ getPageObjects, getService }) { const listingTable = getService('listingTable'); describe('dashboard save', function describeIndexTests() { - this.tags('smoke'); + this.tags('includeFirefox'); const dashboardName = 'Dashboard Save Test'; const dashboardNameEnterKey = 'Dashboard Save Test with Enter Key'; diff --git a/test/functional/apps/dashboard/panel_controls.js b/test/functional/apps/dashboard/panel_controls.js index 6e24b9f3570a3..279adb22a1cfa 100644 --- a/test/functional/apps/dashboard/panel_controls.js +++ b/test/functional/apps/dashboard/panel_controls.js @@ -43,8 +43,6 @@ export default function({ getService, getPageObjects }) { const dashboardName = 'Dashboard Panel Controls Test'; describe('dashboard panel controls', function viewEditModeTests() { - this.tags('smoke'); - before(async function() { await PageObjects.dashboard.initTests(); await PageObjects.dashboard.preserveCrossAppState(); diff --git a/test/functional/apps/dashboard/time_zones.js b/test/functional/apps/dashboard/time_zones.js index b7698a7d6ac4b..564eb790eb8d1 100644 --- a/test/functional/apps/dashboard/time_zones.js +++ b/test/functional/apps/dashboard/time_zones.js @@ -27,7 +27,7 @@ export default function({ getService, getPageObjects }) { const PageObjects = getPageObjects(['dashboard', 'timePicker', 'settings', 'common']); describe('dashboard time zones', function() { - this.tags('smoke'); + this.tags('includeFirefox'); before(async () => { await esArchiver.load('dashboard/current/kibana'); diff --git a/test/functional/apps/discover/_doc_navigation.js b/test/functional/apps/discover/_doc_navigation.js index 08e0cb0b8d23a..ebce8dcafadb4 100644 --- a/test/functional/apps/discover/_doc_navigation.js +++ b/test/functional/apps/discover/_doc_navigation.js @@ -33,7 +33,6 @@ export default function({ getService, getPageObjects }) { // FLAKY: https://github.com/elastic/kibana/issues/62281 describe.skip('doc link in discover', function contextSize() { - this.tags('smoke'); before(async function() { await esArchiver.loadIfNeeded('logstash_functional'); await PageObjects.common.navigateToApp('discover'); diff --git a/test/functional/apps/discover/_field_data.js b/test/functional/apps/discover/_field_data.js index 62d42f3da5c84..ace9710665f10 100644 --- a/test/functional/apps/discover/_field_data.js +++ b/test/functional/apps/discover/_field_data.js @@ -28,7 +28,7 @@ export default function({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']); describe('discover tab', function describeIndexTests() { - this.tags('smoke'); + this.tags('includeFirefox'); before(async function() { await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.load('discover'); diff --git a/test/functional/apps/getting_started/index.js b/test/functional/apps/getting_started/index.js index b73c16e2583b5..41ee71a753712 100644 --- a/test/functional/apps/getting_started/index.js +++ b/test/functional/apps/getting_started/index.js @@ -21,7 +21,7 @@ export default function({ getService, loadTestFile }) { const browser = getService('browser'); describe('Getting Started ', function() { - this.tags(['ciGroup6', 'smoke']); + this.tags(['ciGroup6']); before(async function() { await browser.setWindowSize(1200, 800); diff --git a/test/functional/apps/home/_home.js b/test/functional/apps/home/_home.js index 6587c2f113b7f..3c56c22c046dd 100644 --- a/test/functional/apps/home/_home.js +++ b/test/functional/apps/home/_home.js @@ -25,7 +25,7 @@ export default function({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common', 'home']); describe('Kibana takes you home', function describeIndexTests() { - this.tags('smoke'); + this.tags('includeFirefox'); it('clicking on kibana logo should take you to home page', async () => { await PageObjects.common.navigateToApp('settings'); diff --git a/test/functional/apps/home/_sample_data.ts b/test/functional/apps/home/_sample_data.ts index 5812b9b96e42a..f46b3390cbcf8 100644 --- a/test/functional/apps/home/_sample_data.ts +++ b/test/functional/apps/home/_sample_data.ts @@ -32,8 +32,6 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'header', 'home', 'dashboard', 'timePicker']); describe('sample data', function describeIndexTests() { - this.tags('smoke'); - before(async () => { await security.testUser.setRoles(['kibana_admin', 'kibana_sample_admin']); await PageObjects.common.navigateToUrl('home', 'tutorial_directory/sampleData'); diff --git a/test/functional/apps/management/_create_index_pattern_wizard.js b/test/functional/apps/management/_create_index_pattern_wizard.js index c8d11d8555d37..65d852b249ea0 100644 --- a/test/functional/apps/management/_create_index_pattern_wizard.js +++ b/test/functional/apps/management/_create_index_pattern_wizard.js @@ -25,8 +25,6 @@ export default function({ getService, getPageObjects }) { const PageObjects = getPageObjects(['settings', 'common']); describe('"Create Index Pattern" wizard', function() { - this.tags('smoke'); - before(async function() { // delete .kibana index and then wait for Kibana to re-create it await kibanaServer.uiSettings.replace({}); diff --git a/test/functional/apps/visualize/_experimental_vis.js b/test/functional/apps/visualize/_experimental_vis.js index c45a95abab86e..22d36d671cb68 100644 --- a/test/functional/apps/visualize/_experimental_vis.js +++ b/test/functional/apps/visualize/_experimental_vis.js @@ -24,8 +24,6 @@ export default ({ getService, getPageObjects }) => { const PageObjects = getPageObjects(['visualize']); describe('experimental visualizations in visualize app ', function() { - this.tags('smoke'); - describe('experimental visualizations', () => { beforeEach(async () => { log.debug('navigateToApp visualize'); diff --git a/test/functional/apps/visualize/_gauge_chart.js b/test/functional/apps/visualize/_gauge_chart.js index 7ebb4548f967b..d7a30f39250f3 100644 --- a/test/functional/apps/visualize/_gauge_chart.js +++ b/test/functional/apps/visualize/_gauge_chart.js @@ -28,8 +28,6 @@ export default function({ getService, getPageObjects }) { // FLAKY: https://github.com/elastic/kibana/issues/45089 describe('gauge chart', function indexPatternCreation() { - this.tags('smoke'); - async function initGaugeVis() { log.debug('navigateToApp visualize'); await PageObjects.visualize.navigateToNewVisualization(); diff --git a/test/functional/apps/visualize/_heatmap_chart.js b/test/functional/apps/visualize/_heatmap_chart.js index cbf7ab8df6831..842599fa1d0a1 100644 --- a/test/functional/apps/visualize/_heatmap_chart.js +++ b/test/functional/apps/visualize/_heatmap_chart.js @@ -25,7 +25,6 @@ export default function({ getService, getPageObjects }) { const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']); describe('heatmap chart', function indexPatternCreation() { - this.tags('smoke'); const vizName1 = 'Visualization HeatmapChart'; before(async function() { diff --git a/test/functional/apps/visualize/_inspector.js b/test/functional/apps/visualize/_inspector.js index d989f8e2539a0..256c0362226e5 100644 --- a/test/functional/apps/visualize/_inspector.js +++ b/test/functional/apps/visualize/_inspector.js @@ -24,7 +24,6 @@ export default function({ getService, getPageObjects }) { const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']); describe('inspector', function describeIndexTests() { - this.tags('smoke'); before(async function() { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVerticalBarChart(); diff --git a/test/functional/apps/visualize/_tsvb_chart.ts b/test/functional/apps/visualize/_tsvb_chart.ts index 867db66ac81dc..27a06cc05b45c 100644 --- a/test/functional/apps/visualize/_tsvb_chart.ts +++ b/test/functional/apps/visualize/_tsvb_chart.ts @@ -29,7 +29,6 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['visualize', 'visualBuilder', 'timePicker', 'visChart']); describe('visual builder', function describeIndexTests() { - this.tags('smoke'); beforeEach(async () => { await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader']); await PageObjects.visualize.navigateToNewVisualization(); diff --git a/test/functional/apps/visualize/input_control_vis/chained_controls.js b/test/functional/apps/visualize/input_control_vis/chained_controls.js index b56a37218aba5..b5231f3161377 100644 --- a/test/functional/apps/visualize/input_control_vis/chained_controls.js +++ b/test/functional/apps/visualize/input_control_vis/chained_controls.js @@ -27,7 +27,7 @@ export default function({ getService, getPageObjects }) { const comboBox = getService('comboBox'); describe('chained controls', function() { - this.tags('smoke'); + this.tags('includeFirefox'); before(async () => { await PageObjects.common.navigateToApp('visualize'); diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 6dcd017335c85..8864eda3823ef 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -206,15 +206,17 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider async getFieldsTabCount() { return retry.try(async () => { - const text = await testSubjects.getVisibleText('tab-count-indexedFields'); - return text.replace(/\((.*)\)/, '$1'); + const indexedFieldsTab = await find.byCssSelector('#indexedFields .euiTab__content'); + const text = await indexedFieldsTab.getVisibleText(); + return text.split(/[()]/)[1]; }); } async getScriptedFieldsTabCount() { return await retry.try(async () => { - const theText = await testSubjects.getVisibleText('tab-count-scriptedFields'); - return theText.replace(/\((.*)\)/, '$1'); + const scriptedFieldsTab = await find.byCssSelector('#scriptedFields .euiTab__content'); + const text = await scriptedFieldsTab.getVisibleText(); + return text.split(/[()]/)[1]; }); } @@ -241,13 +243,13 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider async setFieldTypeFilter(type: string) { await find.clickByCssSelector( - 'select[data-test-subj="indexedFieldTypeFilterDropdown"] > option[label="' + type + '"]' + 'select[data-test-subj="indexedFieldTypeFilterDropdown"] > option[value="' + type + '"]' ); } async setScriptedFieldLanguageFilter(language: string) { await find.clickByCssSelector( - 'select[data-test-subj="scriptedFieldLanguageFilterDropdown"] > option[label="' + + 'select[data-test-subj="scriptedFieldLanguageFilterDropdown"] > option[value="' + language + '"]' ); @@ -412,17 +414,17 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider async clickFieldsTab() { log.debug('click Fields tab'); - await testSubjects.click('tab-indexFields'); + await find.clickByCssSelector('#indexedFields'); } async clickScriptedFieldsTab() { log.debug('click Scripted Fields tab'); - await testSubjects.click('tab-scriptedFields'); + await find.clickByCssSelector('#scriptedFields'); } async clickSourceFiltersTab() { log.debug('click Source Filters tab'); - await testSubjects.click('tab-sourceFilters'); + await find.clickByCssSelector('#sourceFilters'); } async editScriptedField(name: string) { diff --git a/test/scripts/jenkins_firefox_smoke.sh b/test/scripts/jenkins_firefox_smoke.sh index 0129d4f1bce9f..2bba6e06d76d7 100755 --- a/test/scripts/jenkins_firefox_smoke.sh +++ b/test/scripts/jenkins_firefox_smoke.sh @@ -6,5 +6,5 @@ checks-reporter-with-killswitch "Firefox smoke test" \ node scripts/functional_tests \ --bail --debug \ --kibana-install-dir "$installDir" \ - --include-tag "smoke" \ + --include-tag "includeFirefox" \ --config test/functional/config.firefox.js; diff --git a/test/scripts/jenkins_xpack_firefox_smoke.sh b/test/scripts/jenkins_xpack_firefox_smoke.sh index 5fe8b41cc0010..fdaee76cafa9d 100755 --- a/test/scripts/jenkins_xpack_firefox_smoke.sh +++ b/test/scripts/jenkins_xpack_firefox_smoke.sh @@ -6,5 +6,5 @@ checks-reporter-with-killswitch "X-Pack firefox smoke test" \ node scripts/functional_tests \ --debug --bail \ --kibana-install-dir "$KIBANA_INSTALL_DIR" \ - --include-tag "smoke" \ + --include-tag "includeFirefox" \ --config test/functional/config.firefox.js; diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 8e5563e4ff674..a8e50e017102f 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -15,7 +15,7 @@ "xpack.endpoint": "plugins/endpoint", "xpack.features": "plugins/features", "xpack.fileUpload": "plugins/file_upload", - "xpack.graph": ["legacy/plugins/graph", "plugins/graph"], + "xpack.graph": ["plugins/graph"], "xpack.grokDebugger": "plugins/grokdebugger", "xpack.idxMgmt": "plugins/index_management", "xpack.indexLifecycleMgmt": "plugins/index_lifecycle_management", diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index 3068cdd0daa5b..af5ace8e3cd3b 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -32,6 +32,7 @@ export function createJestConfig({ kibanaDirectory, xPackKibanaDirectory }) { '^test_utils/enzyme_helpers': `${xPackKibanaDirectory}/test_utils/enzyme_helpers.tsx`, '^test_utils/find_test_subject': `${xPackKibanaDirectory}/test_utils/find_test_subject.ts`, '^test_utils/stub_web_worker': `${xPackKibanaDirectory}/test_utils/stub_web_worker.ts`, + '^(!!)?file-loader!': fileMockPath, }, coverageDirectory: '/../target/kibana-coverage/jest', coverageReporters: !!process.env.CODE_COVERAGE ? ['json'] : ['html'], diff --git a/x-pack/index.js b/x-pack/index.js index 43ae5c3e5c5dd..e65f6cf60928e 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -5,7 +5,6 @@ */ import { xpackMain } from './legacy/plugins/xpack_main'; -import { graph } from './legacy/plugins/graph'; import { monitoring } from './legacy/plugins/monitoring'; import { reporting } from './legacy/plugins/reporting'; import { security } from './legacy/plugins/security'; @@ -18,7 +17,6 @@ import { spaces } from './legacy/plugins/spaces'; import { canvas } from './legacy/plugins/canvas'; import { infra } from './legacy/plugins/infra'; import { taskManager } from './legacy/plugins/task_manager'; -import { rollup } from './legacy/plugins/rollup'; import { siem } from './legacy/plugins/siem'; import { remoteClusters } from './legacy/plugins/remote_clusters'; import { upgradeAssistant } from './legacy/plugins/upgrade_assistant'; @@ -32,7 +30,6 @@ import { triggersActionsUI } from './legacy/plugins/triggers_actions_ui'; module.exports = function(kibana) { return [ xpackMain(kibana), - graph(kibana), monitoring(kibana), reporting(kibana), spaces(kibana), @@ -45,7 +42,6 @@ module.exports = function(kibana) { indexManagement(kibana), infra(kibana), taskManager(kibana), - rollup(kibana), siem(kibana), remoteClusters(kibana), upgradeAssistant(kibana), diff --git a/x-pack/legacy/plugins/actions/server/index.ts b/x-pack/legacy/plugins/actions/server/index.ts index 7235eda88149f..63dd6f99f9c24 100644 --- a/x-pack/legacy/plugins/actions/server/index.ts +++ b/x-pack/legacy/plugins/actions/server/index.ts @@ -6,8 +6,13 @@ import { Root } from 'joi'; import { Legacy } from 'kibana'; import mappings from './mappings.json'; +import { + LegacyPluginApi, + LegacyPluginSpec, + ArrayOrItem, +} from '../../../../../src/legacy/plugin_discovery/types'; -export function actions(kibana: any) { +export function actions(kibana: LegacyPluginApi): ArrayOrItem { return new kibana.Plugin({ id: 'actions', configPrefix: 'xpack.actions', @@ -29,5 +34,5 @@ export function actions(kibana: any) { uiExports: { mappings, }, - }); + } as Legacy.PluginSpecOptions); } diff --git a/x-pack/legacy/plugins/alerting/server/index.ts b/x-pack/legacy/plugins/alerting/server/index.ts index 5bf7cda51bda6..065af7dedebd9 100644 --- a/x-pack/legacy/plugins/alerting/server/index.ts +++ b/x-pack/legacy/plugins/alerting/server/index.ts @@ -7,8 +7,13 @@ import { Legacy } from 'kibana'; import { Root } from 'joi'; import mappings from './mappings.json'; +import { + LegacyPluginApi, + LegacyPluginSpec, + ArrayOrItem, +} from '../../../../../src/legacy/plugin_discovery/types'; -export function alerting(kibana: any) { +export function alerting(kibana: LegacyPluginApi): ArrayOrItem { return new kibana.Plugin({ id: 'alerting', configPrefix: 'xpack.alerting', @@ -31,5 +36,5 @@ export function alerting(kibana: any) { uiExports: { mappings, }, - }); + } as Legacy.PluginSpecOptions); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts index d844ac8b5988d..e0a01e9422c85 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.test.ts @@ -191,7 +191,7 @@ describe('waterfall_helpers', () => { name: 'SELECT FROM products', id: 'mySpanIdB' }, - child_ids: ['mySpanIdA', 'mySpanIdC'] + child: { id: ['mySpanIdA', 'mySpanIdC'] } } as Span, { parent: { id: 'mySpanIdD' }, @@ -294,7 +294,7 @@ describe('waterfall_helpers', () => { name: 'SELECT FROM products', id: 'mySpanIdB' }, - child_ids: ['incorrectId', 'mySpanIdC'] + child: { id: ['incorrectId', 'mySpanIdC'] } } as Span, { parent: { id: 'mySpanIdD' }, diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts index 8a873b2ddf1c9..73193cc7c9dbb 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts @@ -237,7 +237,7 @@ const getWaterfallItems = (items: TraceAPIResponse['trace']['items']) => }); /** - * Changes the parent_id of items based on the child_ids property. + * Changes the parent_id of items based on the child.id property. * Solves the problem of Inferred spans that are created as child of trace spans * when it actually should be its parent. * @param waterfallItems @@ -245,10 +245,10 @@ const getWaterfallItems = (items: TraceAPIResponse['trace']['items']) => const reparentSpans = (waterfallItems: IWaterfallItem[]) => { return waterfallItems.map(waterfallItem => { if (waterfallItem.docType === 'span') { - const { child_ids: childIds } = waterfallItem.doc; - if (childIds) { - childIds.forEach(childId => { - const item = waterfallItems.find(_item => _item.id === childId); + const childId = waterfallItem.doc.child?.id; + if (childId) { + childId.forEach(id => { + const item = waterfallItems.find(_item => _item.id === id); if (item) { item.parentId = waterfallItem.id; } diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts index 306c8e4f3fedb..2f28e37f73f62 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts @@ -2027,7 +2027,7 @@ export const inferredSpans = { id: '41226ae63af4f235', type: 'unknown' }, - child_ids: ['8d80de06aa11a6fc'] + child: { ids: ['8d80de06aa11a6fc'] } }, { container: { diff --git a/x-pack/legacy/plugins/canvas/.storybook/storyshots.test.js b/x-pack/legacy/plugins/canvas/.storybook/storyshots.test.js index 72b0b8f0e533f..a81483d1e7a17 100644 --- a/x-pack/legacy/plugins/canvas/.storybook/storyshots.test.js +++ b/x-pack/legacy/plugins/canvas/.storybook/storyshots.test.js @@ -7,6 +7,7 @@ import path from 'path'; import moment from 'moment'; import 'moment-timezone'; +import ReactDOM from "react-dom"; import initStoryshots, { multiSnapshotWithOptions } from '@storybook/addon-storyshots'; import styleSheetSerializer from 'jest-styled-components/src/styleSheetSerializer'; @@ -24,6 +25,9 @@ moment.tz.setDefault('UTC'); const testTime = new Date(Date.UTC(2019, 5, 1)); // June 1 2019 Date.now = jest.fn(() => testTime); +// Mock telemetry service +jest.mock('../public/lib/ui_metric', () => ({ trackCanvasUiMetric: () => { } })); + // Mock EUI generated ids to be consistently predictable for snapshots. jest.mock(`@elastic/eui/lib/components/form/form_row/make_id`, () => () => `generated-id`); @@ -32,7 +36,7 @@ jest.mock(`@elastic/eui/lib/components/form/form_row/make_id`, () => () => `gene jest.mock('@elastic/eui/lib/components/code/code', () => { const React = require.requireActual('react'); return { - EuiCode: ({children, className}) => ( + EuiCode: ({ children, className }) => ( {children} @@ -61,6 +65,12 @@ jest.mock('@elastic/eui/packages/react-datepicker', () => { }; }); + +// Mock React Portal for components that use modals, tooltips, etc +ReactDOM.createPortal = jest.fn((element) => { + return element; +}); + jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { return { htmlIdGenerator: () => () => `generated-id`, @@ -71,7 +81,7 @@ jest.mock('plugins/interpreter/registries', () => ({})); // Disabling this test due to https://github.com/elastic/eui/issues/2242 jest.mock( - '../public/components/workpad_header/workpad_export/flyout/__examples__/share_website_flyout.stories', + '../public/components/workpad_header/share_menu/flyout/__examples__/share_website_flyout.stories', () => { return 'Disabled Panel'; } diff --git a/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js b/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js index cc74faeac6a96..963cf831ef698 100644 --- a/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js +++ b/x-pack/legacy/plugins/canvas/.storybook/webpack.config.js @@ -177,8 +177,10 @@ module.exports = async ({ config }) => { }), // Mock out libs used by a few componets to avoid loading in kibana_legacy and platform - new webpack.NormalModuleReplacementPlugin(/lib\/notify/, path.resolve(__dirname, '../tasks/mocks/uiNotify')), + new webpack.NormalModuleReplacementPlugin(/(lib)?\/notify/, path.resolve(__dirname, '../tasks/mocks/uiNotify')), new webpack.NormalModuleReplacementPlugin(/lib\/download_workpad/, path.resolve(__dirname, '../tasks/mocks/downloadWorkpad')), + new webpack.NormalModuleReplacementPlugin(/(lib)?\/custom_element_service/, path.resolve(__dirname, '../tasks/mocks/customElementService')), + new webpack.NormalModuleReplacementPlugin(/(lib)?\/ui_metric/, path.resolve(__dirname, '../tasks/mocks/uiMetric')), ); // Tell Webpack about relevant extensions diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/area_chart/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/area_chart/header.png deleted file mode 100644 index 93456066429d9..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/area_chart/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts index 0650ac15c656e..df829e8b97676 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/area_chart/index.ts @@ -5,14 +5,13 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const areaChart: ElementFactory = () => ({ name: 'areaChart', - displayName: 'Area chart', + displayName: 'Area', help: 'A line chart with a filled body', - tags: ['chart'], - image: header, + type: 'chart', + icon: 'visArea', expression: `filters | demodata | pointseries x="time" y="mean(price)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/bubble_chart/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/bubble_chart/header.png deleted file mode 100644 index db541fe7c53b8..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/bubble_chart/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts index 7ab510e419769..7ac1d0ac83b0b 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/bubble_chart/index.ts @@ -5,16 +5,15 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const bubbleChart: ElementFactory = () => ({ name: 'bubbleChart', - displayName: 'Bubble chart', - tags: ['chart'], + displayName: 'Bubble', + type: 'chart', help: 'A customizable bubble chart', width: 700, height: 300, - image: header, + icon: 'heatmap', expression: `filters | demodata | pointseries x="project" y="sum(price)" color="state" size="size(username)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/debug/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/debug/header.png deleted file mode 100644 index 37ab329a49bb8..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/debug/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/debug/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/debug/index.ts index 914982951d664..ec8477f8f1017 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/debug/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/debug/index.ts @@ -5,14 +5,12 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const debug: ElementFactory = () => ({ name: 'debug', - displayName: 'Debug', - tags: ['text'], + displayName: 'Debug data', help: 'Just dumps the configuration of the element', - image: header, + icon: 'bug', expression: `demodata | render as=debug`, }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/donut/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/donut/header.png deleted file mode 100644 index 4bbfb6f8f68fc..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/donut/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/donut/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/donut/index.ts deleted file mode 100644 index 4ea8037d2073e..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/donut/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 { ElementFactory } from '../../../types'; -import header from './header.png'; - -export const donut: ElementFactory = () => ({ - name: 'donut', - displayName: 'Donut chart', - tags: ['chart', 'proportion'], - help: 'A customizable donut chart', - image: header, - expression: `filters -| demodata -| pointseries color="project" size="max(price)" -| pie hole=50 labels=false legend="ne" -| render`, -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/dropdown_filter/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/dropdown_filter/header.png deleted file mode 100644 index 727b4d23941fd..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/dropdown_filter/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/dropdown_filter/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/dropdown_filter/index.ts index bde223d2a606e..bb1c13ca618be 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/dropdown_filter/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/dropdown_filter/index.ts @@ -5,14 +5,13 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const dropdownFilter: ElementFactory = () => ({ - name: 'dropdown_filter', - displayName: 'Dropdown filter', - tags: ['filter'], + name: 'dropdownFilter', + displayName: 'Dropdown select', + type: 'filter', help: 'A dropdown from which you can select values for an "exactly" filter', - image: header, + icon: 'filter', height: 50, expression: `demodata | dropdownControl valueColumn=project filterColumn=project | render`, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/filter_debug/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/filter_debug/index.ts new file mode 100644 index 0000000000000..35a4a75f49c4e --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/filter_debug/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ElementFactory } from '../../../types'; + +export const filterDebug: ElementFactory = () => ({ + name: 'filterDebug', + displayName: 'Debug filter', + help: 'Shows the underlying global filters in a workpad', + icon: 'bug', + expression: `filters +| render as=debug`, +}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/header.png deleted file mode 100644 index 9b6ee47d88698..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts index 7fddf48c70385..9567336decd5d 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_bar_chart/index.ts @@ -5,14 +5,13 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const horizontalBarChart: ElementFactory = () => ({ name: 'horizontalBarChart', - displayName: 'Horizontal bar chart', - tags: ['chart'], + displayName: 'Bar horizontal', + type: 'chart', help: 'A customizable horizontal bar chart', - image: header, + icon: 'visBarHorizontal', expression: `filters | demodata | pointseries x="size(cost)" y="project" color="project" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/header.png deleted file mode 100644 index f28ad4a3ce4be..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts index f4a50a007c5de..529a74893a5de 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_bar/index.ts @@ -6,16 +6,14 @@ import { openSans } from '../../../common/lib/fonts'; import { ElementFactory } from '../../../types'; -import header from './header.png'; export const horizontalProgressBar: ElementFactory = () => ({ name: 'horizontalProgressBar', - displayName: 'Horizontal progress bar', - tags: ['chart', 'proportion'], + displayName: 'Horizontal bar', + type: 'progress', help: 'Displays progress as a portion of a horizontal bar', width: 400, height: 30, - image: header, expression: `filters | demodata | math "mean(percent_uptime)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/header.png deleted file mode 100644 index 2eaeb2e976a78..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts index 9b3aea2e55324..d5eba32325d1a 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/horizontal_progress_pill/index.ts @@ -6,16 +6,14 @@ import { openSans } from '../../../common/lib/fonts'; import { ElementFactory } from '../../../types'; -import header from './header.png'; export const horizontalProgressPill: ElementFactory = () => ({ name: 'horizontalProgressPill', - displayName: 'Horizontal progress pill', - tags: ['chart', 'proportion'], + displayName: 'Horizontal pill', + type: 'progress', help: 'Displays progress as a portion of a horizontal pill', width: 400, height: 30, - image: header, expression: `filters | demodata | math "mean(percent_uptime)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/image/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/image/header.png deleted file mode 100644 index 7f29fc64c36b9..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/image/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/image/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/image/index.ts index eec1e2af61aad..ed7f6a99ddc32 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/image/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/image/index.ts @@ -5,14 +5,13 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const image: ElementFactory = () => ({ name: 'image', displayName: 'Image', - tags: ['graphic'], + type: 'image', help: 'A static image', - image: header, + icon: 'image', expression: `image dataurl=null mode="contain" | render`, }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/index.ts index 6c0cd7eb33dc0..ec3b8a7798be1 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/index.ts @@ -8,8 +8,8 @@ import { applyElementStrings } from '../../i18n/elements'; import { areaChart } from './area_chart'; import { bubbleChart } from './bubble_chart'; import { debug } from './debug'; -import { donut } from './donut'; import { dropdownFilter } from './dropdown_filter'; +import { filterDebug } from './filter_debug'; import { horizontalBarChart } from './horizontal_bar_chart'; import { horizontalProgressBar } from './horizontal_progress_bar'; import { horizontalProgressPill } from './horizontal_progress_pill'; @@ -26,7 +26,6 @@ import { repeatImage } from './repeat_image'; import { revealImage } from './reveal_image'; import { shape } from './shape'; import { table } from './table'; -import { tiltedPie } from './tilted_pie'; import { timeFilter } from './time_filter'; import { verticalBarChart } from './vert_bar_chart'; import { verticalProgressBar } from './vertical_progress_bar'; @@ -39,8 +38,8 @@ const elementSpecs = [ areaChart, bubbleChart, debug, - donut, dropdownFilter, + filterDebug, image, horizontalBarChart, horizontalProgressBar, @@ -56,7 +55,6 @@ const elementSpecs = [ revealImage, shape, table, - tiltedPie, timeFilter, verticalBarChart, verticalProgressBar, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/line_chart/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/line_chart/header.png deleted file mode 100644 index eea133ee3680b..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/line_chart/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts index 5b8533eea65bc..d19ddeb00dd67 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/line_chart/index.ts @@ -5,14 +5,13 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const lineChart: ElementFactory = () => ({ name: 'lineChart', - displayName: 'Line chart', - tags: ['chart'], + displayName: 'Line', + type: 'chart', help: 'A customizable line chart', - image: header, + icon: 'visLine', expression: `filters | demodata | pointseries x="time" y="mean(price)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/markdown/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/markdown/header.png deleted file mode 100644 index a8b8550f5baea..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/markdown/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts index 1c7013834cbe4..7b114daa11870 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/markdown/index.ts @@ -4,15 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import header from './header.png'; - import { ElementFactory } from '../../../types'; export const markdown: ElementFactory = () => ({ name: 'markdown', - displayName: 'Markdown', - tags: ['text'], - help: 'Markup from Markdown', - image: header, + displayName: 'Text', + type: 'text', + help: 'Add text using Markdown', + icon: 'visText', expression: `filters | demodata | markdown "### Welcome to the Markdown element diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/header.png deleted file mode 100644 index 0510342cdc54a..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/index.ts index 7e9aca9c01782..7256657903aab 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/index.ts @@ -5,8 +5,6 @@ */ import { openSans } from '../../../common/lib/fonts'; -import header from './header.png'; - import { ElementFactory } from '../../../types'; import { SetupInitializer } from '../../plugin'; @@ -14,11 +12,11 @@ export const metricElementInitializer: SetupInitializer = (core, return () => ({ name: 'metric', displayName: 'Metric', - tags: ['text'], + type: 'chart', help: 'A number with a label', width: 200, height: 100, - image: header, + icon: 'visMetric', expression: `filters | demodata | math "unique(country)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/pie/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/pie/header.png deleted file mode 100644 index deecd1067427c..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/pie/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/pie/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/pie/index.ts index cfb9031325254..b7606ea94bc9f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/pie/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/pie/index.ts @@ -4,17 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import header from './header.png'; - import { ElementFactory } from '../../../types'; export const pie: ElementFactory = () => ({ name: 'pie', - displayName: 'Pie chart', - tags: ['chart', 'proportion'], + displayName: 'Pie', + type: 'chart', width: 300, height: 300, help: 'A simple pie chart', - image: header, + icon: 'visPie', expression: `filters | demodata | pointseries color="state" size="max(price)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/plot/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/plot/header.png deleted file mode 100644 index d48c789ae5a92..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/plot/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/plot/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/plot/index.ts index dd1660d558667..8648b65def4b2 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/plot/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/plot/index.ts @@ -5,14 +5,12 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const plot: ElementFactory = () => ({ name: 'plot', displayName: 'Coordinate plot', - tags: ['chart'], + type: 'chart', help: 'Mixed line, bar or dot charts', - image: header, expression: `filters | demodata | pointseries x="time" y="sum(price)" color="state" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_gauge/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_gauge/header.png deleted file mode 100644 index 8340c8a53b6ce..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_gauge/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts index 4ec192fb787fe..b21b7df286ace 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_gauge/index.ts @@ -6,16 +6,15 @@ import { openSans } from '../../../common/lib/fonts'; import { ElementFactory } from '../../../types'; -import header from './header.png'; export const progressGauge: ElementFactory = () => ({ name: 'progressGauge', - displayName: 'Progress gauge', - tags: ['chart', 'proportion'], + displayName: 'Gauge', + type: 'progress', help: 'Displays progress as a portion of a gauge', width: 200, height: 200, - image: header, + icon: 'visGoal', expression: `filters | demodata | math "mean(percent_uptime)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/header.png deleted file mode 100644 index b5b708529edd4..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts index 91fcb24996bc0..9ccb9489e8306 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_semicircle/index.ts @@ -6,16 +6,14 @@ import { openSans } from '../../../common/lib/fonts'; import { ElementFactory } from '../../../types'; -import header from './header.png'; export const progressSemicircle: ElementFactory = () => ({ name: 'progressSemicircle', - displayName: 'Progress semicircle', - tags: ['chart', 'proportion'], + displayName: 'Semicircle', + type: 'progress', help: 'Displays progress as a portion of a semicircle', width: 200, height: 100, - image: header, expression: `filters | demodata | math "mean(percent_uptime)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_wheel/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_wheel/header.png deleted file mode 100644 index 71e5d7e29444e..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_wheel/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts index 05c537f88756b..42bf36346c303 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/progress_wheel/index.ts @@ -6,16 +6,14 @@ import { openSans } from '../../../common/lib/fonts'; import { ElementFactory } from '../../../types'; -import header from './header.png'; export const progressWheel: ElementFactory = () => ({ name: 'progressWheel', - displayName: 'Progress wheel', - tags: ['chart', 'proportion'], + displayName: 'Wheel', + type: 'progress', help: 'Displays progress as a portion of a wheel', width: 200, height: 200, - image: header, expression: `filters | demodata | math "mean(percent_uptime)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/repeat_image/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/repeat_image/header.png deleted file mode 100644 index 9843c9a6d02c0..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/repeat_image/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts index df79651620642..0bf0ec4c2dc1f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/repeat_image/index.ts @@ -5,14 +5,12 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const repeatImage: ElementFactory = () => ({ name: 'repeatImage', displayName: 'Image repeat', - tags: ['graphic', 'proportion'], + type: 'image', help: 'Repeats an image N times', - image: header, expression: `filters | demodata | math "mean(cost)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/reveal_image/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/reveal_image/header.png deleted file mode 100644 index 8dc33b5a7259e..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/reveal_image/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts index 01c66ed3a26ec..88000f6b66a91 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/reveal_image/index.ts @@ -5,14 +5,12 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const revealImage: ElementFactory = () => ({ name: 'revealImage', displayName: 'Image reveal', - tags: ['graphic', 'proportion'], + type: 'image', help: 'Reveals a percentage of an image', - image: header, expression: `filters | demodata | math "mean(percent_uptime)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/shape/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/shape/header.png deleted file mode 100644 index 3212d47591c07..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/shape/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/shape/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/shape/index.ts index 3f3954ff02b02..f922473fa818f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/shape/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/shape/index.ts @@ -5,16 +5,15 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const shape: ElementFactory = () => ({ name: 'shape', displayName: 'Shape', - tags: ['graphic'], + type: 'shape', help: 'A customizable shape', width: 200, height: 200, - image: header, + icon: 'node', expression: 'shape "square" fill="#4cbce4" border="rgba(255,255,255,0)" borderWidth=0 maintainAspect=false | render', }); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/table/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/table/header.png deleted file mode 100644 index a883faa693c1f..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/table/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/table/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/table/index.ts index ac13b7dc21293..ec26773fc3bf9 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/table/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/table/index.ts @@ -5,14 +5,13 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const table: ElementFactory = () => ({ name: 'table', displayName: 'Data table', - tags: ['text'], + type: 'chart', help: 'A scrollable grid for displaying data in a tabular format', - image: header, + icon: 'visTable', expression: `filters | demodata | table diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/tilted_pie/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/tilted_pie/header.png deleted file mode 100644 index b3329f991158c..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/tilted_pie/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/tilted_pie/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/tilted_pie/index.ts deleted file mode 100644 index 21d8ba1e1b04a..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/tilted_pie/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. - */ - -import { ElementFactory } from '../../../types'; -import header from './header.png'; - -export const tiltedPie: ElementFactory = () => ({ - name: 'tiltedPie', - displayName: 'Tilted pie chart', - tags: ['chart', 'proportion'], - width: 500, - height: 250, - help: 'A customizable tilted pie chart', - image: header, - expression: `filters -| demodata -| pointseries color="project" size="max(price)" -| pie tilt=0.5 -| render`, -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/time_filter/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/time_filter/header.png deleted file mode 100644 index d36b4cc97e5b1..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/time_filter/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/time_filter/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/time_filter/index.ts index b384707c243d1..702ccb8a2312f 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/time_filter/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/time_filter/index.ts @@ -5,14 +5,13 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const timeFilter: ElementFactory = () => ({ - name: 'time_filter', + name: 'timeFilter', displayName: 'Time filter', - tags: ['filter'], + type: 'filter', help: 'Set a time window', - image: header, + icon: 'calendar', height: 50, expression: `timefilterControl compact=true column=@timestamp | render`, diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/header.png deleted file mode 100644 index 90505dd0dc77d..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts index ac4e3a0a72150..2354be0328e7c 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vert_bar_chart/index.ts @@ -5,14 +5,13 @@ */ import { ElementFactory } from '../../../types'; -import header from './header.png'; export const verticalBarChart: ElementFactory = () => ({ name: 'verticalBarChart', displayName: 'Vertical bar chart', - tags: ['chart'], + type: 'chart', help: 'A customizable vertical bar chart', - image: header, + icon: 'visBarVertical', expression: `filters | demodata | pointseries x="project" y="size(cost)" color="project" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/header.png deleted file mode 100644 index b9ff963e92c31..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts index e12903dafede9..b5e6c9816e3f5 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_bar/index.ts @@ -6,16 +6,14 @@ import { openSans } from '../../../common/lib/fonts'; import { ElementFactory } from '../../../types'; -import header from './header.png'; export const verticalProgressBar: ElementFactory = () => ({ name: 'verticalProgressBar', displayName: 'Vertical progress bar', - tags: ['chart', 'proportion'], + type: 'progress', help: 'Displays progress as a portion of a vertical bar', width: 80, height: 400, - image: header, expression: `filters | demodata | math "mean(percent_uptime)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/header.png b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/header.png deleted file mode 100644 index a4ac6b57da236..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts index 8926a12da8a47..28e80372494db 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/vertical_progress_pill/index.ts @@ -6,16 +6,14 @@ import { openSans } from '../../../common/lib/fonts'; import { ElementFactory } from '../../../types'; -import header from './header.png'; export const verticalProgressPill: ElementFactory = () => ({ name: 'verticalProgressPill', displayName: 'Vertical progress pill', - tags: ['chart', 'proportion'], + type: 'progress', help: 'Displays progress as a portion of a vertical pill', width: 80, height: 400, - image: header, expression: `filters | demodata | math "mean(percent_uptime)" diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx index a1096d50c1653..ee08dfb87e1c1 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx @@ -6,7 +6,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { I18nContext } from 'ui/i18n'; import { CoreStart } from '../../../../../../../src/core/public'; import { StartDeps } from '../../plugin'; import { @@ -30,6 +29,8 @@ const embeddablesRegistry: { } = {}; const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => { + const I18nContext = core.i18n.Context; + return (embeddableObject: IEmbeddable, domNode: HTMLElement) => { return (
({ - name: strings.chart(), - color: euiVisPalette[4], -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/filter.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/filter.ts deleted file mode 100644 index 5249856dec271..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/filter.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { euiPaletteColorBlind } from '@elastic/eui'; -import { TagFactory } from '../../../public/lib/tag'; -import { TagStrings as strings } from '../../../i18n'; - -const euiVisPalette = euiPaletteColorBlind(); - -export const filter: TagFactory = () => ({ - name: strings.filter(), - color: euiVisPalette[1], -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/graphic.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/graphic.ts deleted file mode 100644 index 36d66801ef681..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/graphic.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 { euiPaletteColorBlind } from '@elastic/eui'; -import { TagFactory } from '../../../public/lib/tag'; -import { TagStrings as strings } from '../../../i18n'; -const euiVisPalette = euiPaletteColorBlind(); - -export const graphic: TagFactory = () => ({ - name: strings.graphic(), - color: euiVisPalette[5], -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/index.ts index 2587665a452b5..2e711437c72a8 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/index.ts @@ -4,13 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { chart } from './chart'; -import { filter } from './filter'; -import { graphic } from './graphic'; import { presentation } from './presentation'; -import { proportion } from './proportion'; import { report } from './report'; -import { text } from './text'; // Registry expects a function that returns a spec object -export const tagSpecs = [chart, filter, graphic, presentation, proportion, report, text]; +export const tagSpecs = [presentation, report]; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.ts deleted file mode 100644 index 4d37ecfaa367a..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.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 { euiPaletteColorBlind } from '@elastic/eui'; -import { TagFactory } from '../../../public/lib/tag'; -import { TagStrings as strings } from '../../../i18n'; -const euiVisPalette = euiPaletteColorBlind(); - -export const proportion: TagFactory = () => ({ - name: strings.proportion(), - color: euiVisPalette[3], -}); diff --git a/x-pack/legacy/plugins/canvas/common/lib/constants.ts b/x-pack/legacy/plugins/canvas/common/lib/constants.ts index ac8e80b8d7b89..a37dc3fd6a7b3 100644 --- a/x-pack/legacy/plugins/canvas/common/lib/constants.ts +++ b/x-pack/legacy/plugins/canvas/common/lib/constants.ts @@ -40,3 +40,4 @@ export const API_ROUTE_SHAREABLE_ZIP = '/public/canvas/zip'; export const API_ROUTE_SHAREABLE_RUNTIME = '/public/canvas/runtime'; export const API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD = `/public/canvas/${SHAREABLE_RUNTIME_NAME}.js`; export const CANVAS_EMBEDDABLE_CLASSNAME = `canvasEmbeddable`; +export const CONTEXT_MENU_TOP_BORDER_CLASSNAME = 'canvasContextMenu--topBorder'; diff --git a/x-pack/legacy/plugins/canvas/i18n/components.ts b/x-pack/legacy/plugins/canvas/i18n/components.ts index d0a9051d7af87..7bd16c4933ce1 100644 --- a/x-pack/legacy/plugins/canvas/i18n/components.ts +++ b/x-pack/legacy/plugins/canvas/i18n/components.ts @@ -15,7 +15,7 @@ export const ComponentStrings = { }), getTitleText: () => i18n.translate('xpack.canvas.embedObject.titleText', { - defaultMessage: 'Embed Object', + defaultMessage: 'Add from Visualize library', }), }, AdvancedFilter: { @@ -305,21 +305,21 @@ export const ComponentStrings = { }), }, ElementControls: { - getEditTooltip: () => - i18n.translate('xpack.canvas.elementControls.editToolTip', { - defaultMessage: 'Edit', - }), - getEditAriaLabel: () => - i18n.translate('xpack.canvas.elementControls.editAriaLabel', { - defaultMessage: 'Edit element', + getDeleteAriaLabel: () => + i18n.translate('xpack.canvas.elementControls.deleteAriaLabel', { + defaultMessage: 'Delete element', }), getDeleteTooltip: () => i18n.translate('xpack.canvas.elementControls.deleteToolTip', { defaultMessage: 'Delete', }), - getDeleteAriaLabel: () => - i18n.translate('xpack.canvas.elementControls.deleteAriaLabel', { - defaultMessage: 'Delete element', + getEditAriaLabel: () => + i18n.translate('xpack.canvas.elementControls.editAriaLabel', { + defaultMessage: 'Edit element', + }), + getEditTooltip: () => + i18n.translate('xpack.canvas.elementControls.editToolTip', { + defaultMessage: 'Edit', }), }, ElementSettings: { @@ -336,53 +336,6 @@ export const ComponentStrings = { description: 'This tab contains the settings for how data is displayed in a Canvas element', }), }, - ElementTypes: { - getEditElementTitle: () => - i18n.translate('xpack.canvas.elementTypes.editElementTitle', { - defaultMessage: 'Edit element', - }), - getDeleteElementTitle: (elementName: string) => - i18n.translate('xpack.canvas.elementTypes.deleteElementTitle', { - defaultMessage: `Delete element '{elementName}'?`, - values: { - elementName, - }, - }), - getDeleteElementDescription: () => - i18n.translate('xpack.canvas.elementTypes.deleteElementDescription', { - defaultMessage: 'Are you sure you want to delete this element?', - }), - getCancelButtonLabel: () => - i18n.translate('xpack.canvas.elementTypes.cancelButtonLabel', { - defaultMessage: 'Cancel', - }), - getDeleteButtonLabel: () => - i18n.translate('xpack.canvas.elementTypes.deleteButtonLabel', { - defaultMessage: 'Delete', - }), - getAddNewElementTitle: () => - i18n.translate('xpack.canvas.elementTypes.addNewElementTitle', { - defaultMessage: 'Add new elements', - }), - getAddNewElementDescription: () => - i18n.translate('xpack.canvas.elementTypes.addNewElementDescription', { - defaultMessage: 'Group and save workpad elements to create new elements', - }), - getFindElementPlaceholder: () => - i18n.translate('xpack.canvas.elementTypes.findElementPlaceholder', { - defaultMessage: 'Find element', - }), - getElementsTitle: () => - i18n.translate('xpack.canvas.elementTypes.elementsTitle', { - defaultMessage: 'Elements', - description: 'Title for the "Elements" tab when adding a new element', - }), - getMyElementsTitle: () => - i18n.translate('xpack.canvas.elementTypes.myElementsTitle', { - defaultMessage: 'My elements', - description: 'Title for the "My elements" tab when adding a new element', - }), - }, Error: { getDescription: () => i18n.translate('xpack.canvas.errorComponent.description', { @@ -633,6 +586,61 @@ export const ComponentStrings = { defaultMessage: 'Delete', }), }, + SavedElementsModal: { + getAddNewElementDescription: () => + i18n.translate('xpack.canvas.savedElementsModal.addNewElementDescription', { + defaultMessage: 'Group and save workpad elements to create new elements', + }), + getAddNewElementTitle: () => + i18n.translate('xpack.canvas.savedElementsModal.addNewElementTitle', { + defaultMessage: 'Add new elements', + }), + getCancelButtonLabel: () => + i18n.translate('xpack.canvas.savedElementsModal.cancelButtonLabel', { + defaultMessage: 'Cancel', + }), + getDeleteButtonLabel: () => + i18n.translate('xpack.canvas.savedElementsModal.deleteButtonLabel', { + defaultMessage: 'Delete', + }), + getDeleteElementDescription: () => + i18n.translate('xpack.canvas.savedElementsModal.deleteElementDescription', { + defaultMessage: 'Are you sure you want to delete this element?', + }), + getDeleteElementTitle: (elementName: string) => + i18n.translate('xpack.canvas.savedElementsModal.deleteElementTitle', { + defaultMessage: `Delete element '{elementName}'?`, + values: { + elementName, + }, + }), + getEditElementTitle: () => + i18n.translate('xpack.canvas.savedElementsModal.editElementTitle', { + defaultMessage: 'Edit element', + }), + getElementsTitle: () => + i18n.translate('xpack.canvas.savedElementsModal.elementsTitle', { + defaultMessage: 'Elements', + description: 'Title for the "Elements" tab when adding a new element', + }), + getFindElementPlaceholder: () => + i18n.translate('xpack.canvas.savedElementsModal.findElementPlaceholder', { + defaultMessage: 'Find element', + }), + getModalTitle: () => + i18n.translate('xpack.canvas.savedElementsModal.modalTitle', { + defaultMessage: 'My elements', + }), + getMyElementsTitle: () => + i18n.translate('xpack.canvas.savedElementsModal.myElementsTitle', { + defaultMessage: 'My elements', + description: 'Title for the "My elements" tab when adding a new element', + }), + getSavedElementsModalCloseButtonLabel: () => + i18n.translate('xpack.canvas.workpadHeader.addElementModalCloseButtonLabel', { + defaultMessage: 'Close', + }), + }, ShareWebsiteFlyout: { getRuntimeStepTitle: () => i18n.translate('xpack.canvas.shareWebsiteFlyout.snippetsStep.downloadRuntimeTitle', { @@ -652,7 +660,7 @@ export const ComponentStrings = { defaultMessage: 'Share on a website', }), getUnsupportedRendererWarning: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.unsupportedRendererWarning', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.unsupportedRendererWarning', { defaultMessage: 'This workpad contains render functions that are not supported by the {CANVAS} Shareable Workpad Runtime. These elements will not be rendered:', values: { @@ -900,6 +908,10 @@ export const ComponentStrings = { i18n.translate('xpack.canvas.textStylePicker.alignRightOption', { defaultMessage: 'Align right', }), + getFontColorLabel: () => + i18n.translate('xpack.canvas.textStylePicker.fontColorLabel', { + defaultMessage: 'Font Color', + }), getStyleBoldOption: () => i18n.translate('xpack.canvas.textStylePicker.styleBoldOption', { defaultMessage: 'Bold', @@ -912,10 +924,6 @@ export const ComponentStrings = { i18n.translate('xpack.canvas.textStylePicker.styleUnderlineOption', { defaultMessage: 'Underline', }), - getFontColorLabel: () => - i18n.translate('xpack.canvas.textStylePicker.fontColorLabel', { - defaultMessage: 'Font Color', - }), }, TimePicker: { getApplyButtonLabel: () => @@ -962,6 +970,10 @@ export const ComponentStrings = { description: '"stylesheet" refers to the collection of CSS style rules entered by the user.', }), + getBackgroundColorLabel: () => + i18n.translate('xpack.canvas.workpadConfig.backgroundColorLabel', { + defaultMessage: 'Background color', + }), getFlipDimensionAriaLabel: () => i18n.translate('xpack.canvas.workpadConfig.swapDimensionsAriaLabel', { defaultMessage: `Swap the page's width and height`, @@ -1013,10 +1025,6 @@ export const ComponentStrings = { defaultMessage: 'US Letter', description: 'This is referring to the dimensions of U.S. standard letter paper.', }), - getBackgroundColorLabel: () => - i18n.translate('xpack.canvas.workpadConfig.backgroundColorLabel', { - defaultMessage: 'Background color', - }), }, WorkpadCreate: { getWorkpadCreateButtonLabel: () => @@ -1029,14 +1037,6 @@ export const ComponentStrings = { i18n.translate('xpack.canvas.workpadHeader.addElementButtonLabel', { defaultMessage: 'Add element', }), - getAddElementModalCloseButtonLabel: () => - i18n.translate('xpack.canvas.workpadHeader.addElementModalCloseButtonLabel', { - defaultMessage: 'Close', - }), - getEmbedObjectButtonLabel: () => - i18n.translate('xpack.canvas.workpadHeader.embedObjectButtonLabel', { - defaultMessage: 'Embed object', - }), getFullScreenButtonAriaLabel: () => i18n.translate('xpack.canvas.workpadHeader.fullscreenButtonAriaLabel', { defaultMessage: 'View fullscreen', @@ -1080,9 +1080,9 @@ export const ComponentStrings = { }), }, WorkpadHeaderControlSettings: { - getTooltip: () => - i18n.translate('xpack.canvas.workpadHeaderControlSettings.settingsTooltip', { - defaultMessage: 'Control settings', + getButtonLabel: () => + i18n.translate('xpack.canvas.workpadHeaderControlSettings.buttonLabel', { + defaultMessage: 'Options', }), }, WorkpadHeaderCustomInterval: { @@ -1105,6 +1105,56 @@ export const ComponentStrings = { defaultMessage: 'Set a custom interval', }), }, + WorkpadHeaderElementMenu: { + getAssetsMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.manageAssetsMenuItemLabel', { + defaultMessage: 'Manage assets', + }), + getChartMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.chartMenuItemLabel', { + defaultMessage: 'Chart', + }), + getElementMenuButtonLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.elementMenuButtonLabel', { + defaultMessage: 'Add element', + }), + getElementMenuLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.elementMenuLabel', { + defaultMessage: 'Add an element', + }), + getEmbedObjectMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.embedObjectMenuItemLabel', { + defaultMessage: 'Add from Visualize library', + }), + getFilterMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.filterMenuItemLabel', { + defaultMessage: 'Filter', + }), + getImageMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.imageMenuItemLabel', { + defaultMessage: 'Image', + }), + getMyElementsMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.myElementsMenuItemLabel', { + defaultMessage: 'My elements', + }), + getOtherMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.otherMenuItemLabel', { + defaultMessage: 'Other', + }), + getProgressMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.progressMenuItemLabel', { + defaultMessage: 'Progress', + }), + getShapeMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.shapeMenuItemLabel', { + defaultMessage: 'Shape', + }), + getTextMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderElementMenu.textMenuItemLabel', { + defaultMessage: 'Text', + }), + }, WorkpadHeaderKioskControls: { getCycleFormLabel: () => i18n.translate('xpack.canvas.workpadHeaderKioskControl.cycleFormLabel', { @@ -1129,9 +1179,9 @@ export const ComponentStrings = { defaultMessage: 'Refresh data', }), }, - WorkpadHeaderWorkpadExport: { + WorkpadHeaderShareMenu: { getCopyPDFMessage: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.copyPDFMessage', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.copyPDFMessage', { defaultMessage: 'The {PDF} generation {URL} was copied to your clipboard.', values: { PDF, @@ -1139,15 +1189,15 @@ export const ComponentStrings = { }, }), getCopyReportingConfigMessage: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.copyReportingConfigMessage', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.copyReportingConfigMessage', { defaultMessage: 'Copied reporting configuration to clipboard', }), getCopyShareConfigMessage: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.copyShareConfigMessage', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.copyShareConfigMessage', { defaultMessage: 'Copied share markup to clipboard', }), getExportPDFErrorTitle: (workpadName: string) => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.exportPDFErrorMessage', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.exportPDFErrorMessage', { defaultMessage: "Failed to create {PDF} for '{workpadName}'", values: { PDF, @@ -1155,14 +1205,14 @@ export const ComponentStrings = { }, }), getExportPDFMessage: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.exportPDFMessage', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.exportPDFMessage', { defaultMessage: 'Exporting {PDF}. You can track the progress in Management.', values: { PDF, }, }), getExportPDFTitle: (workpadName: string) => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.exportPDFTitle', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.exportPDFTitle', { defaultMessage: "{PDF} export of workpad '{workpadName}'", values: { PDF, @@ -1170,7 +1220,7 @@ export const ComponentStrings = { }, }), getPDFPanelCopyAriaLabel: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyAriaLabel', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyAriaLabel', { defaultMessage: 'Alternatively, you can generate a {PDF} from a script or with Watcher by using this {URL}. Press Enter to copy the {URL} to clipboard.', values: { @@ -1179,7 +1229,7 @@ export const ComponentStrings = { }, }), getPDFPanelCopyButtonLabel: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyButtonLabel', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyButtonLabel', { defaultMessage: 'Copy {POST} {URL}', values: { POST, @@ -1187,7 +1237,7 @@ export const ComponentStrings = { }, }), getPDFPanelCopyDescription: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyDescription', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyDescription', { defaultMessage: 'Alternatively, copy this {POST} {URL} to call generation from outside {KIBANA} or from Watcher.', values: { @@ -1197,14 +1247,14 @@ export const ComponentStrings = { }, }), getPDFPanelGenerateButtonLabel: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.pdfPanelGenerateButtonLabel', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.pdfPanelGenerateButtonLabel', { defaultMessage: 'Generate {PDF}', values: { PDF, }, }), getPDFPanelGenerateDescription: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.pdfPanelGenerateDescription', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.pdfPanelGenerateDescription', { defaultMessage: '{PDF}s can take a minute or two to generate based on the size of your workpad.', values: { @@ -1212,7 +1262,7 @@ export const ComponentStrings = { }, }), getShareableZipErrorTitle: (workpadName: string) => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.shareWebsiteErrorTitle', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareWebsiteErrorTitle', { defaultMessage: "Failed to create {ZIP} file for '{workpadName}'. The workpad may be too large. You'll need to download the files separately.", values: { @@ -1221,69 +1271,101 @@ export const ComponentStrings = { }, }), getShareDownloadJSONTitle: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.shareDownloadJSONTitle', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareDownloadJSONTitle', { defaultMessage: 'Download as {JSON}', values: { JSON, }, }), getShareDownloadPDFTitle: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.shareDownloadPDFTitle', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareDownloadPDFTitle', { defaultMessage: '{PDF} reports', values: { PDF, }, }), + getShareMenuButtonLabel: () => + i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareMenuButtonLabel', { + defaultMessage: 'Share', + }), getShareWebsiteTitle: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.shareWebsiteTitle', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareWebsiteTitle', { defaultMessage: 'Share on a website', }), getShareWorkpadMessage: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.shareWorkpadMessage', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.shareWorkpadMessage', { defaultMessage: 'Share this workpad', }), getUnknownExportErrorMessage: (type: string) => - i18n.translate('xpack.canvas.workpadHeaderWorkpadExport.unknownExportErrorMessage', { + i18n.translate('xpack.canvas.workpadHeaderShareMenu.unknownExportErrorMessage', { defaultMessage: 'Unknown export type: {type}', values: { type, }, }), }, - WorkpadHeaderWorkpadZoom: { + WorkpadHeaderViewMenu: { + getFullscreenMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.fullscreenMenuLabel', { + defaultMessage: 'Enter fullscreen mode', + }), + getHideEditModeLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.hideEditModeLabel', { + defaultMessage: 'Hide editing controls', + }), + getRefreshMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.refreshMenuItemLabel', { + defaultMessage: 'Refresh data', + }), + getShowEditModeLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.showEditModeLabel', { + defaultMessage: 'Show editing controls', + }), + getViewMenuButtonLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.viewMenuButtonLabel', { + defaultMessage: 'View', + }), + getViewMenuLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.viewMenuLabel', { + defaultMessage: 'View options', + }), getZoomControlsAriaLabel: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadZoom.zoomControlsAriaLabel', { + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomControlsAriaLabel', { defaultMessage: 'Zoom controls', }), getZoomControlsTooltip: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadZoom.zoomControlsTooltip', { + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomControlsTooltip', { defaultMessage: 'Zoom controls', }), getZoomFitToWindowText: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadZoom.zoomFitToWindowText', { + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomFitToWindowText', { defaultMessage: 'Fit to window', }), getZoomInText: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadZoom.zoomInText', { + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomInText', { defaultMessage: 'Zoom in', }), + getZoomMenuItemLabel: () => + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomMenuItemLabel', { + defaultMessage: 'Zoom', + }), getZoomOutText: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadZoom.zoomOutText', { + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomOutText', { defaultMessage: 'Zoom out', }), getZoomPanelTitle: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadZoom.zoomPanelTitle', { + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomPanelTitle', { defaultMessage: 'Zoom', }), getZoomPercentage: (scale: number) => - i18n.translate('xpack.canvas.workpadHeaderWorkpadZoom.zoomResetText', { + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomResetText', { defaultMessage: '{scalePercentage}%', values: { scalePercentage: scale * 100, }, }), getZoomResetText: () => - i18n.translate('xpack.canvas.workpadHeaderWorkpadZoom.zoomPrecentageValue', { + i18n.translate('xpack.canvas.workpadHeaderViewMenu.zoomPrecentageValue', { defaultMessage: 'Reset', }), }, diff --git a/x-pack/legacy/plugins/canvas/i18n/elements/apply_strings.ts b/x-pack/legacy/plugins/canvas/i18n/elements/apply_strings.ts index 41f88a3c75f90..4464ed5dbf185 100644 --- a/x-pack/legacy/plugins/canvas/i18n/elements/apply_strings.ts +++ b/x-pack/legacy/plugins/canvas/i18n/elements/apply_strings.ts @@ -7,8 +7,6 @@ import { ElementFactory } from '../../types'; import { getElementStrings } from './index'; -import { TagStrings } from '../../i18n'; - /** * This function takes a set of Canvas Element specification factories, runs them, * replaces relevant strings (if available) and returns a new factory. We do this @@ -34,17 +32,6 @@ export const applyElementStrings = (elements: ElementFactory[]) => { if (displayName) { result.displayName = displayName; } - - // Set translated tags - if (result.tags) { - result.tags = result.tags.map(tag => { - if (tag in TagStrings) { - return TagStrings[tag](); - } - - return tag; - }); - } } return () => result; diff --git a/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.test.ts b/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.test.ts index ce908ac6c5566..c28229bdab33f 100644 --- a/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.test.ts +++ b/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.test.ts @@ -9,8 +9,6 @@ import { coreMock } from '../../../../../../src/core/public/mocks'; const elementSpecs = initializeElements(coreMock.createSetup() as any, {} as any); -import { TagStrings } from '../tags'; - describe('ElementStrings', () => { const elementStrings = getElementStrings(); const elementNames = elementSpecs.map(spec => spec().name); @@ -37,15 +35,4 @@ describe('ElementStrings', () => { expect(value).toHaveProperty('help'); }); }); - - test('All elements should have tags that are defined', () => { - const tagNames = Object.keys(TagStrings); - - elementSpecs.forEach(spec => { - const element = spec(); - if (element.tags) { - element.tags.forEach((tagName: string) => expect(tagNames).toContain(tagName)); - } - }); - }); }); diff --git a/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.ts b/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.ts index df23dff1c7127..595ef4cb92206 100644 --- a/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.ts +++ b/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.ts @@ -23,7 +23,7 @@ interface ElementStringDict { export const getElementStrings = (): ElementStringDict => ({ areaChart: { displayName: i18n.translate('xpack.canvas.elements.areaChartDisplayName', { - defaultMessage: 'Area chart', + defaultMessage: 'Area', }), help: i18n.translate('xpack.canvas.elements.areaChartHelpText', { defaultMessage: 'A line chart with a filled body', @@ -31,7 +31,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, bubbleChart: { displayName: i18n.translate('xpack.canvas.elements.bubbleChartDisplayName', { - defaultMessage: 'Bubble chart', + defaultMessage: 'Bubble', }), help: i18n.translate('xpack.canvas.elements.bubbleChartHelpText', { defaultMessage: 'A customizable bubble chart', @@ -39,31 +39,31 @@ export const getElementStrings = (): ElementStringDict => ({ }, debug: { displayName: i18n.translate('xpack.canvas.elements.debugDisplayName', { - defaultMessage: 'Debug', + defaultMessage: 'Debug data', }), help: i18n.translate('xpack.canvas.elements.debugHelpText', { defaultMessage: 'Just dumps the configuration of the element', }), }, - donut: { - displayName: i18n.translate('xpack.canvas.elements.donutChartDisplayName', { - defaultMessage: 'Donut chart', - }), - help: i18n.translate('xpack.canvas.elements.donutChartHelpText', { - defaultMessage: 'A customizable donut chart', - }), - }, - dropdown_filter: { + dropdownFilter: { displayName: i18n.translate('xpack.canvas.elements.dropdownFilterDisplayName', { - defaultMessage: 'Dropdown filter', + defaultMessage: 'Dropdown select', }), help: i18n.translate('xpack.canvas.elements.dropdownFilterHelpText', { defaultMessage: 'A dropdown from which you can select values for an "exactly" filter', }), }, + filterDebug: { + displayName: i18n.translate('xpack.canvas.elements.filterDebugDisplayName', { + defaultMessage: 'Debug filters', + }), + help: i18n.translate('xpack.canvas.elements.filterDebugHelpText', { + defaultMessage: 'Shows the underlying global filters in a workpad', + }), + }, horizontalBarChart: { displayName: i18n.translate('xpack.canvas.elements.horizontalBarChartDisplayName', { - defaultMessage: 'Horizontal bar chart', + defaultMessage: 'Horizontal bar', }), help: i18n.translate('xpack.canvas.elements.horizontalBarChartHelpText', { defaultMessage: 'A customizable horizontal bar chart', @@ -71,7 +71,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, horizontalProgressBar: { displayName: i18n.translate('xpack.canvas.elements.horizontalProgressBarDisplayName', { - defaultMessage: 'Horizontal progress bar', + defaultMessage: 'Horizontal bar', }), help: i18n.translate('xpack.canvas.elements.horizontalProgressBarHelpText', { defaultMessage: 'Displays progress as a portion of a horizontal bar', @@ -79,7 +79,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, horizontalProgressPill: { displayName: i18n.translate('xpack.canvas.elements.horizontalProgressPillDisplayName', { - defaultMessage: 'Horizontal progress pill', + defaultMessage: 'Horizontal pill', }), help: i18n.translate('xpack.canvas.elements.horizontalProgressPillHelpText', { defaultMessage: 'Displays progress as a portion of a horizontal pill', @@ -95,7 +95,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, lineChart: { displayName: i18n.translate('xpack.canvas.elements.lineChartDisplayName', { - defaultMessage: 'Line chart', + defaultMessage: 'Line', }), help: i18n.translate('xpack.canvas.elements.lineChartHelpText', { defaultMessage: 'A customizable line chart', @@ -119,7 +119,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, pie: { displayName: i18n.translate('xpack.canvas.elements.pieDisplayName', { - defaultMessage: 'Pie chart', + defaultMessage: 'Pie', }), help: i18n.translate('xpack.canvas.elements.pieHelpText', { defaultMessage: 'Pie chart', @@ -135,7 +135,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, progressGauge: { displayName: i18n.translate('xpack.canvas.elements.progressGaugeDisplayName', { - defaultMessage: 'Progress gauge', + defaultMessage: 'Gauge', }), help: i18n.translate('xpack.canvas.elements.progressGaugeHelpText', { defaultMessage: 'Displays progress as a portion of a gauge', @@ -143,7 +143,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, progressSemicircle: { displayName: i18n.translate('xpack.canvas.elements.progressSemicircleDisplayName', { - defaultMessage: 'Progress semicircle', + defaultMessage: 'Semicircle', }), help: i18n.translate('xpack.canvas.elements.progressSemicircleHelpText', { defaultMessage: 'Displays progress as a portion of a semicircle', @@ -151,7 +151,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, progressWheel: { displayName: i18n.translate('xpack.canvas.elements.progressWheelDisplayName', { - defaultMessage: 'Progress wheel', + defaultMessage: 'Wheel', }), help: i18n.translate('xpack.canvas.elements.progressWheelHelpText', { defaultMessage: 'Displays progress as a portion of a wheel', @@ -189,15 +189,7 @@ export const getElementStrings = (): ElementStringDict => ({ defaultMessage: 'A scrollable grid for displaying data in a tabular format', }), }, - tiltedPie: { - displayName: i18n.translate('xpack.canvas.elements.tiltedPieDisplayName', { - defaultMessage: 'Tilted pie chart', - }), - help: i18n.translate('xpack.canvas.elements.tiltedPieHelpText', { - defaultMessage: 'A customizable tilted pie chart', - }), - }, - time_filter: { + timeFilter: { displayName: i18n.translate('xpack.canvas.elements.timeFilterDisplayName', { defaultMessage: 'Time filter', }), @@ -207,7 +199,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, verticalBarChart: { displayName: i18n.translate('xpack.canvas.elements.verticalBarChartDisplayName', { - defaultMessage: 'Vertical bar chart', + defaultMessage: 'Vertical bar', }), help: i18n.translate('xpack.canvas.elements.verticalBarChartHelpText', { defaultMessage: 'A customizable vertical bar chart', @@ -215,7 +207,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, verticalProgressBar: { displayName: i18n.translate('xpack.canvas.elements.verticalProgressBarDisplayName', { - defaultMessage: 'Vertical progress bar', + defaultMessage: 'Vertical bar', }), help: i18n.translate('xpack.canvas.elements.verticalProgressBarHelpText', { defaultMessage: 'Displays progress as a portion of a vertical bar', @@ -223,7 +215,7 @@ export const getElementStrings = (): ElementStringDict => ({ }, verticalProgressPill: { displayName: i18n.translate('xpack.canvas.elements.verticalProgressPillDisplayName', { - defaultMessage: 'Vertical progress pill', + defaultMessage: 'Vertical pill', }), help: i18n.translate('xpack.canvas.elements.verticalProgressPillHelpText', { defaultMessage: 'Displays progress as a portion of a vertical pill', diff --git a/x-pack/legacy/plugins/canvas/i18n/functions/dict/alter_column.ts b/x-pack/legacy/plugins/canvas/i18n/functions/dict/alter_column.ts index 836c7395ac448..cc601b0ea0e31 100644 --- a/x-pack/legacy/plugins/canvas/i18n/functions/dict/alter_column.ts +++ b/x-pack/legacy/plugins/canvas/i18n/functions/dict/alter_column.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { alterColumn } from '../../../canvas_plugin_src/functions/common/alterColumn'; import { FunctionHelp } from '../function_help'; import { FunctionFactory } from '../../../types'; -import { DATATABLE_COLUMN_TYPES } from '../../../common/lib'; +import { DATATABLE_COLUMN_TYPES } from '../../../common/lib/constants'; export const help: FunctionHelp> = { help: i18n.translate('xpack.canvas.functions.alterColumnHelpText', { diff --git a/x-pack/legacy/plugins/canvas/i18n/functions/dict/timelion.ts b/x-pack/legacy/plugins/canvas/i18n/functions/dict/timelion.ts index e105f7e678af3..41bf86055f1e3 100644 --- a/x-pack/legacy/plugins/canvas/i18n/functions/dict/timelion.ts +++ b/x-pack/legacy/plugins/canvas/i18n/functions/dict/timelion.ts @@ -5,12 +5,12 @@ */ import { i18n } from '@kbn/i18n'; -import { timelion } from '../../../public/functions/timelion'; +import { timelionFunctionFactory } from '../../../public/functions/timelion'; import { FunctionHelp } from '../function_help'; import { FunctionFactory } from '../../../types'; import { ELASTICSEARCH, DATEMATH, MOMENTJS_TIMEZONE_URL } from '../../constants'; -export const help: FunctionHelp> = { +export const help: FunctionHelp>> = { help: i18n.translate('xpack.canvas.functions.timelionHelpText', { defaultMessage: 'Use Timelion to extract one or more timeseries from many sources.', }), diff --git a/x-pack/legacy/plugins/canvas/i18n/tags.ts b/x-pack/legacy/plugins/canvas/i18n/tags.ts index 41007c58d738d..9595554260297 100644 --- a/x-pack/legacy/plugins/canvas/i18n/tags.ts +++ b/x-pack/legacy/plugins/canvas/i18n/tags.ts @@ -7,32 +7,12 @@ import { i18n } from '@kbn/i18n'; export const TagStrings: { [key: string]: () => string } = { - chart: () => - i18n.translate('xpack.canvas.tags.chartTag', { - defaultMessage: 'chart', - }), - filter: () => - i18n.translate('xpack.canvas.tags.filterTag', { - defaultMessage: 'filter', - }), - graphic: () => - i18n.translate('xpack.canvas.tags.graphicTag', { - defaultMessage: 'graphic', - }), presentation: () => i18n.translate('xpack.canvas.tags.presentationTag', { defaultMessage: 'presentation', }), - proportion: () => - i18n.translate('xpack.canvas.tags.proportionTag', { - defaultMessage: 'proportion', - }), report: () => i18n.translate('xpack.canvas.tags.reportTag', { defaultMessage: 'report', }), - text: () => - i18n.translate('xpack.canvas.tags.textTag', { - defaultMessage: 'text', - }), }; diff --git a/x-pack/legacy/plugins/canvas/public/application.tsx b/x-pack/legacy/plugins/canvas/public/application.tsx index f75b3b427c41b..f746a24e9b261 100644 --- a/x-pack/legacy/plugins/canvas/public/application.tsx +++ b/x-pack/legacy/plugins/canvas/public/application.tsx @@ -30,8 +30,12 @@ import { VALUE_CLICK_TRIGGER, ActionByType } from '../../../../../src/plugins/ui /* eslint-disable */ import { ACTION_VALUE_CLICK } from '../../../../../src/plugins/data/public/actions/value_click_action'; /* eslint-enable */ +import { init as initStatsReporter } from './lib/ui_metric'; import { CapabilitiesStrings } from '../i18n'; + +import { startServices, stopServices, services } from './services'; + const { ReadOnlyBadge: strings } = CapabilitiesStrings; let restoreAction: ActionByType | undefined; @@ -50,8 +54,14 @@ export const renderApp = ( { element }: AppMountParameters, canvasStore: Store ) => { + const canvasServices = Object.entries(services).reduce((reduction, [key, provider]) => { + reduction[key] = provider.getService(); + + return reduction; + }, {} as Record); + ReactDOM.render( - + @@ -70,6 +80,8 @@ export const initializeCanvas = async ( startPlugins: CanvasStartDeps, registries: SetupRegistries ) => { + startServices(coreSetup, coreStart, setupPlugins, startPlugins); + // Create Store const canvasStore = await createStore(coreSetup, setupPlugins); @@ -121,10 +133,15 @@ export const initializeCanvas = async ( startPlugins.uiActions.attachAction(VALUE_CLICK_TRIGGER, emptyAction); } + if (setupPlugins.usageCollection) { + initStatsReporter(setupPlugins.usageCollection.reportUiStats); + } + return canvasStore; }; export const teardownCanvas = (coreStart: CoreStart, startPlugins: CanvasStartDeps) => { + stopServices(); destroyRegistries(); resetInterpreter(); diff --git a/x-pack/legacy/plugins/canvas/public/apps/workpad/routes.js b/x-pack/legacy/plugins/canvas/public/apps/workpad/routes.js index 718443fcdd990..4e3920bf34f67 100644 --- a/x-pack/legacy/plugins/canvas/public/apps/workpad/routes.js +++ b/x-pack/legacy/plugins/canvas/public/apps/workpad/routes.js @@ -6,7 +6,7 @@ import { ErrorStrings } from '../../../i18n'; import * as workpadService from '../../lib/workpad_service'; -import { notify } from '../../lib/notify'; +import { notifyService } from '../../services'; import { getBaseBreadcrumb, getWorkpadBreadcrumb, setBreadcrumb } from '../../lib/breadcrumbs'; import { getDefaultWorkpad } from '../../state/defaults'; import { setWorkpad } from '../../state/actions/workpad'; @@ -33,7 +33,9 @@ export const routes = [ dispatch(resetAssets()); router.redirectTo('loadWorkpad', { id: newWorkpad.id, page: 1 }); } catch (err) { - notify.error(err, { title: strings.getCreateFailureErrorMessage() }); + notifyService + .getService() + .error(err, { title: strings.getCreateFailureErrorMessage() }); router.redirectTo('home'); } }, @@ -59,7 +61,9 @@ export const routes = [ // reset transient properties when changing workpads dispatch(setZoomScale(1)); } catch (err) { - notify.error(err, { title: strings.getLoadFailureErrorMessage() }); + notifyService + .getService() + .error(err, { title: strings.getLoadFailureErrorMessage() }); return router.redirectTo('home'); } } diff --git a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.scss b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.scss index c5161439b71c3..c7dae8452a93c 100644 --- a/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.scss +++ b/x-pack/legacy/plugins/canvas/public/apps/workpad/workpad_app/workpad_app.scss @@ -31,7 +31,7 @@ $canvasLayoutFontSize: $euiFontSizeS; .canvasLayout__stageHeader { flex-grow: 0; flex-basis: auto; - padding: ($euiSizeXS + 1px) $euiSize $euiSizeXS; + padding: 1px $euiSize 0; font-size: $canvasLayoutFontSize; border-bottom: $euiBorderThin; background: $euiColorLightestShade; diff --git a/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.stories.storyshot new file mode 100644 index 0000000000000..aff630b21c770 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.stories.storyshot @@ -0,0 +1,761 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/Assets/AssetManager no assets 1`] = ` +Array [ +
, +
, +
+
+ +
+
+
+ Manage workpad assets +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+

+ Below are the image assets in this workpad. Any assets that are currently in use cannot be determined at this time. To reclaim space, delete assets. +

+
+
+
+
+
+
+
+ +

+ Import your assets to get started +

+
+ +
+
+
+
+
+
+
+ +
+
+
+ 0% space used +
+
+
+ +
+
+
+
, +
, +] +`; + +exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` +Array [ +
, +
, +
+
+ +
+
+
+ Manage workpad assets +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+

+ Below are the image assets in this workpad. Any assets that are currently in use cannot be determined at this time. To reclaim space, delete assets. +

+
+
+
+
+
+
+
+
+ Asset thumbnail +
+
+
+
+

+ + airplane + +
+ + + ( + 1 + kb) + + +

+
+
+
+
+ + + +
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ + + +
+
+
+
+
+
+
+
+ Asset thumbnail +
+
+
+
+

+ + marker + +
+ + + ( + 1 + kb) + + +

+
+
+
+
+ + + +
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+ +
+
+
+ 0% space used +
+
+
+ +
+
+
+
, +
, +] +`; diff --git a/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.examples.tsx b/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.stories.tsx similarity index 97% rename from x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.examples.tsx rename to x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.stories.tsx index 045a3b4f64a44..cb42823ccab7b 100644 --- a/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.examples.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/asset_manager/__examples__/asset_manager.stories.tsx @@ -28,12 +28,12 @@ const MARKER: AssetType = { storiesOf('components/Assets/AssetManager', module) .add('no assets', () => ( - // @ts-ignore @types/react has not been updated to support defaultProps yet. )) .add('two assets', () => ( @@ -43,5 +43,6 @@ storiesOf('components/Assets/AssetManager', module) onAssetAdd={action('onAssetAdd')} onAssetCopy={action('onAssetCopy')} onAssetDelete={action('onAssetDelete')} + onClose={action('onClose')} /> )); diff --git a/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_manager.tsx b/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_manager.tsx index 3785f81cc25b9..c27f0c002c3d1 100644 --- a/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_manager.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_manager.tsx @@ -4,13 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiButtonEmpty, - // @ts-ignore (elastic/eui#1557) EuiFilePicker is not exported yet - EuiFilePicker, - // @ts-ignore (elastic/eui#1557) EuiImage is not exported yet - EuiImage, -} from '@elastic/eui'; import PropTypes from 'prop-types'; import React, { Fragment, PureComponent } from 'react'; @@ -22,7 +15,7 @@ import { AssetModal } from './asset_modal'; const { AssetManager: strings } = ComponentStrings; -interface Props { +export interface Props { /** A list of assets, if available */ assetValues: AssetType[]; /** Function to invoke when an asset is selected to be added as an element to the workpad */ @@ -33,13 +26,13 @@ interface Props { onAssetCopy: () => void; /** Function to invoke when an asset is added */ onAssetAdd: (asset: File) => void; + /** Function to invoke when an asset modal is closed */ + onClose: () => void; } interface State { /** The id of the asset to delete, if applicable. Is set if the viewer clicks the delete icon */ deleteId: string | null; - /** Determines if the modal is currently visible */ - isModalVisible: boolean; /** Indicates if the modal is currently loading */ isLoading: boolean; } @@ -51,6 +44,7 @@ export class AssetManager extends PureComponent { onAssetAdd: PropTypes.func.isRequired, onAssetCopy: PropTypes.func.isRequired, onAssetDelete: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, }; public static defaultProps = { @@ -60,12 +54,11 @@ export class AssetManager extends PureComponent { public state = { deleteId: null, isLoading: false, - isModalVisible: false, }; public render() { - const { isModalVisible, isLoading } = this.state; - const { assetValues, onAssetCopy, onAddImageElement } = this.props; + const { isLoading } = this.state; + const { assetValues, onAssetCopy, onAddImageElement, onClose } = this.props; const assetModal = ( { onAssetCopy={onAssetCopy} onAssetCreate={(createdAsset: AssetType) => { onAddImageElement(createdAsset.id); - this.setState({ isModalVisible: false }); + onClose(); }} onAssetDelete={(asset: AssetType) => this.setState({ deleteId: asset.id })} - onClose={() => this.setState({ isModalVisible: false })} + onClose={onClose} onFileUpload={this.handleFileUpload} /> ); @@ -95,14 +88,12 @@ export class AssetManager extends PureComponent { return ( - {strings.getButtonLabel()} - {isModalVisible ? assetModal : null} + {assetModal} {confirmModal} ); } - private showModal = () => this.setState({ isModalVisible: true }); private resetDelete = () => this.setState({ deleteId: null }); private doDelete = () => { diff --git a/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_modal.tsx b/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_modal.tsx index 3dfbb1b1fde3c..8637db8e9f962 100644 --- a/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_modal.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/asset_manager/asset_modal.tsx @@ -98,6 +98,7 @@ export const AssetModal: FunctionComponent = props => { ({ +import { State, ExpressionAstExpression, AssetType } from '../../../types'; + +const mapStateToProps = (state: State) => ({ assets: getAssets(state), selectedPage: getSelectedPage(state), }); -const mapDispatchToProps = dispatch => ({ - onAddImageElement: pageId => assetId => { +const mapDispatchToProps = (dispatch: (action: any) => void) => ({ + onAddImageElement: (pageId: string) => (assetId: string) => { const imageElement = elementsRegistry.get('image'); const elementAST = fromExpression(imageElement.expression); const selector = ['chain', '0', 'arguments', 'dataurl']; - const subExp = [ + const subExp: ExpressionAstExpression[] = [ { type: 'expression', chain: [ @@ -44,11 +51,11 @@ const mapDispatchToProps = dispatch => ({ ], }, ]; - const newAST = set(elementAST, selector, subExp); + const newAST = set(elementAST, selector, subExp); imageElement.expression = toExpression(newAST); dispatch(addElement(pageId, imageElement)); }, - onAssetAdd: (type, content) => { + onAssetAdd: (type: string, content: string) => { // make the ID here and pass it into the action const assetId = getId('asset'); dispatch(createAsset(type, content, assetId)); @@ -56,10 +63,14 @@ const mapDispatchToProps = dispatch => ({ // then return the id, so the caller knows the id that will be created return assetId; }, - onAssetDelete: assetId => dispatch(removeAsset(assetId)), + onAssetDelete: (assetId: string) => dispatch(removeAsset(assetId)), }); -const mergeProps = (stateProps, dispatchProps, ownProps) => { +const mergeProps = ( + stateProps: ReturnType, + dispatchProps: ReturnType, + ownProps: AssetManagerProps +) => { const { assets, selectedPage } = stateProps; const { onAssetAdd } = dispatchProps; const assetValues = Object.values(assets); // pull values out of assets object @@ -70,16 +81,16 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { onAddImageElement: dispatchProps.onAddImageElement(stateProps.selectedPage), selectedPage, assetValues, - onAssetAdd: file => { + onAssetAdd: (file: File) => { const [type, subtype] = get(file, 'type', '').split('/'); if (type === 'image' && VALID_IMAGE_TYPES.indexOf(subtype) >= 0) { return encode(file).then(dataurl => { - const type = 'dataurl'; - const existingId = findExistingAsset(type, dataurl, assetValues); + const dataurlType = 'dataurl'; + const existingId = findExistingAsset(dataurlType, dataurl, assetValues); if (existingId) { return existingId; } - return onAssetAdd(type, dataurl); + return onAssetAdd(dataurlType, dataurl); }); } @@ -88,7 +99,11 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { }; }; -export const AssetManager = compose( +export const AssetManager = compose( connect(mapStateToProps, mapDispatchToProps, mergeProps), - withProps({ onAssetCopy: asset => notify.success(`Copied '${asset.id}' to clipboard`) }) + withKibana, + withProps(({ kibana }: WithKibanaProps) => ({ + onAssetCopy: (asset: AssetType) => + kibana.services.canvas.notify.success(`Copied '${asset.id}' to clipboard`), + })) )(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_grid.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_grid.stories.storyshot deleted file mode 100644 index 6572aa582f48c..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_grid.stories.storyshot +++ /dev/null @@ -1,797 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Storyshots components/Elements/ElementGrid with controls 1`] = ` -
-
-
-
-
- -
-
- - - -
-

- sample description -

-
-
-
-
-
-
- - - -
-
- - - -
-
-
-
-
-
- -
-
- - - -
-

- Aenean eu justo auctor, placerat felis non, scelerisque dolor. -

-
-
-
-
-
-
- - - -
-
- - - -
-
-
-
-
-
- -
-
- - - -
-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce lobortis aliquet arcu ut turpis duis. -

-
-
-
-
-
-
- - - -
-
- - - -
-
-
-
-
-`; - -exports[`Storyshots components/Elements/ElementGrid with controls and filter 1`] = ` -
-
-
-
-
- -
-
- - - -
-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce lobortis aliquet arcu ut turpis duis. -

-
-
-
-
-
-
- - - -
-
- - - -
-
-
-
-
-`; - -exports[`Storyshots components/Elements/ElementGrid with tags filter 1`] = ` -
-
-
-
-
- -
-
- - - -
-

- A static image -

-
-
-
- - - - graphic - - - -
-
-
-
-
-`; - -exports[`Storyshots components/Elements/ElementGrid with text filter 1`] = ` -
-
-
-
-
- -
-
- - - -
-

- A scrollable grid for displaying data in a tabular format -

-
-
-
- - - - text - - - -
-
-
-
-
-`; - -exports[`Storyshots components/Elements/ElementGrid without controls 1`] = ` -
-
-
-
-
- -
-
- - - -
-

- A line chart with a filled body -

-
-
-
- - - - chart - - - -
-
-
-
-
-
- -
-
- - - -
-

- A static image -

-
-
-
- - - - graphic - - - -
-
-
-
-
-
- -
-
- - - -
-

- A scrollable grid for displaying data in a tabular format -

-
-
-
- - - - text - - - -
-
-
-
-
-`; diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/element_grid.tsx b/x-pack/legacy/plugins/canvas/public/components/element_types/element_grid.tsx deleted file mode 100644 index 852b987fcfd24..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/element_grid.tsx +++ /dev/null @@ -1,110 +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 PropTypes from 'prop-types'; -import { map } from 'lodash'; -import { EuiFlexItem, EuiFlexGrid } from '@elastic/eui'; -import { ElementControls } from './element_controls'; -import { CustomElement, ElementSpec } from '../../../types'; -import { ElementCard } from '../element_card'; - -export interface Props { - /** - * list of elements to generate cards for - */ - elements: Array; - /** - * text to filter out cards - */ - filterText: string; - /** - * tags to filter out cards - */ - filterTags: string[]; - /** - * indicate whether or not edit/delete controls should be displayed - */ - showControls: boolean; - /** - * handler invoked when clicking a card - */ - handleClick: (element: ElementSpec | CustomElement) => void; - /** - * click handler for the edit button - */ - onEdit?: (element: ElementSpec | CustomElement) => void; - /** - * click handler for the delete button - */ - onDelete?: (element: ElementSpec | CustomElement) => void; -} - -export const ElementGrid = ({ - elements, - filterText, - filterTags, - handleClick, - onEdit, - onDelete, - showControls, -}: Props) => { - filterText = filterText.toLowerCase(); - - return ( - - {map(elements, (element: ElementSpec | CustomElement, index) => { - const { name, displayName = '', help = '', image, tags = [] } = element; - const whenClicked = () => handleClick(element); - let textMatch = false; - let tagsMatch = false; - - if ( - !filterText.length || - name.toLowerCase().includes(filterText) || - displayName.toLowerCase().includes(filterText) || - help.toLowerCase().includes(filterText) - ) { - textMatch = true; - } - - if (!filterTags.length || filterTags.every(tag => tags.includes(tag))) { - tagsMatch = true; - } - - if (!textMatch || !tagsMatch) { - return null; - } - - return ( - - - {showControls && onEdit && onDelete && ( - onEdit(element)} onDelete={() => onDelete(element)} /> - )} - - ); - })} - - ); -}; - -ElementGrid.propTypes = { - elements: PropTypes.array.isRequired, - handleClick: PropTypes.func.isRequired, - showControls: PropTypes.bool, -}; - -ElementGrid.defaultProps = { - showControls: false, - filterTags: [], - filterText: '', -}; diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/element_types.js b/x-pack/legacy/plugins/canvas/public/components/element_types/element_types.js deleted file mode 100644 index dabf06a24aeb6..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/element_types.js +++ /dev/null @@ -1,224 +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, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { - EuiModalBody, - EuiTabbedContent, - EuiEmptyPrompt, - EuiSearchBar, - EuiSpacer, - EuiOverlayMask, -} from '@elastic/eui'; -import { map, sortBy } from 'lodash'; -import { ConfirmModal } from '../confirm_modal/confirm_modal'; -import { CustomElementModal } from '../custom_element_modal'; -import { getTagsFilter } from '../../lib/get_tags_filter'; -import { extractSearch } from '../../lib/extract_search'; -import { ComponentStrings } from '../../../i18n'; -import { ElementGrid } from './element_grid'; - -const { ElementTypes: strings } = ComponentStrings; - -const tagType = 'badge'; -export class ElementTypes extends Component { - static propTypes = { - addCustomElement: PropTypes.func.isRequired, - addElement: PropTypes.func.isRequired, - customElements: PropTypes.array.isRequired, - elements: PropTypes.object, - filterTags: PropTypes.arrayOf(PropTypes.string).isRequired, - findCustomElements: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - removeCustomElement: PropTypes.func.isRequired, - search: PropTypes.string, - setCustomElements: PropTypes.func.isRequired, - setSearch: PropTypes.func.isRequired, - setFilterTags: PropTypes.func.isRequired, - updateCustomElement: PropTypes.func.isRequired, - }; - - state = { - elementToDelete: null, - elementToEdit: null, - }; - - componentDidMount() { - // fetch custom elements - this.props.findCustomElements(); - } - - _showEditModal = elementToEdit => this.setState({ elementToEdit }); - - _hideEditModal = () => this.setState({ elementToEdit: null }); - - _handleEdit = async (name, description, image) => { - const { updateCustomElement } = this.props; - const { elementToEdit } = this.state; - await updateCustomElement(elementToEdit.id, name, description, image); - this._hideEditModal(); - }; - - _showDeleteModal = elementToDelete => this.setState({ elementToDelete }); - - _hideDeleteModal = () => this.setState({ elementToDelete: null }); - - _handleDelete = async () => { - const { removeCustomElement } = this.props; - const { elementToDelete } = this.state; - await removeCustomElement(elementToDelete.id); - this._hideDeleteModal(); - }; - - _renderEditModal = () => { - const { elementToEdit } = this.state; - - if (!elementToEdit) { - return null; - } - - return ( - - - - ); - }; - - _renderDeleteModal = () => { - const { elementToDelete } = this.state; - - if (!elementToDelete) { - return null; - } - - return ( - - ); - }; - - _sortElements = elements => - sortBy( - map(elements, (element, name) => ({ name, ...element })), - 'displayName' - ); - - render() { - const { - search, - setSearch, - addElement, - addCustomElement, - filterTags, - setFilterTags, - } = this.props; - let { elements, customElements } = this.props; - elements = this._sortElements(elements); - - let customElementContent = ( - {strings.getAddNewElementTitle()}} - body={

{strings.getAddNewElementDescription()}

} - titleSize="s" - /> - ); - - if (customElements.length) { - customElements = this._sortElements(customElements); - customElementContent = ( - - ); - } - - const filters = [getTagsFilter(tagType)]; - const onSearch = ({ queryText }) => { - const { searchTerm, filterTags } = extractSearch(queryText); - setSearch(searchTerm); - setFilterTags(filterTags); - }; - - const tabs = [ - { - id: 'elements', - name: strings.getElementsTitle(), - content: ( -
- - - - -
- ), - }, - { - id: 'customElements', - name: strings.getMyElementsTitle(), - content: ( - - - - - {customElementContent} - - ), - }, - ]; - - return ( - - - - - - {this._renderDeleteModal()} - {this._renderEditModal()} - - ); - } -} diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/index.js b/x-pack/legacy/plugins/canvas/public/components/element_types/index.js deleted file mode 100644 index 8faaf278a07de..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/index.js +++ /dev/null @@ -1,103 +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 PropTypes from 'prop-types'; -import { compose, withProps, withState } from 'recompose'; -import { connect } from 'react-redux'; -import { camelCase } from 'lodash'; -import { cloneSubgraphs } from '../../lib/clone_subgraphs'; -import * as customElementService from '../../lib/custom_element_service'; -import { elementsRegistry } from '../../lib/elements_registry'; -import { notify } from '../../lib/notify'; -import { selectToplevelNodes } from '../../state/actions/transient'; -import { insertNodes, addElement } from '../../state/actions/elements'; -import { getSelectedPage } from '../../state/selectors/workpad'; -import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric'; -import { ElementTypes as Component } from './element_types'; - -const customElementAdded = 'elements-custom-added'; - -const mapStateToProps = state => ({ pageId: getSelectedPage(state) }); - -const mapDispatchToProps = dispatch => ({ - selectToplevelNodes: nodes => - dispatch(selectToplevelNodes(nodes.filter(e => !e.position.parent).map(e => e.id))), - insertNodes: (selectedNodes, pageId) => dispatch(insertNodes(selectedNodes, pageId)), - addElement: (pageId, partialElement) => dispatch(addElement(pageId, partialElement)), -}); - -const mergeProps = (stateProps, dispatchProps, ownProps) => { - const { pageId, ...remainingStateProps } = stateProps; - const { addElement, insertNodes, selectToplevelNodes } = dispatchProps; - const { search, setCustomElements, onClose } = ownProps; - - return { - ...remainingStateProps, - ...ownProps, - // add built-in element to the page - addElement: element => { - addElement(pageId, element); - onClose(); - }, - // add custom element to the page - addCustomElement: customElement => { - const { selectedNodes = [] } = JSON.parse(customElement.content) || {}; - const clonedNodes = selectedNodes && cloneSubgraphs(selectedNodes); - if (clonedNodes) { - insertNodes(clonedNodes, pageId); // first clone and persist the new node(s) - selectToplevelNodes(clonedNodes); // then select the cloned node(s) - } - onClose(); - trackCanvasUiMetric(METRIC_TYPE.LOADED, customElementAdded); - }, - // custom element search - findCustomElements: async text => { - try { - const { customElements } = await customElementService.find(text); - setCustomElements(customElements); - } catch (err) { - notify.error(err, { title: `Couldn't find custom elements` }); - } - }, - // remove custom element - removeCustomElement: async id => { - try { - await customElementService.remove(id).then(); - const { customElements } = await customElementService.find(search); - setCustomElements(customElements); - } catch (err) { - notify.error(err, { title: `Couldn't delete custom elements` }); - } - }, - // update custom element - updateCustomElement: async (id, name, description, image) => { - try { - await customElementService.update(id, { - name: camelCase(name), - displayName: name, - image, - help: description, - }); - const { customElements } = await customElementService.find(search); - setCustomElements(customElements); - } catch (err) { - notify.error(err, { title: `Couldn't update custom elements` }); - } - }, - }; -}; - -export const ElementTypes = compose( - withState('search', 'setSearch', ''), - withState('customElements', 'setCustomElements', []), - withState('filterTags', 'setFilterTags', []), - withProps(() => ({ elements: elementsRegistry.toJS() })), - connect(mapStateToProps, mapDispatchToProps, mergeProps) -)(Component); - -ElementTypes.propTypes = { - onClose: PropTypes.func, -}; diff --git a/x-pack/legacy/plugins/canvas/public/components/popover/index.ts b/x-pack/legacy/plugins/canvas/public/components/popover/index.ts index f560da14079b5..63626f08fa43b 100644 --- a/x-pack/legacy/plugins/canvas/public/components/popover/index.ts +++ b/x-pack/legacy/plugins/canvas/public/components/popover/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { Popover } from './popover'; +export { Popover, ClosePopoverFn } from './popover'; diff --git a/x-pack/legacy/plugins/canvas/public/components/popover/popover.tsx b/x-pack/legacy/plugins/canvas/public/components/popover/popover.tsx index 25b2e6587c869..9f3d86576e6a7 100644 --- a/x-pack/legacy/plugins/canvas/public/components/popover/popover.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/popover/popover.tsx @@ -24,6 +24,8 @@ interface Props { className?: string; } +export type ClosePopoverFn = () => void; + interface State { isPopoverOpen: boolean; } @@ -61,7 +63,7 @@ export class Popover extends Component { })); }; - closePopover = () => { + closePopover: ClosePopoverFn = () => { this.setState({ isPopoverOpen: false, }); diff --git a/x-pack/legacy/plugins/canvas/public/components/render_with_fn/index.js b/x-pack/legacy/plugins/canvas/public/components/render_with_fn/index.js index 68c3ba79dd488..cc234d2287c0c 100644 --- a/x-pack/legacy/plugins/canvas/public/components/render_with_fn/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/render_with_fn/index.js @@ -7,7 +7,7 @@ import { compose, withProps, withPropsOnChange } from 'recompose'; import PropTypes from 'prop-types'; import isEqual from 'react-fast-compare'; -import { notify } from '../../lib/notify'; +import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { RenderWithFn as Component } from './render_with_fn'; import { ElementHandlers } from './lib/handlers'; @@ -19,9 +19,10 @@ export const RenderWithFn = compose( handlers: Object.assign(new ElementHandlers(), handlers), }) ), - withProps({ - onError: notify.error, - }) + withKibana, + withProps(props => ({ + onError: props.kibana.services.canvas.notify.error, + })) )(Component); RenderWithFn.propTypes = { diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_controls.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_controls.stories.storyshot similarity index 88% rename from x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_controls.stories.storyshot rename to x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_controls.stories.storyshot index 5ce6fe8c85589..6f12f68356467 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/__snapshots__/element_controls.stories.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/element_controls.stories.storyshot @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Storyshots components/Elements/ElementControls has two buttons 1`] = ` +exports[`Storyshots components/SavedElementsModal/ElementControls has two buttons 1`] = `
+
+
+
+
+ +
+
+ + + +
+

+ sample description +

+
+
+
+
+
+
+ + + +
+
+ + + +
+
+
+
+
+
+ +
+
+ + + +
+

+ Aenean eu justo auctor, placerat felis non, scelerisque dolor. +

+
+
+
+
+
+
+ + + +
+
+ + + +
+
+
+
+
+
+ +
+
+ + + +
+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce lobortis aliquet arcu ut turpis duis. +

+
+
+
+
+
+
+ + + +
+
+ + + +
+
+
+
+
+`; + +exports[`Storyshots components/SavedElementsModal/ElementGrid with text filter 1`] = ` +
+
+
+`; diff --git a/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/saved_elements_modal.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/saved_elements_modal.stories.storyshot new file mode 100644 index 0000000000000..c73309ad8b14c --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/__snapshots__/saved_elements_modal.stories.storyshot @@ -0,0 +1,933 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/SavedElementsModal no custom elements 1`] = ` +Array [ +
, +
, +
+
+ +
+
+
+ My elements +
+
+
+
+
+
+ +
+ + +
+
+
+
+
+
+ +

+ Add new elements +

+
+
+

+ Group and save workpad elements to create new elements +

+
+ +
+
+
+
+ +
+
+
+
, +
, +] +`; + +exports[`Storyshots components/SavedElementsModal with custom elements 1`] = ` +Array [ +
, +
, +
+
+ +
+
+
+ My elements +
+
+
+
+
+
+ +
+ + +
+
+
+
+
+
+
+ +
+
+ + + +
+

+ sample description +

+
+
+
+
+
+
+ + + +
+
+ + + +
+
+
+
+
+
+ +
+
+ + + +
+

+ Aenean eu justo auctor, placerat felis non, scelerisque dolor. +

+
+
+
+
+
+
+ + + +
+
+ + + +
+
+
+
+
+
+ +
+
+ + + +
+

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce lobortis aliquet arcu ut turpis duis. +

+
+
+
+
+
+
+ + + +
+
+ + + +
+
+
+
+
+
+
+ +
+
+
+
, +
, +] +`; + +exports[`Storyshots components/SavedElementsModal with text filter 1`] = ` +Array [ +
, +
, +
+
+ +
+
+
+ My elements +
+
+
+
+
+
+ +
+ + +
+ +
+
+
+
+
+
+
+
+ +
+
+ + + +
+

+ Aenean eu justo auctor, placerat felis non, scelerisque dolor. +

+
+
+
+
+
+
+ + + +
+
+ + + +
+
+
+
+
+
+
+ +
+
+
+
, +
, +] +`; diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/element_controls.stories.tsx b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/element_controls.stories.tsx similarity index 90% rename from x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/element_controls.stories.tsx rename to x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/element_controls.stories.tsx index 52736ac952e53..5210210ebaa74 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/element_controls.stories.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/element_controls.stories.tsx @@ -9,7 +9,7 @@ import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import { ElementControls } from '../element_controls'; -storiesOf('components/Elements/ElementControls', module) +storiesOf('components/SavedElementsModal/ElementControls', module) .addDecorator(story => (
(
)) - .add('without controls', () => ( - - )) - .add('with controls', () => ( + .add('default', () => ( )) .add('with text filter', () => ( - - )) - .add('with tags filter', () => ( - - )) - .add('with controls and filter', () => ( diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/fixtures/test_elements.tsx b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/fixtures/test_elements.tsx similarity index 93% rename from x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/fixtures/test_elements.tsx rename to x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/fixtures/test_elements.tsx index eec7a86d52f25..d1ff565b4955a 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/__examples__/fixtures/test_elements.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/fixtures/test_elements.tsx @@ -6,41 +6,6 @@ import { elasticLogo } from '../../../../lib/elastic_logo'; -export const testElements = [ - { - name: 'areaChart', - displayName: 'Area chart', - help: 'A line chart with a filled body', - tags: ['chart'], - image: elasticLogo, - expression: `filters - | demodata - | pointseries x="time" y="mean(price)" - | plot defaultStyle={seriesStyle lines=1 fill=1} - | render`, - }, - { - name: 'image', - displayName: 'Image', - help: 'A static image', - tags: ['graphic'], - image: elasticLogo, - expression: `image dataurl=null mode="contain" - | render`, - }, - { - name: 'table', - displayName: 'Data table', - tags: ['text'], - help: 'A scrollable grid for displaying data in a tabular format', - image: elasticLogo, - expression: `filters - | demodata - | table - | render`, - }, -]; - export const testCustomElements = [ { id: 'custom-element-10d625f5-1342-47c9-8f19-d174ea6b65d5', diff --git a/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/saved_elements_modal.stories.tsx b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/saved_elements_modal.stories.tsx new file mode 100644 index 0000000000000..4941d8cb2efa7 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/__examples__/saved_elements_modal.stories.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 { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import { SavedElementsModal } from '../saved_elements_modal'; +import { testCustomElements } from './fixtures/test_elements'; +import { CustomElement } from '../../../../types'; + +storiesOf('components/SavedElementsModal', module) + .add('no custom elements', () => ( + + )) + .add('with custom elements', () => ( + + )) + .add('with text filter', () => ( + + )); diff --git a/x-pack/legacy/plugins/canvas/public/components/element_types/element_controls.tsx b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/element_controls.tsx similarity index 85% rename from x-pack/legacy/plugins/canvas/public/components/element_types/element_controls.tsx rename to x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/element_controls.tsx index a23274296f64f..998b15c15f487 100644 --- a/x-pack/legacy/plugins/canvas/public/components/element_types/element_controls.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/element_controls.tsx @@ -30,7 +30,12 @@ export const ElementControls: FunctionComponent = ({ onDelete, onEdit }) > - + @@ -40,6 +45,7 @@ export const ElementControls: FunctionComponent = ({ onDelete, onEdit }) iconType="trash" aria-label={strings.getDeleteAriaLabel()} onClick={onDelete} + data-test-subj="canvasElementCard__deleteButton" /> diff --git a/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/element_grid.tsx b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/element_grid.tsx new file mode 100644 index 0000000000000..f86e2c0147035 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/element_grid.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 PropTypes from 'prop-types'; +import { map } from 'lodash'; +import { EuiFlexItem, EuiFlexGrid } from '@elastic/eui'; +import { ElementControls } from './element_controls'; +import { CustomElement } from '../../../types'; +import { ElementCard } from '../element_card'; + +export interface Props { + /** + * list of elements to generate cards for + */ + elements: CustomElement[]; + /** + * text to filter out cards + */ + filterText: string; + /** + * handler invoked when clicking a card + */ + onClick: (element: CustomElement) => void; + /** + * click handler for the edit button + */ + onEdit: (element: CustomElement) => void; + /** + * click handler for the delete button + */ + onDelete: (element: CustomElement) => void; +} + +export const ElementGrid = ({ elements, filterText, onClick, onEdit, onDelete }: Props) => { + filterText = filterText.toLowerCase(); + + return ( + + {map(elements, (element: CustomElement, index) => { + const { name, displayName = '', help = '', image } = element; + const whenClicked = () => onClick(element); + + if ( + filterText.length && + !name.toLowerCase().includes(filterText) && + !displayName.toLowerCase().includes(filterText) && + !help.toLowerCase().includes(filterText) + ) { + return null; + } + + return ( + + + onEdit(element)} onDelete={() => onDelete(element)} /> + + ); + })} + + ); +}; + +ElementGrid.propTypes = { + elements: PropTypes.array.isRequired, + filterText: PropTypes.string.isRequired, + onClick: PropTypes.func.isRequired, + onEdit: PropTypes.func.isRequired, + onDelete: PropTypes.func.isRequired, +}; + +ElementGrid.defaultProps = { + filterText: '', +}; diff --git a/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/index.ts b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/index.ts new file mode 100644 index 0000000000000..60d1d7462daa9 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/index.ts @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { compose, withState } from 'recompose'; +import { camelCase } from 'lodash'; +// @ts-ignore Untyped local +import { cloneSubgraphs } from '../../lib/clone_subgraphs'; +import * as customElementService from '../../lib/custom_element_service'; +import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { WithKibanaProps } from '../../'; +// @ts-ignore Untyped local +import { selectToplevelNodes } from '../../state/actions/transient'; +// @ts-ignore Untyped local +import { insertNodes } from '../../state/actions/elements'; +import { getSelectedPage } from '../../state/selectors/workpad'; +import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric'; +import { SavedElementsModal as Component, Props as ComponentProps } from './saved_elements_modal'; +import { State, PositionedElement, CustomElement } from '../../../types'; + +const customElementAdded = 'elements-custom-added'; + +interface OwnProps { + onClose: () => void; +} + +interface OwnPropsWithState extends OwnProps { + customElements: CustomElement[]; + setCustomElements: (customElements: CustomElement[]) => void; + search: string; + setSearch: (search: string) => void; +} + +interface DispatchProps { + selectToplevelNodes: (nodes: PositionedElement[]) => void; + insertNodes: (selectedNodes: PositionedElement[], pageId: string) => void; +} + +interface StateProps { + pageId: string; +} + +const mapStateToProps = (state: State): StateProps => ({ + pageId: getSelectedPage(state), +}); + +const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ + selectToplevelNodes: (nodes: PositionedElement[]) => + dispatch( + selectToplevelNodes( + nodes + .filter((e: PositionedElement): boolean => !e.position.parent) + .map((e: PositionedElement): string => e.id) + ) + ), + insertNodes: (selectedNodes: PositionedElement[], pageId: string) => + dispatch(insertNodes(selectedNodes, pageId)), +}); + +const mergeProps = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ownProps: OwnPropsWithState & WithKibanaProps +): ComponentProps => { + const { pageId } = stateProps; + const { onClose, search, setCustomElements } = ownProps; + + const findCustomElements = async () => { + const { customElements } = await customElementService.find(search); + setCustomElements(customElements); + }; + + return { + ...ownProps, + // add custom element to the page + addCustomElement: (customElement: CustomElement) => { + const { selectedNodes = [] } = JSON.parse(customElement.content) || {}; + const clonedNodes = selectedNodes && cloneSubgraphs(selectedNodes); + if (clonedNodes) { + dispatchProps.insertNodes(clonedNodes, pageId); // first clone and persist the new node(s) + dispatchProps.selectToplevelNodes(clonedNodes); // then select the cloned node(s) + } + onClose(); + trackCanvasUiMetric(METRIC_TYPE.LOADED, customElementAdded); + }, + // custom element search + findCustomElements: async (text?: string) => { + try { + await findCustomElements(); + } catch (err) { + ownProps.kibana.services.canvas.notify.error(err, { + title: `Couldn't find custom elements`, + }); + } + }, + // remove custom element + removeCustomElement: async (id: string) => { + try { + await customElementService.remove(id); + await findCustomElements(); + } catch (err) { + ownProps.kibana.services.canvas.notify.error(err, { + title: `Couldn't delete custom elements`, + }); + } + }, + // update custom element + updateCustomElement: async (id: string, name: string, description: string, image: string) => { + try { + await customElementService.update(id, { + name: camelCase(name), + displayName: name, + image, + help: description, + }); + await findCustomElements(); + } catch (err) { + ownProps.kibana.services.canvas.notify.error(err, { + title: `Couldn't update custom elements`, + }); + } + }, + }; +}; + +export const SavedElementsModal = compose( + withKibana, + withState('search', 'setSearch', ''), + withState('customElements', 'setCustomElements', []), + connect(mapStateToProps, mapDispatchToProps, mergeProps) +)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx new file mode 100644 index 0000000000000..dba97a15fee5c --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.tsx @@ -0,0 +1,217 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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, ChangeEvent, FunctionComponent, useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiEmptyPrompt, + EuiFieldSearch, + EuiSpacer, + EuiOverlayMask, + EuiButton, +} from '@elastic/eui'; +import { map, sortBy } from 'lodash'; +import { ComponentStrings } from '../../../i18n'; +import { CustomElement } from '../../../types'; +import { ConfirmModal } from '../confirm_modal/confirm_modal'; +import { CustomElementModal } from '../custom_element_modal'; +import { ElementGrid } from './element_grid'; + +const { SavedElementsModal: strings } = ComponentStrings; + +export interface Props { + /** + * Adds the custom element to the workpad + */ + addCustomElement: (customElement: CustomElement) => void; + /** + * Queries ES for custom element saved objects + */ + findCustomElements: () => void; + /** + * Handler invoked when the modal closes + */ + onClose: () => void; + /** + * Deletes the custom element + */ + removeCustomElement: (id: string) => void; + /** + * Saved edits to the custom element + */ + updateCustomElement: (id: string, name: string, description: string, image: string) => void; + /** + * Array of custom elements to display + */ + customElements: CustomElement[]; + /** + * Text used to filter custom elements list + */ + search: string; + /** + * Setter for search text + */ + setSearch: (search: string) => void; +} + +export const SavedElementsModal: FunctionComponent = ({ + search, + setSearch, + customElements, + addCustomElement, + findCustomElements, + onClose, + removeCustomElement, + updateCustomElement, +}) => { + const [elementToDelete, setElementToDelete] = useState(null); + const [elementToEdit, setElementToEdit] = useState(null); + + useEffect(() => { + findCustomElements(); + }); + + const showEditModal = (element: CustomElement) => setElementToEdit(element); + const hideEditModal = () => setElementToEdit(null); + + const handleEdit = async (name: string, description: string, image: string) => { + if (elementToEdit) { + await updateCustomElement(elementToEdit.id, name, description, image); + } + hideEditModal(); + }; + + const showDeleteModal = (element: CustomElement) => setElementToDelete(element); + const hideDeleteModal = () => setElementToDelete(null); + + const handleDelete = async () => { + if (elementToDelete) { + await removeCustomElement(elementToDelete.id); + } + hideDeleteModal(); + }; + + const renderEditModal = () => { + if (!elementToEdit) { + return null; + } + + return ( + + + + ); + }; + + const renderDeleteModal = () => { + if (!elementToDelete) { + return null; + } + + return ( + + ); + }; + + const sortElements = (elements: CustomElement[]): CustomElement[] => + sortBy( + map(elements, (element, name) => ({ name, ...element })), + 'displayName' + ); + + const onSearch = (e: ChangeEvent) => setSearch(e.target.value); + + let customElementContent = ( + {strings.getAddNewElementTitle()}} + body={

{strings.getAddNewElementDescription()}

} + titleSize="s" + /> + ); + + if (customElements.length) { + customElementContent = ( + + ); + } + + return ( + + + + + + {strings.getModalTitle()} + + + + + + + {customElementContent} + + + + {strings.getSavedElementsModalCloseButtonLabel()} + + + + + + {renderDeleteModal()} + {renderEditModal()} + + ); +}; + +SavedElementsModal.propTypes = { + addCustomElement: PropTypes.func.isRequired, + findCustomElements: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, + removeCustomElement: PropTypes.func.isRequired, + updateCustomElement: PropTypes.func.isRequired, +}; diff --git a/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.stories.storyshot index d46a509251d35..ac25cbe0b6832 100644 --- a/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.stories.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/sidebar_header/__examples__/__snapshots__/sidebar_header.stories.storyshot @@ -37,6 +37,7 @@ exports[`Storyshots components/Sidebar/SidebarHeader default 1`] = ` + +
+
+ + + +
+
+ + + +
+
+ + + +
@@ -134,6 +235,7 @@ exports[`Storyshots components/Sidebar/SidebarHeader without layer controls 1`] +
+
+`; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.examples.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.examples.tsx new file mode 100644 index 0000000000000..9aca5ce33ba02 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.examples.tsx @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import React from 'react'; +import { ElementSpec } from '../../../../../types'; +import { ElementMenu } from '../element_menu'; + +const testElements: { [key: string]: ElementSpec } = { + areaChart: { + name: 'areaChart', + displayName: 'Area chart', + help: 'A line chart with a filled body', + type: 'chart', + expression: `filters + | demodata + | pointseries x="time" y="mean(price)" + | plot defaultStyle={seriesStyle lines=1 fill=1} + | render`, + }, + debug: { + name: 'debug', + displayName: 'Debug data', + help: 'Just dumps the configuration of the element', + icon: 'bug', + expression: `demodata + | render as=debug`, + }, + dropdownFilter: { + name: 'dropdownFilter', + displayName: 'Dropdown select', + type: 'filter', + help: 'A dropdown from which you can select values for an "exactly" filter', + icon: 'filter', + height: 50, + expression: `demodata + | dropdownControl valueColumn=project filterColumn=project | render`, + filter: '', + }, + filterDebug: { + name: 'filterDebug', + displayName: 'Debug filter', + help: 'Shows the underlying global filters in a workpad', + icon: 'bug', + expression: `filters + | render as=debug`, + }, + image: { + name: 'image', + displayName: 'Image', + help: 'A static image', + type: 'image', + expression: `image dataurl=null mode="contain" + | render`, + }, + markdown: { + name: 'markdown', + displayName: 'Text', + type: 'text', + help: 'Add text using Markdown', + icon: 'visText', + expression: `filters +| demodata +| markdown "### Welcome to the Markdown element + +Good news! You're already connected to some demo data! + +The data table contains +**{{rows.length}} rows**, each containing +the following columns: +{{#each columns}} +**{{name}}** +{{/each}} + +You can use standard Markdown in here, but you can also access your piped-in data using Handlebars. If you want to know more, check out the [Handlebars documentation](https://handlebarsjs.com/guide/expressions.html). + +#### Enjoy!" | render`, + }, + progressGauge: { + name: 'progressGauge', + displayName: 'Gauge', + type: 'progress', + help: 'Displays progress as a portion of a gauge', + width: 200, + height: 200, + icon: 'visGoal', + expression: `filters + | demodata + | math "mean(percent_uptime)" + | progress shape="gauge" label={formatnumber 0%} font={font size=24 family="Helvetica" color="#000000" align=center} + | render`, + }, + revealImage: { + name: 'revealImage', + displayName: 'Image reveal', + type: 'image', + help: 'Reveals a percentage of an image', + expression: `filters + | demodata + | math "mean(percent_uptime)" + | revealImage origin=bottom image=null + | render`, + }, + shape: { + name: 'shape', + displayName: 'Shape', + type: 'shape', + help: 'A customizable shape', + width: 200, + height: 200, + icon: 'node', + expression: + 'shape "square" fill="#4cbce4" border="rgba(255,255,255,0)" borderWidth=0 maintainAspect=false | render', + }, + table: { + name: 'table', + displayName: 'Data table', + type: 'chart', + help: 'A scrollable grid for displaying data in a tabular format', + expression: `filters + | demodata + | table + | render`, + }, + timeFilter: { + name: 'timeFilter', + displayName: 'Time filter', + type: 'filter', + help: 'Set a time window', + icon: 'calendar', + height: 50, + expression: `timefilterControl compact=true column=@timestamp + | render`, + filter: 'timefilter column=@timestamp from=now-24h to=now', + }, +}; + +const mockRenderEmbedPanel = () =>
; + +storiesOf('components/WorkpadHeader/ElementMenu', module).add('default', () => ( + +)); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/element_menu.scss b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/element_menu.scss new file mode 100644 index 0000000000000..a946ee5519ce4 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/element_menu.scss @@ -0,0 +1,3 @@ +.canvasElementMenu__popoverButton { + margin-right: $euiSizeS; +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx new file mode 100644 index 0000000000000..a416adfe77469 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/element_menu.tsx @@ -0,0 +1,216 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sortBy } from 'lodash'; +import React, { Fragment, FunctionComponent, useState } from 'react'; +import PropTypes from 'prop-types'; +import { + EuiButton, + EuiContextMenu, + EuiIcon, + EuiContextMenuPanelItemDescriptor, +} from '@elastic/eui'; +import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../../common/lib'; +import { ComponentStrings } from '../../../../i18n/components'; +import { ElementSpec } from '../../../../types'; +import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; +import { getId } from '../../../lib/get_id'; +import { Popover, ClosePopoverFn } from '../../popover'; +// @ts-ignore Untyped local +import { AssetManager } from '../../asset_manager'; +import { SavedElementsModal } from '../../saved_elements_modal'; + +interface CategorizedElementLists { + [key: string]: ElementSpec[]; +} + +interface ElementTypeMeta { + [key: string]: { name: string; icon: string }; +} + +export const { WorkpadHeaderElementMenu: strings } = ComponentStrings; + +// label and icon for the context menu item for each element type +const elementTypeMeta: ElementTypeMeta = { + chart: { name: strings.getChartMenuItemLabel(), icon: 'visArea' }, + filter: { name: strings.getFilterMenuItemLabel(), icon: 'filter' }, + image: { name: strings.getImageMenuItemLabel(), icon: 'image' }, + other: { name: strings.getOtherMenuItemLabel(), icon: 'empty' }, + progress: { name: strings.getProgressMenuItemLabel(), icon: 'visGoal' }, + shape: { name: strings.getShapeMenuItemLabel(), icon: 'node' }, + text: { name: strings.getTextMenuItemLabel(), icon: 'visText' }, +}; + +const getElementType = (element: ElementSpec): string => + element && element.type && Object.keys(elementTypeMeta).includes(element.type) + ? element.type + : 'other'; + +const categorizeElementsByType = (elements: ElementSpec[]): { [key: string]: ElementSpec[] } => { + elements = sortBy(elements, 'displayName'); + + const categories: CategorizedElementLists = { other: [] }; + + elements.forEach((element: ElementSpec) => { + const type = getElementType(element); + + if (categories[type]) { + categories[type].push(element); + } else { + categories[type] = [element]; + } + }); + + return categories; +}; + +export interface Props { + /** + * Dictionary of elements from elements registry + */ + elements: { [key: string]: ElementSpec }; + /** + * Handler for adding a selected element to the workpad + */ + addElement: (element: ElementSpec) => void; + /** + * Renders embeddable flyout + */ + renderEmbedPanel: (onClose: () => void) => JSX.Element; +} + +export const ElementMenu: FunctionComponent = ({ + elements, + addElement, + renderEmbedPanel, +}) => { + const [isAssetModalVisible, setAssetModalVisible] = useState(false); + const [isEmbedPanelVisible, setEmbedPanelVisible] = useState(false); + const [isSavedElementsModalVisible, setSavedElementsModalVisible] = useState(false); + + const hideAssetModal = () => setAssetModalVisible(false); + const showAssetModal = () => setAssetModalVisible(true); + const showEmbedPanel = () => setEmbedPanelVisible(false); + const hideEmbedPanel = () => setEmbedPanelVisible(false); + const hideSavedElementsModal = () => setSavedElementsModalVisible(false); + const showSavedElementsModal = () => setSavedElementsModalVisible(true); + + const { + chart: chartElements, + filter: filterElements, + image: imageElements, + other: otherElements, + progress: progressElements, + shape: shapeElements, + text: textElements, + } = categorizeElementsByType(Object.values(elements)); + + const getPanelTree = (closePopover: ClosePopoverFn) => { + const elementToMenuItem = (element: ElementSpec): EuiContextMenuPanelItemDescriptor => ({ + name: element.displayName || element.name, + icon: element.icon, + onClick: () => { + addElement(element); + closePopover(); + }, + }); + + const elementListToMenuItems = (elementList: ElementSpec[]) => { + const type = getElementType(elementList[0]); + const { name, icon } = elementTypeMeta[type] || elementTypeMeta.other; + + if (elementList.length > 1) { + return { + name, + icon: , + panel: { + id: getId('element-type'), + title: name, + items: elementList.map(elementToMenuItem), + }, + }; + } + + return elementToMenuItem(elementList[0]); + }; + + return { + id: 0, + title: strings.getElementMenuLabel(), + items: [ + elementListToMenuItems(textElements), + elementListToMenuItems(shapeElements), + elementListToMenuItems(chartElements), + elementListToMenuItems(imageElements), + elementListToMenuItems(filterElements), + elementListToMenuItems(progressElements), + elementListToMenuItems(otherElements), + { + name: strings.getMyElementsMenuItemLabel(), + className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, + 'data-test-subj': 'saved-elements-menu-option', + icon: , + onClick: () => { + showSavedElementsModal(); + closePopover(); + }, + }, + { + name: strings.getAssetsMenuItemLabel(), + icon: , + onClick: () => { + showAssetModal(); + closePopover(); + }, + }, + { + name: strings.getEmbedObjectMenuItemLabel(), + className: CONTEXT_MENU_TOP_BORDER_CLASSNAME, + icon: , + onClick: () => { + showEmbedPanel(); + closePopover(); + }, + }, + ], + }; + }; + + const exportControl = (togglePopover: React.MouseEventHandler) => ( + + {strings.getElementMenuButtonLabel()} + + ); + + return ( + + + {({ closePopover }: { closePopover: ClosePopoverFn }) => ( + + )} + + {isAssetModalVisible ? : null} + {isEmbedPanelVisible ? renderEmbedPanel(hideEmbedPanel) : null} + {isSavedElementsModalVisible ? : null} + + ); +}; + +ElementMenu.propTypes = { + elements: PropTypes.object, + addElement: PropTypes.func.isRequired, +}; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/index.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/index.tsx new file mode 100644 index 0000000000000..40571a9341f69 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/element_menu/index.tsx @@ -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 React from 'react'; +import { connect } from 'react-redux'; +import { compose, withProps } from 'recompose'; +import { Dispatch } from 'redux'; +import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public/'; +import { State, ElementSpec } from '../../../../types'; +// @ts-ignore Untyped local +import { elementsRegistry } from '../../../lib/elements_registry'; +import { ElementMenu as Component, Props as ComponentProps } from './element_menu'; +// @ts-ignore Untyped local +import { addElement } from '../../../state/actions/elements'; +import { getSelectedPage } from '../../../state/selectors/workpad'; +import { AddEmbeddablePanel } from '../../embeddable_flyout'; + +interface StateProps { + pageId: string; +} + +interface DispatchProps { + addElement: (pageId: string) => (partialElement: ElementSpec) => void; +} + +const mapStateToProps = (state: State) => ({ + pageId: getSelectedPage(state), +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + addElement: (pageId: string) => (element: ElementSpec) => dispatch(addElement(pageId, element)), +}); + +const mergeProps = (stateProps: StateProps, dispatchProps: DispatchProps) => ({ + ...stateProps, + ...dispatchProps, + addElement: dispatchProps.addElement(stateProps.pageId), + // Moved this section out of the main component to enable stories + renderEmbedPanel: (onClose: () => void) => , +}); + +export const ElementMenu = compose( + connect(mapStateToProps, mapDispatchToProps, mergeProps), + withKibana, + withProps(() => ({ elements: elementsRegistry.toJS() })) +)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/pdf_panel.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/pdf_panel.stories.storyshot similarity index 93% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/pdf_panel.stories.storyshot rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/pdf_panel.stories.storyshot index 43adcb37c5f4c..9ad2714a78ec9 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/pdf_panel.stories.storyshot +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/pdf_panel.stories.storyshot @@ -1,11 +1,11 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Storyshots components/Export/PDFPanel default 1`] = ` +exports[`Storyshots components/WorkpadHeader/ShareMenu/PDFPanel default 1`] = `
+
+
+ +
+
+
+`; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/pdf_panel.stories.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/pdf_panel.stories.tsx similarity index 92% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/pdf_panel.stories.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/pdf_panel.stories.tsx index 76a40f51148a7..eb99dbc494a32 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/pdf_panel.stories.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/pdf_panel.stories.tsx @@ -8,7 +8,7 @@ import { action } from '@storybook/addon-actions'; import React from 'react'; import { PDFPanel } from '../pdf_panel'; -storiesOf('components/Export/PDFPanel', module) +storiesOf('components/WorkpadHeader/ShareMenu/PDFPanel', module) .addParameters({ info: { inline: true, diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/workpad_export.examples.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.examples.tsx similarity index 79% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/workpad_export.examples.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.examples.tsx index 92e7cca40ee3a..ab9137b1676c9 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/workpad_export.examples.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.examples.tsx @@ -6,10 +6,10 @@ import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; -import { WorkpadExport } from '../workpad_export'; +import { ShareMenu } from '../share_menu'; -storiesOf('components/Export/WorkpadExport', module).add('enabled', () => ( - ( + { diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/__examples__/share_website_flyout.stories.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/__examples__/share_website_flyout.stories.tsx similarity index 93% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/__examples__/share_website_flyout.stories.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/__examples__/share_website_flyout.stories.tsx index af30d8d4fc20b..886ddcfd763e1 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/__examples__/share_website_flyout.stories.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/__examples__/share_website_flyout.stories.tsx @@ -8,7 +8,7 @@ import { action } from '@storybook/addon-actions'; import React from 'react'; import { ShareWebsiteFlyout } from '../share_website_flyout'; -storiesOf('components/Export/ShareWebsiteFlyout', module) +storiesOf('components/WorkpadHeader/ShareMenu/ShareWebsiteFlyout', module) .addParameters({ info: { inline: true, diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts similarity index 85% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/index.ts rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts index 2bf3e1f0ef1f4..4377635acac88 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/index.ts +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/index.ts @@ -6,34 +6,29 @@ import { connect } from 'react-redux'; import { compose, withProps } from 'recompose'; -// @ts-ignore Untyped local import { getWorkpad, getRenderedWorkpad, getRenderedWorkpadExpressions, } from '../../../../state/selectors/workpad'; // @ts-ignore Untyped local -import { notify } from '../../../../lib/notify'; -// @ts-ignore Untyped local import { downloadRenderedWorkpad, downloadRuntime, downloadZippedRuntime, - // @ts-ignore Untyped local } from '../../../../lib/download_workpad'; import { ShareWebsiteFlyout as Component, Props as ComponentProps } from './share_website_flyout'; import { State, CanvasWorkpad } from '../../../../../types'; import { CanvasRenderedWorkpad } from '../../../../../shareable_runtime/types'; -// @ts-ignore Untyped local. -import { fetch, arrayBufferFetch } from '../../../../../common/lib/fetch'; +import { arrayBufferFetch } from '../../../../../common/lib/fetch'; import { API_ROUTE_SHAREABLE_ZIP } from '../../../../../common/lib/constants'; import { renderFunctionNames } from '../../../../../shareable_runtime/supported_renderers'; import { ComponentStrings } from '../../../../../i18n/components'; import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public/'; -import { OnCloseFn } from '../workpad_export'; +import { OnCloseFn } from '../share_menu'; import { WithKibanaProps } from '../../../../index'; -const { WorkpadHeaderWorkpadExport: strings } = ComponentStrings; +const { WorkpadHeaderShareMenu: strings } = ComponentStrings; const getUnsupportedRenderers = (state: State) => { const renderers: string[] = []; @@ -74,7 +69,7 @@ export const ShareWebsiteFlyout = compose unsupportedRenderers, onClose, onCopy: () => { - notify.info(strings.getCopyShareConfigMessage()); + kibana.services.canvas.notify.info(strings.getCopyShareConfigMessage()); }, onDownload: type => { switch (type) { @@ -90,7 +85,9 @@ export const ShareWebsiteFlyout = compose .post(`${basePath}${API_ROUTE_SHAREABLE_ZIP}`, JSON.stringify(renderedWorkpad)) .then(blob => downloadZippedRuntime(blob.data)) .catch((err: Error) => { - notify.error(err, { title: strings.getShareableZipErrorTitle(workpad.name) }); + kibana.services.canvas.notify.error(err, { + title: strings.getShareableZipErrorTitle(workpad.name), + }); }); return; default: diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/runtime_step.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx similarity index 100% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/runtime_step.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/runtime_step.tsx diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/share_website_flyout.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/share_website_flyout.tsx similarity index 98% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/share_website_flyout.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/share_website_flyout.tsx index 8dcbb18ffed86..5fd381baa73f5 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/share_website_flyout.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/share_website_flyout.tsx @@ -24,7 +24,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { ComponentStrings } from '../../../../../i18n/components'; import { ZIP, CANVAS, HTML } from '../../../../../i18n/constants'; -import { OnCloseFn } from '../workpad_export'; +import { OnCloseFn } from '../share_menu'; import { WorkpadStep } from './workpad_step'; import { RuntimeStep } from './runtime_step'; import { SnippetsStep } from './snippets_step'; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/snippets_step.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx similarity index 98% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/snippets_step.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx index c19ad6d77b131..81f559651eb25 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/flyout/snippets_step.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/flyout/snippets_step.tsx @@ -42,7 +42,7 @@ export const SnippetsStep: FC<{ onCopy: OnCopyFn }> = ({ onCopy }) => ( ({ workpad: getWorkpad(state), @@ -50,7 +41,7 @@ interface Props { pageCount: number; } -export const WorkpadExport = compose( +export const ShareMenu = compose( connect(mapStateToProps), withKibana, withProps( @@ -66,10 +57,10 @@ export const WorkpadExport = compose( onCopy: type => { switch (type) { case 'pdf': - notify.info(strings.getCopyPDFMessage()); + kibana.services.canvas.notify.info(strings.getCopyPDFMessage()); break; case 'reportingConfig': - notify.info(strings.getCopyReportingConfigMessage()); + kibana.services.canvas.notify.info(strings.getCopyReportingConfigMessage()); break; default: throw new Error(strings.getUnknownExportErrorMessage(type)); @@ -80,7 +71,7 @@ export const WorkpadExport = compose( case 'pdf': return createPdf(workpad, { pageCount }, kibana.services.http.basePath) .then(({ data }: { data: { job: { id: string } } }) => { - notify.info(strings.getExportPDFMessage(), { + kibana.services.canvas.notify.info(strings.getExportPDFMessage(), { title: strings.getExportPDFTitle(workpad.name), }); @@ -88,7 +79,9 @@ export const WorkpadExport = compose( jobCompletionNotifications.add(data.job.id); }) .catch((err: Error) => { - notify.error(err, { title: strings.getExportPDFErrorTitle(workpad.name) }); + kibana.services.canvas.notify.error(err, { + title: strings.getExportPDFErrorTitle(workpad.name), + }); }); case 'json': downloadWorkpad(workpad.id); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/pdf_panel.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/pdf_panel.tsx similarity index 92% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/pdf_panel.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/pdf_panel.tsx index ef70079cf697b..a178964e0b566 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/pdf_panel.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/pdf_panel.tsx @@ -9,7 +9,7 @@ import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; import { Clipboard } from '../../clipboard'; import { ComponentStrings } from '../../../../i18n/components'; -const { WorkpadHeaderWorkpadExport: strings } = ComponentStrings; +const { WorkpadHeaderShareMenu: strings } = ComponentStrings; interface Props { /** The URL that will invoke PDF Report generation. */ @@ -24,7 +24,7 @@ interface Props { * A panel displayed in the Export Menu with options in which to generate PDF Reports. */ export const PDFPanel = ({ pdfURL, onExport, onCopy }: Props) => ( -
+

{strings.getPDFPanelGenerateDescription()}

diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/workpad_export.scss b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/share_menu.scss similarity index 61% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/workpad_export.scss rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/share_menu.scss index 44209aaa72d63..03227f77e0de5 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/workpad_export.scss +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/share_menu.scss @@ -1,8 +1,8 @@ -.canvasWorkpadExport__panelContent { +.canvasShareMenu__panelContent { padding: $euiSize; } -.canvasWorkpadExport__reportingConfig { +.canvasShareMenu__reportingConfig { .euiCodeBlock__pre { @include euiScrollBar; overflow-x: auto; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/workpad_export.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx similarity index 71% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/workpad_export.tsx rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx index 522be043ec457..621077c29c368 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/workpad_export.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/share_menu.tsx @@ -6,16 +6,14 @@ import React, { FunctionComponent, useState } from 'react'; import PropTypes from 'prop-types'; -import { EuiButtonIcon, EuiContextMenu, EuiIcon } from '@elastic/eui'; -// @ts-ignore Untyped local -import { Popover } from '../../popover'; +import { EuiButtonEmpty, EuiContextMenu, EuiIcon } from '@elastic/eui'; +import { ComponentStrings } from '../../../../i18n/components'; +import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; +import { Popover, ClosePopoverFn } from '../../popover'; import { PDFPanel } from './pdf_panel'; import { ShareWebsiteFlyout } from './flyout'; -import { ComponentStrings } from '../../../../i18n/components'; -const { WorkpadHeaderWorkpadExport: strings } = ComponentStrings; - -type ClosePopoverFn = () => void; +const { WorkpadHeaderShareMenu: strings } = ComponentStrings; type CopyTypes = 'pdf' | 'reportingConfig'; type ExportTypes = 'pdf' | 'json'; @@ -39,31 +37,13 @@ export interface Props { /** * The Menu for Exporting a Workpad from Canvas. */ -export const WorkpadExport: FunctionComponent = ({ onCopy, onExport, getExportUrl }) => { +export const ShareMenu: FunctionComponent = ({ onCopy, onExport, getExportUrl }) => { const [showFlyout, setShowFlyout] = useState(false); const onClose = () => { setShowFlyout(false); }; - // TODO: Fix all of this magic from EUI; this code is boilerplate from - // EUI examples and isn't easily typed. - const flattenPanelTree = (tree: any, array: any[] = []) => { - array.push(tree); - - if (tree.items) { - tree.items.forEach((item: any) => { - const { panel } = item; - if (panel) { - flattenPanelTree(panel, array); - item.panel = panel.id; - } - }); - } - - return array; - }; - const getPDFPanel = (closePopover: ClosePopoverFn) => { return ( = ({ onCopy, onExport, getE ], }); - const exportControl = (togglePopover: React.MouseEventHandler) => ( - + const shareControl = (togglePopover: React.MouseEventHandler) => ( + + {strings.getShareMenuButtonLabel()} + ); const flyout = showFlyout ? : null; return (
- + {({ closePopover }: { closePopover: ClosePopoverFn }) => ( = ({ onCopy, onExport, getE ); }; -WorkpadExport.propTypes = { +ShareMenu.propTypes = { onCopy: PropTypes.func.isRequired, onExport: PropTypes.func.isRequired, getExportUrl: PropTypes.func.isRequired, diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.test.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.test.ts rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/utils.test.ts diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/utils.ts similarity index 100% rename from x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/utils.ts rename to x-pack/legacy/plugins/canvas/public/components/workpad_header/share_menu/utils.ts diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/__examples__/__snapshots__/view_menu.stories.storyshot b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/__examples__/__snapshots__/view_menu.stories.storyshot new file mode 100644 index 0000000000000..e1ecee0e152be --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/__examples__/__snapshots__/view_menu.stories.storyshot @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/WorkpadHeader/ViewMenu edit mode 1`] = ` +
+
+ +
+
+`; + +exports[`Storyshots components/WorkpadHeader/ViewMenu read only mode 1`] = ` +
+
+ +
+
+`; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.tsx new file mode 100644 index 0000000000000..60837ac1218e6 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/__examples__/view_menu.stories.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 { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import React from 'react'; +import { ViewMenu } from '../view_menu'; + +storiesOf('components/WorkpadHeader/ViewMenu', module) + .add('edit mode', () => ( + + )) + .add('read only mode', () => ( + + )); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/index.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/index.ts new file mode 100644 index 0000000000000..eee613183639c --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/index.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +import { compose, withHandlers } from 'recompose'; +import { Dispatch } from 'redux'; +import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public/'; +import { zoomHandlerCreators } from '../../../lib/app_handler_creators'; +import { State, CanvasWorkpadBoundingBox } from '../../../../types'; +// @ts-ignore Untyped local +import { fetchAllRenderables } from '../../../state/actions/elements'; +// @ts-ignore Untyped local +import { setZoomScale, setFullscreen, selectToplevelNodes } from '../../../state/actions/transient'; +// @ts-ignore Untyped local +import { setWriteable } from '../../../state/actions/workpad'; +import { getZoomScale, canUserWrite } from '../../../state/selectors/app'; +import { + getWorkpadBoundingBox, + getWorkpadWidth, + getWorkpadHeight, + isWriteable, +} from '../../../state/selectors/workpad'; +import { ViewMenu as Component, Props as ComponentProps } from './view_menu'; +import { getFitZoomScale } from './lib/get_fit_zoom_scale'; + +interface StateProps { + zoomScale: number; + boundingBox: CanvasWorkpadBoundingBox; + workpadWidth: number; + workpadHeight: number; + isWriteable: boolean; +} + +interface DispatchProps { + setWriteable: (isWorkpadWriteable: boolean) => void; + setZoomScale: (scale: number) => void; + setFullscreen: (showFullscreen: boolean) => void; +} + +const mapStateToProps = (state: State) => ({ + zoomScale: getZoomScale(state), + boundingBox: getWorkpadBoundingBox(state), + workpadWidth: getWorkpadWidth(state), + workpadHeight: getWorkpadHeight(state), + isWriteable: isWriteable(state) && canUserWrite(state), +}); + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + setZoomScale: (scale: number) => dispatch(setZoomScale(scale)), + setWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)), + setFullscreen: (value: boolean) => { + dispatch(setFullscreen(value)); + if (value) { + dispatch(selectToplevelNodes([])); + } + }, + doRefresh: () => dispatch(fetchAllRenderables()), +}); + +const mergeProps = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ownProps: ComponentProps +): ComponentProps => { + const { boundingBox, workpadWidth, workpadHeight, ...remainingStateProps } = stateProps; + return { + ...remainingStateProps, + ...dispatchProps, + ...ownProps, + toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable), + enterFullscreen: () => dispatchProps.setFullscreen(true), + fitToWindow: () => getFitZoomScale(boundingBox, workpadWidth, workpadHeight), + }; +}; + +export const ViewMenu = compose( + connect(mapStateToProps, mapDispatchToProps, mergeProps), + withKibana, + withHandlers(zoomHandlerCreators) +)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/lib/get_fit_zoom_scale.ts b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/lib/get_fit_zoom_scale.ts new file mode 100644 index 0000000000000..783d6340c33c4 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/lib/get_fit_zoom_scale.ts @@ -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 { + CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR, + WORKPAD_CANVAS_BUFFER, +} from '../../../../../common/lib'; +import { CanvasWorkpadBoundingBox } from '../../../../../types'; + +export const getFitZoomScale = ( + boundingBox: CanvasWorkpadBoundingBox, + workpadWidth: number, + workpadHeight: number +) => { + const canvasLayoutContent = document.querySelector( + `#${CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR}` + ) as HTMLElement; + const layoutWidth = canvasLayoutContent.clientWidth; + const layoutHeight = canvasLayoutContent.clientHeight; + const offsetLeft = boundingBox.left; + const offsetTop = boundingBox.top; + const offsetRight = boundingBox.right - workpadWidth; + const offsetBottom = boundingBox.bottom - workpadHeight; + const boundingWidth = + workpadWidth + + Math.max(Math.abs(offsetLeft), Math.abs(offsetRight)) * 2 + + WORKPAD_CANVAS_BUFFER; + const boundingHeight = + workpadHeight + + Math.max(Math.abs(offsetTop), Math.abs(offsetBottom)) * 2 + + WORKPAD_CANVAS_BUFFER; + const xScale = layoutWidth / boundingWidth; + const yScale = layoutHeight / boundingHeight; + + return Math.min(xScale, yScale); +}; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx new file mode 100644 index 0000000000000..d1e08c5809579 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx @@ -0,0 +1,172 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 PropTypes from 'prop-types'; +import { + EuiButtonEmpty, + EuiContextMenu, + EuiIcon, + EuiContextMenuPanelItemDescriptor, +} from '@elastic/eui'; +import { MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from '../../../../common/lib/constants'; +import { ComponentStrings } from '../../../../i18n/components'; +import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; +import { Popover, ClosePopoverFn } from '../../popover'; + +const { WorkpadHeaderViewMenu: strings } = ComponentStrings; + +const QUICK_ZOOM_LEVELS = [0.5, 1, 2]; + +export interface Props { + /** + * Is the workpad edittable? + */ + isWriteable: boolean; + /** + * current workpad zoom level + */ + zoomScale: number; + /** + * zooms to fit entire workpad into view + */ + fitToWindow: () => void; + /** + * handler to set the workpad zoom level to a specific value + */ + setZoomScale: (scale: number) => void; + /** + * handler to increase the workpad zoom level + */ + zoomIn: () => void; + /** + * handler to decrease workpad zoom level + */ + zoomOut: () => void; + /** + * reset zoom to 100% + */ + resetZoom: () => void; + /** + * toggle edit/read only mode + */ + toggleWriteable: () => void; + /** + * enter fullscreen mode + */ + enterFullscreen: () => void; + /** + * triggers a refresh of the workpad + */ + doRefresh: () => void; +} + +export const ViewMenu: FunctionComponent = ({ + doRefresh, + enterFullscreen, + fitToWindow, + isWriteable, + resetZoom, + setZoomScale, + toggleWriteable, + zoomIn, + zoomOut, + zoomScale, +}) => { + const viewControl = (togglePopover: React.MouseEventHandler) => ( + + {strings.getViewMenuButtonLabel()} + + ); + + const getScaleMenuItems = (): EuiContextMenuPanelItemDescriptor[] => + QUICK_ZOOM_LEVELS.map((scale: number) => ({ + name: strings.getZoomPercentage(scale), + icon: 'empty', + onClick: () => setZoomScale(scale), + })); + + const getZoomMenuItems = (): EuiContextMenuPanelItemDescriptor[] => [ + { + name: strings.getZoomFitToWindowText(), + icon: 'empty', + onClick: fitToWindow, + disabled: zoomScale === MAX_ZOOM_LEVEL, + }, + ...getScaleMenuItems(), + { + name: strings.getZoomInText(), + icon: 'magnifyWithPlus', + onClick: zoomIn, + disabled: zoomScale === MAX_ZOOM_LEVEL, + className: 'canvasContextMenu--topBorder', + }, + { + name: strings.getZoomOutText(), + icon: 'magnifyWithMinus', + onClick: zoomOut, + disabled: zoomScale <= MIN_ZOOM_LEVEL, + }, + { + name: strings.getZoomResetText(), + icon: 'empty', + onClick: resetZoom, + disabled: zoomScale >= MAX_ZOOM_LEVEL, + className: 'canvasContextMenu--topBorder', + }, + ]; + + const getPanelTree = (closePopover: ClosePopoverFn) => ({ + id: 0, + title: strings.getViewMenuLabel(), + items: [ + { + name: strings.getFullscreenMenuItemLabel(), + icon: , + onClick: () => { + enterFullscreen(); + closePopover(); + }, + }, + { + name: isWriteable ? strings.getHideEditModeLabel() : strings.getShowEditModeLabel(), + icon: , + onClick: () => { + toggleWriteable(); + closePopover(); + }, + }, + { + name: strings.getRefreshMenuItemLabel(), + icon: 'refresh', + onClick: () => { + doRefresh(); + }, + }, + { + name: strings.getZoomMenuItemLabel(), + icon: 'magnifyWithPlus', + panel: { + id: 1, + title: strings.getZoomMenuItemLabel(), + items: getZoomMenuItems(), + }, + }, + ], + }); + + return ( + + {({ closePopover }: { closePopover: ClosePopoverFn }) => ( + + )} + + ); +}; + +ViewMenu.propTypes = { + isWriteable: PropTypes.bool.isRequired, +}; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/workpad_export.examples.storyshot b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/workpad_export.examples.storyshot deleted file mode 100644 index ef96320e7bc65..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_export/__examples__/__snapshots__/workpad_export.examples.storyshot +++ /dev/null @@ -1,41 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Storyshots components/Export/WorkpadExport enabled 1`] = ` -
-
-
- - - -
-
-
-`; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.tsx index 31ad0593f58bb..253e6c68cfc9e 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_header.tsx @@ -4,38 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { FunctionComponent } from 'react'; import PropTypes from 'prop-types'; // @ts-ignore no @types definition import { Shortcuts } from 'react-shortcuts'; -import { - EuiFlexItem, - EuiFlexGroup, - EuiButtonIcon, - EuiButton, - EuiButtonEmpty, - EuiOverlayMask, - EuiModal, - EuiModalFooter, - EuiToolTip, -} from '@elastic/eui'; - +import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { ComponentStrings } from '../../../i18n'; - -// @ts-ignore untyped local -import { AssetManager } from '../asset_manager'; -// @ts-ignore untyped local -import { ElementTypes } from '../element_types'; import { ToolTipShortcut } from '../tool_tip_shortcut/'; -import { AddEmbeddablePanel } from '../embeddable_flyout'; -// @ts-ignore untyped local import { ControlSettings } from './control_settings'; // @ts-ignore untyped local import { RefreshControl } from './refresh_control'; // @ts-ignore untyped local import { FullscreenControl } from './fullscreen_control'; -import { WorkpadExport } from './workpad_export'; -import { WorkpadZoom } from './workpad_zoom'; +import { ElementMenu } from './element_menu'; +import { ShareMenu } from './share_menu'; +import { ViewMenu } from './view_menu'; const { WorkpadHeader: strings } = ComponentStrings; @@ -43,23 +26,20 @@ export interface Props { isWriteable: boolean; toggleWriteable: () => void; canUserWrite: boolean; - selectedPage: string; } -interface State { - isModalVisible: boolean; - isPanelVisible: boolean; -} - -export class WorkpadHeader extends React.PureComponent { - static propTypes = { - isWriteable: PropTypes.bool, - toggleWriteable: PropTypes.func, +export const WorkpadHeader: FunctionComponent = ({ + isWriteable, + canUserWrite, + toggleWriteable, +}) => { + const keyHandler = (action: string) => { + if (action === 'EDITING') { + toggleWriteable(); + } }; - state = { isModalVisible: false, isPanelVisible: false }; - - _fullscreenButton = ({ toggleFullscreen }: { toggleFullscreen: () => void }) => ( + const fullscreenButton = ({ toggleFullscreen }: { toggleFullscreen: () => void }) => ( { ); - _keyHandler = (action: string) => { - if (action === 'EDITING') { - this.props.toggleWriteable(); - } - }; - - _hideElementModal = () => this.setState({ isModalVisible: false }); - _showElementModal = () => this.setState({ isModalVisible: true }); - - _hideEmbeddablePanel = () => this.setState({ isPanelVisible: false }); - _showEmbeddablePanel = () => this.setState({ isPanelVisible: true }); - - _elementAdd = () => ( - - - - - - {strings.getAddElementModalCloseButtonLabel()} - - - - - ); - - _embeddableAdd = () => ; - - _getEditToggleToolTipText = () => { - if (!this.props.canUserWrite) { + const getEditToggleToolTipText = () => { + if (!canUserWrite) { return strings.getNoWritePermissionTooltipText(); } - const content = this.props.isWriteable + const content = isWriteable ? strings.getHideEditControlTooltip() : strings.getShowEditControlTooltip(); return content; }; - _getEditToggleToolTip = ({ textOnly } = { textOnly: false }) => { - const content = this._getEditToggleToolTipText(); + const getEditToggleToolTip = ({ textOnly } = { textOnly: false }) => { + const content = getEditToggleToolTipText(); if (textOnly) { return content; @@ -135,86 +83,67 @@ export class WorkpadHeader extends React.PureComponent { ); }; - render() { - const { isWriteable, canUserWrite, toggleWriteable } = this.props; - const { isModalVisible, isPanelVisible } = this.state; - - return ( -
- {isModalVisible ? this._elementAdd() : null} - {isPanelVisible ? this._embeddableAdd() : null} - - - - - - - - - - - {this._fullscreenButton} - - - - - - - - - {canUserWrite && ( - - )} - - - - - - - {isWriteable ? ( + return ( + + + + {isWriteable && ( - - - - - - - {strings.getEmbedObjectButtonLabel()} - - - - - {strings.getAddElementButtonLabel()} - - - + - ) : null} + )} + + + + + + + + + -
- ); - } -} + + + + + {canUserWrite && ( + + )} + + + + + + + + + {fullscreenButton} + + + + + ); +}; + +WorkpadHeader.propTypes = { + isWriteable: PropTypes.bool, + toggleWriteable: PropTypes.func, + canUserWrite: PropTypes.bool, +}; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/index.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/index.tsx deleted file mode 100644 index b22a9d35aa793..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/index.tsx +++ /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 { compose, withHandlers } from 'recompose'; -import { connect } from 'react-redux'; -import { Dispatch } from 'redux'; -import { getZoomScale } from '../../../state/selectors/app'; -import { - getWorkpadBoundingBox, - getWorkpadWidth, - getWorkpadHeight, -} from '../../../state/selectors/workpad'; -// @ts-ignore unconverted local file -import { setZoomScale } from '../../../state/actions/transient'; -import { zoomHandlerCreators } from '../../../lib/app_handler_creators'; -import { WorkpadZoom as Component, Props as ComponentProps } from './workpad_zoom'; -import { State } from '../../../../types'; - -const mapStateToProps = (state: State) => { - return { - zoomScale: getZoomScale(state), - boundingBox: getWorkpadBoundingBox(state), - workpadWidth: getWorkpadWidth(state), - workpadHeight: getWorkpadHeight(state), - }; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - setZoomScale: (scale: number) => dispatch(setZoomScale(scale)), -}); - -export const WorkpadZoom = compose( - connect(mapStateToProps, mapDispatchToProps), - withHandlers(zoomHandlerCreators) -)(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/workpad_zoom.scss b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/workpad_zoom.scss deleted file mode 100644 index 44209aaa72d63..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/workpad_zoom.scss +++ /dev/null @@ -1,11 +0,0 @@ -.canvasWorkpadExport__panelContent { - padding: $euiSize; -} - -.canvasWorkpadExport__reportingConfig { - .euiCodeBlock__pre { - @include euiScrollBar; - overflow-x: auto; - white-space: pre; - } -} diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/workpad_zoom.tsx b/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/workpad_zoom.tsx deleted file mode 100644 index 4e37a525761cd..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_header/workpad_zoom/workpad_zoom.tsx +++ /dev/null @@ -1,176 +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, { MouseEventHandler, PureComponent } from 'react'; -import PropTypes from 'prop-types'; -import { - EuiButtonIcon, - EuiContextMenu, - EuiContextMenuPanelDescriptor, - EuiContextMenuPanelItemDescriptor, -} from '@elastic/eui'; -// @ts-ignore untyped local -import { Popover } from '../../popover'; -import { - MAX_ZOOM_LEVEL, - MIN_ZOOM_LEVEL, - WORKPAD_CANVAS_BUFFER, - CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR, -} from '../../../../common/lib/constants'; - -import { ComponentStrings } from '../../../../i18n'; -const { WorkpadHeaderWorkpadZoom: strings } = ComponentStrings; - -export interface Props { - /** - * current workpad zoom level - */ - zoomScale: number; - /** - * minimum bounding box for the workpad - */ - boundingBox: { left: number; right: number; top: number; bottom: number }; - /** - * width of the workpad page - */ - workpadWidth: number; - /** - * height of the workpad page - */ - workpadHeight: number; - /** - * handler to set the workpad zoom level to a specific value - */ - setZoomScale: (scale: number) => void; - /** - * handler to increase the workpad zoom level - */ - zoomIn: () => void; - /** - * handler to decrease workpad zoom level - */ - zoomOut: () => void; - /** - * reset zoom to 100% - */ - resetZoom: () => void; -} - -const QUICK_ZOOM_LEVELS = [0.5, 1, 2]; - -export class WorkpadZoom extends PureComponent { - static propTypes = { - zoomScale: PropTypes.number.isRequired, - setZoomScale: PropTypes.func.isRequired, - zoomIn: PropTypes.func.isRequired, - zoomOut: PropTypes.func.isRequired, - resetZoom: PropTypes.func.isRequired, - boundingBox: PropTypes.shape({ - left: PropTypes.number.isRequired, - right: PropTypes.number.isRequired, - top: PropTypes.number.isRequired, - bottom: PropTypes.number.isRequired, - }), - workpadWidth: PropTypes.number.isRequired, - workpadHeight: PropTypes.number.isRequired, - }; - - _fitToWindow = () => { - const { boundingBox, setZoomScale, workpadWidth, workpadHeight } = this.props; - const canvasLayoutContent = document.querySelector( - `#${CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR}` - ) as HTMLElement; - const layoutWidth = canvasLayoutContent.clientWidth; - const layoutHeight = canvasLayoutContent.clientHeight; - const offsetLeft = boundingBox.left; - const offsetTop = boundingBox.top; - const offsetRight = boundingBox.right - workpadWidth; - const offsetBottom = boundingBox.bottom - workpadHeight; - const boundingWidth = - workpadWidth + - Math.max(Math.abs(offsetLeft), Math.abs(offsetRight)) * 2 + - WORKPAD_CANVAS_BUFFER; - const boundingHeight = - workpadHeight + - Math.max(Math.abs(offsetTop), Math.abs(offsetBottom)) * 2 + - WORKPAD_CANVAS_BUFFER; - const xScale = layoutWidth / boundingWidth; - const yScale = layoutHeight / boundingHeight; - - setZoomScale(Math.min(xScale, yScale)); - }; - - _button = (togglePopover: MouseEventHandler) => ( - - ); - - _getScaleMenuItems = (): EuiContextMenuPanelItemDescriptor[] => - QUICK_ZOOM_LEVELS.map(scale => ({ - name: strings.getZoomPercentage(scale), - icon: 'empty', - onClick: () => this.props.setZoomScale(scale), - })); - - _getPanels = (): EuiContextMenuPanelDescriptor[] => { - const { zoomScale, zoomIn, zoomOut, resetZoom } = this.props; - const items: EuiContextMenuPanelItemDescriptor[] = [ - { - name: strings.getZoomFitToWindowText(), - icon: 'empty', - onClick: this._fitToWindow, - disabled: zoomScale === MAX_ZOOM_LEVEL, - }, - ...this._getScaleMenuItems(), - { - name: strings.getZoomInText(), - icon: 'magnifyWithPlus', - onClick: zoomIn, - disabled: zoomScale === MAX_ZOOM_LEVEL, - className: 'canvasContextMenu--topBorder', - }, - { - name: strings.getZoomOutText(), - icon: 'magnifyWithMinus', - onClick: zoomOut, - disabled: zoomScale <= MIN_ZOOM_LEVEL, - }, - { - name: strings.getZoomResetText(), - icon: 'empty', - onClick: resetZoom, - disabled: zoomScale >= MAX_ZOOM_LEVEL, - className: 'canvasContextMenu--topBorder', - }, - ]; - - const panels: EuiContextMenuPanelDescriptor[] = [ - { - id: 0, - title: strings.getZoomPanelTitle(), - items, - }, - ]; - - return panels; - }; - - render() { - return ( - - {() => } - - ); - } -} diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/index.js b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/index.js index 226ad420535bd..9379379e54d97 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/index.js @@ -9,7 +9,6 @@ import { connect } from 'react-redux'; import { compose, withState, getContext, withHandlers, withProps } from 'recompose'; import moment from 'moment'; import * as workpadService from '../../lib/workpad_service'; -import { notify } from '../../lib/notify'; import { canUserWrite } from '../../state/selectors/app'; import { getWorkpad } from '../../state/selectors/workpad'; import { getId } from '../../lib/get_id'; @@ -32,7 +31,11 @@ export const WorkpadLoader = compose( }), connect(mapStateToProps), withState('workpads', 'setWorkpads', null), - withHandlers({ + withKibana, + withProps(({ kibana }) => ({ + notify: kibana.services.canvas.notify, + })), + withHandlers(({ kibana }) => ({ // Workpad creation via navigation createWorkpad: props => async workpad => { // workpad data uploaded, create and load it @@ -41,7 +44,9 @@ export const WorkpadLoader = compose( await workpadService.create(workpad); props.router.navigateTo('loadWorkpad', { id: workpad.id, page: 1 }); } catch (err) { - notify.error(err, { title: errors.getUploadFailureErrorMessage() }); + kibana.services.canvas.notify.error(err, { + title: errors.getUploadFailureErrorMessage(), + }); } return; } @@ -55,7 +60,7 @@ export const WorkpadLoader = compose( const workpads = await workpadService.find(text); setWorkpads(workpads); } catch (err) { - notify.error(err, { title: errors.getFindFailureErrorMessage() }); + kibana.services.canvas.notify.error(err, { title: errors.getFindFailureErrorMessage() }); } }, @@ -71,7 +76,7 @@ export const WorkpadLoader = compose( await workpadService.create(workpad); props.router.navigateTo('loadWorkpad', { id: workpad.id, page: 1 }); } catch (err) { - notify.error(err, { title: errors.getCloneFailureErrorMessage() }); + kibana.services.canvas.notify.error(err, { title: errors.getCloneFailureErrorMessage() }); } }, @@ -92,7 +97,7 @@ export const WorkpadLoader = compose( return Promise.all(removeWorkpads).then(results => { let redirectHome = false; - const [passes, errors] = results.reduce( + const [passes, errored] = results.reduce( ([passes, errors], result) => { if (result.id === loadedWorkpad && !result.err) { redirectHome = true; @@ -116,8 +121,8 @@ export const WorkpadLoader = compose( workpads: remainingWorkpads, }; - if (errors.length > 0) { - notify.error(errors.getDeleteFailureErrorMessage()); + if (errored.length > 0) { + kibana.services.canvas.notify.error(errors.getDeleteFailureErrorMessage()); } setWorkpads(workpadState); @@ -126,11 +131,10 @@ export const WorkpadLoader = compose( props.router.navigateTo('home'); } - return errors.map(({ id }) => id); + return errored.map(({ id }) => id); }); }, - }), - withKibana, + })), withProps(props => ({ formatDate: date => { const dateFormat = props.kibana.services.uiSettings.get('dateFormat'); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/upload_workpad.js b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/upload_workpad.js index a7fcf7449ce40..fd25fb03a9ca9 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/upload_workpad.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/upload_workpad.js @@ -6,12 +6,11 @@ import { get } from 'lodash'; import { getId } from '../../lib/get_id'; -import { notify } from '../../lib/notify'; import { ErrorStrings } from '../../../i18n'; const { WorkpadFileUpload: errors } = ErrorStrings; -export const uploadWorkpad = (file, onUpload) => { +export const uploadWorkpad = (file, onUpload, notify) => { if (!file) { return; } diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_dropzone/index.js b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_dropzone/index.js index ac716d37f532d..ab0c064d5ef07 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_dropzone/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_dropzone/index.js @@ -6,7 +6,6 @@ import PropTypes from 'prop-types'; import { compose, withHandlers } from 'recompose'; -import { notify } from '../../../lib/notify'; import { uploadWorkpad } from '../upload_workpad'; import { ErrorStrings } from '../../../../i18n'; import { WorkpadDropzone as Component } from './workpad_dropzone'; @@ -14,7 +13,7 @@ import { WorkpadDropzone as Component } from './workpad_dropzone'; const { WorkpadFileUpload: errors } = ErrorStrings; export const WorkpadDropzone = compose( - withHandlers({ + withHandlers(({ notify }) => ({ onDropAccepted: ({ onUpload }) => ([file]) => uploadWorkpad(file, onUpload), onDropRejected: () => ([file]) => { notify.warning(errors.getAcceptJSONOnlyErrorMessage(), { @@ -23,7 +22,7 @@ export const WorkpadDropzone = compose( : errors.getFileUploadFailureWithoutFileNameErrorMessage(), }); }, - }) + })) )(Component); WorkpadDropzone.propTypes = { diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_loader.js b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_loader.js index 04378e5603c4b..cb5af27144c7f 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_loader.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_loader.js @@ -249,7 +249,11 @@ export class WorkpadLoader extends React.PureComponent { return ( - + uploadWorkpad(file, this.onUpload)} + onChange={([file]) => uploadWorkpad(file, this.onUpload, this.props.notify)} accept="application/json" disabled={createPending || !canUserWrite} /> diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_templates/index.js b/x-pack/legacy/plugins/canvas/public/components/workpad_templates/index.js index 139d0f283bf1a..1890ca1f9d2d6 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_templates/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_templates/index.js @@ -7,9 +7,9 @@ import PropTypes from 'prop-types'; import { compose, getContext, withHandlers, withProps } from 'recompose'; import * as workpadService from '../../lib/workpad_service'; -import { notify } from '../../lib/notify'; import { getId } from '../../lib/get_id'; import { templatesRegistry } from '../../lib/templates_registry'; +import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { WorkpadTemplates as Component } from './workpad_templates'; export const WorkpadTemplates = compose( @@ -19,7 +19,8 @@ export const WorkpadTemplates = compose( withProps(() => ({ templates: templatesRegistry.toJS(), })), - withHandlers({ + withKibana, + withHandlers(({ kibana }) => ({ // Clone workpad given an id cloneWorkpad: props => workpad => { workpad.id = getId('workpad'); @@ -31,7 +32,9 @@ export const WorkpadTemplates = compose( return workpadService .create(workpad) .then(() => props.router.navigateTo('loadWorkpad', { id: workpad.id, page: 1 })) - .catch(err => notify.error(err, { title: `Couldn't clone workpad template` })); + .catch(err => + kibana.services.canvas.notify.error(err, { title: `Couldn't clone workpad template` }) + ); }, - }) + })) )(Component); diff --git a/x-pack/legacy/plugins/canvas/public/functions/index.ts b/x-pack/legacy/plugins/canvas/public/functions/index.ts index 27fb7d83274a4..5e098d8f175c5 100644 --- a/x-pack/legacy/plugins/canvas/public/functions/index.ts +++ b/x-pack/legacy/plugins/canvas/public/functions/index.ts @@ -4,16 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ExpressionsSetup } from 'src/plugins/expressions/public'; import { asset } from './asset'; import { filtersFunctionFactory } from './filters'; -import { timelion } from './timelion'; +import { timelionFunctionFactory } from './timelion'; import { toFunctionFactory } from './to'; +import { CanvasSetupDeps, CoreSetup } from '../plugin'; export interface InitializeArguments { - typesRegistry: ExpressionsSetup['__LEGACY']['types']; + prependBasePath: CoreSetup['http']['basePath']['prepend']; + typesRegistry: CanvasSetupDeps['expressions']['__LEGACY']['types']; + timefilter: CanvasSetupDeps['data']['query']['timefilter']['timefilter']; } export function initFunctions(initialize: InitializeArguments) { - return [asset, filtersFunctionFactory(initialize), timelion, toFunctionFactory(initialize)]; + return [ + asset, + filtersFunctionFactory(initialize), + timelionFunctionFactory(initialize), + toFunctionFactory(initialize), + ]; } diff --git a/x-pack/legacy/plugins/canvas/public/functions/timelion.ts b/x-pack/legacy/plugins/canvas/public/functions/timelion.ts index ae87e858cf796..e59d798108945 100644 --- a/x-pack/legacy/plugins/canvas/public/functions/timelion.ts +++ b/x-pack/legacy/plugins/canvas/public/functions/timelion.ts @@ -6,8 +6,6 @@ import { flatten } from 'lodash'; import moment from 'moment-timezone'; -import chrome from 'ui/chrome'; -import { npStart } from 'ui/new_platform'; import { TimeRange } from 'src/plugins/data/common'; import { ExpressionFunctionDefinition, DatatableRow } from 'src/plugins/expressions/public'; import { fetch } from '../../common/lib/fetch'; @@ -15,6 +13,7 @@ import { fetch } from '../../common/lib/fetch'; import { buildBoolArray } from '../../server/lib/build_bool_array'; import { Datatable, Filter } from '../../types'; import { getFunctionHelp } from '../../i18n'; +import { InitializeArguments } from './'; interface Arguments { query: string; @@ -30,13 +29,17 @@ interface Arguments { * @param timeRange time range to parse * @param timeZone time zone to do the parsing in */ -function parseDateMath(timeRange: TimeRange, timeZone: string) { +function parseDateMath( + timeRange: TimeRange, + timeZone: string, + timefilter: InitializeArguments['timefilter'] +) { // the datemath plugin always parses dates by using the current default moment time zone. // to use the configured time zone, we are switching just for the bounds calculation. const defaultTimezone = moment().zoneName(); moment.tz.setDefault(timeZone); - const parsedRange = npStart.plugins.data.query.timefilter.timefilter.calculateBounds(timeRange); + const parsedRange = timefilter.calculateBounds(timeRange); // reset default moment timezone moment.tz.setDefault(defaultTimezone); @@ -44,96 +47,100 @@ function parseDateMath(timeRange: TimeRange, timeZone: string) { return parsedRange; } -export function timelion(): ExpressionFunctionDefinition< +type TimelionFunction = ExpressionFunctionDefinition< 'timelion', Filter, Arguments, Promise -> { - const { help, args: argHelp } = getFunctionHelp().timelion; +>; - return { - name: 'timelion', - type: 'datatable', - inputTypes: ['filter'], - help, - args: { - query: { - types: ['string'], - aliases: ['_', 'q'], - help: argHelp.query, - default: '".es(*)"', - }, - interval: { - types: ['string'], - help: argHelp.interval, - default: 'auto', - }, - from: { - types: ['string'], - help: argHelp.from, - default: 'now-1y', - }, - to: { - types: ['string'], - help: argHelp.to, - default: 'now', - }, - timezone: { - types: ['string'], - help: argHelp.timezone, - default: 'UTC', +export function timelionFunctionFactory(initialize: InitializeArguments): () => TimelionFunction { + return () => { + const { help, args: argHelp } = getFunctionHelp().timelion; + + return { + name: 'timelion', + type: 'datatable', + inputTypes: ['filter'], + help, + args: { + query: { + types: ['string'], + aliases: ['_', 'q'], + help: argHelp.query, + default: '".es(*)"', + }, + interval: { + types: ['string'], + help: argHelp.interval, + default: 'auto', + }, + from: { + types: ['string'], + help: argHelp.from, + default: 'now-1y', + }, + to: { + types: ['string'], + help: argHelp.to, + default: 'now', + }, + timezone: { + types: ['string'], + help: argHelp.timezone, + default: 'UTC', + }, }, - }, - fn: (input, args): Promise => { - // Timelion requires a time range. Use the time range from the timefilter element in the - // workpad, if it exists. Otherwise fall back on the function args. - const timeFilter = input.and.find(and => and.type === 'time'); - const range = timeFilter - ? { min: timeFilter.from, max: timeFilter.to } - : parseDateMath({ from: args.from, to: args.to }, args.timezone); + fn: (input, args): Promise => { + // Timelion requires a time range. Use the time range from the timefilter element in the + // workpad, if it exists. Otherwise fall back on the function args. + const timeFilter = input.and.find(and => and.type === 'time'); + const range = timeFilter + ? { min: timeFilter.from, max: timeFilter.to } + : parseDateMath({ from: args.from, to: args.to }, args.timezone, initialize.timefilter); - const body = { - extended: { - es: { - filter: { - bool: { - must: buildBoolArray(input.and), + const body = { + extended: { + es: { + filter: { + bool: { + must: buildBoolArray(input.and), + }, }, }, }, - }, - sheet: [args.query], - time: { - from: range.min, - to: range.max, - interval: args.interval, - timezone: args.timezone, - }, - }; + sheet: [args.query], + time: { + from: range.min, + to: range.max, + interval: args.interval, + timezone: args.timezone, + }, + }; - return fetch(chrome.addBasePath(`/api/timelion/run`), { - method: 'POST', - responseType: 'json', - data: body, - }).then(resp => { - const seriesList = resp.data.sheet[0].list; - const rows = flatten( - seriesList.map((series: { data: any[]; label: string }) => - series.data.map(row => ({ '@timestamp': row[0], value: row[1], label: series.label })) - ) - ) as DatatableRow[]; + return fetch(initialize.prependBasePath(`/api/timelion/run`), { + method: 'POST', + responseType: 'json', + data: body, + }).then(resp => { + const seriesList = resp.data.sheet[0].list; + const rows = flatten( + seriesList.map((series: { data: any[]; label: string }) => + series.data.map(row => ({ '@timestamp': row[0], value: row[1], label: series.label })) + ) + ) as DatatableRow[]; - return { - type: 'datatable', - columns: [ - { name: '@timestamp', type: 'date' }, - { name: 'value', type: 'number' }, - { name: 'label', type: 'string' }, - ], - rows, - }; - }); - }, + return { + type: 'datatable', + columns: [ + { name: '@timestamp', type: 'date' }, + { name: 'value', type: 'number' }, + { name: 'label', type: 'string' }, + ], + rows, + }; + }); + }, + }; }; } diff --git a/x-pack/legacy/plugins/canvas/public/index.ts b/x-pack/legacy/plugins/canvas/public/index.ts index b8358bfe022e6..b053920fec6e4 100644 --- a/x-pack/legacy/plugins/canvas/public/index.ts +++ b/x-pack/legacy/plugins/canvas/public/index.ts @@ -10,6 +10,7 @@ import { CoreStart, } from '../../../../../src/core/public'; import { CanvasSetup, CanvasStart, CanvasSetupDeps, CanvasStartDeps, CanvasPlugin } from './plugin'; +import { CanvasServices } from './services'; export const plugin: PluginInitializer< CanvasSetup, @@ -22,7 +23,7 @@ export const plugin: PluginInitializer< export interface WithKibanaProps { kibana: { - services: CoreStart & CanvasStartDeps; + services: CoreStart & CanvasStartDeps & { canvas: CanvasServices }; }; } diff --git a/x-pack/legacy/plugins/canvas/public/legacy.ts b/x-pack/legacy/plugins/canvas/public/legacy.ts index 4af7c9b2bd057..5bb628909c32e 100644 --- a/x-pack/legacy/plugins/canvas/public/legacy.ts +++ b/x-pack/legacy/plugins/canvas/public/legacy.ts @@ -21,8 +21,10 @@ const shimCoreStart = { }; const shimSetupPlugins: CanvasSetupDeps = { + data: npSetup.plugins.data, expressions: npSetup.plugins.expressions, home: npSetup.plugins.home, + usageCollection: npSetup.plugins.usageCollection, }; const shimStartPlugins: CanvasStartDeps = { ...npStart.plugins, diff --git a/x-pack/legacy/plugins/canvas/public/lib/custom_element_service.ts b/x-pack/legacy/plugins/canvas/public/lib/custom_element_service.ts index 478e2f8f18cf5..4118bb81b8363 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/custom_element_service.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/custom_element_service.ts @@ -5,9 +5,7 @@ */ import { AxiosPromise } from 'axios'; -// @ts-ignore unconverted local file import { API_ROUTE_CUSTOM_ELEMENT } from '../../common/lib/constants'; -// @ts-ignore unconverted local file import { fetch } from '../../common/lib/fetch'; import { CustomElement } from '../../types'; import { getCoreStart } from '../legacy'; @@ -25,7 +23,7 @@ export const get = (customElementId: string): Promise => .get(`${getApiPath()}/${customElementId}`) .then(({ data: element }: { data: CustomElement }) => element); -export const update = (id: string, element: CustomElement): AxiosPromise => +export const update = (id: string, element: Partial): AxiosPromise => fetch.put(`${getApiPath()}/${id}`, element); export const remove = (id: string): AxiosPromise => fetch.delete(`${getApiPath()}/${id}`); diff --git a/x-pack/legacy/plugins/canvas/public/lib/default_header.png b/x-pack/legacy/plugins/canvas/public/lib/default_header.png deleted file mode 100644 index 0b5c5b8f58f9b..0000000000000 Binary files a/x-pack/legacy/plugins/canvas/public/lib/default_header.png and /dev/null differ diff --git a/x-pack/legacy/plugins/canvas/public/lib/download_workpad.ts b/x-pack/legacy/plugins/canvas/public/lib/download_workpad.ts index e4866641fd9e1..fb038d8b6ace2 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/download_workpad.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/download_workpad.ts @@ -6,8 +6,7 @@ import fileSaver from 'file-saver'; import { API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD } from '../../common/lib/constants'; import { ErrorStrings } from '../../i18n'; -// @ts-ignore untyped local -import { notify } from './notify'; +import { notifyService } from '../services'; // @ts-ignore untyped local import * as workpadService from './workpad_service'; import { CanvasRenderedWorkpad } from '../../shareable_runtime/types'; @@ -20,7 +19,7 @@ export const downloadWorkpad = async (workpadId: string) => { const jsonBlob = new Blob([JSON.stringify(workpad)], { type: 'application/json' }); fileSaver.saveAs(jsonBlob, `canvas-workpad-${workpad.name}-${workpad.id}.json`); } catch (err) { - notify.error(err, { title: strings.getDownloadFailureErrorMessage() }); + notifyService.getService().error(err, { title: strings.getDownloadFailureErrorMessage() }); } }; @@ -32,7 +31,9 @@ export const downloadRenderedWorkpad = async (renderedWorkpad: CanvasRenderedWor `canvas-embed-workpad-${renderedWorkpad.name}-${renderedWorkpad.id}.json` ); } catch (err) { - notify.error(err, { title: strings.getDownloadRenderedWorkpadFailureErrorMessage() }); + notifyService + .getService() + .error(err, { title: strings.getDownloadRenderedWorkpadFailureErrorMessage() }); } }; @@ -42,7 +43,9 @@ export const downloadRuntime = async (basePath: string) => { window.open(path); return; } catch (err) { - notify.error(err, { title: strings.getDownloadRuntimeFailureErrorMessage() }); + notifyService + .getService() + .error(err, { title: strings.getDownloadRuntimeFailureErrorMessage() }); } }; @@ -51,6 +54,8 @@ export const downloadZippedRuntime = async (data: any) => { const zip = new Blob([data], { type: 'octet/stream' }); fileSaver.saveAs(zip, 'canvas-workpad-embed.zip'); } catch (err) { - notify.error(err, { title: strings.getDownloadZippedRuntimeFailureErrorMessage() }); + notifyService + .getService() + .error(err, { title: strings.getDownloadZippedRuntimeFailureErrorMessage() }); } }; diff --git a/x-pack/legacy/plugins/canvas/public/lib/element.ts b/x-pack/legacy/plugins/canvas/public/lib/element.ts index 121c253668ed9..ef1cf601b6e26 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/element.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/element.ts @@ -5,18 +5,16 @@ */ import { ElementSpec } from '../../types'; -import defaultHeader from './default_header.png'; -import { tagsRegistry } from './tags_registry'; export class Element { /** The name of the Element. This must match the name of the function that is used to create the `type: render` object */ public name: string; /** A more friendly name for the Element */ public displayName: string; - /** Relevant labels to help identify the elements */ - public tags: string[]; - /** An image to use in the Element type selector */ - public image: string; + /** The type of the Element */ + public type?: string; + /** The name of the EUI icon to display in the element menu */ + public icon: string; /** A sentence or few about what this Element does */ public help: string; /** A default expression that allows Canvas to render the Element */ @@ -28,23 +26,17 @@ export class Element { public height?: number; constructor(config: ElementSpec) { - const { name, image, displayName, tags, expression, filter, help, width, height } = config; + const { name, icon, displayName, type, expression, filter, help, width, height } = config; this.name = name; this.displayName = displayName || name; - this.image = image || defaultHeader; + this.icon = icon || 'empty'; this.help = help || ''; if (!config.expression) { throw new Error('Element types must have a default expression'); } - this.tags = tags || []; - - this.tags.forEach(tag => { - if (!tagsRegistry.get(tag)) { - tagsRegistry.register(() => ({ name: tag, color: '#666666' })); - } - }); + this.type = type; this.expression = expression; this.filter = filter; this.width = width || 500; diff --git a/x-pack/legacy/plugins/canvas/public/lib/element_handler_creators.ts b/x-pack/legacy/plugins/canvas/public/lib/element_handler_creators.ts index bce6bc51b366c..a8744b4820842 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/element_handler_creators.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/element_handler_creators.ts @@ -4,14 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Http2ServerResponse } from 'http2'; import { camelCase } from 'lodash'; // @ts-ignore unconverted local file import { getClipboardData, setClipboardData } from './clipboard'; // @ts-ignore unconverted local file import { cloneSubgraphs } from './clone_subgraphs'; -// @ts-ignore unconverted local file -import { notify } from './notify'; +import { notifyService } from '../services'; import * as customElementService from './custom_element_service'; import { getId } from './get_id'; import { PositionedElement } from '../../types'; @@ -86,15 +84,17 @@ export const basicHandlerCreators = { customElementService .create(customElement) .then(() => - notify.success( - `Custom element '${customElement.displayName || customElement.id}' was saved`, - { - 'data-test-subj': 'canvasCustomElementCreate-success', - } - ) + notifyService + .getService() + .success( + `Custom element '${customElement.displayName || customElement.id}' was saved`, + { + 'data-test-subj': 'canvasCustomElementCreate-success', + } + ) ) - .catch((result: Http2ServerResponse) => - notify.warning(result, { + .catch((error: Error) => + notifyService.getService().warning(error, { title: `Custom element '${customElement.displayName || customElement.id}' was not saved`, }) @@ -138,13 +138,13 @@ export const clipboardHandlerCreators = { if (selectedNodes.length) { setClipboardData({ selectedNodes }); removeNodes(selectedNodes.map(extractId), pageId); - notify.success('Cut element to clipboard'); + notifyService.getService().success('Cut element to clipboard'); } }, copyNodes: ({ selectedNodes }: Props) => (): void => { if (selectedNodes.length) { setClipboardData({ selectedNodes }); - notify.success('Copied element to clipboard'); + notifyService.getService().success('Copied element to clipboard'); } }, pasteNodes: ({ insertNodes, pageId, selectToplevelNodes }: Props) => (): void => { diff --git a/x-pack/legacy/plugins/canvas/public/lib/es_service.ts b/x-pack/legacy/plugins/canvas/public/lib/es_service.ts index 32f4fe041423c..6aa4968f29155 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/es_service.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/es_service.ts @@ -10,8 +10,7 @@ import { API_ROUTE } from '../../common/lib/constants'; // @ts-ignore untyped local import { fetch } from '../../common/lib/fetch'; import { ErrorStrings } from '../../i18n'; -// @ts-ignore untyped local -import { notify } from './notify'; +import { notifyService } from '../services'; import { getCoreStart } from '../legacy'; const { esService: strings } = ErrorStrings; @@ -38,7 +37,7 @@ export const getFields = (index = '_all') => { .sort() ) .catch((err: Error) => - notify.error(err, { + notifyService.getService().error(err, { title: strings.getFieldsFetchErrorMessage(index), }) ); @@ -57,7 +56,9 @@ export const getIndices = () => return savedObject.attributes.title; }); }) - .catch((err: Error) => notify.error(err, { title: strings.getIndicesFetchErrorMessage() })); + .catch((err: Error) => + notifyService.getService().error(err, { title: strings.getIndicesFetchErrorMessage() }) + ); export const getDefaultIndex = () => { const defaultIndexId = getAdvancedSettings().get('defaultIndex'); @@ -66,6 +67,10 @@ export const getDefaultIndex = () => { ? getSavedObjectsClient() .get('index-pattern', defaultIndexId) .then(defaultIndex => defaultIndex.attributes.title) - .catch(err => notify.error(err, { title: strings.getDefaultIndexFetchErrorMessage() })) + .catch(err => + notifyService + .getService() + .error(err, { title: strings.getDefaultIndexFetchErrorMessage() }) + ) : Promise.resolve(''); }; diff --git a/x-pack/legacy/plugins/canvas/public/lib/flatten_panel_tree.ts b/x-pack/legacy/plugins/canvas/public/lib/flatten_panel_tree.ts new file mode 100644 index 0000000000000..a059d07725727 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/lib/flatten_panel_tree.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// TODO: Fix all of this magic from EUI; this code is boilerplate from +// EUI examples and isn't easily typed. +export const flattenPanelTree = (tree: any, array: any[] = []) => { + array.push(tree); + + if (tree.items) { + tree.items.forEach((item: any) => { + const { panel } = item; + if (panel) { + flattenPanelTree(panel, array); + item.panel = panel.id; + } + }); + } + + return array; +}; diff --git a/x-pack/legacy/plugins/canvas/public/lib/notify.js b/x-pack/legacy/plugins/canvas/public/lib/notify.js deleted file mode 100644 index 64876a02a3c64..0000000000000 --- a/x-pack/legacy/plugins/canvas/public/lib/notify.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 { get } from 'lodash'; -import { getCoreStart, getStartPlugins } from '../legacy'; - -const getToastNotifications = function() { - return getCoreStart().notifications.toasts; -}; - -const formatMsg = function(...args) { - return getStartPlugins().__LEGACY.formatMsg(...args); -}; - -const getToast = (err, opts = {}) => { - const errData = get(err, 'response') || err; - const errMsg = formatMsg(errData); - const { title, ...rest } = opts; - let text = null; - - if (title) { - text = errMsg; - } - - return { - ...rest, - title: title || errMsg, - text, - }; -}; - -export const notify = { - /* - * @param {(string | Object)} err: message or Error object - * @param {Object} opts: option to override toast title or icon, see https://github.com/elastic/kibana/blob/master/src/legacy/ui/public/notify/toasts/TOAST_NOTIFICATIONS.md - */ - error(err, opts) { - getToastNotifications().addDanger(getToast(err, opts)); - }, - warning(err, opts) { - getToastNotifications().addWarning(getToast(err, opts)); - }, - info(err, opts) { - getToastNotifications().add(getToast(err, opts)); - }, - success(err, opts) { - getToastNotifications().addSuccess(getToast(err, opts)); - }, -}; diff --git a/x-pack/legacy/plugins/canvas/public/lib/run_interpreter.ts b/x-pack/legacy/plugins/canvas/public/lib/run_interpreter.ts index fbbaf0ccf280e..df338f40e08d9 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/run_interpreter.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/run_interpreter.ts @@ -6,8 +6,7 @@ import { fromExpression, getType } from '@kbn/interpreter/common'; import { ExpressionValue, ExpressionAstExpression } from 'src/plugins/expressions/public'; -// @ts-ignore Untyped Local -import { notify } from './notify'; +import { notifyService } from '../services'; import { CanvasStartDeps, CanvasSetupDeps } from '../plugin'; @@ -85,7 +84,7 @@ export async function runInterpreter( throw new Error(`Ack! I don't know how to render a '${getType(renderable)}'`); } catch (err) { - notify.error(err); + notifyService.getService().error(err); throw err; } } diff --git a/x-pack/legacy/plugins/canvas/public/lib/ui_metric.ts b/x-pack/legacy/plugins/canvas/public/lib/ui_metric.ts index 33976a147df46..2a1a4b88b7264 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/ui_metric.ts +++ b/x-pack/legacy/plugins/canvas/public/lib/ui_metric.ts @@ -4,10 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - createUiStatsReporter, - METRIC_TYPE, -} from '../../../../../../src/legacy/core_plugins/ui_metric/public'; +import { UiStatsMetricType, METRIC_TYPE } from '@kbn/analytics'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; -export const trackCanvasUiMetric = createUiStatsReporter('canvas'); export { METRIC_TYPE }; + +export let reportUiStats: UsageCollectionSetup['reportUiStats'] | undefined; + +export function init(_reportUiStats: UsageCollectionSetup['reportUiStats']): void { + reportUiStats = _reportUiStats; +} + +export function trackCanvasUiMetric(metricType: UiStatsMetricType, name: string | string[]) { + if (!reportUiStats) { + return; + } + + reportUiStats('canvas', metricType, name); +} diff --git a/x-pack/legacy/plugins/canvas/public/plugin.tsx b/x-pack/legacy/plugins/canvas/public/plugin.tsx index 3ea3ce625ca71..36ce1974be272 100644 --- a/x-pack/legacy/plugins/canvas/public/plugin.tsx +++ b/x-pack/legacy/plugins/canvas/public/plugin.tsx @@ -10,8 +10,10 @@ import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public'; import { initLoadingIndicator } from './lib/loading_indicator'; import { featureCatalogueEntry } from './feature_catalogue_entry'; import { ExpressionsSetup, ExpressionsStart } from '../../../../../src/plugins/expressions/public'; +import { DataPublicPluginSetup } from '../../../../../src/plugins/data/public'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; import { EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; +import { UsageCollectionSetup } from '../../../../../src/plugins/usage_collection/public'; import { Start as InspectorStart } from '../../../../../src/plugins/inspector/public'; // @ts-ignore untyped local import { argTypeSpecs } from './expression_types/arg_types'; @@ -20,7 +22,7 @@ import { legacyRegistries } from './legacy_plugin_support'; import { getPluginApi, CanvasApi } from './plugin_api'; import { initFunctions } from './functions'; import { CanvasSrcPlugin } from '../canvas_plugin_src/plugin'; -export { CoreStart }; +export { CoreStart, CoreSetup }; /** * These are the private interfaces for the services your plugin depends on. @@ -28,14 +30,17 @@ export { CoreStart }; */ // This interface will be built out as we require other plugins for setup export interface CanvasSetupDeps { + data: DataPublicPluginSetup; expressions: ExpressionsSetup; home: HomePublicPluginSetup; + usageCollection?: UsageCollectionSetup; } export interface CanvasStartDeps { embeddable: EmbeddableStart; expressions: ExpressionsStart; inspector: InspectorStart; + uiActions: UiActionsStart; __LEGACY: { absoluteToParsedUrl: (url: string, basePath: string) => any; @@ -94,7 +99,13 @@ export class CanvasPlugin canvasApi.addTypes(legacyRegistries.types.getOriginalFns()); // Register core canvas stuff - canvasApi.addFunctions(initFunctions({ typesRegistry: plugins.expressions.__LEGACY.types })); + canvasApi.addFunctions( + initFunctions({ + timefilter: plugins.data.query.timefilter.timefilter, + prependBasePath: core.http.basePath.prepend, + typesRegistry: plugins.expressions.__LEGACY.types, + }) + ); canvasApi.addArgumentUIs(argTypeSpecs); canvasApi.addTransitions(transitions); diff --git a/x-pack/legacy/plugins/canvas/public/services/index.ts b/x-pack/legacy/plugins/canvas/public/services/index.ts new file mode 100644 index 0000000000000..12c0a687bf308 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/services/index.ts @@ -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 { CoreSetup, CoreStart } from '../../../../../../src/core/public'; +import { CanvasSetupDeps, CanvasStartDeps } from '../plugin'; +import { notifyServiceFactory } from './notify'; + +export type CanvasServiceFactory = ( + coreSetup: CoreSetup, + coreStart: CoreStart, + canvasSetupPlugins: CanvasSetupDeps, + canvasStartPlugins: CanvasStartDeps +) => Service; + +class CanvasServiceProvider { + private factory: CanvasServiceFactory; + private service: Service | undefined; + + constructor(factory: CanvasServiceFactory) { + this.factory = factory; + } + + start( + coreSetup: CoreSetup, + coreStart: CoreStart, + canvasSetupPlugins: CanvasSetupDeps, + canvasStartPlugins: CanvasStartDeps + ) { + this.service = this.factory(coreSetup, coreStart, canvasSetupPlugins, canvasStartPlugins); + } + + getService(): Service { + if (!this.service) { + throw new Error('Service not ready'); + } + + return this.service; + } + + stop() { + this.service = undefined; + } +} + +export type ServiceFromProvider

= P extends CanvasServiceProvider ? T : never; + +export const services = { + notify: new CanvasServiceProvider(notifyServiceFactory), +}; + +export interface CanvasServices { + notify: ServiceFromProvider; +} + +export const startServices = ( + coreSetup: CoreSetup, + coreStart: CoreStart, + canvasSetupPlugins: CanvasSetupDeps, + canvasStartPlugins: CanvasStartDeps +) => { + Object.entries(services).forEach(([key, provider]) => + provider.start(coreSetup, coreStart, canvasSetupPlugins, canvasStartPlugins) + ); +}; + +export const stopServices = () => { + Object.entries(services).forEach(([key, provider]) => provider.stop()); +}; + +export const { notify: notifyService } = services; diff --git a/x-pack/legacy/plugins/canvas/public/services/notify.ts b/x-pack/legacy/plugins/canvas/public/services/notify.ts new file mode 100644 index 0000000000000..3e18e2178a818 --- /dev/null +++ b/x-pack/legacy/plugins/canvas/public/services/notify.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash'; +import { CanvasServiceFactory } from '.'; +import { formatMsg } from '../../../../../../src/plugins/kibana_legacy/public'; +import { ToastInputFields } from '../../../../../../src/core/public'; + +const getToast = (err: Error | string, opts: ToastInputFields = {}) => { + const errData = (get(err, 'response') || err) as Error | string; + const errMsg = formatMsg(errData); + const { title, ...rest } = opts; + let text; + + if (title) { + text = errMsg; + } + + return { + ...rest, + title: title || errMsg, + text, + }; +}; + +interface NotifyService { + error: (err: string | Error, opts?: ToastInputFields) => void; + warning: (err: string | Error, opts?: ToastInputFields) => void; + info: (err: string | Error, opts?: ToastInputFields) => void; + success: (err: string | Error, opts?: ToastInputFields) => void; +} + +export const notifyServiceFactory: CanvasServiceFactory = (setup, start) => { + const toasts = start.notifications.toasts; + + return { + /* + * @param {(string | Object)} err: message or Error object + * @param {Object} opts: option to override toast title or icon, see https://github.com/elastic/kibana/blob/master/src/legacy/ui/public/notify/toasts/TOAST_NOTIFICATIONS.md + */ + error(err, opts) { + toasts.addDanger(getToast(err, opts)); + }, + warning(err, opts) { + toasts.addWarning(getToast(err, opts)); + }, + info(err, opts) { + toasts.add(getToast(err, opts)); + }, + success(err, opts) { + toasts.addSuccess(getToast(err, opts)); + }, + }; +}; diff --git a/x-pack/legacy/plugins/canvas/public/state/actions/elements.js b/x-pack/legacy/plugins/canvas/public/state/actions/elements.js index 1798aaab22f06..f4a3393b8962d 100644 --- a/x-pack/legacy/plugins/canvas/public/state/actions/elements.js +++ b/x-pack/legacy/plugins/canvas/public/state/actions/elements.js @@ -13,9 +13,9 @@ import { getPages, getNodeById, getNodes, getSelectedPageIndex } from '../select import { getValue as getResolvedArgsValue } from '../selectors/resolved_args'; import { getDefaultElement } from '../defaults'; import { ErrorStrings } from '../../../i18n'; -import { notify } from '../../lib/notify'; import { runInterpreter, interpretAst } from '../../lib/run_interpreter'; import { subMultitree } from '../../lib/aeroelastic/functional'; +import { services } from '../../services'; import { selectToplevelNodes } from './transient'; import * as args from './resolved_args'; @@ -134,7 +134,7 @@ const fetchRenderableWithContextFn = ({ dispatch }, element, ast, context) => { dispatch(getAction(renderable)); }) .catch(err => { - notify.error(err); + services.notify.getService().error(err); dispatch(getAction(err)); }); }; @@ -176,7 +176,7 @@ export const fetchAllRenderables = createThunk( return runInterpreter(ast, null, { castToRender: true }) .then(renderable => ({ path: argumentPath, value: renderable })) .catch(err => { - notify.error(err); + services.notify.getService().error(err); return { path: argumentPath, value: err }; }); }); @@ -293,7 +293,7 @@ const setAst = createThunk('setAst', ({ dispatch }, ast, element, pageId, doRend const expression = toExpression(ast); dispatch(setExpression(expression, element.id, pageId, doRender)); } catch (err) { - notify.error(err); + services.notify.getService().error(err); // TODO: remove this, may have been added just to cause a re-render, but why? dispatch(setExpression(element.expression, element.id, pageId, doRender)); diff --git a/x-pack/legacy/plugins/canvas/public/state/middleware/es_persist.js b/x-pack/legacy/plugins/canvas/public/state/middleware/es_persist.js index bcbfc3544981a..a197cdf893244 100644 --- a/x-pack/legacy/plugins/canvas/public/state/middleware/es_persist.js +++ b/x-pack/legacy/plugins/canvas/public/state/middleware/es_persist.js @@ -14,7 +14,7 @@ import { setAssets, resetAssets } from '../actions/assets'; import * as transientActions from '../actions/transient'; import * as resolvedArgsActions from '../actions/resolved_args'; import { update, updateAssets, updateWorkpad } from '../../lib/workpad_service'; -import { notify } from '../../lib/notify'; +import { services } from '../../services'; import { canUserWrite } from '../selectors/app'; const { esPersist: strings } = ErrorStrings; @@ -62,15 +62,15 @@ export const esPersistMiddleware = ({ getState }) => { const statusCode = err.response && err.response.status; switch (statusCode) { case 400: - return notify.error(err.response, { + return services.notify.getService().error(err.response, { title: strings.getSaveFailureTitle(), }); case 413: - return notify.error(strings.getTooLargeErrorMessage(), { + return services.notify.getService().error(strings.getTooLargeErrorMessage(), { title: strings.getSaveFailureTitle(), }); default: - return notify.error(err, { + return services.notify.getService().error(err, { title: strings.getUpdateFailureTitle(), }); } diff --git a/x-pack/legacy/plugins/canvas/public/state/selectors/workpad.ts b/x-pack/legacy/plugins/canvas/public/state/selectors/workpad.ts index 84fab0cb0ae6d..1623035bd25ba 100644 --- a/x-pack/legacy/plugins/canvas/public/state/selectors/workpad.ts +++ b/x-pack/legacy/plugins/canvas/public/state/selectors/workpad.ts @@ -11,7 +11,12 @@ import { safeElementFromExpression, fromExpression } from '@kbn/interpreter/comm import { append } from '../../lib/modify_path'; import { getAssets } from './assets'; import { State, CanvasWorkpad, CanvasPage, CanvasElement, ResolvedArgType } from '../../../types'; -import { ExpressionContext, CanvasGroup, PositionedElement } from '../../../types'; +import { + ExpressionContext, + CanvasGroup, + PositionedElement, + CanvasWorkpadBoundingBox, +} from '../../../types'; import { ExpressionAstArgument, ExpressionAstFunction, @@ -91,7 +96,7 @@ export function getWorkpadWidth(state: State): number { return get(state, append(workpadRoot, 'width')); } -export function getWorkpadBoundingBox(state: State) { +export function getWorkpadBoundingBox(state: State): CanvasWorkpadBoundingBox { return getPages(state).reduce( (boundingBox, page) => { page.elements.forEach(({ position }) => { diff --git a/x-pack/legacy/plugins/canvas/public/style/index.scss b/x-pack/legacy/plugins/canvas/public/style/index.scss index 56f9ed8d18cbe..ba0845862368a 100644 --- a/x-pack/legacy/plugins/canvas/public/style/index.scss +++ b/x-pack/legacy/plugins/canvas/public/style/index.scss @@ -52,7 +52,8 @@ @import '../components/tooltip_annotation/tooltip_annotation'; @import '../components/workpad/workpad'; @import '../components/workpad_header/control_settings/control_settings'; -@import '../components/workpad_header/workpad_export/workpad_export'; +@import '../components/workpad_header/element_menu/element_menu'; +@import '../components/workpad_header/share_menu/share_menu'; @import '../components/workpad_loader/workpad_loader'; @import '../components/workpad_loader/workpad_dropzone/workpad_dropzone'; @import '../components/workpad_page/workpad_page'; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/api/index.ts b/x-pack/legacy/plugins/canvas/shareable_runtime/api/index.ts index b05379df6b0b1..0780ab46cd873 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/api/index.ts +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/api/index.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import 'core-js/stable'; +import 'regenerator-runtime/runtime'; import 'whatwg-fetch'; -import 'babel-polyfill'; export * from './shareable'; diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/test/workpads/austin.json b/x-pack/legacy/plugins/canvas/shareable_runtime/test/workpads/austin.json index f9f999440c7ce..b725afab2b10f 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/test/workpads/austin.json +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/test/workpads/austin.json @@ -28878,7 +28878,7 @@ "type": "render", "as": "markdown", "value": { - "content": "```\nexport const githubLimitGauge = () => ({\n name: 'githubLimitGauge',\n displayName: 'Github Limit Gauge',\n help: 'A progress pill displaying...',\n image: header,\n expression: `github_rate_limit \n | filterrows fn={getCell name | eq core} \n | if \n condition={math limit | eq 0} \n then=0 \n else={math \"remaining / limit\"}\n | progress \n label=\"Core\"\n shape=\"horizontalPill\" \n | render\n `,\n };\n}\n```", + "content": "```\nexport const githubLimitGauge = () => ({\n name: 'githubLimitGauge',\n displayName: 'Github Limit Gauge',\n help: 'A progress pill displaying...',\n expression: `github_rate_limit \n | filterrows fn={getCell name | eq core} \n | if \n condition={math limit | eq 0} \n then=0 \n else={math \"remaining / limit\"}\n | progress \n label=\"Core\"\n shape=\"horizontalPill\" \n | render\n `,\n };\n}\n```", "font": { "type": "style", "spec": { @@ -28919,7 +28919,7 @@ "type": "render", "as": "markdown", "value": { - "content": "```\nexport function randomNumber() {\n return {\n name: 'randomNumber',\n displayName: 'Random Number',\n help: 'A random number between 0 and 1.',\n image: header,\n expression: \n 'random \n | math \"round(value, 3)\" \n | metric \"Random Number\"\n ',\n };\n}\n```", + "content": "```\nexport function randomNumber() {\n return {\n name: 'randomNumber',\n displayName: 'Random Number',\n help: 'A random number between 0 and 1.',\n expression: \n 'random \n | math \"round(value, 3)\" \n | metric \"Random Number\"\n ',\n };\n}\n```", "font": { "type": "style", "spec": { diff --git a/x-pack/legacy/plugins/canvas/tasks/mocks/customElementService.js b/x-pack/legacy/plugins/canvas/tasks/mocks/customElementService.js new file mode 100644 index 0000000000000..3162638cb6c5d --- /dev/null +++ b/x-pack/legacy/plugins/canvas/tasks/mocks/customElementService.js @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const testCustomElements = [ + { + id: 'custom-element-10d625f5-1342-47c9-8f19-d174ea6b65d5', + name: 'customElement1', + displayName: 'Custom Element 1', + help: 'sample description', + image: '', + content: `{\"selectedNodes\":[{\"id\":\"element-3383b40a-de5d-4efb-8719-f4d8cffbfa74\",\"position\":{\"left\":142,\"top\":146,\"width\":700,\"height\":300,\"angle\":0,\"parent\":null,\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| pointseries x=\\\"project\\\" y=\\\"sum(price)\\\" color=\\\"state\\\" size=\\\"size(username)\\\"\\n| plot defaultStyle={seriesStyle points=5 fill=1}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"pointseries\",\"arguments\":{\"x\":[\"project\"],\"y\":[\"sum(price)\"],\"color\":[\"state\"],\"size\":[\"size(username)\"]}},{\"type\":\"function\",\"function\":\"plot\",\"arguments\":{\"defaultStyle\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"seriesStyle\",\"arguments\":{\"points\":[5],\"fill\":[1]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}}]}`, + }, + { + id: 'custom-element-b22d8d10-6116-46fb-9b46-c3f3340d3aaa', + name: 'customElement2', + displayName: 'Custom Element 2', + help: 'Aenean eu justo auctor, placerat felis non, scelerisque dolor. ', + image: '', + content: `{\"selectedNodes\":[{\"id\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"position\":{\"left\":250,\"top\":119,\"width\":340,\"height\":517,\"angle\":0,\"parent\":null,\"type\":\"group\"},\"expression\":\"shape fill=\\\"rgba(255,255,255,0)\\\" | render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"shape\",\"arguments\":{\"fill\":[\"rgba(255,255,255,0)\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-e2c658ee-7614-4d92-a46e-2b1a81a24485\",\"position\":{\"left\":250,\"top\":405,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\\"## Jane Doe\\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"## Jane Doe\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-3d16765e-5251-4954-8e2a-6c64ed465b73\",\"position\":{\"left\":250,\"top\":480,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\\"### Developer\\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render css=\\\".canvasRenderEl h3 {\\ncolor: #444444;\\n}\\\"\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"### Developer\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{\"css\":[\".canvasRenderEl h3 {\\ncolor: #444444;\\n}\"]}}]}},{\"id\":\"element-624675cf-46e9-4545-b86a-5409bbe53ac1\",\"position\":{\"left\":250,\"top\":555,\"width\":340,\"height\":81,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\n \\\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. \\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. \"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-c2916246-26dd-4c65-91c6-d1ad3f1791ee\",\"position\":{\"left\":293,\"top\":119,\"width\":254,\"height\":252,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"image dataurl={asset \\\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\\\"} mode=\\\"contain\\\"\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"image\",\"arguments\":{\"dataurl\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"asset\",\"arguments\":{\"_\":[\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\"]}}]}],\"mode\":[\"contain\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}}]}`, + }, + { + id: 'custom-element-', + name: 'customElement3', + displayName: 'Custom Element 3', + help: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce lobortis aliquet arcu ut turpis duis.', + image: '', + content: `{\"selectedNodes\":[{\"id\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"position\":{\"left\":250,\"top\":119,\"width\":340,\"height\":517,\"angle\":0,\"parent\":null,\"type\":\"group\"},\"expression\":\"shape fill=\\\"rgba(255,255,255,0)\\\" | render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"shape\",\"arguments\":{\"fill\":[\"rgba(255,255,255,0)\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-e2c658ee-7614-4d92-a46e-2b1a81a24485\",\"position\":{\"left\":250,\"top\":405,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\\"## Jane Doe\\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"## Jane Doe\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-3d16765e-5251-4954-8e2a-6c64ed465b73\",\"position\":{\"left\":250,\"top\":480,\"width\":340,\"height\":75,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\\"### Developer\\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render css=\\\".canvasRenderEl h3 {\\ncolor: #444444;\\n}\\\"\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"### Developer\"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{\"css\":[\".canvasRenderEl h3 {\\ncolor: #444444;\\n}\"]}}]}},{\"id\":\"element-624675cf-46e9-4545-b86a-5409bbe53ac1\",\"position\":{\"left\":250,\"top\":555,\"width\":340,\"height\":81,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"filters\\n| demodata\\n| markdown \\n \\\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. \\\" \\n font={font family=\\\"'Open Sans', Helvetica, Arial, sans-serif\\\" size=14 align=\\\"center\\\" color=\\\"#000000\\\" weight=\\\"normal\\\" underline=false italic=false}\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"filters\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"demodata\",\"arguments\":{}},{\"type\":\"function\",\"function\":\"markdown\",\"arguments\":{\"_\":[\"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vel sollicitudin mauris, ut scelerisque urna. \"],\"font\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"font\",\"arguments\":{\"family\":[\"'Open Sans', Helvetica, Arial, sans-serif\"],\"size\":[14],\"align\":[\"center\"],\"color\":[\"#000000\"],\"weight\":[\"normal\"],\"underline\":[false],\"italic\":[false]}}]}]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}},{\"id\":\"element-c2916246-26dd-4c65-91c6-d1ad3f1791ee\",\"position\":{\"left\":293,\"top\":119,\"width\":254,\"height\":252,\"angle\":0,\"parent\":\"group-dccf4ed7-1593-49a0-9902-caf4d4a4b7f5\",\"type\":\"element\"},\"expression\":\"image dataurl={asset \\\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\\\"} mode=\\\"contain\\\"\\n| render\",\"ast\":{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"image\",\"arguments\":{\"dataurl\":[{\"type\":\"expression\",\"chain\":[{\"type\":\"function\",\"function\":\"asset\",\"arguments\":{\"_\":[\"asset-0c6f377f-771e-432e-8e2e-15c3e9142ad6\"]}}]}],\"mode\":[\"contain\"]}},{\"type\":\"function\",\"function\":\"render\",\"arguments\":{}}]}}]}`, + }, +]; + +export const create = () => {}; + +export const get = () => {}; + +export const update = () => {}; + +export const remove = () => {}; + +export const find = () => {}; diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/index.ts b/x-pack/legacy/plugins/canvas/tasks/mocks/uiMetric.js similarity index 83% rename from x-pack/plugins/transform/public/app/components/pivot_preview/index.ts rename to x-pack/legacy/plugins/canvas/tasks/mocks/uiMetric.js index 049e73d6309fc..c7e7088812148 100644 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/index.ts +++ b/x-pack/legacy/plugins/canvas/tasks/mocks/uiMetric.js @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { PivotPreview } from './pivot_preview'; +export const trackCanvasUiMetric = () => {}; diff --git a/x-pack/legacy/plugins/canvas/types/canvas.ts b/x-pack/legacy/plugins/canvas/types/canvas.ts index f0137479a0b7f..0250b921aadb6 100644 --- a/x-pack/legacy/plugins/canvas/types/canvas.ts +++ b/x-pack/legacy/plugins/canvas/types/canvas.ts @@ -56,3 +56,10 @@ export type CanvasTemplate = CanvasWorkpad & { help: string; tags: string[]; }; + +export interface CanvasWorkpadBoundingBox { + left: number; + right: number; + top: number; + bottom: number; +} diff --git a/x-pack/legacy/plugins/canvas/types/elements.ts b/x-pack/legacy/plugins/canvas/types/elements.ts index acb1cb9cd7625..86356f5bd32a9 100644 --- a/x-pack/legacy/plugins/canvas/types/elements.ts +++ b/x-pack/legacy/plugins/canvas/types/elements.ts @@ -9,10 +9,10 @@ import { CanvasElement } from '.'; export interface ElementSpec { name: string; - image: string; + icon?: string; expression: string; displayName?: string; - tags?: string[]; + type?: string; help?: string; filter?: string; width?: number; @@ -42,10 +42,6 @@ export interface CustomElement { * base 64 data URL string of the preview image */ image?: string; - /** - * tags associated with the element - */ - tags?: string[]; /** * the element object stringified */ diff --git a/x-pack/legacy/plugins/graph/index.ts b/x-pack/legacy/plugins/graph/index.ts deleted file mode 100644 index 5c7f8fa46c18b..0000000000000 --- a/x-pack/legacy/plugins/graph/index.ts +++ /dev/null @@ -1,32 +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. - */ - -// @ts-ignore -import migrations from './migrations'; -import mappings from './mappings.json'; -import { LegacyPluginInitializer } from '../../../../src/legacy/plugin_discovery/types'; - -export const graph: LegacyPluginInitializer = kibana => { - return new kibana.Plugin({ - id: 'graph', - configPrefix: 'xpack.graph', - require: ['kibana', 'elasticsearch', 'xpack_main'], - uiExports: { - mappings, - migrations, - }, - - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - canEditDrillDownUrls: Joi.boolean().default(true), - savePolicy: Joi.string() - .valid(['config', 'configAndDataWithConsent', 'configAndData', 'none']) - .default('configAndData'), - }).default(); - }, - }); -}; diff --git a/x-pack/legacy/plugins/graph/mappings.json b/x-pack/legacy/plugins/graph/mappings.json deleted file mode 100644 index f1950c459eee5..0000000000000 --- a/x-pack/legacy/plugins/graph/mappings.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "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" - } - } - } -} diff --git a/x-pack/legacy/plugins/graph/migrations.js b/x-pack/legacy/plugins/graph/migrations.js deleted file mode 100644 index 0cefe6217b45d..0000000000000 --- a/x-pack/legacy/plugins/graph/migrations.js +++ /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 { get } from 'lodash'; - -export default { - 'graph-workspace': { - '7.0.0': doc => { - // Set new "references" attribute - doc.references = doc.references || []; - // Migrate index pattern - const wsState = get(doc, 'attributes.wsState'); - if (typeof wsState !== 'string') { - return doc; - } - let state; - try { - state = JSON.parse(JSON.parse(wsState)); - } catch (e) { - // Let it go, the data is invalid and we'll leave it as is - return doc; - } - const { indexPattern } = state; - if (!indexPattern) { - return doc; - } - state.indexPatternRefName = 'indexPattern_0'; - delete state.indexPattern; - doc.attributes.wsState = JSON.stringify(JSON.stringify(state)); - doc.references.push({ - name: 'indexPattern_0', - type: 'index-pattern', - id: indexPattern, - }); - return doc; - }, - }, -}; diff --git a/x-pack/legacy/plugins/graph/migrations.test.js b/x-pack/legacy/plugins/graph/migrations.test.js deleted file mode 100644 index 93162d94857ce..0000000000000 --- a/x-pack/legacy/plugins/graph/migrations.test.js +++ /dev/null @@ -1,102 +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 migrations from './migrations'; - -describe('graph-workspace', () => { - describe('7.0.0', () => { - const migration = migrations['graph-workspace']['7.0.0']; - - test('returns doc on empty object', () => { - expect(migration({})).toMatchInlineSnapshot(` -Object { - "references": Array [], -} -`); - }); - - test('returns doc when wsState is not a string', () => { - const doc = { - id: '1', - attributes: { - wsState: true, - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "wsState": true, - }, - "id": "1", - "references": Array [], -} -`); - }); - - test('returns doc when wsState is not valid JSON', () => { - const doc = { - id: '1', - attributes: { - wsState: '123abc', - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "wsState": "123abc", - }, - "id": "1", - "references": Array [], -} -`); - }); - - test('returns doc when "indexPattern" is missing from wsState', () => { - const doc = { - id: '1', - attributes: { - wsState: JSON.stringify(JSON.stringify({ foo: true })), - }, - }; - expect(migration(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "wsState": "\\"{\\\\\\"foo\\\\\\":true}\\"", - }, - "id": "1", - "references": Array [], -} -`); - }); - - test('extract "indexPattern" attribute from doc', () => { - const doc = { - id: '1', - attributes: { - wsState: JSON.stringify(JSON.stringify({ foo: true, indexPattern: 'pattern*' })), - bar: true, - }, - }; - const migratedDoc = migration(doc); - expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "bar": true, - "wsState": "\\"{\\\\\\"foo\\\\\\":true,\\\\\\"indexPatternRefName\\\\\\":\\\\\\"indexPattern_0\\\\\\"}\\"", - }, - "id": "1", - "references": Array [ - Object { - "id": "pattern*", - "name": "indexPattern_0", - "type": "index-pattern", - }, - ], -} -`); - }); - }); -}); diff --git a/x-pack/legacy/plugins/ingest_manager/index.ts b/x-pack/legacy/plugins/ingest_manager/index.ts index 47c6478f66471..df9923d9f11ec 100644 --- a/x-pack/legacy/plugins/ingest_manager/index.ts +++ b/x-pack/legacy/plugins/ingest_manager/index.ts @@ -4,43 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import { resolve } from 'path'; -import { - savedObjectMappings, - OUTPUT_SAVED_OBJECT_TYPE, - AGENT_CONFIG_SAVED_OBJECT_TYPE, - DATASOURCE_SAVED_OBJECT_TYPE, - PACKAGES_SAVED_OBJECT_TYPE, -} from '../../../plugins/ingest_manager/server'; - -// TODO https://github.com/elastic/kibana/issues/46373 -// const INDEX_NAMES = { -// INGEST: '.kibana', -// }; export function ingestManager(kibana: any) { return new kibana.Plugin({ id: 'ingestManager', publicDir: resolve(__dirname, '../../../plugins/ingest_manager/public'), - uiExports: { - savedObjectSchemas: { - [AGENT_CONFIG_SAVED_OBJECT_TYPE]: { - isNamespaceAgnostic: true, - // indexPattern: INDEX_NAMES.INGEST, - }, - [OUTPUT_SAVED_OBJECT_TYPE]: { - isNamespaceAgnostic: true, - // indexPattern: INDEX_NAMES.INGEST, - }, - [DATASOURCE_SAVED_OBJECT_TYPE]: { - isNamespaceAgnostic: true, - // indexPattern: INDEX_NAMES.INGEST, - }, - [PACKAGES_SAVED_OBJECT_TYPE]: { - isNamespaceAgnostic: true, - // indexPattern: INDEX_NAMES.INGEST, - }, - }, - mappings: savedObjectMappings, - }, }); } diff --git a/x-pack/legacy/plugins/maps/index.js b/x-pack/legacy/plugins/maps/index.js index 8546e3712c763..d1e8892fa2c98 100644 --- a/x-pack/legacy/plugins/maps/index.js +++ b/x-pack/legacy/plugins/maps/index.js @@ -57,7 +57,6 @@ export function maps(kibana) { tilemap: _.get(mapConfig, 'tilemap', []), }; }, - embeddableFactories: ['plugins/maps/embeddable/map_embeddable_factory'], styleSheetPaths: `${__dirname}/public/index.scss`, savedObjectSchemas: { 'maps-telemetry': { diff --git a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts deleted file mode 100644 index 90b17412377f5..0000000000000 --- a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.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. - */ - -/* - Maintain legacy embeddable legacy present while apps switch over - */ - -import { npSetup, npStart } from 'ui/new_platform'; -import { - bindSetupCoreAndPlugins, - bindStartCoreAndPlugins, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../plugins/maps/public/plugin'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MAP_SAVED_OBJECT_TYPE } from '../../../../../plugins/maps/common/constants'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { MapEmbeddableFactory } from '../../../../../plugins/maps/public/embeddable'; - -bindSetupCoreAndPlugins(npSetup.core, npSetup.plugins); -bindStartCoreAndPlugins(npStart.core, npStart.plugins); - -export * from '../../../../../plugins/maps/public/embeddable/map_embeddable_factory'; - -npSetup.plugins.embeddable.registerEmbeddableFactory( - MAP_SAVED_OBJECT_TYPE, - new MapEmbeddableFactory() -); diff --git a/x-pack/legacy/plugins/rollup/common/index.ts b/x-pack/legacy/plugins/rollup/common/index.ts deleted file mode 100644 index 526af055a3ef6..0000000000000 --- a/x-pack/legacy/plugins/rollup/common/index.ts +++ /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 { LICENSE_TYPE_BASIC, LicenseType } from '../../../common/constants'; - -export const PLUGIN = { - ID: 'rollup', - MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_BASIC as LicenseType, - getI18nName: (i18n: any): string => { - return i18n.translate('xpack.rollupJobs.appName', { - defaultMessage: 'Rollup jobs', - }); - }, -}; - -export * from '../../../../plugins/rollup/common'; diff --git a/x-pack/legacy/plugins/rollup/index.ts b/x-pack/legacy/plugins/rollup/index.ts deleted file mode 100644 index f33ae7cfee0a2..0000000000000 --- a/x-pack/legacy/plugins/rollup/index.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 { PluginInitializerContext } from 'src/core/server'; -import { RollupSetup } from '../../../plugins/rollup/server'; -import { PLUGIN } from './common'; -import { plugin } from './server'; - -export function rollup(kibana: any) { - return new kibana.Plugin({ - id: PLUGIN.ID, - configPrefix: 'xpack.rollup', - require: ['kibana', 'elasticsearch', 'xpack_main'], - init(server: any) { - const { core: coreSetup, plugins } = server.newPlatform.setup; - const { usageCollection, visTypeTimeseries, indexManagement } = plugins; - - const rollupSetup = (plugins.rollup as unknown) as RollupSetup; - - const initContext = ({ - config: rollupSetup.__legacy.config, - logger: rollupSetup.__legacy.logger, - } as unknown) as PluginInitializerContext; - - const rollupPluginInstance = plugin(initContext); - - rollupPluginInstance.setup(coreSetup, { - usageCollection, - visTypeTimeseries, - indexManagement, - __LEGACY: { - plugins: { - xpack_main: server.plugins.xpack_main, - rollup: server.plugins[PLUGIN.ID], - }, - }, - }); - }, - }); -} diff --git a/x-pack/legacy/plugins/rollup/kibana.json b/x-pack/legacy/plugins/rollup/kibana.json deleted file mode 100644 index 78458c9218be3..0000000000000 --- a/x-pack/legacy/plugins/rollup/kibana.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "rollup", - "version": "kibana", - "requiredPlugins": [ - "home", - "index_management", - "visTypeTimeseries", - "indexPatternManagement" - ], - "optionalPlugins": [ - "usageCollection" - ], - "server": true, - "ui": false -} diff --git a/x-pack/legacy/plugins/rollup/server/lib/__tests__/fixtures/jobs.js b/x-pack/legacy/plugins/rollup/server/lib/__tests__/fixtures/jobs.js deleted file mode 100644 index eb16b211da3fd..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/__tests__/fixtures/jobs.js +++ /dev/null @@ -1,98 +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 jobs = [ - { - "job_id" : "foo1", - "rollup_index" : "foo_rollup", - "index_pattern" : "foo-*", - "fields" : { - "node" : [ - { - "agg" : "terms" - } - ], - "temperature" : [ - { - "agg" : "min" - }, - { - "agg" : "max" - }, - { - "agg" : "sum" - } - ], - "timestamp" : [ - { - "agg" : "date_histogram", - "time_zone" : "UTC", - "interval" : "1h", - "delay": "7d" - } - ], - "voltage" : [ - { - "agg" : "histogram", - "interval": 5 - }, - { - "agg" : "sum" - } - ] - } - }, - { - "job_id" : "foo2", - "rollup_index" : "foo_rollup", - "index_pattern" : "foo-*", - "fields" : { - "host" : [ - { - "agg" : "terms" - } - ], - "timestamp" : [ - { - "agg" : "date_histogram", - "time_zone" : "UTC", - "interval" : "1h", - "delay": "7d" - } - ], - "voltage" : [ - { - "agg" : "histogram", - "interval": 20 - } - ] - } - }, - { - "job_id" : "foo3", - "rollup_index" : "foo_rollup", - "index_pattern" : "foo-*", - "fields" : { - "timestamp" : [ - { - "agg" : "date_histogram", - "time_zone" : "PST", - "interval" : "1h", - "delay": "7d" - } - ], - "voltage" : [ - { - "agg" : "histogram", - "interval": 5 - }, - { - "agg" : "sum" - } - ] - } - } -]; diff --git a/x-pack/legacy/plugins/rollup/server/lib/call_with_request_factory/call_with_request_factory.ts b/x-pack/legacy/plugins/rollup/server/lib/call_with_request_factory/call_with_request_factory.ts deleted file mode 100644 index 883b3552a7c02..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/call_with_request_factory/call_with_request_factory.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ElasticsearchServiceSetup } from 'kibana/server'; -import { once } from 'lodash'; -import { elasticsearchJsPlugin } from '../../client/elasticsearch_rollup'; - -const callWithRequest = once((elasticsearchService: ElasticsearchServiceSetup) => { - const config = { plugins: [elasticsearchJsPlugin] }; - return elasticsearchService.createClient('rollup', config); -}); - -export const callWithRequestFactory = ( - elasticsearchService: ElasticsearchServiceSetup, - request: any -) => { - return (...args: any[]) => { - return ( - callWithRequest(elasticsearchService) - .asScoped(request) - // @ts-ignore - .callAsCurrentUser(...args) - ); - }; -}; diff --git a/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.js b/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.js deleted file mode 100644 index b6cea09e0ea3c..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/license_pre_routing_factory.test.js +++ /dev/null @@ -1,62 +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 '.'; -import { - LICENSE_STATUS_VALID, - LICENSE_STATUS_INVALID, -} from '../../../../../common/constants/license_status'; -import { kibanaResponseFactory } from '../../../../../../../src/core/server'; - -describe('licensePreRoutingFactory()', () => { - let mockServer; - let mockLicenseCheckResults; - - beforeEach(() => { - mockServer = { - plugins: { - xpack_main: { - info: { - feature: () => ({ - getLicenseCheckResults: () => mockLicenseCheckResults, - }), - }, - }, - }, - }; - }); - - describe('status is invalid', () => { - beforeEach(() => { - mockLicenseCheckResults = { - status: LICENSE_STATUS_INVALID, - }; - }); - - it('replies with 403', () => { - const routeWithLicenseCheck = licensePreRoutingFactory(mockServer, () => {}); - const stubRequest = {}; - const response = routeWithLicenseCheck({}, stubRequest, kibanaResponseFactory); - expect(response.status).to.be(403); - }); - }); - - describe('status is valid', () => { - beforeEach(() => { - mockLicenseCheckResults = { - status: LICENSE_STATUS_VALID, - }; - }); - - it('replies with nothing', () => { - const routeWithLicenseCheck = licensePreRoutingFactory(mockServer, () => null); - const stubRequest = {}; - const response = routeWithLicenseCheck({}, stubRequest, kibanaResponseFactory); - expect(response).to.be(null); - }); - }); -}); diff --git a/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts b/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/license_pre_routing_factory.ts deleted file mode 100644 index 353510d96a00d..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/license_pre_routing_factory.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 { - KibanaRequest, - KibanaResponseFactory, - RequestHandler, - RequestHandlerContext, -} from 'src/core/server'; -import { PLUGIN } from '../../../common'; -import { LICENSE_STATUS_VALID } from '../../../../../common/constants/license_status'; -import { ServerShim } from '../../types'; - -export const licensePreRoutingFactory = ( - server: ServerShim, - handler: RequestHandler -): RequestHandler => { - const xpackMainPlugin = server.plugins.xpack_main; - - // License checking and enable/disable logic - return function licensePreRouting( - ctx: RequestHandlerContext, - request: KibanaRequest, - response: KibanaResponseFactory - ) { - const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults(); - const { status } = licenseCheckResults; - - if (status !== LICENSE_STATUS_VALID) { - return response.customError({ - body: { - message: licenseCheckResults.messsage, - }, - statusCode: 403, - }); - } - - return handler(ctx, request, response); - }; -}; diff --git a/x-pack/legacy/plugins/rollup/server/plugin.ts b/x-pack/legacy/plugins/rollup/server/plugin.ts deleted file mode 100644 index 05c22b030fff9..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/plugin.ts +++ /dev/null @@ -1,95 +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 { CoreSetup, Plugin, PluginInitializerContext, Logger } from 'src/core/server'; -import { first } from 'rxjs/operators'; -import { i18n } from '@kbn/i18n'; - -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { VisTypeTimeseriesSetup } from 'src/plugins/vis_type_timeseries/server'; -import { IndexManagementPluginSetup } from '../../../../plugins/index_management/server'; -import { registerLicenseChecker } from '../../../server/lib/register_license_checker'; -import { PLUGIN } from '../common'; -import { ServerShim, RouteDependencies } from './types'; - -import { - registerIndicesRoute, - registerFieldsForWildcardRoute, - registerSearchRoute, - registerJobsRoute, -} from './routes/api'; - -import { registerRollupUsageCollector } from './collectors'; - -import { rollupDataEnricher } from './rollup_data_enricher'; -import { registerRollupSearchStrategy } from './lib/search_strategies'; - -export class RollupsServerPlugin implements Plugin { - log: Logger; - - constructor(private readonly initializerContext: PluginInitializerContext) { - this.log = initializerContext.logger.get(); - } - - async setup( - { http, elasticsearch: elasticsearchService }: CoreSetup, - { - __LEGACY: serverShim, - usageCollection, - visTypeTimeseries, - indexManagement, - }: { - __LEGACY: ServerShim; - usageCollection?: UsageCollectionSetup; - visTypeTimeseries?: VisTypeTimeseriesSetup; - indexManagement?: IndexManagementPluginSetup; - } - ) { - const elasticsearch = await elasticsearchService.adminClient; - const router = http.createRouter(); - const routeDependencies: RouteDependencies = { - elasticsearch, - elasticsearchService, - router, - }; - - registerLicenseChecker( - serverShim as any, - PLUGIN.ID, - PLUGIN.getI18nName(i18n), - PLUGIN.MINIMUM_LICENSE_REQUIRED - ); - - registerIndicesRoute(routeDependencies, serverShim); - registerFieldsForWildcardRoute(routeDependencies, serverShim); - registerSearchRoute(routeDependencies, serverShim); - registerJobsRoute(routeDependencies, serverShim); - - if (usageCollection) { - this.initializerContext.config.legacy.globalConfig$ - .pipe(first()) - .toPromise() - .then(config => { - registerRollupUsageCollector(usageCollection, config.kibana.index); - }) - .catch(e => { - this.log.warn(`Registering Rollup collector failed: ${e}`); - }); - } - - if (indexManagement && indexManagement.indexDataEnricher) { - indexManagement.indexDataEnricher.add(rollupDataEnricher); - } - - if (visTypeTimeseries) { - const { addSearchStrategy } = visTypeTimeseries; - registerRollupSearchStrategy(routeDependencies, addSearchStrategy); - } - } - - start() {} - - stop() {} -} diff --git a/x-pack/legacy/plugins/rollup/server/routes/api/index.ts b/x-pack/legacy/plugins/rollup/server/routes/api/index.ts deleted file mode 100644 index 146c3e973f9ea..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/routes/api/index.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 { registerIndicesRoute } from './indices'; -export { registerFieldsForWildcardRoute } from './index_patterns'; -export { registerSearchRoute } from './search'; -export { registerJobsRoute } from './jobs'; diff --git a/x-pack/legacy/plugins/rollup/server/routes/api/index_patterns.ts b/x-pack/legacy/plugins/rollup/server/routes/api/index_patterns.ts deleted file mode 100644 index 2516840bd9537..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/routes/api/index_patterns.ts +++ /dev/null @@ -1,131 +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 { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'src/core/server'; - -import { indexBy } from 'lodash'; -import { IndexPatternsFetcher } from '../../shared_imports'; -import { RouteDependencies, ServerShim } from '../../types'; -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -import { isEsError } from '../../lib/is_es_error'; -import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; -import { getCapabilitiesForRollupIndices } from '../../lib/map_capabilities'; -import { mergeCapabilitiesWithFields, Field } from '../../lib/merge_capabilities_with_fields'; - -const parseMetaFields = (metaFields: string | string[]) => { - let parsedFields: string[] = []; - if (typeof metaFields === 'string') { - parsedFields = JSON.parse(metaFields); - } else { - parsedFields = metaFields; - } - return parsedFields; -}; - -const getFieldsForWildcardRequest = async (context: any, request: any, response: any) => { - const { callAsCurrentUser } = context.core.elasticsearch.dataClient; - const indexPatterns = new IndexPatternsFetcher(callAsCurrentUser); - const { pattern, meta_fields: metaFields } = request.query; - - let parsedFields: string[] = []; - try { - parsedFields = parseMetaFields(metaFields); - } catch (error) { - return response.badRequest({ - body: error, - }); - } - - try { - const fields = await indexPatterns.getFieldsForWildcard({ - pattern, - metaFields: parsedFields, - }); - - return response.ok({ - body: { fields }, - headers: { - 'content-type': 'application/json', - }, - }); - } catch (error) { - return response.notFound(); - } -}; - -/** - * Get list of fields for rollup index pattern, in the format of regular index pattern fields - */ -export function registerFieldsForWildcardRoute(deps: RouteDependencies, legacy: ServerShim) { - const handler: RequestHandler = async (ctx, request, response) => { - const { params, meta_fields: metaFields } = request.query; - - try { - // Make call and use field information from response - const { payload } = await getFieldsForWildcardRequest(ctx, request, response); - const fields = payload.fields; - const parsedParams = JSON.parse(params); - const rollupIndex = parsedParams.rollup_index; - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const rollupFields: Field[] = []; - const fieldsFromFieldCapsApi: { [key: string]: any } = indexBy(fields, 'name'); - const rollupIndexCapabilities = getCapabilitiesForRollupIndices( - await callWithRequest('rollup.rollupIndexCapabilities', { - indexPattern: rollupIndex, - }) - )[rollupIndex].aggs; - // Keep meta fields - metaFields.forEach( - (field: string) => - fieldsFromFieldCapsApi[field] && rollupFields.push(fieldsFromFieldCapsApi[field]) - ); - const mergedRollupFields = mergeCapabilitiesWithFields( - rollupIndexCapabilities, - fieldsFromFieldCapsApi, - rollupFields - ); - return response.ok({ body: { fields: mergedRollupFields } }); - } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - return response.internalError({ body: err }); - } - }; - - deps.router.get( - { - path: '/api/index_patterns/rollup/_fields_for_wildcard', - validate: { - query: schema.object({ - pattern: schema.string(), - meta_fields: schema.arrayOf(schema.string(), { - defaultValue: [], - }), - params: schema.string({ - validate(value) { - try { - const params = JSON.parse(value); - const keys = Object.keys(params); - const { rollup_index: rollupIndex } = params; - - if (!rollupIndex) { - return '[request query.params]: "rollup_index" is required'; - } else if (keys.length > 1) { - const invalidParams = keys.filter(key => key !== 'rollup_index'); - return `[request query.params]: ${invalidParams.join(', ')} is not allowed`; - } - } catch (err) { - return '[request query.params]: expected JSON string'; - } - }, - }), - }), - }, - }, - licensePreRoutingFactory(legacy, handler) - ); -} diff --git a/x-pack/legacy/plugins/rollup/server/routes/api/indices.ts b/x-pack/legacy/plugins/rollup/server/routes/api/indices.ts deleted file mode 100644 index e78f09a71876b..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/routes/api/indices.ts +++ /dev/null @@ -1,175 +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 { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'src/core/server'; -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -import { isEsError } from '../../lib/is_es_error'; -import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; -import { getCapabilitiesForRollupIndices } from '../../lib/map_capabilities'; -import { API_BASE_PATH } from '../../../common'; -import { RouteDependencies, ServerShim } from '../../types'; - -type NumericField = - | 'long' - | 'integer' - | 'short' - | 'byte' - | 'scaled_float' - | 'double' - | 'float' - | 'half_float'; - -interface FieldCapability { - date?: any; - keyword?: any; - long?: any; - integer?: any; - short?: any; - byte?: any; - double?: any; - float?: any; - half_float?: any; - scaled_float?: any; -} - -interface FieldCapabilities { - fields: FieldCapability[]; -} - -function isNumericField(fieldCapability: FieldCapability) { - const numericTypes = [ - 'long', - 'integer', - 'short', - 'byte', - 'double', - 'float', - 'half_float', - 'scaled_float', - ]; - return numericTypes.some(numericType => fieldCapability[numericType as NumericField] != null); -} - -export function registerIndicesRoute(deps: RouteDependencies, legacy: ServerShim) { - const getIndicesHandler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - - try { - const data = await callWithRequest('rollup.rollupIndexCapabilities', { - indexPattern: '_all', - }); - return response.ok({ body: getCapabilitiesForRollupIndices(data) }); - } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - return response.internalError({ body: err }); - } - }; - - const validateIndexPatternHandler: RequestHandler = async ( - ctx, - request, - response - ) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - - try { - const { indexPattern } = request.params; - const [fieldCapabilities, rollupIndexCapabilities]: [ - FieldCapabilities, - { [key: string]: any } - ] = await Promise.all([ - callWithRequest('rollup.fieldCapabilities', { indexPattern }), - callWithRequest('rollup.rollupIndexCapabilities', { indexPattern }), - ]); - - const doesMatchIndices = Object.entries(fieldCapabilities.fields).length !== 0; - const doesMatchRollupIndices = Object.entries(rollupIndexCapabilities).length !== 0; - - const dateFields: string[] = []; - const numericFields: string[] = []; - const keywordFields: string[] = []; - - const fieldCapabilitiesEntries = Object.entries(fieldCapabilities.fields); - - fieldCapabilitiesEntries.forEach( - ([fieldName, fieldCapability]: [string, FieldCapability]) => { - if (fieldCapability.date) { - dateFields.push(fieldName); - return; - } - - if (isNumericField(fieldCapability)) { - numericFields.push(fieldName); - return; - } - - if (fieldCapability.keyword) { - keywordFields.push(fieldName); - } - } - ); - - const body = { - doesMatchIndices, - doesMatchRollupIndices, - dateFields, - numericFields, - keywordFields, - }; - - return response.ok({ body }); - } catch (err) { - // 404s are still valid results. - if (err.statusCode === 404) { - const notFoundBody = { - doesMatchIndices: false, - doesMatchRollupIndices: false, - dateFields: [], - numericFields: [], - keywordFields: [], - }; - return response.ok({ body: notFoundBody }); - } - - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - - return response.internalError({ body: err }); - } - }; - - /** - * Returns a list of all rollup index names - */ - deps.router.get( - { - path: `${API_BASE_PATH}/indices`, - validate: false, - }, - licensePreRoutingFactory(legacy, getIndicesHandler) - ); - - /** - * Returns information on validity of an index pattern for creating a rollup job: - * - Does the index pattern match any indices? - * - Does the index pattern match rollup indices? - * - Which date fields, numeric fields, and keyword fields are available in the matching indices? - */ - deps.router.get( - { - path: `${API_BASE_PATH}/index_pattern_validity/{indexPattern}`, - validate: { - params: schema.object({ - indexPattern: schema.string(), - }), - }, - }, - licensePreRoutingFactory(legacy, validateIndexPatternHandler) - ); -} diff --git a/x-pack/legacy/plugins/rollup/server/routes/api/jobs.ts b/x-pack/legacy/plugins/rollup/server/routes/api/jobs.ts deleted file mode 100644 index e45713e2b807c..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/routes/api/jobs.ts +++ /dev/null @@ -1,178 +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 { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'src/core/server'; -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -import { isEsError } from '../../lib/is_es_error'; -import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; -import { API_BASE_PATH } from '../../../common'; -import { RouteDependencies, ServerShim } from '../../types'; - -export function registerJobsRoute(deps: RouteDependencies, legacy: ServerShim) { - const getJobsHandler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - - try { - const data = await callWithRequest('rollup.jobs'); - return response.ok({ body: data }); - } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - return response.internalError({ body: err }); - } - }; - - const createJobsHandler: RequestHandler = async (ctx, request, response) => { - try { - const { id, ...rest } = request.body.job; - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - // Create job. - await callWithRequest('rollup.createJob', { - id, - body: rest, - }); - // Then request the newly created job. - const results = await callWithRequest('rollup.job', { id }); - return response.ok({ body: results.jobs[0] }); - } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - return response.internalError({ body: err }); - } - }; - - const startJobsHandler: RequestHandler = async (ctx, request, response) => { - try { - const { jobIds } = request.body; - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - - const data = await Promise.all( - jobIds.map((id: string) => callWithRequest('rollup.startJob', { id })) - ).then(() => ({ success: true })); - return response.ok({ body: data }); - } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - return response.internalError({ body: err }); - } - }; - - const stopJobsHandler: RequestHandler = async (ctx, request, response) => { - try { - const { jobIds } = request.body; - // For our API integration tests we need to wait for the jobs to be stopped - // in order to be able to delete them sequencially. - const { waitForCompletion } = request.query; - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const stopRollupJob = (id: string) => - callWithRequest('rollup.stopJob', { - id, - waitForCompletion: waitForCompletion === 'true', - }); - const data = await Promise.all(jobIds.map(stopRollupJob)).then(() => ({ success: true })); - return response.ok({ body: data }); - } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - return response.internalError({ body: err }); - } - }; - - const deleteJobsHandler: RequestHandler = async (ctx, request, response) => { - try { - const { jobIds } = request.body; - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - const data = await Promise.all( - jobIds.map((id: string) => callWithRequest('rollup.deleteJob', { id })) - ).then(() => ({ success: true })); - return response.ok({ body: data }); - } catch (err) { - // There is an issue opened on ES to handle the following error correctly - // https://github.com/elastic/elasticsearch/issues/42908 - // Until then we'll modify the response here. - if (err.response && err.response.includes('Job must be [STOPPED] before deletion')) { - err.status = 400; - err.statusCode = 400; - err.displayName = 'Bad request'; - err.message = JSON.parse(err.response).task_failures[0].reason.reason; - } - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - return response.internalError({ body: err }); - } - }; - - deps.router.get( - { - path: `${API_BASE_PATH}/jobs`, - validate: false, - }, - licensePreRoutingFactory(legacy, getJobsHandler) - ); - - deps.router.put( - { - path: `${API_BASE_PATH}/create`, - validate: { - body: schema.object({ - job: schema.object( - { - id: schema.string(), - }, - { unknowns: 'allow' } - ), - }), - }, - }, - licensePreRoutingFactory(legacy, createJobsHandler) - ); - - deps.router.post( - { - path: `${API_BASE_PATH}/start`, - validate: { - body: schema.object({ - jobIds: schema.arrayOf(schema.string()), - }), - query: schema.maybe( - schema.object({ - waitForCompletion: schema.maybe(schema.string()), - }) - ), - }, - }, - licensePreRoutingFactory(legacy, startJobsHandler) - ); - - deps.router.post( - { - path: `${API_BASE_PATH}/stop`, - validate: { - body: schema.object({ - jobIds: schema.arrayOf(schema.string()), - }), - }, - }, - licensePreRoutingFactory(legacy, stopJobsHandler) - ); - - deps.router.post( - { - path: `${API_BASE_PATH}/delete`, - validate: { - body: schema.object({ - jobIds: schema.arrayOf(schema.string()), - }), - }, - }, - licensePreRoutingFactory(legacy, deleteJobsHandler) - ); -} diff --git a/x-pack/legacy/plugins/rollup/server/routes/api/search.ts b/x-pack/legacy/plugins/rollup/server/routes/api/search.ts deleted file mode 100644 index 97999a4b5ce8d..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/routes/api/search.ts +++ /dev/null @@ -1,50 +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 { schema } from '@kbn/config-schema'; -import { RequestHandler } from 'src/core/server'; -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -import { isEsError } from '../../lib/is_es_error'; -import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; -import { API_BASE_PATH } from '../../../common'; -import { RouteDependencies, ServerShim } from '../../types'; - -export function registerSearchRoute(deps: RouteDependencies, legacy: ServerShim) { - const handler: RequestHandler = async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(deps.elasticsearchService, request); - try { - const requests = request.body.map(({ index, query }: { index: string; query: any }) => - callWithRequest('rollup.search', { - index, - rest_total_hits_as_int: true, - body: query, - }) - ); - const data = await Promise.all(requests); - return response.ok({ body: data }); - } catch (err) { - if (isEsError(err)) { - return response.customError({ statusCode: err.statusCode, body: err }); - } - return response.internalError({ body: err }); - } - }; - - deps.router.post( - { - path: `${API_BASE_PATH}/search`, - validate: { - body: schema.arrayOf( - schema.object({ - index: schema.string(), - query: schema.any(), - }) - ), - }, - }, - licensePreRoutingFactory(legacy, handler) - ); -} diff --git a/x-pack/legacy/plugins/rollup/server/types.ts b/x-pack/legacy/plugins/rollup/server/types.ts deleted file mode 100644 index bcc6770e9b8ee..0000000000000 --- a/x-pack/legacy/plugins/rollup/server/types.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 { IRouter, ElasticsearchServiceSetup, IClusterClient } from 'src/core/server'; -import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; - -export interface ServerShim { - plugins: { - xpack_main: XPackMainPlugin; - rollup: any; - }; -} - -export interface RouteDependencies { - router: IRouter; - elasticsearchService: ElasticsearchServiceSetup; - elasticsearch: IClusterClient; -} diff --git a/x-pack/legacy/plugins/rollup/tsconfig.json b/x-pack/legacy/plugins/rollup/tsconfig.json deleted file mode 100644 index 618c6c3e97b57..0000000000000 --- a/x-pack/legacy/plugins/rollup/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "../../../tsconfig.json" -} diff --git a/x-pack/legacy/plugins/task_manager/server/index.ts b/x-pack/legacy/plugins/task_manager/server/index.ts index ff25d8a1e0e5d..3ea687f7003f4 100644 --- a/x-pack/legacy/plugins/task_manager/server/index.ts +++ b/x-pack/legacy/plugins/task_manager/server/index.ts @@ -15,19 +15,26 @@ export { LegacyTaskManagerApi, getTaskManagerSetup, getTaskManagerStart } from ' // Once all plugins are migrated to NP, this can be removed // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { TaskManager } from '../../../../plugins/task_manager/server/task_manager'; +import { + LegacyPluginApi, + LegacyPluginSpec, + ArrayOrItem, +} from '../../../../../src/legacy/plugin_discovery/types'; const savedObjectSchemas = { task: { hidden: true, isNamespaceAgnostic: true, convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id`, + // legacy config is marked as any in core, no choice here + // eslint-disable-next-line @typescript-eslint/no-explicit-any indexPattern(config: any) { return config.get('xpack.task_manager.index'); }, }, }; -export function taskManager(kibana: any) { +export function taskManager(kibana: LegacyPluginApi): ArrayOrItem { return new kibana.Plugin({ id: 'task_manager', require: ['kibana', 'elasticsearch', 'xpack_main'], @@ -58,7 +65,11 @@ export function taskManager(kibana: any) { // instead we will start the internal Task Manager plugin when // all legacy plugins have finished initializing // Once all plugins are migrated to NP, this can be removed - this.kbnServer.afterPluginsInit(() => { + + // the typing for the lagcy server isn't quite correct, so + // we'll bypase it for now + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this as any).kbnServer.afterPluginsInit(() => { taskManagerPlugin.start(); }); return taskManagerPlugin; @@ -71,5 +82,5 @@ export function taskManager(kibana: any) { migrations, savedObjectSchemas, }, - }); + } as Legacy.PluginSpecOptions); } diff --git a/x-pack/legacy/plugins/task_manager/server/legacy.ts b/x-pack/legacy/plugins/task_manager/server/legacy.ts index cd2047b757e61..0d50828004a94 100644 --- a/x-pack/legacy/plugins/task_manager/server/legacy.ts +++ b/x-pack/legacy/plugins/task_manager/server/legacy.ts @@ -49,10 +49,10 @@ export function createLegacyApi(legacyTaskManager: Promise): Legacy fetch: (opts: SearchOpts) => legacyTaskManager.then((tm: TaskManager) => tm.fetch(opts)), get: (id: string) => legacyTaskManager.then((tm: TaskManager) => tm.get(id)), remove: (id: string) => legacyTaskManager.then((tm: TaskManager) => tm.remove(id)), - schedule: (taskInstance: TaskInstanceWithDeprecatedFields, options?: any) => + schedule: (taskInstance: TaskInstanceWithDeprecatedFields, options?: object) => legacyTaskManager.then((tm: TaskManager) => tm.schedule(taskInstance, options)), runNow: (taskId: string) => legacyTaskManager.then((tm: TaskManager) => tm.runNow(taskId)), - ensureScheduled: (taskInstance: TaskInstanceWithId, options?: any) => + ensureScheduled: (taskInstance: TaskInstanceWithId, options?: object) => legacyTaskManager.then((tm: TaskManager) => tm.ensureScheduled(taskInstance, options)), }; } diff --git a/x-pack/legacy/plugins/task_manager/server/migrations.ts b/x-pack/legacy/plugins/task_manager/server/migrations.ts index 97c4f97f59c58..1c2cf73d0fe13 100644 --- a/x-pack/legacy/plugins/task_manager/server/migrations.ts +++ b/x-pack/legacy/plugins/task_manager/server/migrations.ts @@ -7,7 +7,7 @@ import { SavedObject } from '../../../../../src/core/server'; export const migrations = { task: { - '7.4.0': (doc: SavedObject>) => ({ + '7.4.0': (doc: SavedObject>) => ({ ...doc, updated_at: new Date().toISOString(), }), @@ -18,7 +18,7 @@ export const migrations = { function moveIntervalIntoSchedule({ attributes: { interval, ...attributes }, ...doc -}: SavedObject>) { +}: SavedObject>) { return { ...doc, attributes: { diff --git a/x-pack/legacy/plugins/uptime/common/constants/index.ts b/x-pack/legacy/plugins/uptime/common/constants/index.ts index 74783cf46550f..72d498056d6b3 100644 --- a/x-pack/legacy/plugins/uptime/common/constants/index.ts +++ b/x-pack/legacy/plugins/uptime/common/constants/index.ts @@ -9,6 +9,7 @@ export { CHART_FORMAT_LIMITS } from './chart_format_limits'; export { CLIENT_DEFAULTS } from './client_defaults'; export { CONTEXT_DEFAULTS } from './context_defaults'; export * from './capabilities'; +export * from './settings_defaults'; export { PLUGIN } from './plugin'; export { QUERY, STATES } from './query'; export * from './ui'; diff --git a/x-pack/legacy/plugins/uptime/common/constants/settings_defaults.ts b/x-pack/legacy/plugins/uptime/common/constants/settings_defaults.ts new file mode 100644 index 0000000000000..b7986679a09ca --- /dev/null +++ b/x-pack/legacy/plugins/uptime/common/constants/settings_defaults.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DynamicSettings } from '../runtime_types'; + +export const DYNAMIC_SETTINGS_DEFAULTS: DynamicSettings = { + heartbeatIndices: 'heartbeat-8*', + certThresholds: { + expiration: 30, + age: 365, + }, +}; diff --git a/x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts b/x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts index 985b51891da99..da887cc5055c1 100644 --- a/x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts +++ b/x-pack/legacy/plugins/uptime/common/runtime_types/dynamic_settings.ts @@ -6,19 +6,15 @@ import * as t from 'io-ts'; -export const CertificatesStatesThresholdType = t.interface({ - warningState: t.number, - errorState: t.number, +export const CertStateThresholdsType = t.type({ + age: t.number, + expiration: t.number, }); -export const DynamicSettingsType = t.intersection([ - t.type({ - heartbeatIndices: t.string, - }), - t.partial({ - certificatesThresholds: CertificatesStatesThresholdType, - }), -]); +export const DynamicSettingsType = t.type({ + heartbeatIndices: t.string, + certThresholds: CertStateThresholdsType, +}); export const DynamicSettingsSaveType = t.intersection([ t.type({ @@ -31,12 +27,4 @@ export const DynamicSettingsSaveType = t.intersection([ export type DynamicSettings = t.TypeOf; export type DynamicSettingsSaveResponse = t.TypeOf; -export type CertificatesStatesThreshold = t.TypeOf; - -export const defaultDynamicSettings: DynamicSettings = { - heartbeatIndices: 'heartbeat-8*', - certificatesThresholds: { - errorState: 7, - warningState: 30, - }, -}; +export type CertStateThresholds = t.TypeOf; diff --git a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx b/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx index acfe2ada5b68d..6328789d03f29 100644 --- a/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/overview/empty_state/__tests__/empty_state.test.tsx @@ -7,8 +7,7 @@ import React from 'react'; import { EmptyStateComponent } from '../empty_state'; import { StatesIndexStatus } from '../../../../../common/runtime_types'; -import { IHttpFetchError } from '../../../../../../../../../target/types/core/public/http'; -import { HttpFetchError } from '../../../../../../../../../src/core/public/http/http_fetch_error'; +import { HttpFetchError, IHttpFetchError } from 'src/core/public'; import { mountWithRouter, shallowWithRouter } from '../../../../lib'; describe('EmptyState component', () => { diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap index 36bc9bb860211..96d472c91680d 100644 --- a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/certificate_form.test.tsx.snap @@ -52,17 +52,18 @@ exports[`CertificateForm shallow renders expected elements for valid props 1`] = } > diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap index 93151198c0f49..3b0c6d99fd9f8 100644 --- a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/__snapshots__/indices_form.test.tsx.snap @@ -52,17 +52,18 @@ exports[`CertificateForm shallow renders expected elements for valid props 1`] = } > diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx index a3158f3d72445..3d4bd58aabe0f 100644 --- a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/certificate_form.test.tsx @@ -13,12 +13,13 @@ describe('CertificateForm', () => { expect( shallowWithRouter( ) diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx index 654d51019d4e5..07a3bf81e39d8 100644 --- a/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/settings/__tests__/indices_form.test.tsx @@ -13,12 +13,13 @@ describe('CertificateForm', () => { expect( shallowWithRouter( ) diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/certificate_form.tsx b/x-pack/legacy/plugins/uptime/public/components/settings/certificate_form.tsx index 5103caee1e1c0..209e38785e165 100644 --- a/x-pack/legacy/plugins/uptime/public/components/settings/certificate_form.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/settings/certificate_form.tsx @@ -6,155 +6,157 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useSelector } from 'react-redux'; import { EuiDescribedFormGroup, EuiFormRow, EuiCode, EuiFieldNumber, + EuiText, EuiTitle, EuiSpacer, - EuiSelect, EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; -import { defaultDynamicSettings, DynamicSettings } from '../../../common/runtime_types'; -import { selectDynamicSettings } from '../../state/selectors'; +import { CertStateThresholds } from '../../../common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants'; +import { SettingsFormProps } from '../../pages/settings'; -type NumStr = string | number; - -export type OnFieldChangeType = (field: string, value?: NumStr) => void; - -export interface SettingsFormProps { - onChange: OnFieldChangeType; - formFields: DynamicSettings | null; - fieldErrors: any; - isDisabled: boolean; +interface ChangedValues { + heartbeatIndices?: string; + certThresholds?: Partial; } +export type OnFieldChangeType = (changedValues: ChangedValues) => void; + export const CertificateExpirationForm: React.FC = ({ + loading, onChange, formFields, fieldErrors, isDisabled, -}) => { - const dss = useSelector(selectDynamicSettings); - - return ( - <> - -

+}) => ( + <> + +

+ +

+
+ + +

+ } + description={ + + } + > + {DYNAMIC_SETTINGS_DEFAULTS.certThresholds.expiration} + ), + }} /> - - - - - - } - description={ + isInvalid={!!fieldErrors?.certificatesThresholds?.expirationThresholdError} + label={ } > - {defaultDynamicSettings?.certificatesThresholds?.errorState} - ), - }} - /> - } - isInvalid={!!fieldErrors?.certificatesThresholds?.errorState} - label={ - + + + onChange({ + certThresholds: { + expiration: Number(e.target.value), + }, + }) + } /> - } - > - - - - onChange( - 'certificatesThresholds.errorState', - value === '' ? undefined : Number(value) - ) - } + + + + - - - - - - - {defaultDynamicSettings?.certificatesThresholds?.warningState} - ), - }} - /> - } - isInvalid={!!fieldErrors?.certificatesThresholds?.warningState} - label={ - + + + + {DYNAMIC_SETTINGS_DEFAULTS.certThresholds.age}, + }} + /> + } + isInvalid={!!fieldErrors?.certificatesThresholds?.ageThresholdError} + label={ + + } + > + + + + onChange({ + certThresholds: { age: Number(e.currentTarget.value) }, + }) + } /> - } - > - - - - onChange('certificatesThresholds.warningState', Number(event.currentTarget.value)) - } + + + + - - - - - - - - - ); -}; + + + + + + +); diff --git a/x-pack/legacy/plugins/uptime/public/components/settings/indices_form.tsx b/x-pack/legacy/plugins/uptime/public/components/settings/indices_form.tsx index c28eca2ea229e..b9a5ca0e730de 100644 --- a/x-pack/legacy/plugins/uptime/public/components/settings/indices_form.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/settings/indices_form.tsx @@ -6,7 +6,6 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useSelector } from 'react-redux'; import { EuiDescribedFormGroup, EuiFormRow, @@ -15,76 +14,72 @@ import { EuiTitle, EuiSpacer, } from '@elastic/eui'; -import { defaultDynamicSettings } from '../../../common/runtime_types'; -import { selectDynamicSettings } from '../../state/selectors'; -import { SettingsFormProps } from './certificate_form'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants'; +import { SettingsFormProps } from '../../pages/settings'; export const IndicesForm: React.FC = ({ onChange, + loading, formFields, fieldErrors, isDisabled, -}) => { - const dss = useSelector(selectDynamicSettings); - - return ( - <> - -

+}) => ( + <> + +

+ +

+
+ + + +

+ } + description={ + + } + > + {DYNAMIC_SETTINGS_DEFAULTS.heartbeatIndices}, + }} /> - -
- - - - } - description={ + isInvalid={!!fieldErrors?.heartbeatIndices} + label={ } > - {defaultDynamicSettings.heartbeatIndices}, - }} - /> - } - isInvalid={!!fieldErrors?.heartbeatIndices} - label={ - - } - > - onChange('heartbeatIndices', event.currentTarget.value)} - /> - - - - ); -}; + disabled={isDisabled} + isLoading={loading} + value={formFields?.heartbeatIndices || ''} + onChange={(event: any) => onChange({ heartbeatIndices: event.currentTarget.value })} + /> + + + +); diff --git a/x-pack/legacy/plugins/uptime/public/pages/settings.tsx b/x-pack/legacy/plugins/uptime/public/pages/settings.tsx index 6defb96e0da3d..d8c2a78092854 100644 --- a/x-pack/legacy/plugins/uptime/public/pages/settings.tsx +++ b/x-pack/legacy/plugins/uptime/public/pages/settings.tsx @@ -17,8 +17,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { useDispatch, useSelector } from 'react-redux'; -import { cloneDeep, isEqual, set } from 'lodash'; -import { i18n } from '@kbn/i18n'; +import { isEqual } from 'lodash'; import { Link } from 'react-router-dom'; import { selectDynamicSettings } from '../state/selectors'; import { getDynamicSettings, setDynamicSettings } from '../state/actions/dynamic_settings'; @@ -32,21 +31,38 @@ import { CertificateExpirationForm, OnFieldChangeType, } from '../components/settings/certificate_form'; - -const getFieldErrors = (formFields: DynamicSettings | null) => { +import * as Translations from './translations'; + +interface SettingsPageFieldErrors { + heartbeatIndices: 'May not be blank' | ''; + certificatesThresholds: { + expirationThresholdError: string | null; + ageThresholdError: string | null; + } | null; +} + +export interface SettingsFormProps { + loading: boolean; + onChange: OnFieldChangeType; + formFields: DynamicSettings | null; + fieldErrors: SettingsPageFieldErrors | null; + isDisabled: boolean; +} + +const getFieldErrors = (formFields: DynamicSettings | null): SettingsPageFieldErrors | null => { if (formFields) { const blankStr = 'May not be blank'; - const { certificatesThresholds, heartbeatIndices } = formFields; + const { certThresholds: certificatesThresholds, heartbeatIndices } = formFields; const heartbeatIndErr = heartbeatIndices.match(/^\S+$/) ? '' : blankStr; - const errorStateErr = certificatesThresholds?.errorState ? null : blankStr; - const warningStateErr = certificatesThresholds?.warningState ? null : blankStr; + const expirationThresholdError = certificatesThresholds?.expiration ? null : blankStr; + const ageThresholdError = certificatesThresholds?.age ? null : blankStr; return { heartbeatIndices: heartbeatIndErr, certificatesThresholds: - errorStateErr || warningStateErr + expirationThresholdError || ageThresholdError ? { - errorState: errorStateErr, - warningState: warningStateErr, + expirationThresholdError, + ageThresholdError, } : null, }; @@ -57,10 +73,7 @@ const getFieldErrors = (formFields: DynamicSettings | null) => { export const SettingsPage = () => { const dss = useSelector(selectDynamicSettings); - const settingsBreadcrumbText = i18n.translate('xpack.uptime.settingsBreadcrumbText', { - defaultMessage: 'Settings', - }); - useBreadcrumbs([{ text: settingsBreadcrumbText }]); + useBreadcrumbs([{ text: Translations.settings.breadcrumbText }]); useUptimeTelemetry(UptimePage.Settings); @@ -70,21 +83,28 @@ export const SettingsPage = () => { dispatch(getDynamicSettings()); }, [dispatch]); - const [formFields, setFormFields] = useState(dss.settings || null); + const [formFields, setFormFields] = useState( + dss.settings ? { ...dss.settings } : null + ); - if (!dss.loadError && formFields == null && dss.settings) { - setFormFields({ ...dss.settings }); + if (!dss.loadError && formFields === null && dss.settings) { + setFormFields(Object.assign({}, { ...dss.settings })); } const fieldErrors = getFieldErrors(formFields); const isFormValid = !(fieldErrors && Object.values(fieldErrors).find(v => !!v)); - const onChangeFormField: OnFieldChangeType = (field, value) => { + const onChangeFormField: OnFieldChangeType = changedField => { if (formFields) { - const newFormFields = cloneDeep(formFields); - set(newFormFields, field, value); - setFormFields(cloneDeep(newFormFields)); + setFormFields({ + heartbeatIndices: changedField.heartbeatIndices ?? formFields.heartbeatIndices, + certThresholds: Object.assign( + {}, + formFields.certThresholds, + changedField?.certThresholds ?? null + ), + }); } }; @@ -95,27 +115,18 @@ export const SettingsPage = () => { } }; - const resetForm = () => { - if (formFields && dss.settings) { - setFormFields({ ...dss.settings }); - } - }; + const resetForm = () => setFormFields(dss.settings ? { ...dss.settings } : null); - const isFormDirty = dss.settings ? !isEqual(dss.settings, formFields) : true; + const isFormDirty = !isEqual(dss.settings, formFields); const canEdit: boolean = !!useKibana().services?.application?.capabilities.uptime.configureSettings || false; const isFormDisabled = dss.loading || !canEdit; - const editNoticeTitle = i18n.translate('xpack.uptime.settings.cannotEditTitle', { - defaultMessage: 'You do not have permission to edit settings.', - }); - const editNoticeText = i18n.translate('xpack.uptime.settings.cannotEditText', { - defaultMessage: - "Your user currently has 'Read' permissions for the Uptime app. Enable a permissions-level of 'All' to edit these settings.", - }); const cannotEditNotice = canEdit ? null : ( <> - {editNoticeText} + + {Translations.settings.editNoticeText} + ); @@ -124,9 +135,7 @@ export const SettingsPage = () => { <> - {i18n.translate('xpack.uptime.settings.returnToOverviewLinkLabel', { - defaultMessage: 'Return to overview', - })} + {Translations.settings.returnToOverviewLinkLabel} @@ -139,12 +148,14 @@ export const SettingsPage = () => {
('CREATE_ML_JOB'); -export const getMLCapabilitiesAction = createAsyncAction( +export const getMLCapabilitiesAction = createAsyncAction( 'GET_ML_CAPABILITIES' ); diff --git a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts index 66b376c3ac36f..ff9fcd0573257 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/__tests__/snapshot.test.ts @@ -6,7 +6,7 @@ import { fetchSnapshotCount } from '../snapshot'; import { apiService } from '../utils'; -import { HttpFetchError } from '../../../../../../../../src/core/public/http/http_fetch_error'; +import { HttpFetchError } from 'src/core/public'; describe('snapshot API', () => { let fetchMock: jest.SpyInstance>; diff --git a/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts b/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts index f10745a50f56a..16b90e9921428 100644 --- a/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts +++ b/x-pack/legacy/plugins/uptime/public/state/api/ml_anomaly.ts @@ -8,7 +8,7 @@ import moment from 'moment'; import { apiService } from './utils'; import { AnomalyRecords, AnomalyRecordsParams } from '../actions'; import { API_URLS, ML_JOB_ID, ML_MODULE_ID } from '../../../common/constants'; -import { PrivilegesResponse } from '../../../../../../plugins/ml/common/types/privileges'; +import { MlCapabilitiesResponse } from '../../../../../../plugins/ml/common/types/capabilities'; import { CreateMLJobSuccess, DeleteJobResults, @@ -20,7 +20,7 @@ import { JobExistResult } from '../../../../../../plugins/ml/common/types/data_r export const getMLJobId = (monitorId: string) => `${monitorId}_${ML_JOB_ID}`.toLowerCase(); -export const getMLCapabilities = async (): Promise => { +export const getMLCapabilities = async (): Promise => { return await apiService.get(API_URLS.ML_CAPABILITIES); }; diff --git a/x-pack/legacy/plugins/uptime/public/state/effects/__tests__/fetch_effect.test.ts b/x-pack/legacy/plugins/uptime/public/state/effects/__tests__/fetch_effect.test.ts index 0354cfeac7b07..4ec35d8cd6c6f 100644 --- a/x-pack/legacy/plugins/uptime/public/state/effects/__tests__/fetch_effect.test.ts +++ b/x-pack/legacy/plugins/uptime/public/state/effects/__tests__/fetch_effect.test.ts @@ -7,7 +7,7 @@ import { call, put } from 'redux-saga/effects'; import { fetchEffectFactory } from '../fetch_effect'; import { indexStatusAction } from '../../actions'; -import { HttpFetchError } from '../../../../../../../../src/core/public/http/http_fetch_error'; +import { HttpFetchError } from 'src/core/public'; import { StatesIndexStatus } from '../../../../common/runtime_types'; import { fetchIndexStatus } from '../../api'; diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/dynamic_settings.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/dynamic_settings.ts index f003565e9873e..a9ad58e64e552 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/dynamic_settings.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/dynamic_settings.ts @@ -31,23 +31,19 @@ export const dynamicSettingsReducer = handleActions( ...state, loading: true, }), - [String(getDynamicSettingsSuccess)]: (state, action: Action) => { - return { - loading: false, - settings: action.payload, - }; - }, - [String(getDynamicSettingsFail)]: (state, action: Action) => { - return { - loading: false, - loadError: action.payload, - }; - }, + [String(getDynamicSettingsSuccess)]: (_state, action: Action) => ({ + loading: false, + settings: action.payload, + }), + [String(getDynamicSettingsFail)]: (_state, action: Action) => ({ + loading: false, + loadError: action.payload, + }), [String(setDynamicSettings)]: state => ({ ...state, loading: true, }), - [String(setDynamicSettingsSuccess)]: (state, action: Action) => ({ + [String(setDynamicSettingsSuccess)]: (_state, action: Action) => ({ settings: action.payload, saveSucceded: true, loading: false, diff --git a/x-pack/legacy/plugins/uptime/public/state/reducers/ml_anomaly.ts b/x-pack/legacy/plugins/uptime/public/state/reducers/ml_anomaly.ts index 9d59d39634327..df5e825c3488b 100644 --- a/x-pack/legacy/plugins/uptime/public/state/reducers/ml_anomaly.ts +++ b/x-pack/legacy/plugins/uptime/public/state/reducers/ml_anomaly.ts @@ -17,7 +17,7 @@ import { import { getAsyncInitialState, handleAsyncAction } from './utils'; import { IHttpFetchError } from '../../../../../../../target/types/core/public/http'; import { AsyncInitialState } from './types'; -import { PrivilegesResponse } from '../../../../../../plugins/ml/common/types/privileges'; +import { MlCapabilitiesResponse } from '../../../../../../plugins/ml/common/types/capabilities'; import { CreateMLJobSuccess, DeleteJobResults } from '../actions/types'; import { JobExistResult } from '../../../../../../plugins/ml/common/types/data_recognizer'; @@ -26,7 +26,7 @@ export interface MLJobState { createJob: AsyncInitialState; deleteJob: AsyncInitialState; anomalies: AsyncInitialState; - mlCapabilities: AsyncInitialState; + mlCapabilities: AsyncInitialState; } const initialState: MLJobState = { diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts b/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts index 2b7c04178e9b4..ba5e5abf588b8 100644 --- a/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts +++ b/x-pack/legacy/plugins/uptime/public/state/selectors/__tests__/index.test.ts @@ -6,6 +6,7 @@ import { getBasePath, isIntegrationsPopupOpen } from '../index'; import { AppState } from '../../../state'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; describe('state selectors', () => { const state: AppState = { @@ -20,6 +21,7 @@ describe('state selectors', () => { loading: false, }, dynamicSettings: { + settings: DYNAMIC_SETTINGS_DEFAULTS, loading: false, }, monitor: { diff --git a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts b/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts index 7260c61f44147..15fc8b8a7b173 100644 --- a/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts +++ b/x-pack/legacy/plugins/uptime/public/state/selectors/index.ts @@ -24,9 +24,7 @@ export const monitorLocationsSelector = (state: AppState, monitorId: string) => export const monitorStatusSelector = (state: AppState) => state.monitorStatus.status; -export const selectDynamicSettings = (state: AppState) => { - return state.dynamicSettings; -}; +export const selectDynamicSettings = (state: AppState) => state.dynamicSettings; export const selectIndexPattern = ({ indexPattern }: AppState) => { return { indexPattern: indexPattern.index_pattern, loading: indexPattern.loading }; diff --git a/x-pack/package.json b/x-pack/package.json index 7d2dcb789c2ad..ca73884b8c7c7 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -121,7 +121,7 @@ "cheerio": "0.22.0", "commander": "3.0.2", "copy-webpack-plugin": "^5.0.4", - "cypress": "^4.2.0", + "cypress": "^4.4.1", "cypress-multi-reporters": "^1.2.3", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.2", @@ -185,7 +185,7 @@ "@elastic/ems-client": "7.8.0", "@elastic/eui": "22.3.0", "@elastic/filesaver": "1.1.2", - "@elastic/maki": "6.2.0", + "@elastic/maki": "6.3.0", "@elastic/node-crypto": "1.1.1", "@elastic/numeral": "2.4.0", "@kbn/babel-preset": "1.0.0", @@ -250,7 +250,7 @@ "graphql-tag": "^2.9.2", "graphql-tools": "^3.0.2", "h2o2": "^8.1.2", - "handlebars": "4.5.3", + "handlebars": "4.7.6", "history": "4.9.0", "history-extra": "^5.0.1", "i18n-iso-countries": "^4.3.1", diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 82cc09f5e9eca..d6c85606edc2c 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -263,7 +263,7 @@ Kibana ships with a set of built-in action types: | Type | Id | Description | | ------------------------- | ------------- | ------------------------------------------------------------------ | -| [Server log](#server-log) | `.log` | Logs messages to the Kibana log using `server.log()` | +| [Server log](#server-log) | `.server-log` | Logs messages to the Kibana log using Kibana's logger | | [Email](#email) | `.email` | Sends an email using SMTP | | [Slack](#slack) | `.slack` | Posts a message to a slack channel | | [Index](#index) | `.index` | Indexes document(s) into Elasticsearch | diff --git a/x-pack/plugins/actions/common/types.ts b/x-pack/plugins/actions/common/types.ts index 61b338d47b9f5..49e8f3e80b14a 100644 --- a/x-pack/plugins/actions/common/types.ts +++ b/x-pack/plugins/actions/common/types.ts @@ -19,6 +19,8 @@ export interface ActionResult { id: string; actionTypeId: string; name: string; + // This will have to remain `any` until we can extend Action Executors with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any config: Record; isPreconfigured: boolean; } diff --git a/x-pack/plugins/actions/server/action_type_registry.mock.ts b/x-pack/plugins/actions/server/action_type_registry.mock.ts index 6a806d1fa531c..d14d0ca2ddf84 100644 --- a/x-pack/plugins/actions/server/action_type_registry.mock.ts +++ b/x-pack/plugins/actions/server/action_type_registry.mock.ts @@ -14,6 +14,7 @@ const createActionTypeRegistryMock = () => { list: jest.fn(), ensureActionTypeEnabled: jest.fn(), isActionTypeEnabled: jest.fn(), + isActionExecutable: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/actions/server/action_type_registry.test.ts b/x-pack/plugins/actions/server/action_type_registry.test.ts index 26bd68adfc4b6..3be2f26557079 100644 --- a/x-pack/plugins/actions/server/action_type_registry.test.ts +++ b/x-pack/plugins/actions/server/action_type_registry.test.ts @@ -28,6 +28,16 @@ beforeEach(() => { ), actionsConfigUtils: mockedActionsConfig, licenseState: mockedLicenseState, + preconfiguredActions: [ + { + actionTypeId: 'foo', + config: {}, + id: 'my-slack1', + name: 'Slack #xyz', + secrets: {}, + isPreconfigured: true, + }, + ], }; }); @@ -194,6 +204,19 @@ describe('isActionTypeEnabled', () => { expect(mockedActionsConfig.isActionTypeEnabled).toHaveBeenCalledWith('foo'); }); + test('should call isActionExecutable of the actions config', async () => { + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + actionTypeRegistry.isActionExecutable('my-slack1', 'foo'); + expect(mockedActionsConfig.isActionTypeEnabled).toHaveBeenCalledWith('foo'); + }); + + test('should return true when isActionTypeEnabled is false and isLicenseValidForActionType is true and it has preconfigured connectors', async () => { + mockedActionsConfig.isActionTypeEnabled.mockReturnValue(false); + mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); + + expect(actionTypeRegistry.isActionExecutable('my-slack1', 'foo')).toEqual(true); + }); + test('should call isLicenseValidForActionType of the license state', async () => { mockedLicenseState.isLicenseValidForActionType.mockReturnValue({ isValid: true }); actionTypeRegistry.isActionTypeEnabled('foo'); diff --git a/x-pack/plugins/actions/server/action_type_registry.ts b/x-pack/plugins/actions/server/action_type_registry.ts index c1d979feacc1d..723982b11e1cc 100644 --- a/x-pack/plugins/actions/server/action_type_registry.ts +++ b/x-pack/plugins/actions/server/action_type_registry.ts @@ -8,7 +8,7 @@ import Boom from 'boom'; import { i18n } from '@kbn/i18n'; import { RunContext, TaskManagerSetupContract } from '../../task_manager/server'; import { ExecutorError, TaskRunnerFactory, ILicenseState } from './lib'; -import { ActionType } from './types'; +import { ActionType, PreConfiguredAction } from './types'; import { ActionType as CommonActionType } from '../common'; import { ActionsConfigurationUtilities } from './actions_config'; @@ -17,6 +17,7 @@ export interface ActionTypeRegistryOpts { taskRunnerFactory: TaskRunnerFactory; actionsConfigUtils: ActionsConfigurationUtilities; licenseState: ILicenseState; + preconfiguredActions: PreConfiguredAction[]; } export class ActionTypeRegistry { @@ -25,12 +26,14 @@ export class ActionTypeRegistry { private readonly taskRunnerFactory: TaskRunnerFactory; private readonly actionsConfigUtils: ActionsConfigurationUtilities; private readonly licenseState: ILicenseState; + private readonly preconfiguredActions: PreConfiguredAction[]; constructor(constructorParams: ActionTypeRegistryOpts) { this.taskManager = constructorParams.taskManager; this.taskRunnerFactory = constructorParams.taskRunnerFactory; this.actionsConfigUtils = constructorParams.actionsConfigUtils; this.licenseState = constructorParams.licenseState; + this.preconfiguredActions = constructorParams.preconfiguredActions; } /** @@ -58,6 +61,19 @@ export class ActionTypeRegistry { ); } + /** + * Returns true if action type is enabled or it is a preconfigured action type. + */ + public isActionExecutable(actionId: string, actionTypeId: string) { + return ( + this.isActionTypeEnabled(actionTypeId) || + (!this.isActionTypeEnabled(actionTypeId) && + this.preconfiguredActions.find( + preconfiguredAction => preconfiguredAction.id === actionId + ) !== undefined) + ); + } + /** * Registers an action type to the action type registry */ @@ -81,7 +97,7 @@ export class ActionTypeRegistry { title: actionType.name, type: `actions:${actionType.id}`, maxAttempts: actionType.maxAttempts || 1, - getRetry(attempts: number, error: any) { + getRetry(attempts: number, error: unknown) { if (error instanceof ExecutorError) { return error.retry == null ? false : error.retry; } diff --git a/x-pack/plugins/actions/server/actions_client.mock.ts b/x-pack/plugins/actions/server/actions_client.mock.ts index 431bfb1e99c3b..64b43e1ab6bbc 100644 --- a/x-pack/plugins/actions/server/actions_client.mock.ts +++ b/x-pack/plugins/actions/server/actions_client.mock.ts @@ -7,9 +7,10 @@ import { ActionsClient } from './actions_client'; type ActionsClientContract = PublicMethodsOf; +export type ActionsClientMock = jest.Mocked; const createActionsClientMock = () => { - const mocked: jest.Mocked = { + const mocked: ActionsClientMock = { create: jest.fn(), get: jest.fn(), delete: jest.fn(), @@ -19,6 +20,8 @@ const createActionsClientMock = () => { return mocked; }; -export const actionsClientMock = { +export const actionsClientMock: { + create: () => ActionsClientMock; +} = { create: createActionsClientMock, }; diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 955e1569380a5..c96c993fef606 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -44,6 +44,7 @@ beforeEach(() => { ), actionsConfigUtils: actionsConfigMock.create(), licenseState: mockedLicenseState, + preconfiguredActions: [], }; actionTypeRegistry = new ActionTypeRegistry(actionTypeRegistryParams); actionsClient = new ActionsClient({ @@ -221,6 +222,7 @@ describe('create()', () => { ), actionsConfigUtils: localConfigUtils, licenseState: licenseStateMock.create(), + preconfiguredActions: [], }; actionTypeRegistry = new ActionTypeRegistry(localActionTypeRegistryParams); @@ -348,9 +350,6 @@ describe('get()', () => { actionTypeId: '.slack', isPreconfigured: true, name: 'test', - config: { - foo: 'bar', - }, }); expect(savedObjectsClient.get).not.toHaveBeenCalled(); }); @@ -418,9 +417,6 @@ describe('getAll()', () => { actionTypeId: '.slack', isPreconfigured: true, name: 'test', - config: { - foo: 'bar', - }, referencedByCount: 2, }, ]); diff --git a/x-pack/plugins/actions/server/actions_client.ts b/x-pack/plugins/actions/server/actions_client.ts index 8f73bfb31ea4d..618bc8a85e856 100644 --- a/x-pack/plugins/actions/server/actions_client.ts +++ b/x-pack/plugins/actions/server/actions_client.ts @@ -135,7 +135,7 @@ export class ActionsClient { id, actionTypeId: result.attributes.actionTypeId as string, name: result.attributes.name as string, - config: result.attributes.config as Record, + config: result.attributes.config as Record, isPreconfigured: false, }; } @@ -152,7 +152,6 @@ export class ActionsClient { id, actionTypeId: preconfiguredActionsList.actionTypeId, name: preconfiguredActionsList.name, - config: preconfiguredActionsList.config, isPreconfigured: true, }; } @@ -184,7 +183,6 @@ export class ActionsClient { id: preconfiguredAction.id, actionTypeId: preconfiguredAction.actionTypeId, name: preconfiguredAction.name, - config: preconfiguredAction.config, isPreconfigured: true, })), ].sort((a, b) => a.name.localeCompare(b.name)); @@ -230,7 +228,7 @@ async function injectExtraFindData( scopedClusterClient: IScopedClusterClient, actionResults: ActionResult[] ): Promise { - const aggs: Record = {}; + const aggs: Record = {}; for (const actionResult of actionResults) { aggs[actionResult.id] = { filter: { diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts index 658f8f3fd8cf9..265ff267f222e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts @@ -30,7 +30,7 @@ const NO_OP_FN = () => {}; const services = { log: NO_OP_FN, - callCluster: async (path: string, opts: any) => {}, + callCluster: async (path: string, opts: unknown) => {}, savedObjectsClient: savedObjectsClientMock.create(), }; @@ -52,7 +52,7 @@ describe('actionTypeRegistry.get() works', () => { describe('config validation', () => { test('config validation succeeds when config is valid', () => { - const config: Record = { + const config: Record = { service: 'gmail', from: 'bob@example.com', }; @@ -74,7 +74,7 @@ describe('config validation', () => { }); test('config validation fails when config is not valid', () => { - const baseConfig: Record = { + const baseConfig: Record = { from: 'bob@example.com', }; @@ -177,7 +177,7 @@ describe('config validation', () => { describe('secrets validation', () => { test('secrets validation succeeds when secrets is valid', () => { - const secrets: Record = { + const secrets: Record = { user: 'bob', password: 'supersecret', }; @@ -185,7 +185,7 @@ describe('secrets validation', () => { }); test('secrets validation succeeds when secrets props are null/undefined', () => { - const secrets: Record = { + const secrets: Record = { user: null, password: null, }; @@ -197,7 +197,7 @@ describe('secrets validation', () => { describe('params validation', () => { test('params validation succeeds when params is valid', () => { - const params: Record = { + const params: Record = { to: ['bob@example.com'], subject: 'this is a test', message: 'this is the message', diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.ts b/x-pack/plugins/actions/server/builtin_action_types/email.ts index ca8d089ad2946..7ddb123a4d780 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.ts @@ -30,10 +30,10 @@ const ConfigSchema = schema.object(ConfigSchemaProps); function validateConfig( configurationUtilities: ActionsConfigurationUtilities, - configObject: any + configObject: unknown ): string | void { // avoids circular reference ... - const config: ActionTypeConfigType = configObject; + const config = configObject as ActionTypeConfigType; // Make sure service is set, or if not, both host/port must be set. // If service is set, host/port are ignored, when the email is sent. @@ -95,9 +95,9 @@ const ParamsSchema = schema.object( } ); -function validateParams(paramsObject: any): string | void { +function validateParams(paramsObject: unknown): string | void { // avoids circular reference ... - const params: ActionParamsType = paramsObject; + const params = paramsObject as ActionParamsType; const { to, cc, bcc } = params; const addrs = to.length + cc.length + bcc.length; 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 ec495aed7675a..ed57e44c3f0b3 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 @@ -43,7 +43,7 @@ describe('actionTypeRegistry.get() works', () => { describe('config validation', () => { test('config validation succeeds when config is valid', () => { - const config: Record = { + const config: Record = { index: 'testing-123', refresh: false, }; @@ -97,7 +97,7 @@ describe('config validation', () => { }); test('config validation fails when config is not valid', () => { - const baseConfig: Record = { + const baseConfig: Record = { indeX: 'bob', }; @@ -111,7 +111,7 @@ describe('config validation', () => { describe('params validation', () => { test('params validation succeeds when params is valid', () => { - const params: Record = { + const params: Record = { documents: [{ rando: 'thing' }], }; expect(validateParams(actionType, params)).toMatchInlineSnapshot(` 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 ff7b27b3f51fc..32f5e23015700 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 @@ -72,13 +72,12 @@ async function executor( bulkBody.push(document); } - const bulkParams: any = { + const bulkParams: unknown = { index, body: bulkBody, + refresh: config.refresh, }; - bulkParams.refresh = config.refresh; - let result; try { result = await services.callCluster('bulk', bulkParams); diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts index ac21905ede11c..9150633f06117 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts @@ -27,6 +27,7 @@ export function createActionTypeRegistry(): { ), actionsConfigUtils: actionsConfigMock.create(), licenseState: licenseStateMock.create(), + preconfiguredActions: [], }); registerBuiltInActionTypes({ logger, diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts index cc9d36ff86342..92f88ebe0be22 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/post_pagerduty.ts @@ -9,7 +9,7 @@ import { Services } from '../../types'; interface PostPagerdutyOptions { apiUrl: string; - data: any; + data: unknown; headers: Record; services: Services; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts index 42160dc2fc22b..a02f79a49e8e2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts @@ -63,12 +63,15 @@ describe('send_email module', () => { }); test('handles unauthenticated email using not secure host/port', async () => { - const sendEmailOptions = getSendEmailOptions(); + const sendEmailOptions = getSendEmailOptions({ + transport: { + host: 'example.com', + port: 1025, + }, + }); delete sendEmailOptions.transport.service; delete sendEmailOptions.transport.user; delete sendEmailOptions.transport.password; - sendEmailOptions.transport.host = 'example.com'; - sendEmailOptions.transport.port = 1025; const result = await sendEmail(mockLogger, sendEmailOptions); expect(result).toBe(sendMailMockResult); expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` @@ -105,13 +108,17 @@ describe('send_email module', () => { }); test('handles unauthenticated email using secure host/port', async () => { - const sendEmailOptions = getSendEmailOptions(); + const sendEmailOptions = getSendEmailOptions({ + transport: { + host: 'example.com', + port: 1025, + secure: true, + }, + }); delete sendEmailOptions.transport.service; delete sendEmailOptions.transport.user; delete sendEmailOptions.transport.password; - sendEmailOptions.transport.host = 'example.com'; - sendEmailOptions.transport.port = 1025; - sendEmailOptions.transport.secure = true; + const result = await sendEmail(mockLogger, sendEmailOptions); expect(result).toBe(sendMailMockResult); expect(createTransportMock.mock.calls[0]).toMatchInlineSnapshot(` @@ -154,19 +161,22 @@ describe('send_email module', () => { }); }); -function getSendEmailOptions(): any { +function getSendEmailOptions({ content = {}, routing = {}, transport = {} } = {}) { return { content: { + ...content, message: 'a message', subject: 'a subject', }, routing: { + ...routing, from: 'fred@example.com', to: ['jim@example.com'], cc: ['bob@example.com', 'robert@example.com'], bcc: [], }, transport: { + ...transport, service: 'whatever', user: 'elastic', password: 'changeme', diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts index ffbf7485a8b0b..869db34f034ae 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts @@ -43,13 +43,13 @@ export interface Content { } // send an email -export async function sendEmail(logger: Logger, options: SendEmailOptions): Promise { +export async function sendEmail(logger: Logger, options: SendEmailOptions): Promise { const { transport, routing, content } = options; const { service, host, port, secure, user, password } = transport; const { from, to, cc, bcc } = routing; const { subject, message } = content; - const transportConfig: Record = {}; + const transportConfig: Record = {}; if (user != null && password != null) { transportConfig.auth = { diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts index e5521558bc2da..e54c8179ae7b4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.test.ts @@ -22,7 +22,7 @@ const postPagerdutyMock = postPagerduty as jest.Mock; const ACTION_TYPE_ID = '.pagerduty'; const services: Services = { - callCluster: async (path: string, opts: any) => {}, + callCluster: async (path: string, opts: unknown) => {}, savedObjectsClient: savedObjectsClientMock.create(), }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts index f4d69a4a39e40..0c8802060164d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts @@ -68,9 +68,8 @@ const ParamsSchema = schema.object( { validate: validateParams } ); -function validateParams(paramsObject: any): string | void { - const params: ActionParamsType = paramsObject; - const { timestamp } = params; +function validateParams(paramsObject: unknown): string | void { + const { timestamp } = paramsObject as ActionParamsType; if (timestamp != null) { try { const date = Date.parse(timestamp); @@ -218,11 +217,23 @@ async function executor( const AcknowledgeOrResolve = new Set([EVENT_ACTION_ACKNOWLEDGE, EVENT_ACTION_RESOLVE]); -function getBodyForEventAction(actionId: string, params: ActionParamsType): any { +function getBodyForEventAction(actionId: string, params: ActionParamsType): unknown { const eventAction = params.eventAction || EVENT_ACTION_TRIGGER; const dedupKey = params.dedupKey || `action:${actionId}`; - const data: any = { + const data: { + event_action: ActionParamsType['eventAction']; + dedup_key: string; + payload?: { + summary: string; + source: string; + severity: string; + timestamp?: string; + component?: string; + group?: string; + class?: string; + }; + } = { event_action: eventAction, dedup_key: dedupKey, }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts b/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts index bb806f8ae36fc..3ce01c59596f6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/server_log.test.ts @@ -91,7 +91,7 @@ describe('execute()', () => { await actionType.executor({ actionId, services: { - callCluster: async (path: string, opts: any) => {}, + callCluster: async (path: string, opts: unknown) => {}, savedObjectsClient: savedObjectsClientMock.create(), }, params: { message: 'message text here', level: 'info' }, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts index fb296089e9ec5..9166f53cf757e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts @@ -57,14 +57,14 @@ export const handleCreateIncident = async ({ comments && Array.isArray(comments) && comments.length > 0 && - mapping.get('comments').actionType !== 'nothing' + mapping.get('comments')?.actionType !== 'nothing' ) { comments = transformComments(comments, params, ['informationAdded']); res.comments = [ ...(await createComments( serviceNow, res.incidentId, - mapping.get('comments').target, + mapping.get('comments')!.target, comments )), ]; @@ -103,11 +103,11 @@ export const handleUpdateIncident = async ({ comments && Array.isArray(comments) && comments.length > 0 && - mapping.get('comments').actionType !== 'nothing' + mapping.get('comments')?.actionType !== 'nothing' ) { comments = transformComments(comments, params, ['informationAdded']); res.comments = [ - ...(await createComments(serviceNow, incidentId, mapping.get('comments').target, comments)), + ...(await createComments(serviceNow, incidentId, mapping.get('comments')!.target, comments)), ]; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts index 750fda93b60d6..0a26996ea8d69 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/helpers.ts @@ -39,7 +39,7 @@ export const buildMap = (mapping: MapEntry[]): Mapping => { }, new Map()); }; -export const mapParams = (params: any, mapping: Mapping) => { +export const mapParams = (params: Record, mapping: Mapping) => { return Object.keys(params).reduce((prev: KeyAny, curr: string): KeyAny => { const field = mapping.get(curr); if (field) { @@ -61,11 +61,11 @@ export const prepareFieldsForTransformation = ({ defaultPipes = ['informationCreated'], }: PrepareFieldsForTransformArgs): PipedField[] => { return Object.keys(params.incident) - .filter(p => mapping.get(p).actionType !== 'nothing') + .filter(p => mapping.get(p)!.actionType !== 'nothing') .map(p => ({ key: p, - value: params.incident[p], - actionType: mapping.get(p).actionType, + value: params.incident[p] as string, + actionType: mapping.get(p)!.actionType, pipes: [...defaultPipes], })) .map(p => ({ diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts index 1a23354e6490d..08b837cf8d0a5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts @@ -22,7 +22,7 @@ jest.mock('./action_handlers'); const handleIncidentMock = handleIncident as jest.Mock; const services: Services = { - callCluster: async (path: string, opts: any) => {}, + callCluster: async (path: string, opts: unknown) => {}, savedObjectsClient: savedObjectsClientMock.create(), }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index a63c2fd3a6ceb..5066190d4fe56 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -85,7 +85,7 @@ async function serviceNowExecutor( const { comments, incidentId, ...restParams } = params; const mapping = buildMap(configurationMapping); - const incident = mapParams(restParams, mapping); + const incident = mapParams((restParams as unknown) as Record, mapping); const serviceNow = new ServiceNow({ url: apiUrl, username, password }); const handlerInput = { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts index cc07a0b90330d..ed9cfe67a19a1 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts @@ -49,14 +49,14 @@ class ServiceNow { }: { url: string; method?: Method; - data?: any; + data?: unknown; }): Promise { const res = await this.axios(url, { method, data }); this._throwIfNotAlive(res.status, res.headers['content-type']); return res; } - private _patch({ url, data }: { url: string; data: any }): Promise { + private _patch({ url, data }: { url: string; data: unknown }): Promise { return this._request({ url, method: 'patch', diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index 71b05be8f3e4d..c5ef282aeffa7 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -30,10 +30,10 @@ export type CasesConfigurationType = TypeOf; export type MapEntry = TypeOf; export type Comment = TypeOf; -export type Mapping = Map; +export type Mapping = Map>; export interface Params extends ExecutorParams { - incident: Record; + incident: Record; } export interface CreateHandlerArguments { serviceNow: ServiceNow; @@ -66,7 +66,7 @@ export interface AppendFieldArgs { } export interface KeyAny { - [index: string]: string; + [index: string]: unknown; } export interface AppendInformationFieldArgs { diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts index 49b0b84e9dbb5..22c4b63474fdc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ActionType, Services, ActionTypeExecutorOptions } from '../types'; +import { + ActionType, + Services, + ActionTypeExecutorOptions, + ActionTypeExecutorResult, +} from '../types'; import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import { validateParams, validateSecrets } from '../lib'; import { getActionType } from './slack'; @@ -13,7 +18,7 @@ import { actionsConfigMock } from '../actions_config.mock'; const ACTION_TYPE_ID = '.slack'; const services: Services = { - callCluster: async (path: string, opts: any) => {}, + callCluster: async (path: string, opts: unknown) => {}, savedObjectsClient: savedObjectsClientMock.create(), }; @@ -21,7 +26,7 @@ let actionType: ActionType; beforeAll(() => { actionType = getActionType({ - async executor(options: ActionTypeExecutorOptions): Promise {}, + async executor() {}, configurationUtilities: actionsConfigMock.create(), }); }); @@ -117,7 +122,7 @@ describe('validateActionTypeSecrets()', () => { describe('execute()', () => { beforeAll(() => { - async function mockSlackExecutor(options: ActionTypeExecutorOptions): Promise { + async function mockSlackExecutor(options: ActionTypeExecutorOptions) { const { params } = options; const { message } = params; if (message == null) throw new Error('message property required in parameter'); @@ -130,7 +135,9 @@ describe('execute()', () => { return { text: `slack mockExecutor success: ${message}`, - }; + actionId: '', + status: 'ok', + } as ActionTypeExecutorResult; } actionType = getActionType({ @@ -148,10 +155,12 @@ describe('execute()', () => { params: { message: 'this invocation should succeed' }, }); expect(response).toMatchInlineSnapshot(` -Object { - "text": "slack mockExecutor success: this invocation should succeed", -} -`); + Object { + "actionId": "", + "status": "ok", + "text": "slack mockExecutor success: this invocation should succeed", + } + `); }); test('calls the mock executor with failure', async () => { diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.ts index e51ef3f67bd65..edf3e485f3c57 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.ts @@ -156,7 +156,7 @@ async function slackExecutor( return successResult(actionId, result); } -function successResult(actionId: string, data: any): ActionTypeExecutorResult { +function successResult(actionId: string, data: unknown): ActionTypeExecutorResult { return { status: 'ok', data, actionId }; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts index 03658b3b1dd85..1beaf92f3f48b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts @@ -22,7 +22,7 @@ const axiosRequestMock = axios.request as jest.Mock; const ACTION_TYPE_ID = '.webhook'; const services: Services = { - callCluster: async (path: string, opts: any) => {}, + callCluster: async (path: string, opts: unknown) => {}, savedObjectsClient: savedObjectsClientMock.create(), }; @@ -44,7 +44,7 @@ describe('actionType', () => { describe('secrets validation', () => { test('succeeds when secrets is valid', () => { - const secrets: Record = { + const secrets: Record = { user: 'bob', password: 'supersecret', }; @@ -60,20 +60,18 @@ describe('secrets validation', () => { }); test('succeeds when basic authentication credentials are omitted', () => { - expect(() => { - validateSecrets(actionType, {}).toEqual({}); - }); + expect(validateSecrets(actionType, {})).toEqual({ password: null, user: null }); }); }); describe('config validation', () => { - const defaultValues: Record = { + const defaultValues: Record = { headers: null, method: 'post', }; test('config validation passes when only required fields are provided', () => { - const config: Record = { + const config: Record = { url: 'http://mylisteningserver:9200/endpoint', }; expect(validateConfig(actionType, config)).toEqual({ @@ -84,7 +82,7 @@ describe('config validation', () => { test('config validation passes when valid methods are provided', () => { ['post', 'put'].forEach(method => { - const config: Record = { + const config: Record = { url: 'http://mylisteningserver:9200/endpoint', method, }; @@ -96,7 +94,7 @@ describe('config validation', () => { }); test('should validate and throw error when method on config is invalid', () => { - const config: Record = { + const config: Record = { url: 'http://mylisteningserver:9200/endpoint', method: 'https', }; @@ -110,7 +108,7 @@ describe('config validation', () => { }); test('config validation passes when a url is specified', () => { - const config: Record = { + const config: Record = { url: 'http://mylisteningserver:9200/endpoint', }; expect(validateConfig(actionType, config)).toEqual({ @@ -120,6 +118,8 @@ describe('config validation', () => { }); test('config validation passes when valid headers are provided', () => { + // any for testing + // eslint-disable-next-line @typescript-eslint/no-explicit-any const config: Record = { url: 'http://mylisteningserver:9200/endpoint', headers: { @@ -133,7 +133,7 @@ describe('config validation', () => { }); test('should validate and throw error when headers on config is invalid', () => { - const config: Record = { + const config: Record = { url: 'http://mylisteningserver:9200/endpoint', headers: 'application/json', }; @@ -147,6 +147,8 @@ describe('config validation', () => { }); test('config validation passes when kibana config whitelists the url', () => { + // any for testing + // eslint-disable-next-line @typescript-eslint/no-explicit-any const config: Record = { url: 'http://mylisteningserver.com:9200/endpoint', headers: { @@ -171,6 +173,8 @@ describe('config validation', () => { }, }); + // any for testing + // eslint-disable-next-line @typescript-eslint/no-explicit-any const config: Record = { url: 'http://mylisteningserver.com:9200/endpoint', headers: { @@ -188,12 +192,12 @@ describe('config validation', () => { describe('params validation', () => { test('param validation passes when no fields are provided as none are required', () => { - const params: Record = {}; + const params: Record = {}; expect(validateParams(actionType, params)).toEqual({}); }); test('params validation passes when a valid body is provided', () => { - const params: Record = { + const params: Record = { body: 'count: {{ctx.payload.hits.total}}', }; expect(validateParams(actionType, params)).toEqual({ diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts index 6173edc2df15a..0191ff1d9dbc1 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.ts @@ -160,7 +160,7 @@ export async function executor( } // Action Executor Result w/ internationalisation -function successResult(actionId: string, data: any): ActionTypeExecutorResult { +function successResult(actionId: string, data: unknown): ActionTypeExecutorResult { return { status: 'ok', data, actionId }; } diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts index 51e87dbd75b48..161a6c31d4e59 100644 --- a/x-pack/plugins/actions/server/config.test.ts +++ b/x-pack/plugins/actions/server/config.test.ts @@ -7,7 +7,7 @@ import { configSchema } from './config'; describe('config validation', () => { test('action defaults', () => { - const config: Record = {}; + const config: Record = {}; expect(configSchema.validate(config)).toMatchInlineSnapshot(` Object { "enabled": true, @@ -23,7 +23,7 @@ describe('config validation', () => { }); test('action with preconfigured actions', () => { - const config: Record = { + const config: Record = { preconfigured: [ { id: 'my-slack1', diff --git a/x-pack/plugins/actions/server/constants/plugin.ts b/x-pack/plugins/actions/server/constants/plugin.ts index 68082ccaa1399..7d20eb6990247 100644 --- a/x-pack/plugins/actions/server/constants/plugin.ts +++ b/x-pack/plugins/actions/server/constants/plugin.ts @@ -9,6 +9,7 @@ import { LICENSE_TYPE_BASIC, LicenseType } from '../../../../legacy/common/const export const PLUGIN = { ID: 'actions', MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_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', { defaultMessage: 'Actions', diff --git a/x-pack/plugins/actions/server/create_execute_function.test.ts b/x-pack/plugins/actions/server/create_execute_function.test.ts index 6bdd30848e4b7..1b7752588e3d3 100644 --- a/x-pack/plugins/actions/server/create_execute_function.test.ts +++ b/x-pack/plugins/actions/server/create_execute_function.test.ts @@ -282,4 +282,65 @@ describe('execute()', () => { }) ).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`); }); + + test('should skip ensure action type if action type is preconfigured and license is valid', async () => { + const mockedActionTypeRegistry = actionTypeRegistryMock.create(); + const getScopedSavedObjectsClient = jest.fn().mockReturnValueOnce(savedObjectsClient); + const executeFn = createExecuteFunction({ + getBasePath, + taskManager: mockTaskManager, + getScopedSavedObjectsClient, + isESOUsingEphemeralEncryptionKey: false, + actionTypeRegistry: mockedActionTypeRegistry, + preconfiguredActions: [ + { + actionTypeId: 'mock-action', + config: {}, + id: 'my-slack1', + name: 'Slack #xyz', + secrets: {}, + isPreconfigured: true, + }, + ], + }); + mockedActionTypeRegistry.ensureActionTypeEnabled.mockImplementation(() => { + throw new Error('Fail'); + }); + mockedActionTypeRegistry.isActionExecutable.mockImplementation(() => true); + savedObjectsClient.get.mockResolvedValueOnce({ + id: '123', + type: 'action', + attributes: { + actionTypeId: 'mock-action', + }, + references: [], + }); + savedObjectsClient.create.mockResolvedValueOnce({ + id: '234', + type: 'action_task_params', + attributes: {}, + references: [], + }); + + await executeFn({ + id: '123', + params: { baz: false }, + spaceId: 'default', + apiKey: null, + }); + expect(getScopedSavedObjectsClient).toHaveBeenCalledWith({ + getBasePath: expect.anything(), + headers: {}, + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + }); + }); }); diff --git a/x-pack/plugins/actions/server/create_execute_function.ts b/x-pack/plugins/actions/server/create_execute_function.ts index 4a9ddf412b7cc..db38431b02cac 100644 --- a/x-pack/plugins/actions/server/create_execute_function.ts +++ b/x-pack/plugins/actions/server/create_execute_function.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from '../../../../src/core/server'; +import { SavedObjectsClientContract, KibanaRequest } from '../../../../src/core/server'; import { TaskManagerStartContract } from '../../task_manager/server'; import { GetBasePathFunction, @@ -15,7 +15,7 @@ import { interface CreateExecuteFunctionOptions { taskManager: TaskManagerStartContract; - getScopedSavedObjectsClient: (request: any) => SavedObjectsClientContract; + getScopedSavedObjectsClient: (request: KibanaRequest) => SavedObjectsClientContract; getBasePath: GetBasePathFunction; isESOUsingEphemeralEncryptionKey: boolean; actionTypeRegistry: ActionTypeRegistryContract; @@ -24,7 +24,7 @@ interface CreateExecuteFunctionOptions { export interface ExecuteOptions { id: string; - params: Record; + params: Record; spaceId: string; apiKey: string | null; } @@ -52,7 +52,7 @@ export function createExecuteFunction({ // Since we're using API keys and accessing elasticsearch can only be done // via a request, we're faking one with the proper authorization headers. - const fakeRequest: any = { + const fakeRequest: unknown = { headers: requestHeaders, getBasePath: () => getBasePath(spaceId), path: '/', @@ -67,10 +67,12 @@ export function createExecuteFunction({ }, }; - const savedObjectsClient = getScopedSavedObjectsClient(fakeRequest); + const savedObjectsClient = getScopedSavedObjectsClient(fakeRequest as KibanaRequest); const actionTypeId = await getActionTypeId(id); - actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); + if (!actionTypeRegistry.isActionExecutable(id, actionTypeId)) { + actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); + } const actionTaskParamsRecord = await savedObjectsClient.create('action_task_params', { actionId: id, diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index 124e5951c714b..d6719dc08225e 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -224,6 +224,50 @@ test('throws an error if actionType is not enabled', async () => { expect(actionTypeRegistry.ensureActionTypeEnabled).toHaveBeenCalledWith('test'); }); +test('should not throws an error if actionType is preconfigured', async () => { + const actionType: jest.Mocked = { + id: 'test', + name: 'Test', + minimumLicenseRequired: 'basic', + executor: jest.fn(), + }; + const actionSavedObject = { + id: '1', + type: 'action', + attributes: { + actionTypeId: 'test', + config: { + bar: true, + }, + secrets: { + baz: true, + }, + }, + references: [], + }; + savedObjectsClient.get.mockResolvedValueOnce(actionSavedObject); + encryptedSavedObjectsPlugin.getDecryptedAsInternalUser.mockResolvedValueOnce(actionSavedObject); + actionTypeRegistry.get.mockReturnValueOnce(actionType); + actionTypeRegistry.ensureActionTypeEnabled.mockImplementationOnce(() => { + throw new Error('not enabled for test'); + }); + actionTypeRegistry.isActionExecutable.mockImplementationOnce(() => true); + await actionExecutor.execute(executeParams); + + expect(actionTypeRegistry.ensureActionTypeEnabled).toHaveBeenCalledTimes(0); + expect(actionType.executor).toHaveBeenCalledWith({ + actionId: '1', + services: expect.anything(), + config: { + bar: true, + }, + secrets: { + baz: true, + }, + params: { foo: true }, + }); +}); + test('throws an error when passing isESOUsingEphemeralEncryptionKey with value of true', async () => { const customActionExecutor = new ActionExecutor({ isESOUsingEphemeralEncryptionKey: true }); customActionExecutor.initialize({ diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index a33fb8830a930..59f8f3747273f 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -32,7 +32,7 @@ export interface ActionExecutorContext { export interface ExecuteOptions { actionId: string; request: KibanaRequest; - params: Record; + params: Record; } export type ActionExecutorContract = PublicMethodsOf; @@ -90,12 +90,14 @@ export class ActionExecutor { namespace.namespace ); - actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); + if (!actionTypeRegistry.isActionExecutable(actionId, actionTypeId)) { + actionTypeRegistry.ensureActionTypeEnabled(actionTypeId); + } const actionType = actionTypeRegistry.get(actionTypeId); - let validatedParams: Record; - let validatedConfig: Record; - let validatedSecrets: Record; + let validatedParams: Record; + let validatedConfig: Record; + let validatedSecrets: Record; try { validatedParams = validateParams(actionType, params); @@ -174,8 +176,8 @@ function actionErrorToMessage(result: ActionTypeExecutorResult): string { interface ActionInfo { actionTypeId: string; name: string; - config: any; - secrets: any; + config: unknown; + secrets: unknown; } async function getActionInfo( diff --git a/x-pack/plugins/actions/server/lib/executor_error.ts b/x-pack/plugins/actions/server/lib/executor_error.ts index 56ccbf14e6a45..c25033b2a27a2 100644 --- a/x-pack/plugins/actions/server/lib/executor_error.ts +++ b/x-pack/plugins/actions/server/lib/executor_error.ts @@ -5,9 +5,9 @@ */ export class ExecutorError extends Error { - readonly data?: any; + readonly data?: unknown; readonly retry: boolean | Date; - constructor(message?: string, data?: any, retry: boolean | Date = false) { + constructor(message?: string, data?: unknown, retry: boolean | Date = false) { super(message); this.data = data; this.retry = retry; diff --git a/x-pack/plugins/actions/server/lib/license_state.test.ts b/x-pack/plugins/actions/server/lib/license_state.test.ts index eb10e69a444e8..0a474ec3ae3ea 100644 --- a/x-pack/plugins/actions/server/lib/license_state.test.ts +++ b/x-pack/plugins/actions/server/lib/license_state.test.ts @@ -5,16 +5,16 @@ */ import { ActionType } from '../types'; -import { BehaviorSubject } from 'rxjs'; +import { Subject } from 'rxjs'; import { LicenseState, ILicenseState } from './license_state'; import { licensingMock } from '../../../licensing/server/mocks'; import { ILicense } from '../../../licensing/server'; describe('checkLicense()', () => { - let getRawLicense: any; + const getRawLicense = jest.fn(); beforeEach(() => { - getRawLicense = jest.fn(); + jest.resetAllMocks(); }); describe('status is LICENSE_STATUS_INVALID', () => { @@ -53,7 +53,7 @@ describe('checkLicense()', () => { }); describe('isLicenseValidForActionType', () => { - let license: BehaviorSubject; + let license: Subject; let licenseState: ILicenseState; const fooActionType: ActionType = { id: 'foo', @@ -63,7 +63,7 @@ describe('isLicenseValidForActionType', () => { }; beforeEach(() => { - license = new BehaviorSubject(null as any); + license = new Subject(); licenseState = new LicenseState(license); }); @@ -75,7 +75,7 @@ describe('isLicenseValidForActionType', () => { }); test('should return false when license not available', () => { - license.next({ isAvailable: false } as any); + license.next(createUnavailableLicense()); expect(licenseState.isLicenseValidForActionType(fooActionType)).toEqual({ isValid: false, reason: 'unavailable', @@ -114,7 +114,7 @@ describe('isLicenseValidForActionType', () => { }); describe('ensureLicenseForActionType()', () => { - let license: BehaviorSubject; + let license: Subject; let licenseState: ILicenseState; const fooActionType: ActionType = { id: 'foo', @@ -124,7 +124,7 @@ describe('ensureLicenseForActionType()', () => { }; beforeEach(() => { - license = new BehaviorSubject(null as any); + license = new Subject(); licenseState = new LicenseState(license); }); @@ -137,7 +137,7 @@ describe('ensureLicenseForActionType()', () => { }); test('should throw when license not available', () => { - license.next({ isAvailable: false } as any); + license.next(createUnavailableLicense()); expect(() => licenseState.ensureLicenseForActionType(fooActionType) ).toThrowErrorMatchingInlineSnapshot( @@ -175,3 +175,9 @@ describe('ensureLicenseForActionType()', () => { licenseState.ensureLicenseForActionType(fooActionType); }); }); + +function createUnavailableLicense() { + const unavailableLicense = licensingMock.createLicenseMock(); + unavailableLicense.isAvailable = false; + return unavailableLicense; +} diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts index e2a6128aea203..08c5b90edbcb7 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts @@ -6,7 +6,7 @@ import { ActionExecutorContract } from './action_executor'; import { ExecutorError } from './executor_error'; -import { Logger, CoreStart } from '../../../../../src/core/server'; +import { Logger, CoreStart, KibanaRequest } from '../../../../../src/core/server'; import { RunContext } from '../../../task_manager/server'; import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server'; import { ActionTypeDisabledError } from './errors'; @@ -60,7 +60,7 @@ export class TaskRunnerFactory { return { async run() { - const { spaceId, actionTaskParamsId } = taskInstance.params; + const { spaceId, actionTaskParamsId } = taskInstance.params as Record; const namespace = spaceIdToNamespace(spaceId); const { @@ -78,7 +78,7 @@ export class TaskRunnerFactory { // Since we're using API keys and accessing elasticsearch can only be done // via a request, we're faking one with the proper authorization headers. - const fakeRequest: any = { + const fakeRequest = ({ headers: requestHeaders, getBasePath: () => getBasePath(spaceId), path: '/', @@ -91,7 +91,7 @@ export class TaskRunnerFactory { url: '/', }, }, - }; + } as unknown) as KibanaRequest; let executorResult: ActionTypeExecutorResult; try { diff --git a/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts b/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts index b7d408985ed9f..1ccd25664374d 100644 --- a/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts +++ b/x-pack/plugins/actions/server/lib/validate_with_schema.test.ts @@ -49,7 +49,7 @@ test('should validate when there are no individual validators', () => { }); test('should validate when validators return incoming value', () => { - const selfValidator = { validate: (value: any) => value }; + const selfValidator = { validate: (value: Record) => value }; const actionType: ActionType = { id: 'foo', name: 'bar', @@ -76,8 +76,8 @@ test('should validate when validators return incoming value', () => { }); test('should validate when validators return different values', () => { - const returnedValue: any = { something: { shaped: 'differently' } }; - const selfValidator = { validate: (value: any) => returnedValue }; + const returnedValue = { something: { shaped: 'differently' } }; + const selfValidator = { validate: () => returnedValue }; const actionType: ActionType = { id: 'foo', name: 'bar', @@ -105,7 +105,7 @@ test('should validate when validators return different values', () => { test('should throw with expected error when validators fail', () => { const erroringValidator = { - validate: (value: any) => { + validate: () => { throw new Error('test error'); }, }; diff --git a/x-pack/plugins/actions/server/lib/validate_with_schema.ts b/x-pack/plugins/actions/server/lib/validate_with_schema.ts index 2306a726229e3..021c460f4c815 100644 --- a/x-pack/plugins/actions/server/lib/validate_with_schema.ts +++ b/x-pack/plugins/actions/server/lib/validate_with_schema.ts @@ -7,15 +7,15 @@ import Boom from 'boom'; import { ActionType } from '../types'; -export function validateParams(actionType: ActionType, value: any) { +export function validateParams(actionType: ActionType, value: unknown) { return validateWithSchema(actionType, 'params', value); } -export function validateConfig(actionType: ActionType, value: any) { +export function validateConfig(actionType: ActionType, value: unknown) { return validateWithSchema(actionType, 'config', value); } -export function validateSecrets(actionType: ActionType, value: any) { +export function validateSecrets(actionType: ActionType, value: unknown) { return validateWithSchema(actionType, 'secrets', value); } @@ -24,33 +24,40 @@ type ValidKeys = 'params' | 'config' | 'secrets'; function validateWithSchema( actionType: ActionType, key: ValidKeys, - value: any -): Record { - if (actionType.validate == null) return value; - - let name; - try { - switch (key) { - case 'params': - name = 'action params'; - if (actionType.validate.params == null) return value; - return actionType.validate.params.validate(value); - - case 'config': - name = 'action type config'; - if (actionType.validate.config == null) return value; - return actionType.validate.config.validate(value); - - case 'secrets': - name = 'action type secrets'; - if (actionType.validate.secrets == null) return value; - return actionType.validate.secrets.validate(value); + value: unknown +): Record { + if (actionType.validate) { + let name; + try { + switch (key) { + case 'params': + name = 'action params'; + if (actionType.validate.params) { + return actionType.validate.params.validate(value); + } + break; + case 'config': + name = 'action type config'; + if (actionType.validate.config) { + return actionType.validate.config.validate(value); + } + + break; + case 'secrets': + name = 'action type secrets'; + if (actionType.validate.secrets) { + return actionType.validate.secrets.validate(value); + } + break; + default: + // should never happen, but left here for future-proofing + throw new Error(`invalid actionType validate key: ${key}`); + } + } catch (err) { + // we can't really i18n this yet, since the err.message isn't i18n'd itself + throw Boom.badRequest(`error validating ${name}: ${err.message}`); } - } catch (err) { - // we can't really i18n this yet, since the err.message isn't i18n'd itself - throw Boom.badRequest(`error validating ${name}: ${err.message}`); } - // should never happen, but left here for future-proofing - throw new Error(`invalid actionType validate key: ${key}`); + return value as Record; } diff --git a/x-pack/plugins/actions/server/mocks.ts b/x-pack/plugins/actions/server/mocks.ts index bc4268bb69872..95c8b094dfd70 100644 --- a/x-pack/plugins/actions/server/mocks.ts +++ b/x-pack/plugins/actions/server/mocks.ts @@ -20,6 +20,7 @@ const createStartMock = () => { const mock: jest.Mocked = { execute: jest.fn(), isActionTypeEnabled: jest.fn(), + isActionExecutable: jest.fn(), getActionsClientWithRequest: jest.fn().mockResolvedValue(actionsClientMock.create()), preconfiguredActions: [], }; diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index fa5b2f9399a4d..2b334953063d1 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from '../../../../src/core/server'; +import { PluginInitializerContext, RequestHandlerContext } from '../../../../src/core/server'; import { coreMock, httpServerMock } from '../../../../src/core/server/mocks'; import { licensingMock } from '../../licensing/server/mocks'; import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks'; @@ -72,7 +72,9 @@ describe('Actions Plugin', () => { }); it('should log warning when Encrypted Saved Objects plugin is using an ephemeral encryption key', async () => { - await plugin.setup(coreSetup, pluginsSetup); + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await plugin.setup(coreSetup as any, pluginsSetup); expect(pluginsSetup.encryptedSavedObjects.usingEphemeralEncryptionKey).toEqual(true); expect(context.logger.get().warn).toHaveBeenCalledWith( 'APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.' @@ -81,7 +83,9 @@ describe('Actions Plugin', () => { describe('routeHandlerContext.getActionsClient()', () => { it('should not throw error when ESO plugin not using a generated key', async () => { - await plugin.setup(coreSetup, { + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await plugin.setup(coreSetup as any, { ...pluginsSetup, encryptedSavedObjects: { ...pluginsSetup.encryptedSavedObjects, @@ -93,8 +97,8 @@ describe('Actions Plugin', () => { const handler = coreSetup.http.registerRouteHandlerContext.mock.calls[0]; expect(handler[0]).toEqual('actions'); - const actionsContextHandler = (await handler[1]( - { + const actionsContextHandler = ((await handler[1]( + ({ core: { savedObjects: { client: {}, @@ -103,32 +107,34 @@ describe('Actions Plugin', () => { adminClient: jest.fn(), }, }, - } as any, + } as unknown) as RequestHandlerContext, httpServerMock.createKibanaRequest(), httpServerMock.createResponseFactory() - )) as any; - actionsContextHandler.getActionsClient(); + )) as unknown) as RequestHandlerContext['actions']; + actionsContextHandler!.getActionsClient(); }); it('should throw error when ESO plugin using a generated key', async () => { - await plugin.setup(coreSetup, pluginsSetup); + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await plugin.setup(coreSetup as any, pluginsSetup); expect(coreSetup.http.registerRouteHandlerContext).toHaveBeenCalledTimes(1); const handler = coreSetup.http.registerRouteHandlerContext.mock.calls[0]; expect(handler[0]).toEqual('actions'); - const actionsContextHandler = (await handler[1]( - { + const actionsContextHandler = ((await handler[1]( + ({ core: { savedObjects: { client: {}, }, }, - } as any, + } as unknown) as RequestHandlerContext, httpServerMock.createKibanaRequest(), httpServerMock.createResponseFactory() - )) as any; - expect(() => actionsContextHandler.getActionsClient()).toThrowErrorMatchingInlineSnapshot( + )) as unknown) as RequestHandlerContext['actions']; + expect(() => actionsContextHandler!.getActionsClient()).toThrowErrorMatchingInlineSnapshot( `"Unable to create actions client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml"` ); }); @@ -144,13 +150,17 @@ describe('Actions Plugin', () => { }; beforeEach(async () => { - setup = await plugin.setup(coreSetup, pluginsSetup); + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + setup = await plugin.setup(coreSetup as any, pluginsSetup); }); it('should throw error when license type is invalid', async () => { expect(() => setup.registerType({ ...sampleActionType, + // we're faking an invalid value, this requires stripping the typing + // eslint-disable-next-line @typescript-eslint/no-explicit-any minimumLicenseRequired: 'foo' as any, }) ).toThrowErrorMatchingInlineSnapshot(`"\\"foo\\" is not a valid license type"`); @@ -211,7 +221,9 @@ describe('Actions Plugin', () => { describe('getActionsClientWithRequest()', () => { it('should not throw error when ESO plugin not using a generated key', async () => { - await plugin.setup(coreSetup, { + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await plugin.setup(coreSetup as any, { ...pluginsSetup, encryptedSavedObjects: { ...pluginsSetup.encryptedSavedObjects, @@ -224,7 +236,9 @@ describe('Actions Plugin', () => { }); it('should throw error when ESO plugin using generated key', async () => { - await plugin.setup(coreSetup, pluginsSetup); + // coreMock.createSetup doesn't support Plugin generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await plugin.setup(coreSetup as any, pluginsSetup); const pluginStart = plugin.start(coreStart, pluginsStart); expect(pluginsSetup.encryptedSavedObjects.usingEphemeralEncryptionKey).toEqual(true); diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index a8ab3bbb2fad2..4c2c8d214f976 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -65,6 +65,7 @@ export interface PluginSetupContract { export interface PluginStartContract { isActionTypeEnabled(id: string): boolean; + isActionExecutable(actionId: string, actionTypeId: string): boolean; execute(options: ExecuteOptions): Promise; getActionsClientWithRequest(request: KibanaRequest): Promise>; preconfiguredActions: PreConfiguredAction[]; @@ -117,7 +118,10 @@ export class ActionsPlugin implements Plugin, Plugi this.preconfiguredActions = []; } - public async setup(core: CoreSetup, plugins: ActionsPluginsSetup): Promise { + public async setup( + core: CoreSetup, + plugins: ActionsPluginsSetup + ): Promise { this.licenseState = new LicenseState(plugins.licensing.license$); this.isESOUsingEphemeralEncryptionKey = plugins.encryptedSavedObjects.usingEphemeralEncryptionKey; @@ -167,6 +171,7 @@ export class ActionsPlugin implements Plugin, Plugi taskManager: plugins.taskManager, actionsConfigUtils, licenseState: this.licenseState, + preconfiguredActions: this.preconfiguredActions, }); this.taskRunnerFactory = taskRunnerFactory; this.actionTypeRegistry = actionTypeRegistry; @@ -182,7 +187,7 @@ export class ActionsPlugin implements Plugin, Plugi const usageCollection = plugins.usageCollection; if (usageCollection) { - core.getStartServices().then(async ([, startPlugins]: [CoreStart, any, any]) => { + core.getStartServices().then(async ([, startPlugins]) => { registerActionsUsageCollector(usageCollection, startPlugins.taskManager); initializeActionsTelemetry( @@ -268,6 +273,9 @@ export class ActionsPlugin implements Plugin, Plugi isActionTypeEnabled: id => { return this.actionTypeRegistry!.isActionTypeEnabled(id); }, + isActionExecutable: (actionId: string, actionTypeId: string) => { + return this.actionTypeRegistry!.isActionExecutable(actionId, actionTypeId); + }, // Ability to get an actions client from legacy code async getActionsClientWithRequest(request: KibanaRequest) { if (isESOUsingEphemeralEncryptionKey === true) { @@ -299,7 +307,7 @@ export class ActionsPlugin implements Plugin, Plugi private createRouteHandlerContext = ( defaultKibanaIndex: string - ): IContextProvider, 'actions'> => { + ): IContextProvider, 'actions'> => { const { actionTypeRegistry, isESOUsingEphemeralEncryptionKey, preconfiguredActions } = this; return async function actionsRouteHandlerContext(context, request) { diff --git a/x-pack/plugins/actions/server/routes/_mock_handler_arguments.ts b/x-pack/plugins/actions/server/routes/_mock_handler_arguments.ts index ec525adb8eab6..06f65ff23106c 100644 --- a/x-pack/plugins/actions/server/routes/_mock_handler_arguments.ts +++ b/x-pack/plugins/actions/server/routes/_mock_handler_arguments.ts @@ -7,12 +7,17 @@ import { RequestHandlerContext, KibanaRequest, KibanaResponseFactory } from 'kibana/server'; import { identity } from 'lodash'; import { httpServerMock } from '../../../../../src/core/server/mocks'; +import { ActionType } from '../../common'; +import { ActionsClientMock, actionsClientMock } from '../actions_client.mock'; export function mockHandlerArguments( - { actionsClient, listTypes: listTypesRes = [] }: any, - req: any, + { + actionsClient = actionsClientMock.create(), + listTypes: listTypesRes = [], + }: { actionsClient?: ActionsClientMock; listTypes?: ActionType[] }, + req: unknown, res?: Array> -): [RequestHandlerContext, KibanaRequest, KibanaResponseFactory] { +): [RequestHandlerContext, KibanaRequest, KibanaResponseFactory] { const listTypes = jest.fn(() => listTypesRes); return [ ({ @@ -31,7 +36,7 @@ export function mockHandlerArguments( }, }, } as unknown) as RequestHandlerContext, - req as KibanaRequest, + req as KibanaRequest, mockResponseFactory(res), ]; } diff --git a/x-pack/plugins/actions/server/routes/create.test.ts b/x-pack/plugins/actions/server/routes/create.test.ts index 22cf0dd7f8ace..1fa85c86e0651 100644 --- a/x-pack/plugins/actions/server/routes/create.test.ts +++ b/x-pack/plugins/actions/server/routes/create.test.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { createActionRoute } from './create'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess, ActionTypeDisabledError } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; +import { actionsClientMock } from '../actions_client.mock'; jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), @@ -20,7 +21,7 @@ beforeEach(() => { describe('createActionRoute', () => { it('creates an action with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); createActionRoute(router, licenseState); @@ -40,10 +41,11 @@ describe('createActionRoute', () => { name: 'My name', actionTypeId: 'abc', config: { foo: true }, + isPreconfigured: false, }; - const actionsClient = { - create: jest.fn().mockResolvedValueOnce(createResult), - }; + + const actionsClient = actionsClientMock.create(); + actionsClient.create.mockResolvedValueOnce(createResult); const [context, req, res] = mockHandlerArguments( { actionsClient }, @@ -83,22 +85,22 @@ describe('createActionRoute', () => { it('ensures the license allows creating actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); createActionRoute(router, licenseState); const [, handler] = router.post.mock.calls[0]; - const actionsClient = { - create: jest.fn().mockResolvedValueOnce({ - id: '1', - name: 'My name', - actionTypeId: 'abc', - config: { foo: true }, - }), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.create.mockResolvedValueOnce({ + id: '1', + name: 'My name', + actionTypeId: 'abc', + config: { foo: true }, + isPreconfigured: false, + }); - const [context, req, res] = mockHandlerArguments(actionsClient, {}); + const [context, req, res] = mockHandlerArguments({ actionsClient }, {}); await handler(context, req, res); @@ -107,7 +109,7 @@ describe('createActionRoute', () => { it('ensures the license check prevents creating actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -117,16 +119,16 @@ describe('createActionRoute', () => { const [, handler] = router.post.mock.calls[0]; - const actionsClient = { - create: jest.fn().mockResolvedValueOnce({ - id: '1', - name: 'My name', - actionTypeId: 'abc', - config: { foo: true }, - }), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.create.mockResolvedValueOnce({ + id: '1', + name: 'My name', + actionTypeId: 'abc', + config: { foo: true }, + isPreconfigured: false, + }); - const [context, req, res] = mockHandlerArguments(actionsClient, {}); + const [context, req, res] = mockHandlerArguments({ actionsClient }, {}); expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); @@ -135,15 +137,14 @@ describe('createActionRoute', () => { it('ensures the action type gets validated for the license', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); createActionRoute(router, licenseState); const [, handler] = router.post.mock.calls[0]; - const actionsClient = { - create: jest.fn().mockRejectedValue(new ActionTypeDisabledError('Fail', 'license_invalid')), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.create.mockRejectedValue(new ActionTypeDisabledError('Fail', 'license_invalid')); const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok', 'forbidden']); diff --git a/x-pack/plugins/actions/server/routes/create.ts b/x-pack/plugins/actions/server/routes/create.ts index 0cd3143dc48d1..7f239bda41a60 100644 --- a/x-pack/plugins/actions/server/routes/create.ts +++ b/x-pack/plugins/actions/server/routes/create.ts @@ -36,9 +36,9 @@ export const createActionRoute = (router: IRouter, licenseState: ILicenseState) }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any>, + req: KibanaRequest>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.actions) { diff --git a/x-pack/plugins/actions/server/routes/delete.test.ts b/x-pack/plugins/actions/server/routes/delete.test.ts index 6fb526628cb02..e63989e27a57c 100644 --- a/x-pack/plugins/actions/server/routes/delete.test.ts +++ b/x-pack/plugins/actions/server/routes/delete.test.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { deleteActionRoute } from './delete'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; +import { actionsClientMock } from '../mocks'; jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), @@ -20,7 +21,7 @@ beforeEach(() => { describe('deleteActionRoute', () => { it('deletes an action with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); deleteActionRoute(router, licenseState); @@ -35,9 +36,8 @@ describe('deleteActionRoute', () => { } `); - const actionsClient = { - delete: jest.fn().mockResolvedValueOnce({}), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.delete.mockResolvedValueOnce({}); const [context, req, res] = mockHandlerArguments( { actionsClient }, @@ -65,19 +65,21 @@ describe('deleteActionRoute', () => { it('ensures the license allows deleting actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); deleteActionRoute(router, licenseState); const [, handler] = router.delete.mock.calls[0]; - const actionsClient = { - delete: jest.fn().mockResolvedValueOnce({}), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.delete.mockResolvedValueOnce({}); - const [context, req, res] = mockHandlerArguments(actionsClient, { - params: { id: '1' }, - }); + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + params: { id: '1' }, + } + ); await handler(context, req, res); @@ -86,7 +88,7 @@ describe('deleteActionRoute', () => { it('ensures the license check prevents deleting actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -96,13 +98,15 @@ describe('deleteActionRoute', () => { const [, handler] = router.delete.mock.calls[0]; - const actionsClient = { - delete: jest.fn().mockResolvedValueOnce({}), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.delete.mockResolvedValueOnce({}); - const [context, req, res] = mockHandlerArguments(actionsClient, { - id: '1', - }); + const [context, req, res] = mockHandlerArguments( + { actionsClient }, + { + id: '1', + } + ); expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); diff --git a/x-pack/plugins/actions/server/routes/delete.ts b/x-pack/plugins/actions/server/routes/delete.ts index ffd1f0faabbab..b76aa27fa5d9b 100644 --- a/x-pack/plugins/actions/server/routes/delete.ts +++ b/x-pack/plugins/actions/server/routes/delete.ts @@ -37,9 +37,9 @@ export const deleteActionRoute = (router: IRouter, licenseState: ILicenseState) }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, any, any>, + req: KibanaRequest, unknown, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.actions) { return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); diff --git a/x-pack/plugins/actions/server/routes/execute.test.ts b/x-pack/plugins/actions/server/routes/execute.test.ts index 3a3ed1257f576..1cd6343a39dcf 100644 --- a/x-pack/plugins/actions/server/routes/execute.test.ts +++ b/x-pack/plugins/actions/server/routes/execute.test.ts @@ -5,7 +5,7 @@ */ import { executeActionRoute } from './execute'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { ActionExecutorContract, verifyApiAccess, ActionTypeDisabledError } from '../lib'; @@ -21,7 +21,7 @@ beforeEach(() => { describe('executeActionRoute', () => { it('executes an action with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const [context, req, res] = mockHandlerArguments( {}, @@ -77,7 +77,7 @@ describe('executeActionRoute', () => { it('returns a "204 NO CONTENT" when the executor returns a nullish value', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const [context, req, res] = mockHandlerArguments( {}, @@ -115,7 +115,7 @@ describe('executeActionRoute', () => { it('ensures the license allows action execution', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const [context, req, res] = mockHandlerArguments( {}, @@ -147,7 +147,7 @@ describe('executeActionRoute', () => { it('ensures the license check prevents action execution', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -183,7 +183,7 @@ describe('executeActionRoute', () => { it('ensures the action type gets validated for the license', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const [context, req, res] = mockHandlerArguments( {}, diff --git a/x-pack/plugins/actions/server/routes/execute.ts b/x-pack/plugins/actions/server/routes/execute.ts index 52d0aa706e703..79d3c1dfb8d22 100644 --- a/x-pack/plugins/actions/server/routes/execute.ts +++ b/x-pack/plugins/actions/server/routes/execute.ts @@ -43,9 +43,9 @@ export const executeActionRoute = ( }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, TypeOf, any>, + req: KibanaRequest, unknown, TypeOf>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); const { params } = req.body; const { id } = req.params; diff --git a/x-pack/plugins/actions/server/routes/get.test.ts b/x-pack/plugins/actions/server/routes/get.test.ts index f4e834a5b767c..f701be579d99d 100644 --- a/x-pack/plugins/actions/server/routes/get.test.ts +++ b/x-pack/plugins/actions/server/routes/get.test.ts @@ -5,10 +5,11 @@ */ import { getActionRoute } from './get'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; +import { actionsClientMock } from '../actions_client.mock'; jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), @@ -21,7 +22,7 @@ beforeEach(() => { describe('getActionRoute', () => { it('gets an action with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getActionRoute(router, licenseState); @@ -41,10 +42,11 @@ describe('getActionRoute', () => { actionTypeId: '2', name: 'action name', config: {}, + isPreconfigured: false, }; - const actionsClient = { - get: jest.fn().mockResolvedValueOnce(getResult), - }; + + const actionsClient = actionsClientMock.create(); + actionsClient.get.mockResolvedValueOnce(getResult); const [context, req, res] = mockHandlerArguments( { actionsClient }, @@ -60,6 +62,7 @@ describe('getActionRoute', () => { "actionTypeId": "2", "config": Object {}, "id": "1", + "isPreconfigured": false, "name": "action name", }, } @@ -75,23 +78,23 @@ describe('getActionRoute', () => { it('ensures the license allows getting actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getActionRoute(router, licenseState); const [, handler] = router.get.mock.calls[0]; - const actionsClient = { - get: jest.fn().mockResolvedValueOnce({ - id: '1', - actionTypeId: '2', - name: 'action name', - config: {}, - }), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.get.mockResolvedValueOnce({ + id: '1', + actionTypeId: '2', + name: 'action name', + config: {}, + isPreconfigured: false, + }); const [context, req, res] = mockHandlerArguments( - actionsClient, + { actionsClient }, { params: { id: '1' }, }, @@ -105,7 +108,7 @@ describe('getActionRoute', () => { it('ensures the license check prevents getting actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -115,17 +118,17 @@ describe('getActionRoute', () => { const [, handler] = router.get.mock.calls[0]; - const actionsClient = { - get: jest.fn().mockResolvedValueOnce({ - id: '1', - actionTypeId: '2', - name: 'action name', - config: {}, - }), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.get.mockResolvedValueOnce({ + id: '1', + actionTypeId: '2', + name: 'action name', + config: {}, + isPreconfigured: false, + }); const [context, req, res] = mockHandlerArguments( - actionsClient, + { actionsClient }, { params: { id: '1' }, }, diff --git a/x-pack/plugins/actions/server/routes/get.ts b/x-pack/plugins/actions/server/routes/get.ts index cd29e54556b02..30e4289ed30c8 100644 --- a/x-pack/plugins/actions/server/routes/get.ts +++ b/x-pack/plugins/actions/server/routes/get.ts @@ -32,9 +32,9 @@ export const getActionRoute = (router: IRouter, licenseState: ILicenseState) => }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, any, any>, + req: KibanaRequest, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.actions) { return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); diff --git a/x-pack/plugins/actions/server/routes/get_all.test.ts b/x-pack/plugins/actions/server/routes/get_all.test.ts index 6499427b8c1a5..e00054fd3746f 100644 --- a/x-pack/plugins/actions/server/routes/get_all.test.ts +++ b/x-pack/plugins/actions/server/routes/get_all.test.ts @@ -5,10 +5,11 @@ */ import { getAllActionRoute } from './get_all'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; +import { actionsClientMock } from '../actions_client.mock'; jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), @@ -21,7 +22,7 @@ beforeEach(() => { describe('getAllActionRoute', () => { it('get all actions with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAllActionRoute(router, licenseState); @@ -36,9 +37,8 @@ describe('getAllActionRoute', () => { } `); - const actionsClient = { - getAll: jest.fn().mockResolvedValueOnce([]), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.getAll.mockResolvedValueOnce([]); const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']); @@ -57,7 +57,7 @@ describe('getAllActionRoute', () => { it('ensures the license allows getting all actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAllActionRoute(router, licenseState); @@ -72,9 +72,8 @@ describe('getAllActionRoute', () => { } `); - const actionsClient = { - getAll: jest.fn().mockResolvedValueOnce([]), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.getAll.mockResolvedValueOnce([]); const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']); @@ -85,7 +84,7 @@ describe('getAllActionRoute', () => { it('ensures the license check prevents getting all actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -104,9 +103,8 @@ describe('getAllActionRoute', () => { } `); - const actionsClient = { - getAll: jest.fn().mockResolvedValueOnce([]), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.getAll.mockResolvedValueOnce([]); const [context, req, res] = mockHandlerArguments({ actionsClient }, {}, ['ok']); diff --git a/x-pack/plugins/actions/server/routes/get_all.ts b/x-pack/plugins/actions/server/routes/get_all.ts index c70a13bc01c9f..f6745427f74c7 100644 --- a/x-pack/plugins/actions/server/routes/get_all.ts +++ b/x-pack/plugins/actions/server/routes/get_all.ts @@ -25,9 +25,9 @@ export const getAllActionRoute = (router: IRouter, licenseState: ILicenseState) }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, + req: KibanaRequest, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.actions) { return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); diff --git a/x-pack/plugins/actions/server/routes/list_action_types.test.ts b/x-pack/plugins/actions/server/routes/list_action_types.test.ts index 76fb636a75be7..205752d5a49d1 100644 --- a/x-pack/plugins/actions/server/routes/list_action_types.test.ts +++ b/x-pack/plugins/actions/server/routes/list_action_types.test.ts @@ -5,10 +5,11 @@ */ import { listActionTypesRoute } from './list_action_types'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; +import { LicenseType } from '../../../../plugins/licensing/server'; jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), @@ -21,7 +22,7 @@ beforeEach(() => { describe('listActionTypesRoute', () => { it('lists action types with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); listActionTypesRoute(router, licenseState); @@ -41,6 +42,9 @@ describe('listActionTypesRoute', () => { id: '1', name: 'name', enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'gold' as LicenseType, }, ]; @@ -51,7 +55,10 @@ describe('listActionTypesRoute', () => { "body": Array [ Object { "enabled": true, + "enabledInConfig": true, + "enabledInLicense": true, "id": "1", + "minimumLicenseRequired": "gold", "name": "name", }, ], @@ -67,7 +74,7 @@ describe('listActionTypesRoute', () => { it('ensures the license allows listing action types', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); listActionTypesRoute(router, licenseState); @@ -87,6 +94,9 @@ describe('listActionTypesRoute', () => { id: '1', name: 'name', enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'gold' as LicenseType, }, ]; @@ -105,7 +115,7 @@ describe('listActionTypesRoute', () => { it('ensures the license check prevents listing action types', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -129,6 +139,9 @@ describe('listActionTypesRoute', () => { id: '1', name: 'name', enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'gold' as LicenseType, }, ]; diff --git a/x-pack/plugins/actions/server/routes/list_action_types.ts b/x-pack/plugins/actions/server/routes/list_action_types.ts index 71dcbd2e19bf7..50bc7405f4d6b 100644 --- a/x-pack/plugins/actions/server/routes/list_action_types.ts +++ b/x-pack/plugins/actions/server/routes/list_action_types.ts @@ -25,9 +25,9 @@ export const listActionTypesRoute = (router: IRouter, licenseState: ILicenseStat }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, + req: KibanaRequest, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.actions) { return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); diff --git a/x-pack/plugins/actions/server/routes/update.test.ts b/x-pack/plugins/actions/server/routes/update.test.ts index 161fb4398af1d..6459a34bf0737 100644 --- a/x-pack/plugins/actions/server/routes/update.test.ts +++ b/x-pack/plugins/actions/server/routes/update.test.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import { updateActionRoute } from './update'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { licenseStateMock } from '../lib/license_state.mock'; import { verifyApiAccess, ActionTypeDisabledError } from '../lib'; import { mockHandlerArguments } from './_mock_handler_arguments'; +import { actionsClientMock } from '../actions_client.mock'; jest.mock('../lib/verify_api_access.ts', () => ({ verifyApiAccess: jest.fn(), @@ -20,7 +21,7 @@ beforeEach(() => { describe('updateActionRoute', () => { it('updates an action with proper parameters', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); updateActionRoute(router, licenseState); @@ -40,11 +41,11 @@ describe('updateActionRoute', () => { actionTypeId: 'my-action-type-id', name: 'My name', config: { foo: true }, + isPreconfigured: false, }; - const actionsClient = { - update: jest.fn().mockResolvedValueOnce(updateResult), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.update.mockResolvedValueOnce(updateResult); const [context, req, res] = mockHandlerArguments( { actionsClient }, @@ -86,7 +87,7 @@ describe('updateActionRoute', () => { it('ensures the license allows deleting actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); updateActionRoute(router, licenseState); @@ -97,11 +98,11 @@ describe('updateActionRoute', () => { actionTypeId: 'my-action-type-id', name: 'My name', config: { foo: true }, + isPreconfigured: false, }; - const actionsClient = { - update: jest.fn().mockResolvedValueOnce(updateResult), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.update.mockResolvedValueOnce(updateResult); const [context, req, res] = mockHandlerArguments( { actionsClient }, @@ -125,7 +126,7 @@ describe('updateActionRoute', () => { it('ensures the license check prevents deleting actions', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -140,11 +141,11 @@ describe('updateActionRoute', () => { actionTypeId: 'my-action-type-id', name: 'My name', config: { foo: true }, + isPreconfigured: false, }; - const actionsClient = { - update: jest.fn().mockResolvedValueOnce(updateResult), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.update.mockResolvedValueOnce(updateResult); const [context, req, res] = mockHandlerArguments( { actionsClient }, @@ -168,15 +169,14 @@ describe('updateActionRoute', () => { it('ensures the action type gets validated for the license', async () => { const licenseState = licenseStateMock.create(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); updateActionRoute(router, licenseState); const [, handler] = router.put.mock.calls[0]; - const actionsClient = { - update: jest.fn().mockRejectedValue(new ActionTypeDisabledError('Fail', 'license_invalid')), - }; + const actionsClient = actionsClientMock.create(); + actionsClient.update.mockRejectedValue(new ActionTypeDisabledError('Fail', 'license_invalid')); const [context, req, res] = mockHandlerArguments({ actionsClient }, { params: {}, body: {} }, [ 'ok', diff --git a/x-pack/plugins/actions/server/routes/update.ts b/x-pack/plugins/actions/server/routes/update.ts index 263df678f293d..45ced77be922e 100644 --- a/x-pack/plugins/actions/server/routes/update.ts +++ b/x-pack/plugins/actions/server/routes/update.ts @@ -39,9 +39,9 @@ export const updateActionRoute = (router: IRouter, licenseState: ILicenseState) }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, TypeOf, any>, + req: KibanaRequest, unknown, TypeOf>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.actions) { return res.badRequest({ body: 'RouteHandlerContext is not registered for actions' }); diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index 92e38d77314f8..9dcfdb81f5ebb 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -4,20 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract, SavedObjectAttributes } from '../../../../src/core/server'; +import { + SavedObjectsClientContract, + SavedObjectAttributes, + KibanaRequest, +} from '../../../../src/core/server'; import { ActionTypeRegistry } from './action_type_registry'; import { PluginSetupContract, PluginStartContract } from './plugin'; import { ActionsClient } from './actions_client'; import { LicenseType } from '../../licensing/common/types'; export type WithoutQueryAndParams = Pick>; -export type GetServicesFunction = (request: any) => Services; +export type GetServicesFunction = (request: KibanaRequest) => Services; export type ActionTypeRegistryContract = PublicMethodsOf; export type GetBasePathFunction = (spaceId?: string) => string; export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined; export interface Services { - callCluster(path: string, opts: any): Promise; + callCluster(path: string, opts: unknown): Promise; savedObjectsClient: SavedObjectsClientContract; } @@ -45,8 +49,12 @@ export interface ActionsConfigType { export interface ActionTypeExecutorOptions { actionId: string; services: Services; + // This will have to remain `any` until we can extend Action Executors with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any config: Record; + // eslint-disable-next-line @typescript-eslint/no-explicit-any secrets: Record; + // eslint-disable-next-line @typescript-eslint/no-explicit-any params: Record; } @@ -54,11 +62,15 @@ export interface ActionResult { id: string; actionTypeId: string; name: string; - config: Record; + // This will have to remain `any` until we can extend Action Executors with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any + config?: Record; isPreconfigured: boolean; } export interface PreConfiguredAction extends ActionResult { + // This will have to remain `any` until we can extend Action Executors with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any secrets: Record; } @@ -72,6 +84,8 @@ export interface ActionTypeExecutorResult { status: 'ok' | 'error'; message?: string; serviceMessage?: string; + // This will have to remain `any` until we can extend Action Executors with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any data?: any; retry?: null | boolean | Date; } @@ -82,7 +96,7 @@ export type ExecutorType = ( ) => Promise; interface ValidatorType { - validate(value: any): any; + validate(value: unknown): Record; } export type ActionTypeCreator = (config?: ActionsConfigType) => ActionType; @@ -108,6 +122,13 @@ export interface RawAction extends SavedObjectAttributes { export interface ActionTaskParams extends SavedObjectAttributes { actionId: string; + // Saved Objects won't allow us to enforce unknown rather than any + // eslint-disable-next-line @typescript-eslint/no-explicit-any params: Record; apiKey?: string; } + +export interface ActionTaskExecutorParams { + spaceId: string; + actionTaskParamsId: string; +} diff --git a/x-pack/plugins/actions/server/usage/actions_telemetry.ts b/x-pack/plugins/actions/server/usage/actions_telemetry.ts index eabb38e61d17d..6996ba629f8da 100644 --- a/x-pack/plugins/actions/server/usage/actions_telemetry.ts +++ b/x-pack/plugins/actions/server/usage/actions_telemetry.ts @@ -55,6 +55,8 @@ export async function getTotalCount(callCluster: APICaller, kibanaIndex: string) 0 ), countByType: Object.keys(searchResult.aggregations.byActionTypeId.value.types).reduce( + // ES DSL aggregations are returned as `any` by callCluster + // eslint-disable-next-line @typescript-eslint/no-explicit-any (obj: any, key: string) => ({ ...obj, [key.replace('.', '__')]: searchResult.aggregations.byActionTypeId.value.types[key], diff --git a/x-pack/plugins/alerting/common/alert.ts b/x-pack/plugins/alerting/common/alert.ts index 8f28c8fbaed7f..b410b6aa0187e 100644 --- a/x-pack/plugins/alerting/common/alert.ts +++ b/x-pack/plugins/alerting/common/alert.ts @@ -28,6 +28,8 @@ export interface Alert { consumer: string; schedule: IntervalSchedule; actions: AlertAction[]; + // This will have to remain `any` until we can extend Alert Executors with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any params: Record; scheduledTaskId?: string; createdBy: string | null; diff --git a/x-pack/plugins/alerting/server/alert_type_registry.test.ts b/x-pack/plugins/alerting/server/alert_type_registry.test.ts index b51286281571e..f9df390242cd4 100644 --- a/x-pack/plugins/alerting/server/alert_type_registry.test.ts +++ b/x-pack/plugins/alerting/server/alert_type_registry.test.ts @@ -231,7 +231,7 @@ function alertTypeWithVariables(id: string, context: string, state: string): Ale name: `${id}-name`, actionGroups: [], defaultActionGroupId: id, - executor: (params: any): any => {}, + async executor() {}, }; if (!context && !state) { diff --git a/x-pack/plugins/alerting/server/alert_type_registry.ts b/x-pack/plugins/alerting/server/alert_type_registry.ts index a2be43f9dacbd..55e39b6a817db 100644 --- a/x-pack/plugins/alerting/server/alert_type_registry.ts +++ b/x-pack/plugins/alerting/server/alert_type_registry.ts @@ -77,7 +77,7 @@ export class AlertTypeRegistry { } } -function normalizedActionVariables(actionVariables: any) { +function normalizedActionVariables(actionVariables: AlertType['actionVariables']) { return { context: actionVariables?.context ?? [], state: actionVariables?.state ?? [], diff --git a/x-pack/plugins/alerting/server/alerts_client.mock.ts b/x-pack/plugins/alerting/server/alerts_client.mock.ts index 3189fa214d5f7..1848b3432ae5a 100644 --- a/x-pack/plugins/alerting/server/alerts_client.mock.ts +++ b/x-pack/plugins/alerting/server/alerts_client.mock.ts @@ -7,9 +7,10 @@ import { AlertsClient } from './alerts_client'; type Schema = PublicMethodsOf; +export type AlertsClientMock = jest.Mocked; const createAlertsClientMock = () => { - const mocked: jest.Mocked = { + const mocked: AlertsClientMock = { create: jest.fn(), get: jest.fn(), getAlertState: jest.fn(), @@ -27,6 +28,8 @@ const createAlertsClientMock = () => { return mocked; }; -export const alertsClientMock = { +export const alertsClientMock: { + create: () => AlertsClientMock; +} = { create: createAlertsClientMock, }; diff --git a/x-pack/plugins/alerting/server/alerts_client.test.ts b/x-pack/plugins/alerting/server/alerts_client.test.ts index a9ff5ee8ecdc6..6c7b93aa64003 100644 --- a/x-pack/plugins/alerting/server/alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client.test.ts @@ -5,7 +5,7 @@ */ import uuid from 'uuid'; import { schema } from '@kbn/config-schema'; -import { AlertsClient } from './alerts_client'; +import { AlertsClient, CreateOptions } from './alerts_client'; import { savedObjectsClientMock, loggingServiceMock } from '../../../../src/core/server/mocks'; import { taskManagerMock } from '../../../plugins/task_manager/server/task_manager.mock'; import { alertTypeRegistryMock } from './alert_type_registry.mock'; @@ -45,6 +45,7 @@ beforeEach(() => { }); const mockedDate = new Date('2019-02-12T21:01:22.479Z'); +// eslint-disable-next-line @typescript-eslint/no-explicit-any (global as any).Date = class Date { constructor() { return mockedDate; @@ -54,7 +55,7 @@ const mockedDate = new Date('2019-02-12T21:01:22.479Z'); } }; -function getMockData(overwrites: Record = {}) { +function getMockData(overwrites: Record = {}): CreateOptions['data'] { return { enabled: true, name: 'abc', diff --git a/x-pack/plugins/alerting/server/alerts_client.ts b/x-pack/plugins/alerting/server/alerts_client.ts index 3e4c26d3444c9..ff501055ba9fe 100644 --- a/x-pack/plugins/alerting/server/alerts_client.ts +++ b/x-pack/plugins/alerting/server/alerts_client.ts @@ -24,6 +24,7 @@ import { IntervalSchedule, SanitizedAlert, AlertTaskState, + RawAlertAction, } from './types'; import { validateAlertTypeParams } from './lib'; import { @@ -83,7 +84,7 @@ export interface FindResult { data: SanitizedAlert[]; } -interface CreateOptions { +export interface CreateOptions { data: Omit< Alert, | 'id' @@ -109,7 +110,7 @@ interface UpdateOptions { tags: string[]; schedule: IntervalSchedule; actions: NormalizedAlertAction[]; - params: Record; + params: Record; throttle: string | null; }; } @@ -172,7 +173,7 @@ export class AlertsClient { createdBy: username, updatedBy: username, createdAt: new Date().toISOString(), - params: validatedAlertTypeParams, + params: validatedAlertTypeParams as RawAlert['params'], muteAll: false, mutedInstanceIds: [], }; @@ -337,7 +338,7 @@ export class AlertsClient { ...attributes, ...data, ...apiKeyAttributes, - params: validatedAlertTypeParams, + params: validatedAlertTypeParams as RawAlert['params'], actions, updatedBy: username, }, @@ -667,7 +668,7 @@ export class AlertsClient { private async denormalizeActions( alertActions: NormalizedAlertAction[] ): Promise<{ actions: RawAlert['actions']; references: SavedObjectReference[] }> { - const actionMap = new Map(); + const actionMap = new Map(); // map preconfigured actions for (const alertAction of alertActions) { const action = this.preconfiguredActions.find( @@ -712,8 +713,8 @@ export class AlertsClient { // if action is a save object, than actionTypeId should be under attributes property // if action is a preconfigured, than actionTypeId is the action property const actionTypeId = actionIds.find(actionId => actionId === id) - ? actionMapValue.attributes.actionTypeId - : actionMapValue.actionTypeId; + ? (actionMapValue as SavedObject>).attributes.actionTypeId + : (actionMapValue as RawAlertAction).actionTypeId; return { ...alertAction, actionRef, diff --git a/x-pack/plugins/alerting/server/alerts_client_factory.test.ts b/x-pack/plugins/alerting/server/alerts_client_factory.test.ts index 951d18a33b35f..e5aa0a674eccf 100644 --- a/x-pack/plugins/alerting/server/alerts_client_factory.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client_factory.test.ts @@ -11,16 +11,13 @@ import { taskManagerMock } from '../../../plugins/task_manager/server/task_manag import { KibanaRequest } from '../../../../src/core/server'; import { loggingServiceMock, savedObjectsClientMock } from '../../../../src/core/server/mocks'; import { encryptedSavedObjectsMock } from '../../../plugins/encrypted_saved_objects/server/mocks'; +import { AuthenticatedUser } from '../../../plugins/security/public'; +import { securityMock } from '../../../plugins/security/server/mocks'; jest.mock('./alerts_client'); const savedObjectsClient = savedObjectsClientMock.create(); -const securityPluginSetup = { - authc: { - grantAPIKeyAsInternalUser: jest.fn(), - getCurrentUser: jest.fn(), - }, -}; +const securityPluginSetup = securityMock.createSetup(); const alertsClientFactoryParams: jest.Mocked = { logger: loggingServiceMock.create().get(), taskManager: taskManagerMock.start(), @@ -30,7 +27,7 @@ const alertsClientFactoryParams: jest.Mocked = { encryptedSavedObjectsPlugin: encryptedSavedObjectsMock.createStart(), preconfiguredActions: [], }; -const fakeRequest: Request = { +const fakeRequest = ({ headers: {}, getBasePath: () => '', path: '/', @@ -44,7 +41,7 @@ const fakeRequest: Request = { }, }, getSavedObjectsClient: () => savedObjectsClient, -} as any; +} as unknown) as Request; beforeEach(() => { jest.resetAllMocks(); @@ -86,12 +83,14 @@ test('getUserName() returns a name when security is enabled', async () => { const factory = new AlertsClientFactory(); factory.initialize({ ...alertsClientFactoryParams, - securityPluginSetup: securityPluginSetup as any, + securityPluginSetup, }); factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; - securityPluginSetup.authc.getCurrentUser.mockReturnValueOnce({ username: 'bob' }); + securityPluginSetup.authc.getCurrentUser.mockReturnValueOnce(({ + username: 'bob', + } as unknown) as AuthenticatedUser); const userNameResult = await constructorCall.getUserName(); expect(userNameResult).toEqual('bob'); }); @@ -121,7 +120,7 @@ test('createAPIKey() returns an API key when security is enabled', async () => { const factory = new AlertsClientFactory(); factory.initialize({ ...alertsClientFactoryParams, - securityPluginSetup: securityPluginSetup as any, + securityPluginSetup, }); factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; @@ -129,11 +128,12 @@ test('createAPIKey() returns an API key when security is enabled', async () => { securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockResolvedValueOnce({ api_key: '123', id: 'abc', + name: '', }); const createAPIKeyResult = await constructorCall.createAPIKey(); expect(createAPIKeyResult).toEqual({ apiKeysEnabled: true, - result: { api_key: '123', id: 'abc' }, + result: { api_key: '123', id: 'abc', name: '' }, }); }); @@ -141,7 +141,7 @@ test('createAPIKey() throws when security plugin createAPIKey throws an error', const factory = new AlertsClientFactory(); factory.initialize({ ...alertsClientFactoryParams, - securityPluginSetup: securityPluginSetup as any, + securityPluginSetup, }); factory.create(KibanaRequest.from(fakeRequest), savedObjectsClient); const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0]; diff --git a/x-pack/plugins/alerting/server/constants/plugin.ts b/x-pack/plugins/alerting/server/constants/plugin.ts index 173aa50013b40..9c276ed1d75de 100644 --- a/x-pack/plugins/alerting/server/constants/plugin.ts +++ b/x-pack/plugins/alerting/server/constants/plugin.ts @@ -9,6 +9,8 @@ import { LICENSE_TYPE_BASIC, LicenseType } from '../../../../legacy/common/const export const PLUGIN = { ID: 'alerting', MINIMUM_LICENSE_REQUIRED: LICENSE_TYPE_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 => i18n.translate('xpack.alerting.appName', { defaultMessage: 'Alerting', diff --git a/x-pack/plugins/alerting/server/lib/license_state.test.ts b/x-pack/plugins/alerting/server/lib/license_state.test.ts index 9bbd619dc4868..cbab98a6311dd 100644 --- a/x-pack/plugins/alerting/server/lib/license_state.test.ts +++ b/x-pack/plugins/alerting/server/lib/license_state.test.ts @@ -9,10 +9,10 @@ import { LicenseState } from './license_state'; import { licensingMock } from '../../../../plugins/licensing/server/mocks'; describe('license_state', () => { - let getRawLicense: any; + const getRawLicense = jest.fn(); beforeEach(() => { - getRawLicense = jest.fn(); + jest.resetAllMocks(); }); describe('status is LICENSE_STATUS_INVALID', () => { diff --git a/x-pack/plugins/alerting/server/lib/validate_alert_type_params.ts b/x-pack/plugins/alerting/server/lib/validate_alert_type_params.ts index 248d896c06ac2..b6dcb522f0925 100644 --- a/x-pack/plugins/alerting/server/lib/validate_alert_type_params.ts +++ b/x-pack/plugins/alerting/server/lib/validate_alert_type_params.ts @@ -5,15 +5,15 @@ */ import Boom from 'boom'; -import { AlertType } from '../types'; +import { AlertType, AlertExecutorOptions } from '../types'; -export function validateAlertTypeParams>( +export function validateAlertTypeParams( alertType: AlertType, - params: T -): T { + params: Record +): AlertExecutorOptions['params'] { const validator = alertType.validate && alertType.validate.params; if (!validator) { - return params; + return params as AlertExecutorOptions['params']; } try { diff --git a/x-pack/plugins/alerting/server/plugin.test.ts b/x-pack/plugins/alerting/server/plugin.test.ts index 74a1f2349180e..267e68930a5d7 100644 --- a/x-pack/plugins/alerting/server/plugin.test.ts +++ b/x-pack/plugins/alerting/server/plugin.test.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AlertingPlugin } from './plugin'; +import { AlertingPlugin, AlertingPluginsSetup, AlertingPluginsStart } from './plugin'; import { coreMock } from '../../../../src/core/server/mocks'; import { licensingMock } from '../../../plugins/licensing/server/mocks'; import { encryptedSavedObjectsMock } from '../../../plugins/encrypted_saved_objects/server/mocks'; import { taskManagerMock } from '../../task_manager/server/mocks'; import { eventLogServiceMock } from '../../event_log/server/event_log_service.mock'; +import { KibanaRequest, CoreSetup } from 'kibana/server'; describe('Alerting Plugin', () => { describe('setup()', () => { @@ -20,19 +21,19 @@ describe('Alerting Plugin', () => { const coreSetup = coreMock.createSetup(); const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup(); await plugin.setup( - { + ({ ...coreSetup, http: { ...coreSetup.http, route: jest.fn(), }, - } as any, - { + } as unknown) as CoreSetup, + ({ licensing: licensingMock.createSetup(), encryptedSavedObjects: encryptedSavedObjectsSetup, taskManager: taskManagerMock.createSetup(), eventLog: eventLogServiceMock.create(), - } as any + } as unknown) as AlertingPluginsSetup ); expect(encryptedSavedObjectsSetup.usingEphemeralEncryptionKey).toEqual(true); @@ -58,34 +59,34 @@ describe('Alerting Plugin', () => { const coreSetup = coreMock.createSetup(); const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup(); await plugin.setup( - { + ({ ...coreSetup, http: { ...coreSetup.http, route: jest.fn(), }, - } as any, - { + } as unknown) as CoreSetup, + ({ licensing: licensingMock.createSetup(), encryptedSavedObjects: encryptedSavedObjectsSetup, taskManager: taskManagerMock.createSetup(), eventLog: eventLogServiceMock.create(), - } as any + } as unknown) as AlertingPluginsSetup ); const startContract = plugin.start( - coreMock.createStart() as any, - { + coreMock.createStart() as ReturnType, + ({ actions: { execute: jest.fn(), getActionsClientWithRequest: jest.fn(), }, - } as any + } as unknown) as AlertingPluginsStart ); expect(encryptedSavedObjectsSetup.usingEphemeralEncryptionKey).toEqual(true); expect(() => - startContract.getAlertsClientWithRequest({} as any) + startContract.getAlertsClientWithRequest({} as KibanaRequest) ).toThrowErrorMatchingInlineSnapshot( `"Unable to create alerts client due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml"` ); @@ -101,33 +102,33 @@ describe('Alerting Plugin', () => { usingEphemeralEncryptionKey: false, }; await plugin.setup( - { + ({ ...coreSetup, http: { ...coreSetup.http, route: jest.fn(), }, - } as any, - { + } as unknown) as CoreSetup, + ({ licensing: licensingMock.createSetup(), encryptedSavedObjects: encryptedSavedObjectsSetup, taskManager: taskManagerMock.createSetup(), eventLog: eventLogServiceMock.create(), - } as any + } as unknown) as AlertingPluginsSetup ); const startContract = plugin.start( - coreMock.createStart() as any, - { + coreMock.createStart() as ReturnType, + ({ actions: { execute: jest.fn(), getActionsClientWithRequest: jest.fn(), }, spaces: () => null, - } as any + } as unknown) as AlertingPluginsStart ); - const fakeRequest = { + const fakeRequest = ({ headers: {}, getBasePath: () => '', path: '/', @@ -141,8 +142,8 @@ describe('Alerting Plugin', () => { }, }, getSavedObjectsClient: jest.fn(), - }; - await startContract.getAlertsClientWithRequest(fakeRequest as any); + } as unknown) as KibanaRequest; + await startContract.getAlertsClientWithRequest(fakeRequest); }); }); }); diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index ad39d09bd6d3d..35ebafce9dc67 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -117,7 +117,10 @@ export class AlertingPlugin { .toPromise(); } - public async setup(core: CoreSetup, plugins: AlertingPluginsSetup): Promise { + public async setup( + core: CoreSetup, + plugins: AlertingPluginsSetup + ): Promise { this.licenseState = new LicenseState(plugins.licensing.license$); this.spaces = plugins.spaces?.spacesService; this.security = plugins.security; @@ -157,7 +160,7 @@ export class AlertingPlugin { const usageCollection = plugins.usageCollection; if (usageCollection) { - core.getStartServices().then(async ([, startPlugins]: [CoreStart, any, any]) => { + core.getStartServices().then(async ([, startPlugins]) => { registerAlertsUsageCollector(usageCollection, startPlugins.taskManager); initializeAlertingTelemetry( @@ -246,7 +249,7 @@ export class AlertingPlugin { } private createRouteHandlerContext = (): IContextProvider< - RequestHandler, + RequestHandler, 'alerting' > => { const { alertTypeRegistry, alertsClientFactory } = this; diff --git a/x-pack/plugins/alerting/server/routes/_mock_handler_arguments.ts b/x-pack/plugins/alerting/server/routes/_mock_handler_arguments.ts index 5a1d680eb06f3..6fb3df8446f5c 100644 --- a/x-pack/plugins/alerting/server/routes/_mock_handler_arguments.ts +++ b/x-pack/plugins/alerting/server/routes/_mock_handler_arguments.ts @@ -4,16 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandlerContext, KibanaRequest, KibanaResponseFactory } from 'kibana/server'; +import { + RequestHandlerContext, + KibanaRequest, + KibanaResponseFactory, + IClusterClient, +} from 'kibana/server'; import { identity } from 'lodash'; import { httpServerMock } from '../../../../../src/core/server/mocks'; -import { alertsClientMock } from '../alerts_client.mock'; +import { alertsClientMock, AlertsClientMock } from '../alerts_client.mock'; +import { AlertType } from '../../common'; +import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; export function mockHandlerArguments( - { alertsClient, listTypes: listTypesRes = [], elasticsearch }: any, - req: any, + { + alertsClient = alertsClientMock.create(), + listTypes: listTypesRes = [], + elasticsearch = elasticsearchServiceMock.createSetup(), + }: { + alertsClient?: AlertsClientMock; + listTypes?: AlertType[]; + elasticsearch?: jest.Mocked<{ + adminClient: jest.Mocked; + }>; + }, + req: unknown, res?: Array> -): [RequestHandlerContext, KibanaRequest, KibanaResponseFactory] { +): [RequestHandlerContext, KibanaRequest, KibanaResponseFactory] { const listTypes = jest.fn(() => listTypesRes); return [ ({ @@ -25,7 +42,7 @@ export function mockHandlerArguments( }, }, } as unknown) as RequestHandlerContext, - req as KibanaRequest, + req as KibanaRequest, mockResponseFactory(res), ]; } diff --git a/x-pack/plugins/alerting/server/routes/create.test.ts b/x-pack/plugins/alerting/server/routes/create.test.ts index c6a0da2bd9191..a4910495c8a40 100644 --- a/x-pack/plugins/alerting/server/routes/create.test.ts +++ b/x-pack/plugins/alerting/server/routes/create.test.ts @@ -5,7 +5,7 @@ */ import { createAlertRoute } from './create'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -68,7 +68,7 @@ describe('createAlertRoute', () => { it('creates an alert with proper parameters', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); createAlertRoute(router, licenseState); @@ -134,7 +134,7 @@ describe('createAlertRoute', () => { it('ensures the license allows creating alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); createAlertRoute(router, licenseState); @@ -142,7 +142,7 @@ describe('createAlertRoute', () => { alertsClient.create.mockResolvedValueOnce(createResult); - const [context, req, res] = mockHandlerArguments(alertsClient, {}); + const [context, req, res] = mockHandlerArguments({ alertsClient }, {}); await handler(context, req, res); @@ -151,7 +151,7 @@ describe('createAlertRoute', () => { it('ensures the license check prevents creating alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -163,7 +163,7 @@ describe('createAlertRoute', () => { alertsClient.create.mockResolvedValueOnce(createResult); - const [context, req, res] = mockHandlerArguments(alertsClient, {}); + const [context, req, res] = mockHandlerArguments({ alertsClient }, {}); expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); diff --git a/x-pack/plugins/alerting/server/routes/create.ts b/x-pack/plugins/alerting/server/routes/create.ts index f08460ffcb453..0c038b6490483 100644 --- a/x-pack/plugins/alerting/server/routes/create.ts +++ b/x-pack/plugins/alerting/server/routes/create.ts @@ -54,9 +54,9 @@ export const createAlertRoute = (router: IRouter, licenseState: LicenseState) => handleDisabledApiKeysError( router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any>, + req: KibanaRequest>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { diff --git a/x-pack/plugins/alerting/server/routes/delete.test.ts b/x-pack/plugins/alerting/server/routes/delete.test.ts index 36bb485817c15..416628d015b5a 100644 --- a/x-pack/plugins/alerting/server/routes/delete.test.ts +++ b/x-pack/plugins/alerting/server/routes/delete.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { deleteAlertRoute } from './delete'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -23,7 +23,7 @@ beforeEach(() => { describe('deleteAlertRoute', () => { it('deletes an alert with proper parameters', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); deleteAlertRoute(router, licenseState); @@ -66,7 +66,7 @@ describe('deleteAlertRoute', () => { it('ensures the license allows deleting alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); deleteAlertRoute(router, licenseState); @@ -74,9 +74,12 @@ describe('deleteAlertRoute', () => { alertsClient.delete.mockResolvedValueOnce({}); - const [context, req, res] = mockHandlerArguments(alertsClient, { - params: { id: '1' }, - }); + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + params: { id: '1' }, + } + ); await handler(context, req, res); @@ -85,7 +88,7 @@ describe('deleteAlertRoute', () => { it('ensures the license check prevents deleting alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -97,9 +100,12 @@ describe('deleteAlertRoute', () => { alertsClient.delete.mockResolvedValueOnce({}); - const [context, req, res] = mockHandlerArguments(alertsClient, { - id: '1', - }); + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + id: '1', + } + ); expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); diff --git a/x-pack/plugins/alerting/server/routes/delete.ts b/x-pack/plugins/alerting/server/routes/delete.ts index 8d77c9b395e59..7f6600b1ec48e 100644 --- a/x-pack/plugins/alerting/server/routes/delete.ts +++ b/x-pack/plugins/alerting/server/routes/delete.ts @@ -33,9 +33,9 @@ export const deleteAlertRoute = (router: IRouter, licenseState: LicenseState) => }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, any, any>, + req: KibanaRequest, unknown, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/disable.test.ts b/x-pack/plugins/alerting/server/routes/disable.test.ts index 622b562ec6911..fde095e9145b6 100644 --- a/x-pack/plugins/alerting/server/routes/disable.test.ts +++ b/x-pack/plugins/alerting/server/routes/disable.test.ts @@ -5,7 +5,7 @@ */ import { disableAlertRoute } from './disable'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -23,7 +23,7 @@ beforeEach(() => { describe('disableAlertRoute', () => { it('disables an alert', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); disableAlertRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/disable.ts b/x-pack/plugins/alerting/server/routes/disable.ts index fcc7116a697b1..c7e7b1001f82d 100644 --- a/x-pack/plugins/alerting/server/routes/disable.ts +++ b/x-pack/plugins/alerting/server/routes/disable.ts @@ -33,9 +33,9 @@ export const disableAlertRoute = (router: IRouter, licenseState: LicenseState) = }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, any, any>, + req: KibanaRequest, unknown, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/enable.test.ts b/x-pack/plugins/alerting/server/routes/enable.test.ts index 5a7b027e8ef54..e4e89e3f06380 100644 --- a/x-pack/plugins/alerting/server/routes/enable.test.ts +++ b/x-pack/plugins/alerting/server/routes/enable.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { enableAlertRoute } from './enable'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -22,7 +22,7 @@ beforeEach(() => { describe('enableAlertRoute', () => { it('enables an alert', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); enableAlertRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/enable.ts b/x-pack/plugins/alerting/server/routes/enable.ts index 2283ae4a4c765..3ed4fb0739d3d 100644 --- a/x-pack/plugins/alerting/server/routes/enable.ts +++ b/x-pack/plugins/alerting/server/routes/enable.ts @@ -35,9 +35,9 @@ export const enableAlertRoute = (router: IRouter, licenseState: LicenseState) => handleDisabledApiKeysError( router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, any, any>, + req: KibanaRequest, unknown, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/find.test.ts b/x-pack/plugins/alerting/server/routes/find.test.ts index 391d6df3f9931..cc601bd42b8ca 100644 --- a/x-pack/plugins/alerting/server/routes/find.test.ts +++ b/x-pack/plugins/alerting/server/routes/find.test.ts @@ -5,7 +5,7 @@ */ import { findAlertRoute } from './find'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -24,7 +24,7 @@ beforeEach(() => { describe('findAlertRoute', () => { it('finds alerts with proper parameters', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); findAlertRoute(router, licenseState); @@ -95,7 +95,7 @@ describe('findAlertRoute', () => { it('ensures the license allows finding alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); findAlertRoute(router, licenseState); @@ -108,13 +108,16 @@ describe('findAlertRoute', () => { data: [], }); - const [context, req, res] = mockHandlerArguments(alertsClient, { - query: { - per_page: 1, - page: 1, - default_search_operator: 'OR', - }, - }); + const [context, req, res] = mockHandlerArguments( + { alertsClient }, + { + query: { + per_page: 1, + page: 1, + default_search_operator: 'OR', + }, + } + ); await handler(context, req, res); @@ -123,7 +126,7 @@ describe('findAlertRoute', () => { it('ensures the license check prevents finding alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); diff --git a/x-pack/plugins/alerting/server/routes/find.ts b/x-pack/plugins/alerting/server/routes/find.ts index 0787f5c6b5ad6..c723419a965c5 100644 --- a/x-pack/plugins/alerting/server/routes/find.ts +++ b/x-pack/plugins/alerting/server/routes/find.ts @@ -55,9 +55,9 @@ export const findAlertRoute = (router: IRouter, licenseState: LicenseState) => { }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, any>, + req: KibanaRequest, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/get.test.ts b/x-pack/plugins/alerting/server/routes/get.test.ts index 4506700c6d9cc..7335f13c85a4d 100644 --- a/x-pack/plugins/alerting/server/routes/get.test.ts +++ b/x-pack/plugins/alerting/server/routes/get.test.ts @@ -5,7 +5,7 @@ */ import { getAlertRoute } from './get'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -55,7 +55,7 @@ describe('getAlertRoute', () => { it('gets an alert with proper parameters', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAlertRoute(router, licenseState); const [config, handler] = router.get.mock.calls[0]; @@ -90,7 +90,7 @@ describe('getAlertRoute', () => { it('ensures the license allows getting alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAlertRoute(router, licenseState); @@ -99,7 +99,7 @@ describe('getAlertRoute', () => { alertsClient.get.mockResolvedValueOnce(mockedAlert); const [context, req, res] = mockHandlerArguments( - alertsClient, + { alertsClient }, { params: { id: '1' }, }, @@ -113,7 +113,7 @@ describe('getAlertRoute', () => { it('ensures the license check prevents getting alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -126,7 +126,7 @@ describe('getAlertRoute', () => { alertsClient.get.mockResolvedValueOnce(mockedAlert); const [context, req, res] = mockHandlerArguments( - alertsClient, + { alertsClient }, { params: { id: '1' }, }, diff --git a/x-pack/plugins/alerting/server/routes/get.ts b/x-pack/plugins/alerting/server/routes/get.ts index 39fbe64b62182..6d652d1304f65 100644 --- a/x-pack/plugins/alerting/server/routes/get.ts +++ b/x-pack/plugins/alerting/server/routes/get.ts @@ -33,9 +33,9 @@ export const getAlertRoute = (router: IRouter, licenseState: LicenseState) => { }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, any, any>, + req: KibanaRequest, unknown, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts b/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts index eb51c96b88e5e..20a420ca00986 100644 --- a/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts +++ b/x-pack/plugins/alerting/server/routes/get_alert_state.test.ts @@ -5,10 +5,10 @@ */ import { getAlertStateRoute } from './get_alert_state'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; -import { SavedObjectsErrorHelpers } from 'src/core/server/saved_objects'; +import { SavedObjectsErrorHelpers } from 'src/core/server'; import { alertsClientMock } from '../alerts_client.mock'; const alertsClient = alertsClientMock.create(); @@ -41,7 +41,7 @@ describe('getAlertStateRoute', () => { it('gets alert state', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAlertStateRoute(router, licenseState); @@ -84,7 +84,7 @@ describe('getAlertStateRoute', () => { it('returns NO-CONTENT when alert exists but has no task state yet', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAlertStateRoute(router, licenseState); @@ -127,7 +127,7 @@ describe('getAlertStateRoute', () => { it('returns NOT-FOUND when alert is not found', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); getAlertStateRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/get_alert_state.ts b/x-pack/plugins/alerting/server/routes/get_alert_state.ts index c5493c4abf57a..552bfea22a42b 100644 --- a/x-pack/plugins/alerting/server/routes/get_alert_state.ts +++ b/x-pack/plugins/alerting/server/routes/get_alert_state.ts @@ -33,9 +33,9 @@ export const getAlertStateRoute = (router: IRouter, licenseState: LicenseState) }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, any, any>, + req: KibanaRequest, unknown, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/health.test.ts b/x-pack/plugins/alerting/server/routes/health.test.ts index 42c83a7c04deb..7c50fbf561e59 100644 --- a/x-pack/plugins/alerting/server/routes/health.test.ts +++ b/x-pack/plugins/alerting/server/routes/health.test.ts @@ -5,7 +5,7 @@ */ import { healthRoute } from './health'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; import { verifyApiAccess } from '../lib/license_api_access'; @@ -22,7 +22,7 @@ beforeEach(() => { describe('healthRoute', () => { it('registers the route', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -35,7 +35,7 @@ describe('healthRoute', () => { }); it('queries the usage api', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -64,7 +64,7 @@ describe('healthRoute', () => { }); it('evaluates whether Encrypted Saved Objects is using an ephemeral encryption key', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -88,7 +88,7 @@ describe('healthRoute', () => { }); it('evaluates missing security info from the usage api to mean that the security plugin is disbled', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -112,7 +112,7 @@ describe('healthRoute', () => { }); it('evaluates missing security http info from the usage api to mean that the security plugin is disbled', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -136,7 +136,7 @@ describe('healthRoute', () => { }); it('evaluates security enabled, and missing ssl info from the usage api to mean that the user cannot generate keys', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -162,7 +162,7 @@ describe('healthRoute', () => { }); it('evaluates security enabled, SSL info present but missing http info from the usage api to mean that the user cannot generate keys', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); @@ -188,7 +188,7 @@ describe('healthRoute', () => { }); it('evaluates security and tls enabled to mean that the user can generate keys', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); const licenseState = mockLicenseState(); const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup(); diff --git a/x-pack/plugins/alerting/server/routes/health.ts b/x-pack/plugins/alerting/server/routes/health.ts index fa2358a1f181c..bfdbc95a7d2da 100644 --- a/x-pack/plugins/alerting/server/routes/health.ts +++ b/x-pack/plugins/alerting/server/routes/health.ts @@ -39,9 +39,9 @@ export function healthRoute( }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, + req: KibanaRequest, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); try { const { diff --git a/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts b/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts index 723fd86fca8b5..37b52f1ec7923 100644 --- a/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts +++ b/x-pack/plugins/alerting/server/routes/list_alert_types.test.ts @@ -5,7 +5,7 @@ */ import { listAlertTypesRoute } from './list_alert_types'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -21,7 +21,7 @@ beforeEach(() => { describe('listAlertTypesRoute', () => { it('lists alert types with proper parameters', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); listAlertTypesRoute(router, licenseState); @@ -47,6 +47,7 @@ describe('listAlertTypesRoute', () => { }, ], defaultActionGroupId: 'default', + actionVariables: [], }, ]; @@ -62,6 +63,7 @@ describe('listAlertTypesRoute', () => { "name": "Default", }, ], + "actionVariables": Array [], "defaultActionGroupId": "default", "id": "1", "name": "name", @@ -79,7 +81,7 @@ describe('listAlertTypesRoute', () => { it('ensures the license allows listing alert types', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); listAlertTypesRoute(router, licenseState); @@ -99,6 +101,14 @@ describe('listAlertTypesRoute', () => { id: '1', name: 'name', enabled: true, + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + actionVariables: [], }, ]; @@ -117,7 +127,7 @@ describe('listAlertTypesRoute', () => { it('ensures the license check prevents listing alert types', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); @@ -147,6 +157,7 @@ describe('listAlertTypesRoute', () => { }, ], defaultActionGroupId: 'default', + actionVariables: [], }, ]; diff --git a/x-pack/plugins/alerting/server/routes/list_alert_types.ts b/x-pack/plugins/alerting/server/routes/list_alert_types.ts index 455bc5e378b6d..7ab64cf932051 100644 --- a/x-pack/plugins/alerting/server/routes/list_alert_types.ts +++ b/x-pack/plugins/alerting/server/routes/list_alert_types.ts @@ -26,9 +26,9 @@ export const listAlertTypesRoute = (router: IRouter, licenseState: LicenseState) }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, + req: KibanaRequest, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/mute_all.test.ts b/x-pack/plugins/alerting/server/routes/mute_all.test.ts index 4c880e176d2df..5ef9e3694f8f4 100644 --- a/x-pack/plugins/alerting/server/routes/mute_all.test.ts +++ b/x-pack/plugins/alerting/server/routes/mute_all.test.ts @@ -5,7 +5,7 @@ */ import { muteAllAlertRoute } from './mute_all'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -22,7 +22,7 @@ beforeEach(() => { describe('muteAllAlertRoute', () => { it('mute an alert', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); muteAllAlertRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/mute_all.ts b/x-pack/plugins/alerting/server/routes/mute_all.ts index 29ef7d6b6b03b..d1b4322bd1ccb 100644 --- a/x-pack/plugins/alerting/server/routes/mute_all.ts +++ b/x-pack/plugins/alerting/server/routes/mute_all.ts @@ -33,9 +33,9 @@ export const muteAllAlertRoute = (router: IRouter, licenseState: LicenseState) = }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, any, any>, + req: KibanaRequest, unknown, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/mute_instance.test.ts b/x-pack/plugins/alerting/server/routes/mute_instance.test.ts index 939864972c35d..2e6adedb76df9 100644 --- a/x-pack/plugins/alerting/server/routes/mute_instance.test.ts +++ b/x-pack/plugins/alerting/server/routes/mute_instance.test.ts @@ -5,7 +5,7 @@ */ import { muteAlertInstanceRoute } from './mute_instance'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -22,7 +22,7 @@ beforeEach(() => { describe('muteAlertInstanceRoute', () => { it('mutes an alert instance', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); muteAlertInstanceRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/mute_instance.ts b/x-pack/plugins/alerting/server/routes/mute_instance.ts index 7a071b1535dc7..fbdda62836d74 100644 --- a/x-pack/plugins/alerting/server/routes/mute_instance.ts +++ b/x-pack/plugins/alerting/server/routes/mute_instance.ts @@ -34,9 +34,9 @@ export const muteAlertInstanceRoute = (router: IRouter, licenseState: LicenseSta }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, any, any>, + req: KibanaRequest, unknown, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/unmute_all.test.ts b/x-pack/plugins/alerting/server/routes/unmute_all.test.ts index cd14e9b2e7172..1756dbd3fb41d 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_all.test.ts +++ b/x-pack/plugins/alerting/server/routes/unmute_all.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { unmuteAllAlertRoute } from './unmute_all'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -21,7 +21,7 @@ beforeEach(() => { describe('unmuteAllAlertRoute', () => { it('unmutes an alert', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); unmuteAllAlertRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/unmute_all.ts b/x-pack/plugins/alerting/server/routes/unmute_all.ts index 81e28a81874cd..e09f2fe6b8b93 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_all.ts +++ b/x-pack/plugins/alerting/server/routes/unmute_all.ts @@ -33,9 +33,9 @@ export const unmuteAllAlertRoute = (router: IRouter, licenseState: LicenseState) }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, any, any>, + req: KibanaRequest, unknown, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts b/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts index d74934f691710..9b9542c606741 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts +++ b/x-pack/plugins/alerting/server/routes/unmute_instance.test.ts @@ -5,7 +5,7 @@ */ import { unmuteAlertInstanceRoute } from './unmute_instance'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -22,7 +22,7 @@ beforeEach(() => { describe('unmuteAlertInstanceRoute', () => { it('unmutes an alert instance', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); unmuteAlertInstanceRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/unmute_instance.ts b/x-pack/plugins/alerting/server/routes/unmute_instance.ts index de081ae7f1fcb..64ba22dc3ea0b 100644 --- a/x-pack/plugins/alerting/server/routes/unmute_instance.ts +++ b/x-pack/plugins/alerting/server/routes/unmute_instance.ts @@ -34,9 +34,9 @@ export const unmuteAlertInstanceRoute = (router: IRouter, licenseState: LicenseS }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, any, any>, + req: KibanaRequest, unknown, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/update.test.ts b/x-pack/plugins/alerting/server/routes/update.test.ts index c3628617f861f..cd96f289b8714 100644 --- a/x-pack/plugins/alerting/server/routes/update.test.ts +++ b/x-pack/plugins/alerting/server/routes/update.test.ts @@ -5,7 +5,7 @@ */ import { updateAlertRoute } from './update'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { verifyApiAccess } from '../lib/license_api_access'; import { mockHandlerArguments } from './_mock_handler_arguments'; @@ -45,7 +45,7 @@ describe('updateAlertRoute', () => { it('updates an alert with proper parameters', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); updateAlertRoute(router, licenseState); @@ -128,7 +128,7 @@ describe('updateAlertRoute', () => { it('ensures the license allows updating alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); updateAlertRoute(router, licenseState); @@ -171,7 +171,7 @@ describe('updateAlertRoute', () => { it('ensures the license check prevents updating alerts', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); (verifyApiAccess as jest.Mock).mockImplementation(() => { throw new Error('OMG'); diff --git a/x-pack/plugins/alerting/server/routes/update.ts b/x-pack/plugins/alerting/server/routes/update.ts index 45f7b26b521d4..7f07749311598 100644 --- a/x-pack/plugins/alerting/server/routes/update.ts +++ b/x-pack/plugins/alerting/server/routes/update.ts @@ -56,9 +56,9 @@ export const updateAlertRoute = (router: IRouter, licenseState: LicenseState) => handleDisabledApiKeysError( router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, TypeOf, any>, + req: KibanaRequest, unknown, TypeOf>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/routes/update_api_key.test.ts b/x-pack/plugins/alerting/server/routes/update_api_key.test.ts index 5e9821ac005e2..0347feb24a235 100644 --- a/x-pack/plugins/alerting/server/routes/update_api_key.test.ts +++ b/x-pack/plugins/alerting/server/routes/update_api_key.test.ts @@ -5,7 +5,7 @@ */ import { updateApiKeyRoute } from './update_api_key'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockLicenseState } from '../lib/license_state.mock'; import { mockHandlerArguments } from './_mock_handler_arguments'; import { alertsClientMock } from '../alerts_client.mock'; @@ -22,7 +22,7 @@ beforeEach(() => { describe('updateApiKeyRoute', () => { it('updates api key for an alert', async () => { const licenseState = mockLicenseState(); - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); updateApiKeyRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/update_api_key.ts b/x-pack/plugins/alerting/server/routes/update_api_key.ts index f70d30f0bb5da..9d0c34fc1a015 100644 --- a/x-pack/plugins/alerting/server/routes/update_api_key.ts +++ b/x-pack/plugins/alerting/server/routes/update_api_key.ts @@ -35,9 +35,9 @@ export const updateApiKeyRoute = (router: IRouter, licenseState: LicenseState) = handleDisabledApiKeysError( router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, any, any, any>, + req: KibanaRequest, unknown, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { verifyApiAccess(licenseState); if (!context.alerting) { return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' }); diff --git a/x-pack/plugins/alerting/server/task_runner/alert_task_instance.ts b/x-pack/plugins/alerting/server/task_runner/alert_task_instance.ts index 9f5e3851aa29d..4be506b78493b 100644 --- a/x-pack/plugins/alerting/server/task_runner/alert_task_instance.ts +++ b/x-pack/plugins/alerting/server/task_runner/alert_task_instance.ts @@ -7,10 +7,17 @@ import * as t from 'io-ts'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { ConcreteTaskInstance } from '../../../../plugins/task_manager/server'; -import { SanitizedAlert, AlertTaskState, alertParamsSchema, alertStateSchema } from '../../common'; +import { + SanitizedAlert, + AlertTaskState, + alertParamsSchema, + alertStateSchema, + AlertTaskParams, +} from '../../common'; export interface AlertTaskInstance extends ConcreteTaskInstance { state: AlertTaskState; + params: AlertTaskParams; } const enumerateErrorFields = (e: t.Errors) => diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts index 756080baba626..0e46ef4919626 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.test.ts @@ -51,6 +51,7 @@ const createExecutionHandlerParams = { beforeEach(() => { jest.resetAllMocks(); createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + createExecutionHandlerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); }); test('calls actionsPlugin.execute per selected action', async () => { @@ -111,6 +112,7 @@ test('calls actionsPlugin.execute per selected action', async () => { test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () => { // Mock two calls, one for check against actions[0] and the second for actions[1] + createExecutionHandlerParams.actionsPlugin.isActionExecutable.mockReturnValueOnce(false); createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValueOnce(false); createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValueOnce(true); const executionHandler = createExecutionHandler({ @@ -148,6 +150,50 @@ test(`doesn't call actionsPlugin.execute for disabled actionTypes`, async () => }); }); +test('trow error error message when action type is disabled', async () => { + createExecutionHandlerParams.actionsPlugin.preconfiguredActions = []; + createExecutionHandlerParams.actionsPlugin.isActionExecutable.mockReturnValue(false); + createExecutionHandlerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(false); + const executionHandler = createExecutionHandler({ + ...createExecutionHandlerParams, + actions: [ + ...createExecutionHandlerParams.actions, + { + id: '2', + group: 'default', + actionTypeId: '.slack', + params: { + foo: true, + contextVal: 'My other {{context.value}} goes here', + stateVal: 'My other {{state.value}} goes here', + }, + }, + ], + }); + + await executionHandler({ + actionGroup: 'default', + state: {}, + context: {}, + alertInstanceId: '2', + }); + + expect(createExecutionHandlerParams.actionsPlugin.execute).toHaveBeenCalledTimes(0); + + createExecutionHandlerParams.actionsPlugin.isActionExecutable.mockImplementation(() => true); + const executionHandlerForPreconfiguredAction = createExecutionHandler({ + ...createExecutionHandlerParams, + actions: [...createExecutionHandlerParams.actions], + }); + await executionHandlerForPreconfiguredAction({ + actionGroup: 'default', + state: {}, + context: {}, + alertInstanceId: '2', + }); + expect(createExecutionHandlerParams.actionsPlugin.execute).toHaveBeenCalledTimes(1); +}); + test('limits actionsPlugin.execute per action group', async () => { const executionHandler = createExecutionHandler(createExecutionHandlerParams); await executionHandler({ diff --git a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts index 72f9e70905dc2..5c3e36b88879d 100644 --- a/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerting/server/task_runner/create_execution_handler.ts @@ -71,7 +71,7 @@ export function createExecutionHandler({ const alertLabel = `${alertType.id}:${alertId}: '${alertName}'`; for (const action of actions) { - if (!actionsPlugin.isActionTypeEnabled(action.actionTypeId)) { + if (!actionsPlugin.isActionExecutable(action.id, action.actionTypeId)) { logger.warn( `Alert "${alertId}" skipped scheduling action "${action.id}" because it is disabled` ); diff --git a/x-pack/plugins/alerting/server/task_runner/get_next_run_at.test.ts b/x-pack/plugins/alerting/server/task_runner/get_next_run_at.test.ts index 1c4d8a42d2830..f5914fdf01a16 100644 --- a/x-pack/plugins/alerting/server/task_runner/get_next_run_at.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/get_next_run_at.test.ts @@ -7,6 +7,7 @@ import { getNextRunAt } from './get_next_run_at'; const mockedNow = new Date('2019-06-03T18:55:25.982Z'); +// eslint-disable-next-line @typescript-eslint/no-explicit-any (global as any).Date = class Date extends global.Date { static now() { return mockedNow.getTime(); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 31cc893f785cb..0f600c7df7bf7 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -185,6 +185,7 @@ describe('Task Runner', () => { test('actionsPlugin.execute is called per alert instance that is scheduled', async () => { taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); alertType.executor.mockImplementation( ({ services: executorServices }: AlertExecutorOptions) => { executorServices.alertInstanceFactory('1').scheduleActions('default'); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 1d4b12e96bc76..9c8cf4b1c968d 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -5,7 +5,7 @@ */ import { pick, mapValues, omit, without } from 'lodash'; -import { Logger, SavedObject } from '../../../../../src/core/server'; +import { Logger, SavedObject, KibanaRequest } from '../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; import { ConcreteTaskInstance } from '../../../../plugins/task_manager/server'; import { createExecutionHandler } from './create_execution_handler'; @@ -93,7 +93,7 @@ export class TaskRunner { }, }; - return this.context.getServices(fakeRequest); + return this.context.getServices((fakeRequest as unknown) as KibanaRequest); } private getExecutionHandler( @@ -178,7 +178,7 @@ export class TaskRunner { }; eventLogger.startTiming(event); - let updatedAlertTypeState: void | Record; + let updatedAlertTypeState: void | Record; try { updatedAlertTypeState = await this.alertType.executor({ alertId, diff --git a/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts b/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts index e0cff58c4d40a..64f846d13c0bf 100644 --- a/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts +++ b/x-pack/plugins/alerting/server/task_runner/transform_action_params.ts @@ -29,7 +29,7 @@ export function transformActionParams({ actionParams, state, }: TransformActionParamsOptions): AlertActionParams { - const result = cloneDeep(actionParams, (value: any) => { + const result = cloneDeep(actionParams, (value: unknown) => { if (!isString(value)) return; // when the list of variables we pass in here changes, diff --git a/x-pack/plugins/alerting/server/test_utils/index.ts b/x-pack/plugins/alerting/server/test_utils/index.ts index be9c5493ccf2b..e8089984a786a 100644 --- a/x-pack/plugins/alerting/server/test_utils/index.ts +++ b/x-pack/plugins/alerting/server/test_utils/index.ts @@ -5,7 +5,7 @@ */ interface Resolvable { - resolve: (arg?: T) => void; + resolve: (arg: T) => void; } /** @@ -13,12 +13,10 @@ interface Resolvable { * coordinating async tests. */ export function resolvable(): Promise & Resolvable { - let resolve: (arg?: T) => void; - const result = new Promise(r => { - resolve = r; - }) as any; - - result.resolve = (arg: T) => resolve(arg); - - return result; + let resolve: (arg: T) => void; + return Object.assign(new Promise(r => (resolve = r)), { + resolve(arg: T) { + return setTimeout(() => resolve(arg), 0); + }, + }); } diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 739a0d0aece24..bc98cae65b4e6 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -7,15 +7,22 @@ import { AlertInstance } from './alert_instance'; import { AlertTypeRegistry as OrigAlertTypeRegistry } from './alert_type_registry'; import { PluginSetupContract, PluginStartContract } from './plugin'; -import { SavedObjectAttributes, SavedObjectsClientContract } from '../../../../src/core/server'; +import { + SavedObjectAttributes, + SavedObjectsClientContract, + KibanaRequest, +} from '../../../../src/core/server'; import { Alert, AlertActionParams, ActionGroup } from '../common'; import { AlertsClient } from './alerts_client'; export * from '../common'; +// This will have to remain `any` until we can extend Alert Executors with generics +// eslint-disable-next-line @typescript-eslint/no-explicit-any export type State = Record; +// eslint-disable-next-line @typescript-eslint/no-explicit-any export type Context = Record; export type WithoutQueryAndParams = Pick>; -export type GetServicesFunction = (request: any) => Services; +export type GetServicesFunction = (request: KibanaRequest) => Services; export type GetBasePathFunction = (spaceId?: string) => string; export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined; @@ -29,6 +36,8 @@ declare module 'src/core/server' { } export interface Services { + // This will have to remain `any` until we can extend Alert Services with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any callCluster(path: string, opts: any): Promise; savedObjectsClient: SavedObjectsClientContract; } @@ -42,6 +51,8 @@ export interface AlertExecutorOptions { startedAt: Date; previousStartedAt: Date | null; services: AlertServices; + // This will have to remain `any` until we can extend Alert Executors with generics + // eslint-disable-next-line @typescript-eslint/no-explicit-any params: Record; state: State; spaceId: string; @@ -61,7 +72,7 @@ export interface AlertType { id: string; name: string; validate?: { - params?: { validate: (object: any) => any }; + params?: { validate: (object: unknown) => AlertExecutorOptions['params'] }; }; actionGroups: ActionGroup[]; defaultActionGroupId: ActionGroup['id']; diff --git a/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts b/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts index 9c710fa3b3b8e..2edf1c1061864 100644 --- a/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts +++ b/x-pack/plugins/alerting/server/usage/alerts_telemetry.ts @@ -5,6 +5,7 @@ */ import { APICaller } from 'kibana/server'; +import { SearchResponse } from 'elasticsearch'; const alertTypeMetric = { scripted_metric: { @@ -246,6 +247,8 @@ export async function getTotalCountAggregations(callCluster: APICaller, kibanaIn return { count_total: totalAlertsCount, count_by_type: Object.keys(results.aggregations.byAlertTypeId.value.types).reduce( + // ES DSL aggregations are returned as `any` by callCluster + // eslint-disable-next-line @typescript-eslint/no-explicit-any (obj: any, key: string) => ({ ...obj, [key.replace('.', '__')]: results.aggregations.byAlertTypeId.value.types[key], @@ -284,7 +287,7 @@ export async function getTotalCountAggregations(callCluster: APICaller, kibanaIn } export async function getTotalCountInUse(callCluster: APICaller, kibanaInex: string) { - const searchResult = await callCluster('search', { + const searchResult: SearchResponse = await callCluster('search', { index: kibanaInex, rest_total_hits_as_int: true, body: { @@ -305,6 +308,8 @@ export async function getTotalCountInUse(callCluster: APICaller, kibanaInex: str 0 ), countByType: Object.keys(searchResult.aggregations.byAlertTypeId.value.types).reduce( + // ES DSL aggregations are returned as `any` by callCluster + // eslint-disable-next-line @typescript-eslint/no-explicit-any (obj: any, key: string) => ({ ...obj, [key.replace('.', '__')]: searchResult.aggregations.byAlertTypeId.value.types[key], diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type_params.test.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type_params.test.ts index 33d1e1897e943..c209894fb6e89 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type_params.test.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type_params.test.ts @@ -6,6 +6,7 @@ import { ParamsSchema, Params } from './alert_type_params'; import { runTests } from './lib/core_query_types.test'; +import { TypeOf } from '@kbn/config-schema'; const DefaultParams: Writable> = { index: 'index-name', @@ -21,6 +22,7 @@ const DefaultParams: Writable> = { describe('alertType Params validate()', () => { runTests(ParamsSchema, DefaultParams); + // eslint-disable-next-line @typescript-eslint/no-explicit-any let params: any; beforeEach(() => { params = { ...DefaultParams }; @@ -64,7 +66,7 @@ describe('alertType Params validate()', () => { return () => validate(); } - function validate(): any { + function validate(): TypeOf { return ParamsSchema.validate(params); } }); diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type_params.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type_params.ts index f83d7fa07cd2a..4a822156ebd06 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type_params.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type_params.ts @@ -30,12 +30,12 @@ export const ParamsSchema = schema.object( const betweenComparators = new Set(['between', 'notBetween']); // using direct type not allowed, circular reference, so body is typed to any -function validateParams(anyParams: any): string | undefined { +function validateParams(anyParams: unknown): string | undefined { // validate core query parts, return if it fails validation (returning string) const coreQueryValidated = validateCoreQueryBody(anyParams); if (coreQueryValidated) return coreQueryValidated; - const { thresholdComparator, threshold }: Params = anyParams; + const { thresholdComparator, threshold }: Params = anyParams as Params; if (betweenComparators.has(thresholdComparator) && threshold.length === 1) { return i18n.translate('xpack.alertingBuiltins.indexThreshold.invalidThreshold2ErrorMessage', { diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.test.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.test.ts index 109785b835bdf..6c9c3542aea03 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.test.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.test.ts @@ -19,7 +19,8 @@ const DefaultParams: Writable> = { timeWindowUnit: 'm', }; -export function runTests(schema: ObjectType, defaultTypeParams: Record): void { +export function runTests(schema: ObjectType, defaultTypeParams: Record): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any let params: any; describe('coreQueryTypes', () => { @@ -186,7 +187,7 @@ export function runTests(schema: ObjectType, defaultTypeParams: Record validate(); } - function validate(): any { + function validate(): unknown { return schema.validate(params); } } diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.ts index 6e9c0072bf7b6..c8da61fb56d21 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/core_query_types.ts @@ -41,9 +41,15 @@ export type CoreQueryParams = TypeOf; // Meant to be used in a "subclass"'s schema body validator, so the // anyParams object is assumed to have been validated with the schema // above. -// Using direct type not allowed, circular reference, so body is typed to any. -export function validateCoreQueryBody(anyParams: any): string | undefined { - const { aggType, aggField, groupBy, termField, termSize }: CoreQueryParams = anyParams; +// Using direct type not allowed, circular reference, so body is typed to unknown. +export function validateCoreQueryBody(anyParams: unknown): string | undefined { + const { + aggType, + aggField, + groupBy, + termField, + termSize, + }: CoreQueryParams = anyParams as CoreQueryParams; if (aggType !== 'count' && !aggField) { return i18n.translate('xpack.alertingBuiltins.indexThreshold.aggTypeRequiredErrorMessage', { defaultMessage: '[aggField]: must have a value when [aggType] is "{aggType}"', diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_query.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_query.ts index 1d9cc1c98bc01..9c4133be6f483 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_query.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_query.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SearchResponse } from 'elasticsearch'; import { DEFAULT_GROUPS } from '../index'; import { getDateRangeInfo } from './date_range_info'; import { Logger, CallCluster } from '../../../types'; @@ -35,6 +36,8 @@ export async function timeSeriesQuery( const dateRangeInfo = getDateRangeInfo({ dateStart, dateEnd, window, interval }); // core query + // Constructing a typesafe ES query in JS is problematic, use any escapehatch for now + // eslint-disable-next-line @typescript-eslint/no-explicit-any const esQuery: any = { index, body: { @@ -122,7 +125,7 @@ export async function timeSeriesQuery( }; } - let esResult: any; + let esResult: SearchResponse; const logPrefix = 'indexThreshold timeSeriesQuery: callCluster'; logger.debug(`${logPrefix} call: ${JSON.stringify(esQuery)}`); @@ -147,7 +150,7 @@ export async function timeSeriesQuery( function getResultFromEs( isCountAgg: boolean, isGroupAgg: boolean, - esResult: Record + esResult: SearchResponse ): TimeSeriesResult { const aggregations = esResult?.aggregations || {}; diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.test.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.test.ts index fcbd49b26ffd0..ec164122032cb 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.test.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.test.ts @@ -6,6 +6,7 @@ import { TimeSeriesQuerySchema, TimeSeriesQuery } from './time_series_types'; import { runTests } from './core_query_types.test'; +import { TypeOf } from '@kbn/config-schema'; const DefaultParams: Writable> = { index: 'index-name', @@ -19,6 +20,7 @@ const DefaultParams: Writable> = { describe('TimeSeriesParams validate()', () => { runTests(TimeSeriesQuerySchema, DefaultParams); + // eslint-disable-next-line @typescript-eslint/no-explicit-any let params: any; beforeEach(() => { params = { ...DefaultParams }; @@ -102,7 +104,7 @@ describe('TimeSeriesParams validate()', () => { return () => validate(); } - function validate(): any { + function validate(): TypeOf { return TimeSeriesQuerySchema.validate(params); } }); diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts index abe5d562027eb..40e6f187ce18f 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/lib/time_series_types.ts @@ -48,13 +48,13 @@ export const TimeSeriesQuerySchema = schema.object( } ); -// using direct type not allowed, circular reference, so body is typed to any -function validateBody(anyParams: any): string | undefined { +// using direct type not allowed, circular reference, so body is typed to unknown +function validateBody(anyParams: unknown): string | undefined { // validate core query parts, return if it fails validation (returning string) const coreQueryValidated = validateCoreQueryBody(anyParams); if (coreQueryValidated) return coreQueryValidated; - const { dateStart, dateEnd, interval }: TimeSeriesQuery = anyParams; + const { dateStart, dateEnd, interval } = anyParams as TimeSeriesQuery; // dates already validated in validateDate(), if provided const epochStart = dateStart ? Date.parse(dateStart) : undefined; diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/fields.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/fields.ts index 32d6409d9c9fb..5cc41671f6167 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/fields.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/fields.ts @@ -38,7 +38,7 @@ export function createFieldsRoute(service: Service, router: IRouter, baseRoute: ); async function handler( ctx: RequestHandlerContext, - req: KibanaRequest, + req: KibanaRequest, res: KibanaResponseFactory ): Promise { service.logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`); diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/indices.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/indices.ts index c08450448b44c..ebcf6b4f0e45a 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/indices.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/indices.ts @@ -18,6 +18,7 @@ import { KibanaResponseFactory, IScopedClusterClient, } from 'kibana/server'; +import { SearchResponse } from 'elasticsearch'; import { Service } from '../../../types'; const bodySchema = schema.object({ @@ -40,7 +41,7 @@ export function createIndicesRoute(service: Service, router: IRouter, baseRoute: ); async function handler( ctx: RequestHandlerContext, - req: KibanaRequest, + req: KibanaRequest, res: KibanaResponseFactory ): Promise { const pattern = req.body.pattern; @@ -102,12 +103,14 @@ async function getIndicesFromPattern( }, }, }; - const response = await dataClient.callAsCurrentUser('search', params); - if (response.status === 404 || !response.aggregations) { + const response: SearchResponse = await dataClient.callAsCurrentUser('search', params); + // TODO: Investigate when the status field might appear here, type suggests it shouldn't ever happen + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((response as any).status === 404 || !response.aggregations) { return []; } - return response.aggregations.indices.buckets.map((bucket: any) => bucket.key); + return (response.aggregations as IndiciesAggregation).indices.buckets.map(bucket => bucket.key); } async function getAliasesFromPattern( @@ -137,3 +140,9 @@ async function getAliasesFromPattern( return result; } + +interface IndiciesAggregation { + indices: { + buckets: Array<{ key: string }>; + }; +} diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/time_series_query.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/time_series_query.ts index c8129c2428ee4..201c82060f386 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/time_series_query.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/routes/time_series_query.ts @@ -30,7 +30,7 @@ export function createTimeSeriesQueryRoute(service: Service, router: IRouter, ba ); async function handler( ctx: RequestHandlerContext, - req: KibanaRequest, + req: KibanaRequest, res: KibanaResponseFactory ): Promise { service.logger.debug(`route ${path} request: ${JSON.stringify(req.body)}`); diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts index 8cfd736a336c2..6268f5899d7ff 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts @@ -143,11 +143,15 @@ export const agentConfigurationSearchRoute = createRoute(core => ({ params: { body: t.intersection([ t.type({ service: serviceRt }), - t.partial({ etag: t.string }) + t.partial({ etag: t.string, mark_as_applied_by_agent: t.boolean }) ]) }, handler: async ({ context, request }) => { - const { service, etag } = context.params.body; + const { + service, + etag, + mark_as_applied_by_agent: markAsAppliedByAgent + } = context.params.body; const setup = await setupRequest(context, request); const config = await searchConfigurations({ @@ -166,9 +170,14 @@ export const agentConfigurationSearchRoute = createRoute(core => ({ `Config was found for ${service.name}/${service.environment}` ); - // update `applied_by_agent` field if etags match + // update `applied_by_agent` field + // when `markAsAppliedByAgent` is true (Jaeger agent doesn't have etags) + // or if etags match. // this happens in the background and doesn't block the response - if (etag === config._source.etag && !config._source.applied_by_agent) { + if ( + (markAsAppliedByAgent || etag === config._source.etag) && + !config._source.applied_by_agent + ) { markAppliedByAgent({ id: config._id, body: config._source, setup }); } diff --git a/x-pack/plugins/apm/server/tutorial/index_pattern.json b/x-pack/plugins/apm/server/tutorial/index_pattern.json index ede9ba54681c0..e0d9595585b88 100644 --- a/x-pack/plugins/apm/server/tutorial/index_pattern.json +++ b/x-pack/plugins/apm/server/tutorial/index_pattern.json @@ -1,7 +1,7 @@ { "attributes": { "fieldFormatMap": "{\"client.bytes\":{\"id\":\"bytes\"},\"client.nat.port\":{\"id\":\"string\"},\"client.port\":{\"id\":\"string\"},\"destination.bytes\":{\"id\":\"bytes\"},\"destination.nat.port\":{\"id\":\"string\"},\"destination.port\":{\"id\":\"string\"},\"event.duration\":{\"id\":\"duration\",\"params\":{\"inputFormat\":\"nanoseconds\",\"outputFormat\":\"asMilliseconds\",\"outputPrecision\":1}},\"event.sequence\":{\"id\":\"string\"},\"event.severity\":{\"id\":\"string\"},\"http.request.body.bytes\":{\"id\":\"bytes\"},\"http.request.bytes\":{\"id\":\"bytes\"},\"http.response.body.bytes\":{\"id\":\"bytes\"},\"http.response.bytes\":{\"id\":\"bytes\"},\"http.response.status_code\":{\"id\":\"string\"},\"log.syslog.facility.code\":{\"id\":\"string\"},\"log.syslog.priority\":{\"id\":\"string\"},\"network.bytes\":{\"id\":\"bytes\"},\"package.size\":{\"id\":\"string\"},\"process.parent.pgid\":{\"id\":\"string\"},\"process.parent.pid\":{\"id\":\"string\"},\"process.parent.ppid\":{\"id\":\"string\"},\"process.parent.thread.id\":{\"id\":\"string\"},\"process.pgid\":{\"id\":\"string\"},\"process.pid\":{\"id\":\"string\"},\"process.ppid\":{\"id\":\"string\"},\"process.thread.id\":{\"id\":\"string\"},\"server.bytes\":{\"id\":\"bytes\"},\"server.nat.port\":{\"id\":\"string\"},\"server.port\":{\"id\":\"string\"},\"source.bytes\":{\"id\":\"bytes\"},\"source.nat.port\":{\"id\":\"string\"},\"source.port\":{\"id\":\"string\"},\"system.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.memory.actual.free\":{\"id\":\"bytes\"},\"system.memory.total\":{\"id\":\"bytes\"},\"system.process.cpu.total.norm.pct\":{\"id\":\"percent\"},\"system.process.memory.rss.bytes\":{\"id\":\"bytes\"},\"system.process.memory.size\":{\"id\":\"bytes\"},\"url.port\":{\"id\":\"string\"},\"view spans\":{\"id\":\"url\",\"params\":{\"labelTemplate\":\"View Spans\"}}}", - "fields": "[{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"@timestamp\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.availability_zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.machine.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.region\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.tag\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.runtime\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.data\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.ttl\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.header_flags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.op_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.resolved_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.response_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"ecs.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.stack_trace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.stack_trace.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.dataset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.end\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.ingested\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.kind\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.outcome\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score_norm\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.sequence\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.timezone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.url\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.accessed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.attributes\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.ctime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.device\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.drive_letter\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.gid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.group\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.inode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mime_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mtime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.owner\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.method\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.referrer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.status_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.logger\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.priority\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.application\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.community_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.direction\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.forwarded_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.iana_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.transport\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.build_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.checksum\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.install_scope\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.installed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args_count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.command_line\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.command_line.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.entity_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.exit_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.args_count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.command_line\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.command_line.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.entity_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.executable.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.exit_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.title.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.working_directory.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.strings\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.hive\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.value\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.user\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.author\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.ruleset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.uuid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.state\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.framework\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.cipher\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.certificate\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.certificate_chain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.issuer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.ja3\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.server_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.subject\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.supported_ciphers\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.established\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.next_protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.resumed\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.certificate\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.certificate_chain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.issuer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.ja3s\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.subject\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.version_protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.fragment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.password\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.query\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.scheme\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.username\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.device.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.classification\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.description.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.enumeration\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.report_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.scanner.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.base\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.environmental\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.temporal\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"fields\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"timeseries.instance\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.image.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"docker.container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.containerized\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.build\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.codename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.namespace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.labels.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.annotations.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.replicaset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.deployment.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.statefulset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.image\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.event\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"timestamp.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.request.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.finished\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.response.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.environment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.sampled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.breakdown.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.subtype\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"parent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.listening\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version_major\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"experimental\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.culprit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.grouping_key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.handled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.logger_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.param_message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.total\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.actual.free\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.rss.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.cpu.ns\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.samples.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.bundle_filepath\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"view spans\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.start.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.sync\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.link\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.rows_affected\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.resource\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.message.queue.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.message.age.ms\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.result\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks.*.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.span_count.dropped\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.message.queue.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.message.age.ms\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_id\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_index\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_score\",\"scripted\":false,\"searchable\":false,\"type\":\"number\"}]", + "fields": "[{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"@timestamp\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"client.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.account.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.availability_zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.instance.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.machine.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.region\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.image.tag\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"container.runtime\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"destination.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dll.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.data\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.ttl\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.answers.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.header_flags\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.op_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.class\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.subdomain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.question.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.resolved_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.response_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"dns.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"ecs.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.stack_trace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.stack_trace.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.dataset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.end\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.ingested\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.kind\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.outcome\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.provider\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.risk_score_norm\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.sequence\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.timezone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"event.url\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.accessed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.attributes\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.created\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.ctime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.device\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.drive_letter\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.gid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.group\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.inode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mime_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mode\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.mtime\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.owner\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.path.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.target_path.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.body.content.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.method\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.request.referrer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.body.content.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.status_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.logger\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.file.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.origin.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.facility.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.priority\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"log.syslog.severity.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.application\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.community_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.direction\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.forwarded_ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.iana_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.inner.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.transport\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"network.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.egress.zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.alias\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.interface.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ingress.zone\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.build_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.checksum\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.install_scope\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.installed\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"package.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.args_count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.command_line\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.command_line.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.entity_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.executable.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.exit_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.args\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.args_count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.exists\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.status\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.subject_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.trusted\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.code_signature.valid\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.command_line\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.command_line.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.entity_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.executable\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.executable.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.exit_code\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.hash.sha512\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.title.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.working_directory.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pe.product\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pgid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.pid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.ppid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.start\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.id\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.thread.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.title.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.uptime\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.working_directory.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.strings\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.data.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.hive\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"registry.value\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"related.user\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.author\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.license\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.ruleset\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.uuid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"rule.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"server.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.ephemeral_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.state\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.address\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.number\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.as.organization.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.city_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.continent_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.country_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.location\",\"scripted\":false,\"searchable\":true,\"type\":\"geo_point\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_iso_code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.geo.region_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.mac\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.ip\",\"scripted\":false,\"searchable\":true,\"type\":\"ip\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.nat.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.packets\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"source.user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.framework\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.tactic.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.cipher\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.certificate\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.certificate_chain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.issuer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.ja3\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.server_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.subject\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.supported_ciphers\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.established\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.next_protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.resumed\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.certificate\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.certificate_chain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.md5\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.sha1\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.hash.sha256\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.issuer\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.ja3s\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.subject\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.version_protocol\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tracing.transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.extension\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.fragment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.password\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.path\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.port\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.query\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.registered_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.scheme\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.top_level_domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"url.username\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.email\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.full_name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.domain\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.group.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.hash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.device.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.original.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.family\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.full.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.kernel\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.platform\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.os.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"user_agent.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vlan.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vlan.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.category\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.classification\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.description.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.enumeration\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.reference\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.report_id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.scanner.vendor\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.base\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.environmental\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.temporal\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.score.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"vulnerability.severity\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"agent.hostname\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"fields\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"timeseries.instance\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.image.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"docker.container.labels\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.containerized\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.build\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"host.os.codename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.pod.uid\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.namespace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.node.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.labels.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.annotations.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.replicaset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.deployment.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.statefulset.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"kubernetes.container.image\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"processor.event\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"timestamp.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.request.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"http.response.finished\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"enabled\":false,\"indexed\":false,\"name\":\"http.response.headers\",\"scripted\":false,\"searchable\":false},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.environment\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.language.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.runtime.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"service.framework.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.sampled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.breakdown.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.subtype\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.self_time.sum.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"trace.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"parent.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.listening\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"observer.version_major\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"experimental\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.culprit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.grouping_key\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.code\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.module\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":4,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.exception.handled\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.level\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.logger_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":2,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"error.log.param_message\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.total\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.memory.actual.free\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cpu.total.norm.pct\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.memory.rss.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.cpu.ns\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.samples.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.alloc_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_objects.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.inuse_space.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.duration\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.top.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.function\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.filename\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.stack.line\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.service.version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"sourcemap.bundle_filepath\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"view spans\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"child.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.action\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.start.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"span.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.sync\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.link\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.db.rows_affected\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.resource\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.message.queue.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.message.age.ms\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.us\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.result\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.marks.*.*\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.span_count.dropped\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.message.queue.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.message.age.ms\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_id\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_index\",\"scripted\":false,\"searchable\":false,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":false,\"indexed\":false,\"name\":\"_score\",\"scripted\":false,\"searchable\":false,\"type\":\"number\"}]", "sourceFilters": "[{\"value\":\"sourcemap.sourcemap\"}]", "timeFieldName": "@timestamp" }, diff --git a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts index e98b2f52089b3..f6c4fce76f134 100644 --- a/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts +++ b/x-pack/plugins/apm/typings/es_schemas/raw/span_raw.ts @@ -52,5 +52,5 @@ export interface SpanRaw extends APMBaseDoc { id: string; }; observer?: Observer; - child_ids?: string[]; + child?: { id: string[] }; } diff --git a/x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts b/x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts index f21a9c25b6e64..285a998030cf6 100644 --- a/x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts +++ b/x-pack/plugins/canvas/server/routes/custom_elements/update.test.ts @@ -45,7 +45,6 @@ const customElement: CustomElementPayload = { name: 'MyCustomElement', displayName: 'My Wonderful Custom Element', content: 'This is content', - tags: ['filter', 'graphic'], '@created': '2019-02-08T18:35:23.029Z', '@timestamp': '2019-02-08T18:35:23.029Z', }; diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts index 47f7d503e32b8..29df97c5f8476 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts @@ -39,7 +39,7 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout if (myCaseConfigure.saved_objects.length === 0) { throw Boom.conflict( - 'You can not patch this configuration since you did not created first with a post' + 'You can not patch this configuration since you did not created first with a post.' ); } diff --git a/x-pack/plugins/endpoint/common/generate_data.ts b/x-pack/plugins/endpoint/common/generate_data.ts index 1978d780f54f5..4f5a6eaeb0a5e 100644 --- a/x-pack/plugins/endpoint/common/generate_data.ts +++ b/x-pack/plugins/endpoint/common/generate_data.ts @@ -6,7 +6,16 @@ import uuid from 'uuid'; import seedrandom from 'seedrandom'; -import { AlertEvent, EndpointEvent, HostMetadata, OSFields, HostFields, PolicyData } from './types'; +import { + AlertEvent, + EndpointEvent, + HostFields, + HostMetadata, + OSFields, + PolicyData, + HostPolicyResponse, + HostPolicyResponseActionStatus, +} from './types'; import { factory as policyFactory } from './models/policy_config'; export type Event = AlertEvent | EndpointEvent; @@ -486,6 +495,112 @@ export class EndpointDocGenerator { }; } + /** + * Generates a Host Policy response message + */ + generatePolicyResponse(): HostPolicyResponse { + return { + '@timestamp': new Date().toISOString(), + elastic: { + agent: { + id: 'c2a9093e-e289-4c0a-aa44-8c32a414fa7a', + }, + }, + ecs: { + version: '1.0.0', + }, + event: { + created: '2015-01-01T12:10:30Z', + kind: 'policy_response', + }, + agent: { + version: '6.0.0-rc2', + id: '8a4f500d', + }, + endpoint: { + artifacts: { + 'global-manifest': { + version: '1.2.3', + sha256: 'abcdef', + }, + 'endpointpe-v4-windows': { + version: '1.2.3', + sha256: 'abcdef', + }, + 'user-whitelist-windows': { + version: '1.2.3', + sha256: 'abcdef', + }, + 'global-whitelist-windows': { + version: '1.2.3', + sha256: 'abcdef', + }, + }, + policy: { + applied: { + version: '1.0.0', + id: '17d4b81d-9940-4b64-9de5-3e03ef1fb5cf', + status: HostPolicyResponseActionStatus.success, + response: { + configurations: { + malware: { + status: HostPolicyResponseActionStatus.success, + concerned_actions: ['download_model', 'workflow', 'a_custom_future_action'], + }, + events: { + status: HostPolicyResponseActionStatus.success, + concerned_actions: ['ingest_events_config', 'workflow'], + }, + logging: { + status: HostPolicyResponseActionStatus.success, + concerned_actions: ['configure_elasticsearch_connection'], + }, + streaming: { + status: HostPolicyResponseActionStatus.success, + concerned_actions: [ + 'detect_file_open_events', + 'download_global_artifacts', + 'a_custom_future_action', + ], + }, + }, + actions: { + download_model: { + status: HostPolicyResponseActionStatus.success, + message: 'model downloaded', + }, + ingest_events_config: { + status: HostPolicyResponseActionStatus.success, + message: 'no action taken', + }, + workflow: { + status: HostPolicyResponseActionStatus.success, + message: 'the flow worked well', + }, + a_custom_future_action: { + status: HostPolicyResponseActionStatus.success, + message: 'future message', + }, + configure_elasticsearch_connection: { + status: HostPolicyResponseActionStatus.success, + message: 'some message', + }, + detect_file_open_events: { + status: HostPolicyResponseActionStatus.success, + message: 'some message', + }, + download_global_artifacts: { + status: HostPolicyResponseActionStatus.success, + message: 'some message', + }, + }, + }, + }, + }, + }, + }; + } + private randomN(n: number): number { return Math.floor(this.random() * n); } diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index 49f8ebbd580d8..4da4f9bc797ca 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -573,3 +573,103 @@ export type NewPolicyData = NewDatasource & { } ]; }; + +/** + * the possible status for actions, configurations and overall Policy Response + */ +export enum HostPolicyResponseActionStatus { + success = 'success', + failure = 'failure', + warning = 'warning', +} + +/** + * The details of a given action + */ +interface HostPolicyResponseActionDetails { + status: HostPolicyResponseActionStatus; + message: string; +} + +/** + * A known list of possible Endpoint actions + */ +interface HostPolicyResponseActions { + download_model: HostPolicyResponseActionDetails; + ingest_events_config: HostPolicyResponseActionDetails; + workflow: HostPolicyResponseActionDetails; + configure_elasticsearch_connection: HostPolicyResponseActionDetails; + configure_kernel: HostPolicyResponseActionDetails; + configure_logging: HostPolicyResponseActionDetails; + configure_malware: HostPolicyResponseActionDetails; + connect_kernel: HostPolicyResponseActionDetails; + detect_file_open_events: HostPolicyResponseActionDetails; + detect_file_write_events: HostPolicyResponseActionDetails; + detect_image_load_events: HostPolicyResponseActionDetails; + detect_process_events: HostPolicyResponseActionDetails; + download_global_artifacts: HostPolicyResponseActionDetails; + load_config: HostPolicyResponseActionDetails; + load_malware_model: HostPolicyResponseActionDetails; + read_elasticsearch_config: HostPolicyResponseActionDetails; + read_events_config: HostPolicyResponseActionDetails; + read_kernel_config: HostPolicyResponseActionDetails; + read_logging_config: HostPolicyResponseActionDetails; + read_malware_config: HostPolicyResponseActionDetails; + // The list of possible Actions will change rapidly, so the below entry will allow + // them without us defining them here statically + [key: string]: HostPolicyResponseActionDetails; +} + +interface HostPolicyResponseConfigurationStatus { + status: HostPolicyResponseActionStatus; + concerned_actions: Array; +} + +/** + * Information about the applying of a policy to a given host + */ +export interface HostPolicyResponse { + '@timestamp': string; + elastic: { + agent: { + id: string; + }; + }; + ecs: { + version: string; + }; + event: { + created: string; + kind: string; + }; + agent: { + version: string; + id: string; + }; + endpoint: { + artifacts: {}; + policy: { + applied: { + version: string; + id: string; + status: HostPolicyResponseActionStatus; + response: { + configurations: { + malware: HostPolicyResponseConfigurationStatus; + events: HostPolicyResponseConfigurationStatus; + logging: HostPolicyResponseConfigurationStatus; + streaming: HostPolicyResponseConfigurationStatus; + }; + actions: Partial; + }; + }; + }; + }; +} + +/** + * REST API response for retrieving a host's Policy Response status + */ +export interface GetHostPolicyResponse { + policy_response: HostPolicyResponse; +} diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts index 21871ec8ca849..16a1f96c926b8 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/action.ts @@ -4,14 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HostListPagination, ServerApiError } from '../../types'; -import { HostResultList, HostInfo } from '../../../../../common/types'; +import { ServerApiError } from '../../types'; +import { HostResultList, HostInfo, GetHostPolicyResponse } from '../../../../../common/types'; interface ServerReturnedHostList { type: 'serverReturnedHostList'; payload: HostResultList; } +interface ServerFailedToReturnHostList { + type: 'serverFailedToReturnHostList'; + payload: ServerApiError; +} + interface ServerReturnedHostDetails { type: 'serverReturnedHostDetails'; payload: HostInfo; @@ -22,13 +27,14 @@ interface ServerFailedToReturnHostDetails { payload: ServerApiError; } -interface UserPaginatedHostList { - type: 'userPaginatedHostList'; - payload: HostListPagination; +interface ServerReturnedHostPolicyResponse { + type: 'serverReturnedHostPolicyResponse'; + payload: GetHostPolicyResponse; } export type HostAction = | ServerReturnedHostList + | ServerFailedToReturnHostList | ServerReturnedHostDetails | ServerFailedToReturnHostDetails - | UserPaginatedHostList; + | ServerReturnedHostPolicyResponse; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/host_pagination.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/host_pagination.test.ts new file mode 100644 index 0000000000000..d2e1985d055c6 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/host_pagination.test.ts @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CoreStart, HttpSetup } from 'kibana/public'; +import { DepsStartMock, depsStartMock } from '../../mocks'; +import { AppAction, HostState, HostIndexUIQueryParams } from '../../types'; +import { Immutable, HostResultList } from '../../../../../common/types'; +import { History, createBrowserHistory } from 'history'; +import { hostMiddlewareFactory } from './middleware'; +import { applyMiddleware, Store, createStore } from 'redux'; +import { hostListReducer } from './reducer'; +import { coreMock } from 'src/core/public/mocks'; +import { urlFromQueryParams } from '../../view/hosts/url_from_query_params'; +import { uiQueryParams } from './selectors'; +import { mockHostResultList } from './mock_host_result_list'; +import { MiddlewareActionSpyHelper, createSpyMiddleware } from '../test_utils'; + +describe('host list pagination: ', () => { + let fakeCoreStart: jest.Mocked; + let depsStart: DepsStartMock; + let fakeHttpServices: jest.Mocked; + let history: History; + let store: Store, Immutable>; + let queryParams: () => HostIndexUIQueryParams; + let waitForAction: MiddlewareActionSpyHelper['waitForAction']; + let actionSpyMiddleware; + const getEndpointListApiResponse = (): HostResultList => { + return mockHostResultList({ request_page_size: 1, request_page_index: 1, total: 10 }); + }; + + let historyPush: (params: HostIndexUIQueryParams) => void; + beforeEach(() => { + fakeCoreStart = coreMock.createStart(); + depsStart = depsStartMock(); + fakeHttpServices = fakeCoreStart.http as jest.Mocked; + history = createBrowserHistory(); + const middleware = hostMiddlewareFactory(fakeCoreStart, depsStart); + ({ actionSpyMiddleware, waitForAction } = createSpyMiddleware()); + store = createStore(hostListReducer, applyMiddleware(middleware, actionSpyMiddleware)); + + history.listen(location => { + store.dispatch({ type: 'userChangedUrl', payload: location }); + }); + + queryParams = () => uiQueryParams(store.getState()); + + historyPush = (nextQueryParams: HostIndexUIQueryParams): void => { + return history.push(urlFromQueryParams(nextQueryParams)); + }; + }); + + describe('when the user enteres the host list for the first time', () => { + it('the api is called with page_index and page_size defaulting to 0 and 10 respectively', async () => { + const apiResponse = getEndpointListApiResponse(); + fakeHttpServices.post.mockResolvedValue(apiResponse); + expect(fakeHttpServices.post).not.toHaveBeenCalled(); + + store.dispatch({ + type: 'userChangedUrl', + payload: { + ...history.location, + pathname: '/hosts', + }, + }); + await waitForAction('serverReturnedHostList'); + expect(fakeHttpServices.post).toHaveBeenCalledWith('/api/endpoint/metadata', { + body: JSON.stringify({ + paging_properties: [{ page_index: '0' }, { page_size: '10' }], + }), + }); + }); + }); + describe('when a new page size is passed', () => { + it('should modify the url correctly', () => { + historyPush({ ...queryParams(), page_size: '20' }); + expect(queryParams()).toMatchInlineSnapshot(` + Object { + "page_index": "0", + "page_size": "20", + } + `); + }); + }); + describe('when an invalid page size is passed', () => { + it('should modify the page size in the url to the default page size', () => { + historyPush({ ...queryParams(), page_size: '1' }); + expect(queryParams()).toEqual({ page_index: '0', page_size: '10' }); + }); + }); + + describe('when a negative page size is passed', () => { + it('should modify the page size in the url to the default page size', () => { + historyPush({ ...queryParams(), page_size: '-1' }); + expect(queryParams()).toEqual({ page_index: '0', page_size: '10' }); + }); + }); + + describe('when a new page index is passed', () => { + it('should modify the page index in the url correctly', () => { + historyPush({ ...queryParams(), page_index: '2' }); + expect(queryParams()).toEqual({ page_index: '2', page_size: '10' }); + }); + }); + + describe('when a negative page index is passed', () => { + it('should modify the page index in the url to the default page index', () => { + historyPush({ ...queryParams(), page_index: '-2' }); + expect(queryParams()).toEqual({ page_index: '0', page_size: '10' }); + }); + }); + + describe('when invalid params are passed in the url', () => { + it('ignores non-numeric values for page_index and page_size', () => { + historyPush({ ...queryParams, page_index: 'one', page_size: 'fifty' }); + expect(queryParams()).toEqual({ page_index: '0', page_size: '10' }); + }); + + it('ignores unknown url search params', () => { + store.dispatch({ + type: 'userChangedUrl', + payload: { + ...history.location, + pathname: '/hosts', + search: '?foo=bar', + }, + }); + expect(queryParams()).toEqual({ page_index: '0', page_size: '10' }); + }); + + it('ignores multiple values of the same query params except the last value', () => { + store.dispatch({ + type: 'userChangedUrl', + payload: { + ...history.location, + pathname: '/hosts', + search: '?page_index=2&page_index=3&page_size=20&page_size=50', + }, + }); + expect(queryParams()).toEqual({ page_index: '3', page_size: '50' }); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts index 6148934343635..515c54eab3280 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/index.test.ts @@ -6,12 +6,12 @@ import { createStore, Dispatch, Store } from 'redux'; import { HostAction, hostListReducer } from './index'; -import { HostListState } from '../../types'; +import { HostState } from '../../types'; import { listData } from './selectors'; import { mockHostResultList } from './mock_host_result_list'; describe('HostList store concerns', () => { - let store: Store; + let store: Store; let dispatch: Dispatch; const createTestStore = () => { store = createStore(hostListReducer); @@ -37,6 +37,11 @@ describe('HostList store concerns', () => { pageIndex: 0, total: 0, loading: false, + error: undefined, + details: undefined, + detailsLoading: false, + detailsError: undefined, + location: undefined, }); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts index 8f39baddda00e..1af83a975d1d8 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.test.ts @@ -9,21 +9,23 @@ import { coreMock } from '../../../../../../../../src/core/public/mocks'; import { History, createBrowserHistory } from 'history'; import { hostListReducer, hostMiddlewareFactory } from './index'; import { HostResultList, Immutable } from '../../../../../common/types'; -import { HostListState } from '../../types'; +import { HostState } from '../../types'; import { AppAction } from '../action'; import { listData } from './selectors'; import { DepsStartMock, depsStartMock } from '../../mocks'; import { mockHostResultList } from './mock_host_result_list'; +import { createSpyMiddleware, MiddlewareActionSpyHelper } from '../test_utils'; describe('host list middleware', () => { - const sleep = (ms = 100) => new Promise(wakeup => setTimeout(wakeup, ms)); let fakeCoreStart: jest.Mocked; let depsStart: DepsStartMock; let fakeHttpServices: jest.Mocked; - type HostListStore = Store, Immutable>; + type HostListStore = Store, Immutable>; let store: HostListStore; let getState: HostListStore['getState']; let dispatch: HostListStore['dispatch']; + let waitForAction: MiddlewareActionSpyHelper['waitForAction']; + let actionSpyMiddleware; let history: History; const getEndpointListApiResponse = (): HostResultList => { @@ -33,15 +35,16 @@ describe('host list middleware', () => { fakeCoreStart = coreMock.createStart({ basePath: '/mock' }); depsStart = depsStartMock(); fakeHttpServices = fakeCoreStart.http as jest.Mocked; + ({ actionSpyMiddleware, waitForAction } = createSpyMiddleware()); store = createStore( hostListReducer, - applyMiddleware(hostMiddlewareFactory(fakeCoreStart, depsStart)) + applyMiddleware(hostMiddlewareFactory(fakeCoreStart, depsStart), actionSpyMiddleware) ); getState = store.getState; dispatch = store.dispatch; history = createBrowserHistory(); }); - test('handles `userChangedUrl`', async () => { + it('handles `userChangedUrl`', async () => { const apiResponse = getEndpointListApiResponse(); fakeHttpServices.post.mockResolvedValue(apiResponse); expect(fakeHttpServices.post).not.toHaveBeenCalled(); @@ -53,10 +56,10 @@ describe('host list middleware', () => { pathname: '/hosts', }, }); - await sleep(); + await waitForAction('serverReturnedHostList'); expect(fakeHttpServices.post).toHaveBeenCalledWith('/api/endpoint/metadata', { body: JSON.stringify({ - paging_properties: [{ page_index: 0 }, { page_size: 10 }], + paging_properties: [{ page_index: '0' }, { page_size: '10' }], }), }); expect(listData(getState())).toEqual(apiResponse.hosts.map(hostInfo => hostInfo.metadata)); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts index 83e11f5408bcd..d1b9a2cde4b31 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/middleware.ts @@ -4,34 +4,65 @@ * you may not use this file except in compliance with the Elastic License. */ +import { HostResultList } from '../../../../../common/types'; +import { isOnHostPage, hasSelectedHost, uiQueryParams, listData } from './selectors'; +import { HostState } from '../../types'; import { ImmutableMiddlewareFactory } from '../../types'; -import { pageIndex, pageSize, isOnHostPage, hasSelectedHost, uiQueryParams } from './selectors'; -import { HostListState } from '../../types'; +import { HostPolicyResponse } from '../../../../../common/types'; -export const hostMiddlewareFactory: ImmutableMiddlewareFactory = coreStart => { +export const hostMiddlewareFactory: ImmutableMiddlewareFactory = coreStart => { return ({ getState, dispatch }) => next => async action => { next(action); const state = getState(); if ( - (action.type === 'userChangedUrl' && - isOnHostPage(state) && - hasSelectedHost(state) !== true) || - action.type === 'userPaginatedHostList' + action.type === 'userChangedUrl' && + isOnHostPage(state) && + hasSelectedHost(state) !== true ) { - const hostPageIndex = pageIndex(state); - const hostPageSize = pageSize(state); - const response = await coreStart.http.post('/api/endpoint/metadata', { - body: JSON.stringify({ - paging_properties: [{ page_index: hostPageIndex }, { page_size: hostPageSize }], - }), - }); - response.request_page_index = hostPageIndex; - dispatch({ - type: 'serverReturnedHostList', - payload: response, - }); + const { page_index: pageIndex, page_size: pageSize } = uiQueryParams(state); + try { + const response = await coreStart.http.post('/api/endpoint/metadata', { + body: JSON.stringify({ + paging_properties: [{ page_index: pageIndex }, { page_size: pageSize }], + }), + }); + response.request_page_index = Number(pageIndex); + dispatch({ + type: 'serverReturnedHostList', + payload: response, + }); + } catch (error) { + dispatch({ + type: 'serverFailedToReturnHostList', + payload: error, + }); + } } if (action.type === 'userChangedUrl' && hasSelectedHost(state) !== false) { + // If user navigated directly to a host details page, load the host list + if (listData(state).length === 0) { + const { page_index: pageIndex, page_size: pageSize } = uiQueryParams(state); + try { + const response = await coreStart.http.post('/api/endpoint/metadata', { + body: JSON.stringify({ + paging_properties: [{ page_index: pageIndex }, { page_size: pageSize }], + }), + }); + response.request_page_index = Number(pageIndex); + dispatch({ + type: 'serverReturnedHostList', + payload: response, + }); + } catch (error) { + dispatch({ + type: 'serverFailedToReturnHostList', + payload: error, + }); + return; + } + } + + // call the host details api const { selected_host: selectedHost } = uiQueryParams(state); try { const response = await coreStart.http.get(`/api/endpoint/metadata/${selectedHost}`); @@ -39,6 +70,21 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory = type: 'serverReturnedHostDetails', payload: response, }); + // FIXME: once we have the API implementation in place, we should call it parallel with the above api call and then dispatch this with the results of the second call + dispatch({ + type: 'serverReturnedHostPolicyResponse', + payload: { + policy_response: ({ + endpoint: { + policy: { + applied: { + status: 'success', + }, + }, + }, + } as unknown) as HostPolicyResponse, // Temporary until we get API + }, + }); } catch (error) { dispatch({ type: 'serverFailedToReturnHostDetails', diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts index 298e819645dbe..eb74c40ff3687 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/reducer.ts @@ -4,23 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HostListState, ImmutableReducer } from '../../types'; +import { Immutable } from '../../../../../common/types'; +import { HostState, ImmutableReducer } from '../../types'; import { AppAction } from '../action'; +import { isOnHostPage, hasSelectedHost } from './selectors'; -const initialState = (): HostListState => { +const initialState = (): HostState => { return { hosts: [], pageSize: 10, pageIndex: 0, total: 0, loading: false, - detailsError: undefined, + error: undefined, details: undefined, + detailsLoading: false, + detailsError: undefined, + policyResponse: undefined, location: undefined, }; }; -export const hostListReducer: ImmutableReducer = ( +export const hostListReducer: ImmutableReducer = ( state = initialState(), action ) => { @@ -38,30 +43,82 @@ export const hostListReducer: ImmutableReducer = ( pageSize, pageIndex, loading: false, + error: undefined, + }; + } else if (action.type === 'serverFailedToReturnHostList') { + return { + ...state, + error: action.payload, + loading: false, }; } else if (action.type === 'serverReturnedHostDetails') { return { ...state, details: action.payload.metadata, + detailsLoading: false, + detailsError: undefined, }; } else if (action.type === 'serverFailedToReturnHostDetails') { return { ...state, detailsError: action.payload, + detailsLoading: false, }; - } else if (action.type === 'userPaginatedHostList') { + } else if (action.type === 'serverReturnedHostPolicyResponse') { return { ...state, - ...action.payload, - loading: true, + policyResponse: action.payload.policy_response, }; } else if (action.type === 'userChangedUrl') { + const newState: Immutable = { + ...state, + location: action.payload, + }; + const isCurrentlyOnListPage = isOnHostPage(newState) && !hasSelectedHost(newState); + const wasPreviouslyOnListPage = isOnHostPage(state) && !hasSelectedHost(state); + const isCurrentlyOnDetailsPage = isOnHostPage(newState) && hasSelectedHost(newState); + const wasPreviouslyOnDetailsPage = isOnHostPage(state) && hasSelectedHost(state); + + // if on the host list page for the first time, return new location and load list + if (isCurrentlyOnListPage) { + if (!wasPreviouslyOnListPage) { + return { + ...state, + location: action.payload, + loading: true, + error: undefined, + detailsError: undefined, + }; + } + } else if (isCurrentlyOnDetailsPage) { + // if previous page was the list or another host details page, load host details only + if (wasPreviouslyOnDetailsPage || wasPreviouslyOnListPage) { + return { + ...state, + location: action.payload, + detailsLoading: true, + error: undefined, + detailsError: undefined, + }; + } else { + // if previous page was not host list or host details, load both list and details + return { + ...state, + location: action.payload, + loading: true, + detailsLoading: true, + error: undefined, + detailsError: undefined, + }; + } + } + // otherwise we are not on a host list or details page return { ...state, location: action.payload, + error: undefined, detailsError: undefined, }; } - return state; }; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts index 03cdba8505800..b0711baf9cdff 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/hosts/selectors.ts @@ -6,38 +6,47 @@ import querystring from 'querystring'; import { createSelector } from 'reselect'; import { Immutable } from '../../../../../common/types'; -import { HostListState, HostIndexUIQueryParams } from '../../types'; +import { HostState, HostIndexUIQueryParams } from '../../types'; -export const listData = (state: Immutable) => state.hosts; +const PAGE_SIZES = Object.freeze([10, 20, 50]); -export const pageIndex = (state: Immutable) => state.pageIndex; +export const listData = (state: Immutable) => state.hosts; -export const pageSize = (state: Immutable) => state.pageSize; +export const pageIndex = (state: Immutable): number => state.pageIndex; -export const totalHits = (state: Immutable) => state.total; +export const pageSize = (state: Immutable): number => state.pageSize; -export const isLoading = (state: Immutable) => state.loading; +export const totalHits = (state: Immutable): number => state.total; -export const detailsError = (state: Immutable) => state.detailsError; +export const listLoading = (state: Immutable): boolean => state.loading; -export const detailsData = (state: Immutable) => { - return state.details; -}; +export const listError = (state: Immutable) => state.error; -export const isOnHostPage = (state: Immutable) => +export const detailsData = (state: Immutable) => state.details; + +export const detailsLoading = (state: Immutable): boolean => state.detailsLoading; + +export const detailsError = (state: Immutable) => state.detailsError; + +export const isOnHostPage = (state: Immutable) => state.location ? state.location.pathname === '/hosts' : false; export const uiQueryParams: ( - state: Immutable + state: Immutable ) => Immutable = createSelector( - (state: Immutable) => state.location, - (location: Immutable['location']) => { - const data: HostIndexUIQueryParams = {}; + (state: Immutable) => state.location, + (location: Immutable['location']) => { + const data: HostIndexUIQueryParams = { page_index: '0', page_size: '10' }; if (location) { // Removes the `?` from the beginning of query string if it exists const query = querystring.parse(location.search.slice(1)); - const keys: Array = ['selected_host', 'show']; + const keys: Array = [ + 'selected_host', + 'page_size', + 'page_index', + 'show', + ]; for (const key of keys) { const value = query[key]; @@ -47,12 +56,23 @@ export const uiQueryParams: ( data[key] = value[value.length - 1]; } } + + // Check if page size is an expected size, otherwise default to 10 + if (!PAGE_SIZES.includes(Number(data.page_size))) { + data.page_size = '10'; + } + + // Check if page index is a valid positive integer, otherwise default to 0 + const pageIndexAsNumber = Number(data.page_index); + if (!Number.isFinite(pageIndexAsNumber) || pageIndexAsNumber < 0) { + data.page_index = '0'; + } } return data; } ); -export const hasSelectedHost: (state: Immutable) => boolean = createSelector( +export const hasSelectedHost: (state: Immutable) => boolean = createSelector( uiQueryParams, ({ selected_host: selectedHost }) => { return selectedHost !== undefined; @@ -60,9 +80,19 @@ export const hasSelectedHost: (state: Immutable) => boolean = cre ); /** What policy details panel view to show */ -export const showView: (state: HostListState) => 'policy_response' | 'details' = createSelector( +export const showView: (state: HostState) => 'policy_response' | 'details' = createSelector( uiQueryParams, searchParams => { return searchParams.show === 'policy_response' ? 'policy_response' : 'details'; } ); + +/** + * Returns the Policy Response overall status + */ +export const policyResponseStatus: (state: Immutable) => string = createSelector( + state => state.policyResponse, + policyResponse => { + return (policyResponse && policyResponse?.endpoint?.policy?.applied?.status) || ''; + } +); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts index f407d32cb3b42..0598ce5f38efa 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/types.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/types.ts @@ -22,6 +22,7 @@ import { MalwareFields, UIPolicyConfig, PolicyData, + HostPolicyResponse, } from '../../../common/types'; import { EndpointPluginStartDependencies } from '../../plugin'; import { AppAction } from './store/action'; @@ -88,23 +89,42 @@ export type SubstateMiddlewareFactory = ( middleware: ImmutableMiddleware ) => Middleware<{}, GlobalState, Dispatch>>; -export interface HostListState { +export interface HostState { + /** list of host **/ hosts: HostMetadata[]; + /** number of items per page */ pageSize: number; + /** which page to show */ pageIndex: number; + /** total number of hosts returned */ total: number; + /** list page is retrieving data */ loading: boolean; - detailsError?: ServerApiError; + /** api error from retrieving host list */ + error?: ServerApiError; + /** details data for a specific host */ details?: Immutable; + /** details page is retrieving data */ + detailsLoading: boolean; + /** api error from retrieving host details */ + detailsError?: ServerApiError; + /** Holds the Policy Response for the Host currently being displayed in the details */ + policyResponse?: HostPolicyResponse; + /** current location info */ location?: Immutable; } -export interface HostListPagination { - pageIndex: number; - pageSize: number; -} +/** + * Query params on the host page parsed from the URL + */ export interface HostIndexUIQueryParams { + /** Selected host id shows host details flyout */ selected_host?: string; + /** How many items to show in list */ + page_size?: string; + /** Which page to show */ + page_index?: string; + /** show the policy response or host details */ show?: string; } @@ -257,7 +277,7 @@ export type KeysByValueCriteria = { export type MalwareProtectionOSes = KeysByValueCriteria; export interface GlobalState { - readonly hostList: HostListState; + readonly hostList: HostState; readonly alertList: AlertListState; readonly policyList: PolicyListState; readonly policyDetails: PolicyDetailsState; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx index 336308b2ee271..7d948f54bd0bc 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx @@ -16,12 +16,12 @@ import { import React, { memo, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { HostMetadata } from '../../../../../../common/types'; +import { HostMetadata, HostPolicyResponseActionStatus } from '../../../../../../common/types'; import { FormattedDateAndTime } from '../../formatted_date_time'; import { LinkToApp } from '../../components/link_to_app'; -import { useHostListSelector, useHostLogsUrl } from '../hooks'; +import { useHostSelector, useHostLogsUrl } from '../hooks'; import { urlFromQueryParams } from '../url_from_query_params'; -import { uiQueryParams } from '../../../store/hosts/selectors'; +import { policyResponseStatus, uiQueryParams } from '../../../store/hosts/selectors'; import { useNavigateByRouterEventHandler } from '../../hooks/use_navigate_by_router_event_handler'; const HostIds = styled(EuiListGroupItem)` @@ -31,9 +31,20 @@ const HostIds = styled(EuiListGroupItem)` } `; +const POLICY_STATUS_TO_HEALTH_COLOR = Object.freeze< + { [key in keyof typeof HostPolicyResponseActionStatus]: string } +>({ + success: 'success', + warning: 'warning', + failure: 'danger', +}); + export const HostDetails = memo(({ details }: { details: HostMetadata }) => { const { appId, appPath, url } = useHostLogsUrl(details.host.id); - const queryParams = useHostListSelector(uiQueryParams); + const queryParams = useHostSelector(uiQueryParams); + const policyStatus = useHostSelector( + policyResponseStatus + ) as keyof typeof POLICY_STATUS_TO_HEALTH_COLOR; const detailsResultsUpper = useMemo(() => { return [ { @@ -79,7 +90,10 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { defaultMessage: 'Policy Status', }), description: ( - + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} { onClick={policyStatusClickHandler} > @@ -126,6 +141,7 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { details.host.ip, policyResponseUri.search, policyStatusClickHandler, + policyStatus, ]); return ( diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx index 0c43e18822508..e44a45f300daa 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx @@ -17,9 +17,15 @@ import { useHistory } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; -import { useHostListSelector } from '../hooks'; +import { useHostSelector } from '../hooks'; import { urlFromQueryParams } from '../url_from_query_params'; -import { uiQueryParams, detailsData, detailsError, showView } from '../../../store/hosts/selectors'; +import { + uiQueryParams, + detailsData, + detailsError, + showView, + detailsLoading, +} from '../../../store/hosts/selectors'; import { HostDetails } from './host_details'; import { PolicyResponse } from './policy_response'; import { HostMetadata } from '../../../../../../common/types'; @@ -29,11 +35,12 @@ import { useNavigateByRouterEventHandler } from '../../hooks/use_navigate_by_rou export const HostDetailsFlyout = memo(() => { const history = useHistory(); const { notifications } = useKibana(); - const queryParams = useHostListSelector(uiQueryParams); + const queryParams = useHostSelector(uiQueryParams); const { selected_host: selectedHost, ...queryParamsWithoutSelectedHost } = queryParams; - const details = useHostListSelector(detailsData); - const error = useHostListSelector(detailsError); - const show = useHostListSelector(showView); + const details = useHostSelector(detailsData); + const loading = useHostSelector(detailsLoading); + const error = useHostSelector(detailsError); + const show = useHostSelector(showView); const handleFlyoutClose = useCallback(() => { history.push(urlFromQueryParams(queryParamsWithoutSelectedHost)); @@ -64,7 +71,7 @@ export const HostDetailsFlyout = memo(() => {

- {details === undefined ? : details.host.hostname} + {loading ? : details?.host?.hostname}

@@ -93,7 +100,7 @@ export const HostDetailsFlyout = memo(() => { const PolicyResponseFlyoutPanel = memo<{ hostMeta: HostMetadata; }>(({ hostMeta }) => { - const { show, ...queryParams } = useHostListSelector(uiQueryParams); + const { show, ...queryParams } = useHostSelector(uiQueryParams); const detailsUri = useMemo( () => urlFromQueryParams({ diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/hooks.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/hooks.ts index 7eb51f3a7b294..eb242f5c535f4 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/hooks.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/hooks.ts @@ -6,10 +6,10 @@ import { useSelector } from 'react-redux'; import { useMemo } from 'react'; -import { GlobalState, HostListState } from '../../types'; +import { GlobalState, HostState } from '../../types'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -export function useHostListSelector(selector: (state: HostListState) => TSelected) { +export function useHostSelector(selector: (state: HostState) => TSelected) { return useSelector(function(state: GlobalState) { return selector(state.hostList); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx index 88416b577ed0c..499efb4f4b8ed 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx @@ -14,9 +14,11 @@ import { mockHostResultList, } from '../../store/hosts/mock_host_result_list'; import { AppContextTestRender, createAppRootMockRenderer } from '../../mocks'; -import { HostInfo } from '../../../../../common/types'; +import { HostInfo, HostPolicyResponseActionStatus } from '../../../../../common/types'; +import { EndpointDocGenerator } from '../../../../../common/generate_data'; describe('when on the hosts page', () => { + const docGenerator = new EndpointDocGenerator(); let render: () => ReturnType; let history: AppContextTestRender['history']; let store: AppContextTestRender['store']; @@ -91,6 +93,19 @@ describe('when on the hosts page', () => { describe('when there is a selected host in the url', () => { let hostDetails: HostInfo; + const dispatchServerReturnedHostPolicyResponse = ( + overallStatus: HostPolicyResponseActionStatus = HostPolicyResponseActionStatus.success + ) => { + const policyResponse = docGenerator.generatePolicyResponse(); + policyResponse.endpoint.policy.applied.status = overallStatus; + store.dispatch({ + type: 'serverReturnedHostPolicyResponse', + payload: { + policy_response: policyResponse, + }, + }); + }; + beforeEach(() => { const { host_status, @@ -137,9 +152,8 @@ describe('when on the hosts page', () => { const renderResult = render(); const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); expect(policyStatusLink).not.toBeNull(); - expect(policyStatusLink.textContent).toEqual('Successful'); expect(policyStatusLink.getAttribute('href')).toEqual( - '?selected_host=1&show=policy_response' + '?page_index=0&page_size=10&selected_host=1&show=policy_response' ); }); it('should update the URL when policy status link is clicked', async () => { @@ -150,7 +164,61 @@ describe('when on the hosts page', () => { fireEvent.click(policyStatusLink); }); const changedUrlAction = await userChangedUrlChecker; - expect(changedUrlAction.payload.search).toEqual('?selected_host=1&show=policy_response'); + expect(changedUrlAction.payload.search).toEqual( + '?page_index=0&page_size=10&selected_host=1&show=policy_response' + ); + }); + it('should display Success overall policy status', async () => { + const renderResult = render(); + reactTestingLibrary.act(() => { + dispatchServerReturnedHostPolicyResponse(HostPolicyResponseActionStatus.success); + }); + const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); + expect(policyStatusLink.textContent).toEqual('Success'); + + const policyStatusHealth = await renderResult.findByTestId('policyStatusHealth'); + expect( + policyStatusHealth.querySelector('[data-euiicon-type][color="success"]') + ).not.toBeNull(); + }); + it('should display Warning overall policy status', async () => { + const renderResult = render(); + reactTestingLibrary.act(() => { + dispatchServerReturnedHostPolicyResponse(HostPolicyResponseActionStatus.warning); + }); + const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); + expect(policyStatusLink.textContent).toEqual('Warning'); + + const policyStatusHealth = await renderResult.findByTestId('policyStatusHealth'); + expect( + policyStatusHealth.querySelector('[data-euiicon-type][color="warning"]') + ).not.toBeNull(); + }); + it('should display Failed overall policy status', async () => { + const renderResult = render(); + reactTestingLibrary.act(() => { + dispatchServerReturnedHostPolicyResponse(HostPolicyResponseActionStatus.failure); + }); + const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); + expect(policyStatusLink.textContent).toEqual('Failed'); + + const policyStatusHealth = await renderResult.findByTestId('policyStatusHealth'); + expect( + policyStatusHealth.querySelector('[data-euiicon-type][color="danger"]') + ).not.toBeNull(); + }); + it('should display Unknown overall policy status', async () => { + const renderResult = render(); + reactTestingLibrary.act(() => { + dispatchServerReturnedHostPolicyResponse('' as HostPolicyResponseActionStatus); + }); + const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); + expect(policyStatusLink.textContent).toEqual('Unknown'); + + const policyStatusHealth = await renderResult.findByTestId('policyStatusHealth'); + expect( + policyStatusHealth.querySelector('[data-euiicon-type][color="subdued"]') + ).not.toBeNull(); }); it('should include the link to logs', async () => { const renderResult = render(); @@ -170,11 +238,11 @@ describe('when on the hosts page', () => { }); }); - it('should navigate to logs without full page refresh', async () => { + it('should navigate to logs without full page refresh', () => { expect(coreStart.application.navigateToApp.mock.calls).toHaveLength(1); }); }); - describe('when showing host Policy Response', () => { + describe('when showing host Policy Response panel', () => { let renderResult: ReturnType; beforeEach(async () => { renderResult = render(); @@ -205,7 +273,9 @@ describe('when on the hosts page', () => { it('should include the back to details link', async () => { const subHeaderBackLink = await renderResult.findByTestId('flyoutSubHeaderBackButton'); expect(subHeaderBackLink.textContent).toBe('Endpoint Details'); - expect(subHeaderBackLink.getAttribute('href')).toBe('?selected_host=1'); + expect(subHeaderBackLink.getAttribute('href')).toBe( + '?page_index=0&page_size=10&selected_host=1' + ); }); it('should update URL when back to details link is clicked', async () => { const subHeaderBackLink = await renderResult.findByTestId('flyoutSubHeaderBackButton'); @@ -214,7 +284,9 @@ describe('when on the hosts page', () => { fireEvent.click(subHeaderBackLink); }); const changedUrlAction = await userChangedUrlChecker; - expect(changedUrlAction.payload.search).toEqual('?selected_host=1'); + expect(changedUrlAction.payload.search).toEqual( + '?page_index=0&page_size=10&selected_host=1' + ); }); }); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx index e662bafed6492..5c2922162ce0c 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx @@ -5,31 +5,19 @@ */ import React, { useMemo, useCallback, memo } from 'react'; -import { useDispatch } from 'react-redux'; -import { - EuiPage, - EuiPageBody, - EuiPageHeader, - EuiPageContent, - EuiHorizontalRule, - EuiTitle, - EuiBasicTable, - EuiText, - EuiLink, - EuiHealth, -} from '@elastic/eui'; +import { EuiHorizontalRule, EuiBasicTable, EuiText, EuiLink, EuiHealth } from '@elastic/eui'; +import { useHistory } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; -import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; import { createStructuredSelector } from 'reselect'; import { EuiBasicTableColumn } from '@elastic/eui'; import { HostDetailsFlyout } from './details'; import * as selectors from '../../store/hosts/selectors'; -import { HostAction } from '../../store/hosts/action'; -import { useHostListSelector } from './hooks'; +import { useHostSelector } from './hooks'; import { CreateStructuredSelector } from '../../types'; import { urlFromQueryParams } from './url_from_query_params'; import { HostMetadata, Immutable } from '../../../../../common/types'; +import { PageView } from '../components/page_view'; import { useNavigateByRouterEventHandler } from '../hooks/use_navigate_by_router_event_handler'; const HostLink = memo<{ @@ -49,16 +37,17 @@ const HostLink = memo<{ const selector = (createStructuredSelector as CreateStructuredSelector)(selectors); export const HostList = () => { - const dispatch = useDispatch<(a: HostAction) => void>(); + const history = useHistory(); const { listData, pageIndex, pageSize, totalHits: totalItemCount, - isLoading, + listLoading: loading, + listError, uiQueryParams: queryParams, hasSelectedHost, - } = useHostListSelector(selector); + } = useHostSelector(selector); const paginationSetup = useMemo(() => { return { @@ -73,12 +62,15 @@ export const HostList = () => { const onTableChange = useCallback( ({ page }: { page: { index: number; size: number } }) => { const { index, size } = page; - dispatch({ - type: 'userPaginatedHostList', - payload: { pageIndex: index, pageSize: size }, - }); + history.push( + urlFromQueryParams({ + ...queryParams, + page_index: JSON.stringify(index), + page_size: JSON.stringify(size), + }) + ); }, - [dispatch] + [history, queryParams] ); const columns: Array>> = useMemo(() => { @@ -100,6 +92,7 @@ export const HostList = () => { name: i18n.translate('xpack.endpoint.host.list.policy', { defaultMessage: 'Policy', }), + truncateText: true, render: () => { return 'Policy Name'; }, @@ -134,6 +127,7 @@ export const HostList = () => { name: i18n.translate('xpack.endpoint.host.list.ip', { defaultMessage: 'IP Address', }), + truncateText: true, }, { field: '', @@ -158,59 +152,29 @@ export const HostList = () => { }, [queryParams]); return ( - + {hasSelectedHost && } - - - - -

- -

-
-
- - - - - - - [...listData], [listData])} - columns={columns} - loading={isLoading} - pagination={paginationSetup} - onChange={onTableChange} - /> - -
-
-
+ + + + + [...listData], [listData])} + columns={columns} + loading={loading} + error={listError?.message} + pagination={paginationSetup} + onChange={onTableChange} + /> + ); }; - -const HostPage = styled.div` - .hostPage { - padding: 0; - } - .hostHeader { - background-color: ${props => props.theme.eui.euiColorLightestShade}; - border-bottom: ${props => props.theme.eui.euiBorderThin}; - padding: ${props => - props.theme.eui.euiSizeXL + - ' ' + - 0 + - props.theme.eui.euiSizeXL + - ' ' + - props.theme.eui.euiSizeL}; - margin-bottom: 0; - } - .hostPageContent { - border: none; - } -`; diff --git a/x-pack/plugins/endpoint/server/endpoint_app_context_services.ts b/x-pack/plugins/endpoint/server/endpoint_app_context_services.ts index 4d77c5c68c69f..b087405c7bc5b 100644 --- a/x-pack/plugins/endpoint/server/endpoint_app_context_services.ts +++ b/x-pack/plugins/endpoint/server/endpoint_app_context_services.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { IndexPatternRetriever } from './index_pattern'; -import { AgentService } from '../../ingest_manager/common/types'; +import { AgentService } from '../../ingest_manager/server'; /** * A singleton that holds shared services that are initialized during the start up phase diff --git a/x-pack/plugins/endpoint/server/index_pattern.ts b/x-pack/plugins/endpoint/server/index_pattern.ts index 05cfd91f2777a..dcedd27fc5a3d 100644 --- a/x-pack/plugins/endpoint/server/index_pattern.ts +++ b/x-pack/plugins/endpoint/server/index_pattern.ts @@ -5,7 +5,7 @@ */ import { Logger, LoggerFactory, RequestHandlerContext } from 'kibana/server'; import { EndpointAppConstants } from '../common/types'; -import { ESIndexPatternService } from '../../ingest_manager/common/types'; +import { ESIndexPatternService } from '../../ingest_manager/server'; export interface IndexPatternRetriever { getIndexPattern(ctx: RequestHandlerContext, datasetPath: string): Promise; diff --git a/x-pack/plugins/endpoint/server/mocks.ts b/x-pack/plugins/endpoint/server/mocks.ts index 8d677cf37f066..519ca15cf8427 100644 --- a/x-pack/plugins/endpoint/server/mocks.ts +++ b/x-pack/plugins/endpoint/server/mocks.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AgentService, IngestManagerStartupContract } from '../../ingest_manager/common/types'; +import { AgentService, IngestManagerStartContract } from '../../ingest_manager/server'; /** * Creates a mock IndexPatternRetriever for use in tests. @@ -46,9 +46,9 @@ export const createMockAgentService = (): jest.Mocked => { * @param indexPattern a string index pattern to return when called by a test * @returns the same value as `indexPattern` parameter */ -export const createMockIngestManagerStartupContract = ( +export const createMockIngestManagerStartContract = ( indexPattern: string -): IngestManagerStartupContract => { +): IngestManagerStartContract => { return { esIndexPatternService: { getESIndexPattern: jest.fn().mockResolvedValue(indexPattern), diff --git a/x-pack/plugins/endpoint/server/plugin.test.ts b/x-pack/plugins/endpoint/server/plugin.test.ts index 44b9c3eabbd6d..45e9591a14975 100644 --- a/x-pack/plugins/endpoint/server/plugin.test.ts +++ b/x-pack/plugins/endpoint/server/plugin.test.ts @@ -11,7 +11,7 @@ import { } from './plugin'; import { coreMock } from '../../../../src/core/server/mocks'; import { PluginSetupContract } from '../../features/server'; -import { createMockIngestManagerStartupContract } from './mocks'; +import { createMockIngestManagerStartContract } from './mocks'; describe('test endpoint plugin', () => { let plugin: EndpointPlugin; @@ -51,7 +51,7 @@ describe('test endpoint plugin', () => { it('test properly start plugin', async () => { mockedEndpointPluginStartDependencies = { - ingestManager: createMockIngestManagerStartupContract(''), + ingestManager: createMockIngestManagerStartContract(''), }; await plugin.start(mockCoreStart, mockedEndpointPluginStartDependencies); expect(plugin.getEndpointAppContextService().getAgentService()).toBeTruthy(); diff --git a/x-pack/plugins/endpoint/server/plugin.ts b/x-pack/plugins/endpoint/server/plugin.ts index f7ff32bdbfcbf..f3cc569ad16a7 100644 --- a/x-pack/plugins/endpoint/server/plugin.ts +++ b/x-pack/plugins/endpoint/server/plugin.ts @@ -14,13 +14,13 @@ import { registerResolverRoutes } from './routes/resolver'; import { registerIndexPatternRoute } from './routes/index_pattern'; import { registerEndpointRoutes } from './routes/metadata'; import { IngestIndexPatternRetriever } from './index_pattern'; -import { IngestManagerStartupContract } from '../../ingest_manager/common/types'; +import { IngestManagerStartContract } from '../../ingest_manager/server'; import { EndpointAppContextService } from './endpoint_app_context_services'; export type EndpointPluginStart = void; export type EndpointPluginSetup = void; export interface EndpointPluginStartDependencies { - ingestManager: IngestManagerStartupContract; + ingestManager: IngestManagerStartContract; } export interface EndpointPluginSetupDependencies { diff --git a/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts index 84930118df238..8f0c0b4c2efaf 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts @@ -26,7 +26,7 @@ import { registerEndpointRoutes } from './index'; import { EndpointConfigSchema } from '../../config'; import * as data from '../../test_data/all_metadata_data.json'; import { createMockAgentService, createMockMetadataIndexPatternRetriever } from '../../mocks'; -import { AgentService } from '../../../../ingest_manager/common/types'; +import { AgentService } from '../../../../ingest_manager/server'; import Boom from 'boom'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts index 986486902c3fa..470123ada48ea 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts @@ -42,6 +42,8 @@ describe('indexDocument', () => { }); describe('doesIlmPolicyExist', () => { + // ElasticsearchError can be a bit random in shape, we need an any here + // eslint-disable-next-line @typescript-eslint/no-explicit-any const notFoundError = new Error('Not found') as any; notFoundError.statusCode = 404; @@ -187,6 +189,8 @@ describe('createIndex', () => { }); test(`shouldn't throw when an error of type resource_already_exists_exception is thrown`, async () => { + // ElasticsearchError can be a bit random in shape, we need an any here + // eslint-disable-next-line @typescript-eslint/no-explicit-any const err = new Error('Already exists') as any; err.body = { error: { diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts index 409bb2d00e161..6d5c6b31a637c 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts @@ -5,6 +5,7 @@ */ import { reject, isUndefined } from 'lodash'; +import { SearchResponse, Client } from 'elasticsearch'; import { Logger, ClusterClient } from '../../../../../src/core/server'; import { IEvent } from '../types'; import { FindOptionsType } from '../event_log_client'; @@ -33,8 +34,8 @@ export class ClusterClientAdapter { this.clusterClientPromise = opts.clusterClientPromise; } - public async indexDocument(doc: any): Promise { - await this.callEs('index', doc); + public async indexDocument(doc: unknown): Promise { + await this.callEs>('index', doc); } public async doesIlmPolicyExist(policyName: string): Promise { @@ -51,7 +52,7 @@ export class ClusterClientAdapter { return true; } - public async createIlmPolicy(policyName: string, policy: any): Promise { + public async createIlmPolicy(policyName: string, policy: unknown): Promise { const request = { method: 'PUT', path: `_ilm/policy/${policyName}`, @@ -67,21 +68,27 @@ export class ClusterClientAdapter { public async doesIndexTemplateExist(name: string): Promise { let result; try { - result = await this.callEs('indices.existsTemplate', { name }); + result = await this.callEs>( + 'indices.existsTemplate', + { name } + ); } catch (err) { throw new Error(`error checking existance of index template: ${err.message}`); } return result as boolean; } - public async createIndexTemplate(name: string, template: any): Promise { + public async createIndexTemplate(name: string, template: unknown): Promise { const addTemplateParams = { name, create: true, body: template, }; try { - await this.callEs('indices.putTemplate', addTemplateParams); + await this.callEs>( + 'indices.putTemplate', + addTemplateParams + ); } catch (err) { // The error message doesn't have a type attribute we can look to guarantee it's due // to the template already existing (only long message) so we'll check ourselves to see @@ -97,16 +104,19 @@ export class ClusterClientAdapter { public async doesAliasExist(name: string): Promise { let result; try { - result = await this.callEs('indices.existsAlias', { name }); + result = await this.callEs>( + 'indices.existsAlias', + { name } + ); } catch (err) { throw new Error(`error checking existance of initial index: ${err.message}`); } return result as boolean; } - public async createIndex(name: string, body: any = {}): Promise { + public async createIndex(name: string, body: unknown = {}): Promise { try { - await this.callEs('indices.create', { + await this.callEs>('indices.create', { index: name, body, }); @@ -125,12 +135,12 @@ export class ClusterClientAdapter { ): Promise { try { const { - hits: { - hits, - total: { value: total }, - }, - } = await this.callEs('search', { + hits: { hits, total }, + }: SearchResponse = await this.callEs('search', { index, + // The SearchResponse type only supports total as an int, + // so we're forced to explicitly request that it return as an int + rest_total_hits_as_int: true, body: { size: perPage, from: (page - 1) * perPage, @@ -189,7 +199,7 @@ export class ClusterClientAdapter { page, per_page: perPage, total, - data: hits.map((hit: any) => hit._source) as IEvent[], + data: hits.map(hit => hit._source) as IEvent[], }; } catch (err) { throw new Error( @@ -198,13 +208,15 @@ export class ClusterClientAdapter { } } - private async callEs(operation: string, body?: any): Promise { + // We have a common problem typing ES-DSL Queries + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private async callEs(operation: string, body?: any) { try { this.debug(`callEs(${operation}) calls:`, body); const clusterClient = await this.clusterClientPromise; const result = await clusterClient.callAsInternalUser(operation, body); this.debug(`callEs(${operation}) result:`, result); - return result; + return result as ESQueryResult; } catch (err) { this.debug(`callEs(${operation}) error:`, { message: err.message, @@ -214,7 +226,7 @@ export class ClusterClientAdapter { } } - private debug(message: string, object?: any) { + private debug(message: string, object?: unknown) { const objectString = object == null ? '' : JSON.stringify(object); this.logger.debug(`esContext: ${message} ${objectString}`); } diff --git a/x-pack/plugins/event_log/server/es/documents.ts b/x-pack/plugins/event_log/server/es/documents.ts index 982454e671008..a6af209d6d3a0 100644 --- a/x-pack/plugins/event_log/server/es/documents.ts +++ b/x-pack/plugins/event_log/server/es/documents.ts @@ -9,7 +9,7 @@ import mappings from '../../generated/mappings.json'; // returns the body of an index template used in an ES indices.putTemplate call export function getIndexTemplate(esNames: EsNames) { - const indexTemplateBody: any = { + const indexTemplateBody = { index_patterns: [esNames.indexPatternWithVersion], settings: { number_of_shards: 1, diff --git a/x-pack/plugins/event_log/server/es/names.mock.ts b/x-pack/plugins/event_log/server/es/names.mock.ts index 268421235b4b2..7b3d01f3baa89 100644 --- a/x-pack/plugins/event_log/server/es/names.mock.ts +++ b/x-pack/plugins/event_log/server/es/names.mock.ts @@ -10,7 +10,7 @@ const createNamesMock = () => { const mock: jest.Mocked = { base: '.kibana', alias: '.kibana-event-log-8.0.0', - ilmPolicy: '.kibana-event-log-policy', + ilmPolicy: 'kibana-event-log-policy', indexPattern: '.kibana-event-log-*', indexPatternWithVersion: '.kibana-event-log-8.0.0-*', initialIndex: '.kibana-event-log-8.0.0-000001', diff --git a/x-pack/plugins/event_log/server/es/names.test.ts b/x-pack/plugins/event_log/server/es/names.test.ts index baefd756bb1ed..bc6a4c9a52fac 100644 --- a/x-pack/plugins/event_log/server/es/names.test.ts +++ b/x-pack/plugins/event_log/server/es/names.test.ts @@ -23,4 +23,10 @@ describe('getEsNames()', () => { expect(esNames.initialIndex).toEqual(`${base}-event-log-${version}-000001`); expect(esNames.indexTemplate).toEqual(`${base}-event-log-${version}-template`); }); + + test('ilm policy name does not contain dot prefix', () => { + const base = '.XYZ'; + const esNames = getEsNames(base); + expect(esNames.ilmPolicy).toEqual('XYZ-event-log-policy'); + }); }); diff --git a/x-pack/plugins/event_log/server/es/names.ts b/x-pack/plugins/event_log/server/es/names.ts index d55d02a16fc9a..8cd56a89d3fbe 100644 --- a/x-pack/plugins/event_log/server/es/names.ts +++ b/x-pack/plugins/event_log/server/es/names.ts @@ -22,10 +22,13 @@ export interface EsNames { export function getEsNames(baseName: string): EsNames { const eventLogName = `${baseName}${EVENT_LOG_NAME_SUFFIX}`; const eventLogNameWithVersion = `${eventLogName}${EVENT_LOG_VERSION_SUFFIX}`; + const eventLogPolicyName = `${ + baseName.startsWith('.') ? baseName.substring(1) : baseName + }${EVENT_LOG_NAME_SUFFIX}-policy`; return { base: baseName, alias: eventLogNameWithVersion, - ilmPolicy: `${eventLogName}-policy`, + ilmPolicy: `${eventLogPolicyName}`, indexPattern: `${eventLogName}-*`, indexPatternWithVersion: `${eventLogNameWithVersion}-*`, initialIndex: `${eventLogNameWithVersion}-000001`, diff --git a/x-pack/plugins/event_log/server/event_log_service.test.ts b/x-pack/plugins/event_log/server/event_log_service.test.ts index 3b250b7462009..43883ea4e384c 100644 --- a/x-pack/plugins/event_log/server/event_log_service.test.ts +++ b/x-pack/plugins/event_log/server/event_log_service.test.ts @@ -7,7 +7,7 @@ import { IEventLogConfig } from './types'; import { EventLogService } from './event_log_service'; import { contextMock } from './es/context.mock'; -import { loggingServiceMock } from '../../../../src/core/server/logging/logging_service.mock'; +import { loggingServiceMock } from 'src/core/server/mocks'; const loggingService = loggingServiceMock.create(); const systemLogger = loggingService.get(); diff --git a/x-pack/plugins/event_log/server/event_log_start_service.test.ts b/x-pack/plugins/event_log/server/event_log_start_service.test.ts index a8d75bc6c2e5a..58dd3ae6eb514 100644 --- a/x-pack/plugins/event_log/server/event_log_start_service.test.ts +++ b/x-pack/plugins/event_log/server/event_log_start_service.test.ts @@ -7,8 +7,7 @@ import { EventLogClientService } from './event_log_start_service'; import { contextMock } from './es/context.mock'; import { KibanaRequest } from 'kibana/server'; -import { savedObjectsServiceMock } from 'src/core/server/saved_objects/saved_objects_service.mock'; -import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { savedObjectsClientMock, savedObjectsServiceMock } from 'src/core/server/mocks'; jest.mock('./event_log_client'); @@ -41,7 +40,7 @@ describe('EventLogClientService', () => { function fakeRequest(): KibanaRequest { const savedObjectsClient = savedObjectsClientMock.create(); - return { + return ({ headers: {}, getBasePath: () => '', path: '/', @@ -55,5 +54,5 @@ function fakeRequest(): KibanaRequest { }, }, getSavedObjectsClient: () => savedObjectsClient, - } as any; + } as unknown) as KibanaRequest; } diff --git a/x-pack/plugins/event_log/server/event_logger.test.ts b/x-pack/plugins/event_log/server/event_logger.test.ts index 673bac4f396e1..6a745931420c0 100644 --- a/x-pack/plugins/event_log/server/event_logger.test.ts +++ b/x-pack/plugins/event_log/server/event_logger.test.ts @@ -9,20 +9,20 @@ import { ECS_VERSION } from './types'; import { EventLogService } from './event_log_service'; import { EsContext } from './es/context'; import { contextMock } from './es/context.mock'; -import { loggerMock, MockedLogger } from '../../../../src/core/server/logging/logger.mock'; +import { loggingServiceMock } from 'src/core/server/mocks'; import { delay } from './lib/delay'; import { EVENT_LOGGED_PREFIX } from './event_logger'; const KIBANA_SERVER_UUID = '424-24-2424'; describe('EventLogger', () => { - let systemLogger: MockedLogger; + let systemLogger: ReturnType; let esContext: EsContext; let service: IEventLogService; let eventLogger: IEventLogger; beforeEach(() => { - systemLogger = loggerMock.create(); + systemLogger = loggingServiceMock.createLogger(); esContext = contextMock.create(); service = new EventLogService({ esContext, @@ -153,7 +153,10 @@ describe('EventLogger', () => { }); // return the next logged event; throw if not an event -async function waitForLogEvent(mockLogger: MockedLogger, waitSeconds: number = 1): Promise { +async function waitForLogEvent( + mockLogger: ReturnType, + waitSeconds: number = 1 +): Promise { const result = await waitForLog(mockLogger, waitSeconds); if (typeof result === 'string') throw new Error('expecting an event'); return result; @@ -161,7 +164,7 @@ async function waitForLogEvent(mockLogger: MockedLogger, waitSeconds: number = 1 // return the next logged message; throw if it is an event async function waitForLogMessage( - mockLogger: MockedLogger, + mockLogger: ReturnType, waitSeconds: number = 1 ): Promise { const result = await waitForLog(mockLogger, waitSeconds); @@ -171,7 +174,7 @@ async function waitForLogMessage( // return the next logged message, if it's an event log entry, parse it async function waitForLog( - mockLogger: MockedLogger, + mockLogger: ReturnType, waitSeconds: number = 1 ): Promise { const intervals = 4; diff --git a/x-pack/plugins/event_log/server/event_logger.ts b/x-pack/plugins/event_log/server/event_logger.ts index f5149da069953..bcfd7bd45a6f5 100644 --- a/x-pack/plugins/event_log/server/event_logger.ts +++ b/x-pack/plugins/event_log/server/event_logger.ts @@ -168,7 +168,7 @@ function indexEventDoc(esContext: EsContext, doc: Doc): void { } // whew, the thing that actually writes the event log document! -async function indexLogEventDoc(esContext: EsContext, doc: any) { +async function indexLogEventDoc(esContext: EsContext, doc: unknown) { esContext.logger.debug(`writing to event log: ${JSON.stringify(doc)}`); await esContext.waitTillReady(); await esContext.esAdapter.indexDocument(doc); @@ -176,6 +176,6 @@ async function indexLogEventDoc(esContext: EsContext, doc: any) { } // TODO: write log entry to a bounded queue buffer -function writeLogEventDocOnError(esContext: EsContext, doc: any) { +function writeLogEventDocOnError(esContext: EsContext, doc: unknown) { esContext.logger.warn(`unable to write event doc: ${JSON.stringify(doc)}`); } diff --git a/x-pack/plugins/event_log/server/lib/bounded_queue.test.ts b/x-pack/plugins/event_log/server/lib/bounded_queue.test.ts index c1bb6d70879f3..dd6d15a6e4843 100644 --- a/x-pack/plugins/event_log/server/lib/bounded_queue.test.ts +++ b/x-pack/plugins/event_log/server/lib/bounded_queue.test.ts @@ -5,7 +5,7 @@ */ import { createBoundedQueue } from './bounded_queue'; -import { loggingServiceMock } from '../../../../../src/core/server/logging/logging_service.mock'; +import { loggingServiceMock } from 'src/core/server/mocks'; const loggingService = loggingServiceMock.create(); const logger = loggingService.get(); diff --git a/x-pack/plugins/event_log/server/lib/ready_signal.test.ts b/x-pack/plugins/event_log/server/lib/ready_signal.test.ts index d4dbb9064a1ba..6f1d92034c06f 100644 --- a/x-pack/plugins/event_log/server/lib/ready_signal.test.ts +++ b/x-pack/plugins/event_log/server/lib/ready_signal.test.ts @@ -16,11 +16,11 @@ describe('ReadySignal', () => { test('works as expected', async done => { let value = 41; - timeoutSet(100, () => { + timeoutSet(100, async () => { expect(value).toBe(41); }); - timeoutSet(250, () => readySignal.signal(42)); + timeoutSet(250, async () => readySignal.signal(42)); timeoutSet(400, async () => { expect(value).toBe(42); @@ -35,6 +35,6 @@ describe('ReadySignal', () => { }); }); -function timeoutSet(ms: number, fn: any) { +function timeoutSet(ms: number, fn: () => Promise): void { setTimeout(fn, ms); } diff --git a/x-pack/plugins/event_log/server/plugin.ts b/x-pack/plugins/event_log/server/plugin.ts index e5034f599f118..dd83b2cfb03b8 100644 --- a/x-pack/plugins/event_log/server/plugin.ts +++ b/x-pack/plugins/event_log/server/plugin.ts @@ -120,7 +120,7 @@ export class Plugin implements CorePlugin, + RequestHandler, 'eventLog' > => { return async (context, request) => { diff --git a/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts b/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts index 19933649277aa..2d5e37e870b28 100644 --- a/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts +++ b/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts @@ -11,9 +11,9 @@ import { IEventLogClient } from '../types'; export function mockHandlerArguments( eventLogClient: IEventLogClient, - req: any, + req: unknown, res?: Array> -): [RequestHandlerContext, KibanaRequest, KibanaResponseFactory] { +): [RequestHandlerContext, KibanaRequest, KibanaResponseFactory] { return [ ({ eventLog: { @@ -22,7 +22,7 @@ export function mockHandlerArguments( }, }, } as unknown) as RequestHandlerContext, - req as KibanaRequest, + req as KibanaRequest, mockResponseFactory(res), ]; } diff --git a/x-pack/plugins/event_log/server/routes/find.test.ts b/x-pack/plugins/event_log/server/routes/find.test.ts index 844a84dc117a9..f47df499d742f 100644 --- a/x-pack/plugins/event_log/server/routes/find.test.ts +++ b/x-pack/plugins/event_log/server/routes/find.test.ts @@ -5,7 +5,7 @@ */ import { findRoute } from './find'; -import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { httpServiceMock } from 'src/core/server/mocks'; import { mockHandlerArguments, fakeEvent } from './_mock_handler_arguments'; import { eventLogClientMock } from '../event_log_client.mock'; @@ -17,7 +17,7 @@ beforeEach(() => { describe('find', () => { it('finds events with proper parameters', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); findRoute(router); @@ -56,7 +56,7 @@ describe('find', () => { }); it('supports optional pagination parameters', async () => { - const router: RouterMock = mockRouter.create(); + const router = httpServiceMock.createRouter(); findRoute(router); diff --git a/x-pack/plugins/event_log/server/routes/find.ts b/x-pack/plugins/event_log/server/routes/find.ts index cb170e50fb447..f8e1c842ae436 100644 --- a/x-pack/plugins/event_log/server/routes/find.ts +++ b/x-pack/plugins/event_log/server/routes/find.ts @@ -31,9 +31,9 @@ export const findRoute = (router: IRouter) => { }, router.handleLegacyErrors(async function( context: RequestHandlerContext, - req: KibanaRequest, FindOptionsType, any, any>, + req: KibanaRequest, FindOptionsType, unknown>, res: KibanaResponseFactory - ): Promise> { + ): Promise { if (!context.eventLog) { return res.badRequest({ body: 'RouteHandlerContext is not registered for eventLog' }); } diff --git a/x-pack/plugins/graph/public/_main.scss b/x-pack/plugins/graph/public/_main.scss index 2559b7d1aba5c..0d17015385292 100644 --- a/x-pack/plugins/graph/public/_main.scss +++ b/x-pack/plugins/graph/public/_main.scss @@ -25,3 +25,9 @@ -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent; } + +.gphAppWrapper { + display: flex; + flex-direction: column; + flex-grow: 1; +} diff --git a/x-pack/plugins/graph/public/application.ts b/x-pack/plugins/graph/public/application.ts index fee42bdbeaf3b..35ec0bb2bf6ce 100644 --- a/x-pack/plugins/graph/public/application.ts +++ b/x-pack/plugins/graph/public/application.ts @@ -96,7 +96,7 @@ export const renderApp = ({ appBasePath, element, ...deps }: GraphDependencies) }; }; -const mainTemplate = (basePath: string) => `
+const mainTemplate = (basePath: string) => `
`; @@ -107,14 +107,14 @@ const thirdPartyAngularDependencies = ['ngSanitize', 'ngRoute', 'react', 'ui.boo function mountGraphApp(appBasePath: string, element: HTMLElement) { const mountpoint = document.createElement('div'); - mountpoint.setAttribute('class', 'kbnLocalApplicationWrapper'); + mountpoint.setAttribute('class', 'gphAppWrapper'); // eslint-disable-next-line mountpoint.innerHTML = mainTemplate(appBasePath); // bootstrap angular into detached element and attach it later to // make angular-within-angular possible const $injector = angular.bootstrap(mountpoint, [moduleName]); element.appendChild(mountpoint); - element.setAttribute('class', 'kbnLocalApplicationWrapper'); + element.setAttribute('class', 'gphAppWrapper'); return $injector; } diff --git a/x-pack/plugins/graph/public/index.scss b/x-pack/plugins/graph/public/index.scss index 964ef320e4352..f4e38de3e93a4 100644 --- a/x-pack/plugins/graph/public/index.scss +++ b/x-pack/plugins/graph/public/index.scss @@ -12,5 +12,3 @@ @import './main'; @import './angular/templates/index'; @import './components/index'; -// Local application mount wrapper styles -@import 'src/legacy/core_plugins/kibana/public/local_application_service/index'; diff --git a/x-pack/plugins/graph/server/plugin.ts b/x-pack/plugins/graph/server/plugin.ts index a169953d5a10b..141d5d0ea8db4 100644 --- a/x-pack/plugins/graph/server/plugin.ts +++ b/x-pack/plugins/graph/server/plugin.ts @@ -13,6 +13,7 @@ import { registerExploreRoute } from './routes/explore'; import { HomeServerPluginSetup } from '../../../../src/plugins/home/server'; import { registerSampleData } from './sample_data'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; +import { graphWorkspace } from './saved_objects'; export class GraphPlugin implements Plugin { private licenseState: LicenseState | null = null; @@ -32,6 +33,7 @@ export class GraphPlugin implements Plugin { const licenseState = new LicenseState(); licenseState.start(licensing.license$); this.licenseState = licenseState; + core.savedObjects.registerType(graphWorkspace); if (home) { registerSampleData(home.sampleData, licenseState); diff --git a/x-pack/plugins/graph/server/saved_objects/graph_workspace.ts b/x-pack/plugins/graph/server/saved_objects/graph_workspace.ts new file mode 100644 index 0000000000000..8e8cb64aac1b9 --- /dev/null +++ b/x-pack/plugins/graph/server/saved_objects/graph_workspace.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { SavedObjectsType } from 'kibana/server'; +import { graphMigrations } from './migrations'; + +export const graphWorkspace: SavedObjectsType = { + name: 'graph-workspace', + namespaceType: 'single', + hidden: false, + migrations: graphMigrations, + mappings: { + properties: { + description: { + type: 'text', + }, + kibanaSavedObjectMeta: { + properties: { + searchSourceJSON: { + type: 'text', + }, + }, + }, + numLinks: { + type: 'integer', + }, + numVertices: { + type: 'integer', + }, + title: { + type: 'text', + }, + version: { + type: 'integer', + }, + wsState: { + type: 'text', + }, + }, + }, +}; diff --git a/x-pack/plugins/graph/server/saved_objects/index.ts b/x-pack/plugins/graph/server/saved_objects/index.ts new file mode 100644 index 0000000000000..67d1501950175 --- /dev/null +++ b/x-pack/plugins/graph/server/saved_objects/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { graphWorkspace } from './graph_workspace'; diff --git a/x-pack/plugins/graph/server/saved_objects/migrations.test.ts b/x-pack/plugins/graph/server/saved_objects/migrations.test.ts new file mode 100644 index 0000000000000..ecf1f3ca3b69e --- /dev/null +++ b/x-pack/plugins/graph/server/saved_objects/migrations.test.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { graphMigrations } from './migrations'; +import { SavedObjectUnsanitizedDoc } from 'kibana/server'; + +describe('graph-workspace', () => { + describe('7.0.0', () => { + const migration = graphMigrations['7.0.0']; + + test('returns doc on empty object', () => { + expect(migration({} as SavedObjectUnsanitizedDoc)).toMatchInlineSnapshot(` + Object { + "references": Array [], + } + `); + }); + + test('returns doc when wsState is not a string', () => { + const doc = { + id: '1', + type: 'graph-workspace', + attributes: { + wsState: true, + }, + }; + expect(migration(doc)).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "wsState": true, + }, + "id": "1", + "references": Array [], + "type": "graph-workspace", + } + `); + }); + + test('returns doc when wsState is not valid JSON', () => { + const doc = { + id: '1', + type: 'graph-workspace', + attributes: { + wsState: '123abc', + }, + }; + expect(migration(doc)).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "wsState": "123abc", + }, + "id": "1", + "references": Array [], + "type": "graph-workspace", + } + `); + }); + + test('returns doc when "indexPattern" is missing from wsState', () => { + const doc = { + id: '1', + type: 'graph-workspace', + attributes: { + wsState: JSON.stringify(JSON.stringify({ foo: true })), + }, + }; + expect(migration(doc)).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "wsState": "\\"{\\\\\\"foo\\\\\\":true}\\"", + }, + "id": "1", + "references": Array [], + "type": "graph-workspace", + } + `); + }); + + test('extract "indexPattern" attribute from doc', () => { + const doc = { + id: '1', + type: 'graph-workspace', + attributes: { + wsState: JSON.stringify(JSON.stringify({ foo: true, indexPattern: 'pattern*' })), + bar: true, + }, + }; + const migratedDoc = migration(doc); + expect(migratedDoc).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "bar": true, + "wsState": "\\"{\\\\\\"foo\\\\\\":true,\\\\\\"indexPatternRefName\\\\\\":\\\\\\"indexPattern_0\\\\\\"}\\"", + }, + "id": "1", + "references": Array [ + Object { + "id": "pattern*", + "name": "indexPattern_0", + "type": "index-pattern", + }, + ], + "type": "graph-workspace", + } + `); + }); + }); +}); diff --git a/x-pack/plugins/graph/server/saved_objects/migrations.ts b/x-pack/plugins/graph/server/saved_objects/migrations.ts new file mode 100644 index 0000000000000..e77d2ea0fb7c9 --- /dev/null +++ b/x-pack/plugins/graph/server/saved_objects/migrations.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash'; +import { SavedObjectUnsanitizedDoc } from 'kibana/server'; + +export const graphMigrations = { + '7.0.0': (doc: SavedObjectUnsanitizedDoc) => { + // Set new "references" attribute + doc.references = doc.references || []; + // Migrate index pattern + const wsState = get(doc, 'attributes.wsState'); + if (typeof wsState !== 'string') { + return doc; + } + let state; + try { + state = JSON.parse(JSON.parse(wsState)); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + return doc; + } + const { indexPattern } = state; + if (!indexPattern) { + return doc; + } + state.indexPatternRefName = 'indexPattern_0'; + delete state.indexPattern; + doc.attributes.wsState = JSON.stringify(JSON.stringify(state)); + doc.references.push({ + name: 'indexPattern_0', + type: 'index-pattern', + id: indexPattern, + }); + return doc; + }, +}; diff --git a/x-pack/plugins/infra/common/alerting/logs/types.ts b/x-pack/plugins/infra/common/alerting/logs/types.ts new file mode 100644 index 0000000000000..cbfffbfd8f940 --- /dev/null +++ b/x-pack/plugins/infra/common/alerting/logs/types.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; + +export const LOG_DOCUMENT_COUNT_ALERT_TYPE_ID = 'logs.alert.document.count'; + +export enum Comparator { + GT = 'more than', + GT_OR_EQ = 'more than or equals', + LT = 'less than', + LT_OR_EQ = 'less than or equals', + EQ = 'equals', + NOT_EQ = 'does not equal', + MATCH = 'matches', + NOT_MATCH = 'does not match', + MATCH_PHRASE = 'matches phrase', + NOT_MATCH_PHRASE = 'does not match phrase', +} + +// Maps our comparators to i18n strings, some comparators have more specific wording +// depending on the field type the comparator is being used with. +export const ComparatorToi18nMap = { + [Comparator.GT]: i18n.translate('xpack.infra.logs.alerting.comparator.gt', { + defaultMessage: 'more than', + }), + [Comparator.GT_OR_EQ]: i18n.translate('xpack.infra.logs.alerting.comparator.gtOrEq', { + defaultMessage: 'more than or equals', + }), + [Comparator.LT]: i18n.translate('xpack.infra.logs.alerting.comparator.lt', { + defaultMessage: 'less than', + }), + [Comparator.LT_OR_EQ]: i18n.translate('xpack.infra.logs.alerting.comparator.ltOrEq', { + defaultMessage: 'less than or equals', + }), + [Comparator.EQ]: i18n.translate('xpack.infra.logs.alerting.comparator.eq', { + defaultMessage: 'is', + }), + [Comparator.NOT_EQ]: i18n.translate('xpack.infra.logs.alerting.comparator.notEq', { + defaultMessage: 'is not', + }), + [`${Comparator.EQ}:number`]: i18n.translate('xpack.infra.logs.alerting.comparator.eqNumber', { + defaultMessage: 'equals', + }), + [`${Comparator.NOT_EQ}:number`]: i18n.translate( + 'xpack.infra.logs.alerting.comparator.notEqNumber', + { + defaultMessage: 'does not equal', + } + ), + [Comparator.MATCH]: i18n.translate('xpack.infra.logs.alerting.comparator.match', { + defaultMessage: 'matches', + }), + [Comparator.NOT_MATCH]: i18n.translate('xpack.infra.logs.alerting.comparator.notMatch', { + defaultMessage: 'does not match', + }), + [Comparator.MATCH_PHRASE]: i18n.translate('xpack.infra.logs.alerting.comparator.matchPhrase', { + defaultMessage: 'matches phrase', + }), + [Comparator.NOT_MATCH_PHRASE]: i18n.translate( + 'xpack.infra.logs.alerting.comparator.notMatchPhrase', + { + defaultMessage: 'does not match phrase', + } + ), +}; + +export enum AlertStates { + OK, + ALERT, + NO_DATA, + ERROR, +} + +export interface DocumentCount { + comparator: Comparator; + value: number; +} + +export interface Criterion { + field: string; + comparator: Comparator; + value: string | number; +} + +export interface LogDocumentCountAlertParams { + count: DocumentCount; + criteria: Criterion[]; + timeUnit: 's' | 'm' | 'h' | 'd'; + timeSize: number; +} + +export type TimeUnit = 's' | 'm' | 'h' | 'd'; diff --git a/x-pack/plugins/infra/common/http_api/log_entries/entries.ts b/x-pack/plugins/infra/common/http_api/log_entries/entries.ts index d532c079e3e9c..1c5a2f0fe1ad9 100644 --- a/x-pack/plugins/infra/common/http_api/log_entries/entries.ts +++ b/x-pack/plugins/infra/common/http_api/log_entries/entries.ts @@ -78,11 +78,11 @@ export const logEntryRT = rt.type({ id: rt.string, cursor: logEntriesCursorRT, columns: rt.array(logColumnRT), - context: rt.partial({ - 'log.file.path': rt.string, - 'host.name': rt.string, - 'container.id': rt.string, - }), + context: rt.union([ + rt.type({}), + rt.type({ 'container.id': rt.string }), + rt.type({ 'host.name': rt.string, 'log.file.path': rt.string }), + ]), }); export type LogMessageConstantPart = rt.TypeOf; diff --git a/x-pack/plugins/infra/public/components/alerting/logs/alert_dropdown.tsx b/x-pack/plugins/infra/public/components/alerting/logs/alert_dropdown.tsx new file mode 100644 index 0000000000000..dd888639b6d07 --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/logs/alert_dropdown.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useCallback, useMemo } from 'react'; +import { EuiPopover, EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { AlertFlyout } from './alert_flyout'; +import { useLinkProps } from '../../../hooks/use_link_props'; + +export const AlertDropdown = () => { + const [popoverOpen, setPopoverOpen] = useState(false); + const [flyoutVisible, setFlyoutVisible] = useState(false); + const manageAlertsLinkProps = useLinkProps( + { + app: 'kibana', + hash: 'management/kibana/triggersActions/alerts', + }, + { + hrefOnly: true, + } + ); + + const closePopover = useCallback(() => { + setPopoverOpen(false); + }, [setPopoverOpen]); + + const openPopover = useCallback(() => { + setPopoverOpen(true); + }, [setPopoverOpen]); + + const menuItems = useMemo(() => { + return [ + setFlyoutVisible(true)}> + + , + + + , + ]; + }, [manageAlertsLinkProps]); + + return ( + <> + + + + } + isOpen={popoverOpen} + closePopover={closePopover} + > + + + + + ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/logs/alert_flyout.tsx b/x-pack/plugins/infra/public/components/alerting/logs/alert_flyout.tsx new file mode 100644 index 0000000000000..b18c2e5b8d69c --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/logs/alert_flyout.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useContext } from 'react'; +import { AlertsContextProvider, AlertAdd } from '../../../../../triggers_actions_ui/public'; +import { TriggerActionsContext } from '../../../utils/triggers_actions_context'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../../../../common/alerting/logs/types'; + +interface Props { + visible?: boolean; + setVisible: React.Dispatch>; +} + +export const AlertFlyout = (props: Props) => { + const { triggersActionsUI } = useContext(TriggerActionsContext); + const { services } = useKibana(); + + return ( + <> + {triggersActionsUI && ( + + + + )} + + ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/criteria.tsx b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/criteria.tsx new file mode 100644 index 0000000000000..a9b45a117c281 --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/criteria.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { IFieldType } from 'src/plugins/data/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IErrorObject } from '../../../../../../triggers_actions_ui/public/types'; +import { Criterion } from './criterion'; +import { + LogDocumentCountAlertParams, + Criterion as CriterionType, +} from '../../../../../common/alerting/logs/types'; + +interface Props { + fields: IFieldType[]; + criteria?: LogDocumentCountAlertParams['criteria']; + updateCriterion: (idx: number, params: Partial) => void; + removeCriterion: (idx: number) => void; + errors: IErrorObject; +} + +export const Criteria: React.FC = ({ + fields, + criteria, + updateCriterion, + removeCriterion, + errors, +}) => { + if (!criteria) return null; + return ( + + + {criteria.map((criterion, idx) => { + return ( + 1} + errors={errors[idx.toString()] as IErrorObject} + /> + ); + })} + + + ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/criterion.tsx b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/criterion.tsx new file mode 100644 index 0000000000000..e8cafecd94db1 --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/criterion.tsx @@ -0,0 +1,269 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { + EuiPopoverTitle, + EuiFlexItem, + EuiFlexGroup, + EuiPopover, + EuiSelect, + EuiFieldNumber, + EuiExpression, + EuiFieldText, + EuiButtonIcon, + EuiFormRow, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { IFieldType } from 'src/plugins/data/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IErrorObject } from '../../../../../../triggers_actions_ui/public/types'; +import { + Comparator, + Criterion as CriterionType, + ComparatorToi18nMap, +} from '../../../../../common/alerting/logs/types'; + +const firstCriterionFieldPrefix = i18n.translate( + 'xpack.infra.logs.alertFlyout.firstCriterionFieldPrefix', + { + defaultMessage: 'with', + } +); + +const successiveCriterionFieldPrefix = i18n.translate( + 'xpack.infra.logs.alertFlyout.successiveCriterionFieldPrefix', + { + defaultMessage: 'and', + } +); + +const criterionFieldTitle = i18n.translate('xpack.infra.logs.alertFlyout.criterionFieldTitle', { + defaultMessage: 'Field', +}); + +const criterionComparatorValueTitle = i18n.translate( + 'xpack.infra.logs.alertFlyout.criterionComparatorValueTitle', + { + defaultMessage: 'Comparison : Value', + } +); + +const getCompatibleComparatorsForField = (fieldInfo: IFieldType | undefined) => { + if (fieldInfo?.type === 'number') { + return [ + { value: Comparator.GT, text: ComparatorToi18nMap[Comparator.GT] }, + { value: Comparator.GT_OR_EQ, text: ComparatorToi18nMap[Comparator.GT_OR_EQ] }, + { value: Comparator.LT, text: ComparatorToi18nMap[Comparator.LT] }, + { value: Comparator.LT_OR_EQ, text: ComparatorToi18nMap[Comparator.LT_OR_EQ] }, + { value: Comparator.EQ, text: ComparatorToi18nMap[`${Comparator.EQ}:number`] }, + { value: Comparator.NOT_EQ, text: ComparatorToi18nMap[`${Comparator.NOT_EQ}:number`] }, + ]; + } else if (fieldInfo?.aggregatable) { + return [ + { value: Comparator.EQ, text: ComparatorToi18nMap[Comparator.EQ] }, + { value: Comparator.NOT_EQ, text: ComparatorToi18nMap[Comparator.NOT_EQ] }, + ]; + } else { + return [ + { value: Comparator.MATCH, text: ComparatorToi18nMap[Comparator.MATCH] }, + { value: Comparator.NOT_MATCH, text: ComparatorToi18nMap[Comparator.NOT_MATCH] }, + { value: Comparator.MATCH_PHRASE, text: ComparatorToi18nMap[Comparator.MATCH_PHRASE] }, + { + value: Comparator.NOT_MATCH_PHRASE, + text: ComparatorToi18nMap[Comparator.NOT_MATCH_PHRASE], + }, + ]; + } +}; + +const getFieldInfo = (fields: IFieldType[], fieldName: string): IFieldType | undefined => { + return fields.find(field => { + return field.name === fieldName; + }); +}; + +interface Props { + idx: number; + fields: IFieldType[]; + criterion: CriterionType; + updateCriterion: (idx: number, params: Partial) => void; + removeCriterion: (idx: number) => void; + canDelete: boolean; + errors: IErrorObject; +} + +export const Criterion: React.FC = ({ + idx, + fields, + criterion, + updateCriterion, + removeCriterion, + canDelete, + errors, +}) => { + const [isFieldPopoverOpen, setIsFieldPopoverOpen] = useState(false); + const [isComparatorPopoverOpen, setIsComparatorPopoverOpen] = useState(false); + + const fieldOptions = useMemo(() => { + return fields.map(field => { + return { value: field.name, text: field.name }; + }); + }, [fields]); + + const fieldInfo: IFieldType | undefined = useMemo(() => { + return getFieldInfo(fields, criterion.field); + }, [fields, criterion]); + + const compatibleComparatorOptions = useMemo(() => { + return getCompatibleComparatorsForField(fieldInfo); + }, [fieldInfo]); + + const handleFieldChange = useCallback( + e => { + const fieldName = e.target.value; + const nextFieldInfo = getFieldInfo(fields, fieldName); + // If the field information we're dealing with has changed, reset the comparator and value. + if ( + fieldInfo && + nextFieldInfo && + (fieldInfo.type !== nextFieldInfo.type || + fieldInfo.aggregatable !== nextFieldInfo.aggregatable) + ) { + const compatibleComparators = getCompatibleComparatorsForField(nextFieldInfo); + updateCriterion(idx, { + field: fieldName, + comparator: compatibleComparators[0].value, + value: undefined, + }); + } else { + updateCriterion(idx, { field: fieldName }); + } + }, + [fieldInfo, fields, idx, updateCriterion] + ); + + return ( + + + + + setIsFieldPopoverOpen(true)} + /> + } + isOpen={isFieldPopoverOpen} + closePopover={() => setIsFieldPopoverOpen(false)} + ownFocus + panelPaddingSize="s" + anchorPosition="downLeft" + > +
+ {criterionFieldTitle} + 0} error={errors.field}> + + +
+
+
+ + setIsComparatorPopoverOpen(true)} + /> + } + isOpen={isComparatorPopoverOpen} + closePopover={() => setIsComparatorPopoverOpen(false)} + ownFocus + panelPaddingSize="s" + anchorPosition="downLeft" + > +
+ {criterionComparatorValueTitle} + + + 0} error={errors.comparator}> + + updateCriterion(idx, { comparator: e.target.value as Comparator }) + } + options={compatibleComparatorOptions} + /> + + + + 0} error={errors.value}> + {fieldInfo?.type === 'number' ? ( + { + const number = parseInt(e.target.value, 10); + updateCriterion(idx, { value: number ? number : undefined }); + }} + /> + ) : ( + updateCriterion(idx, { value: e.target.value })} + /> + )} + + + +
+
+
+
+
+ {canDelete && ( + + removeCriterion(idx)} + /> + + )} +
+ ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/document_count.tsx b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/document_count.tsx new file mode 100644 index 0000000000000..308165ce08a9b --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/document_count.tsx @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiPopoverTitle, + EuiFlexItem, + EuiFlexGroup, + EuiPopover, + EuiSelect, + EuiFieldNumber, + EuiExpression, + EuiFormRow, +} from '@elastic/eui'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IErrorObject } from '../../../../../../triggers_actions_ui/public/types'; +import { + Comparator, + ComparatorToi18nMap, + LogDocumentCountAlertParams, +} from '../../../../../common/alerting/logs/types'; + +const documentCountPrefix = i18n.translate('xpack.infra.logs.alertFlyout.documentCountPrefix', { + defaultMessage: 'when', +}); + +const getComparatorOptions = (): Array<{ + value: Comparator; + text: string; +}> => { + return [ + { value: Comparator.LT, text: ComparatorToi18nMap[Comparator.LT] }, + { value: Comparator.LT_OR_EQ, text: ComparatorToi18nMap[Comparator.LT_OR_EQ] }, + { value: Comparator.GT, text: ComparatorToi18nMap[Comparator.GT] }, + { value: Comparator.GT_OR_EQ, text: ComparatorToi18nMap[Comparator.GT_OR_EQ] }, + ]; +}; + +interface Props { + comparator?: Comparator; + value?: number; + updateCount: (params: Partial) => void; + errors: IErrorObject; +} + +export const DocumentCount: React.FC = ({ comparator, value, updateCount, errors }) => { + const [isComparatorPopoverOpen, setComparatorPopoverOpenState] = useState(false); + const [isValuePopoverOpen, setIsValuePopoverOpen] = useState(false); + + const documentCountValue = i18n.translate('xpack.infra.logs.alertFlyout.documentCountValue', { + defaultMessage: '{value, plural, one {log entry} other {log entries}}', + values: { value }, + }); + + return ( + + + setComparatorPopoverOpenState(true)} + /> + } + isOpen={isComparatorPopoverOpen} + closePopover={() => setComparatorPopoverOpenState(false)} + ownFocus + panelPaddingSize="s" + anchorPosition="downLeft" + > +
+ {documentCountPrefix} + updateCount({ comparator: e.target.value as Comparator })} + options={getComparatorOptions()} + /> +
+
+
+ + + setIsValuePopoverOpen(true)} + color={errors.value.length === 0 ? 'secondary' : 'danger'} + /> + } + isOpen={isValuePopoverOpen} + closePopover={() => setIsValuePopoverOpen(false)} + ownFocus + panelPaddingSize="s" + anchorPosition="downLeft" + > +
+ {documentCountValue} + 0} error={errors.value}> + { + const number = parseInt(e.target.value, 10); + updateCount({ value: number ? number : undefined }); + }} + /> + +
+
+
+
+ ); +}; diff --git a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx new file mode 100644 index 0000000000000..3aed0db53bf2c --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/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, { useCallback, useMemo, useEffect, useState } from 'react'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + ForLastExpression, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../triggers_actions_ui/public/common'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { IErrorObject } from '../../../../../../triggers_actions_ui/public/types'; +import { useSource } from '../../../../containers/source'; +import { + LogDocumentCountAlertParams, + Comparator, + TimeUnit, +} from '../../../../../common/alerting/logs/types'; +import { DocumentCount } from './document_count'; +import { Criteria } from './criteria'; + +export interface ExpressionCriteria { + field?: string; + comparator?: Comparator; + value?: string | number; +} + +interface Props { + errors: IErrorObject; + alertParams: Partial; + setAlertParams(key: string, value: any): void; + setAlertProperty(key: string, value: any): void; +} + +const DEFAULT_CRITERIA = { field: 'log.level', comparator: Comparator.EQ, value: 'error' }; + +const DEFAULT_EXPRESSION = { + count: { + value: 75, + comparator: Comparator.GT, + }, + criteria: [DEFAULT_CRITERIA], + timeSize: 5, + timeUnit: 'm', +}; + +export const ExpressionEditor: React.FC = props => { + const { setAlertParams, alertParams, errors } = props; + const { createDerivedIndexPattern } = useSource({ sourceId: 'default' }); + const [timeSize, setTimeSize] = useState(1); + const [timeUnit, setTimeUnit] = useState('m'); + const [hasSetDefaults, setHasSetDefaults] = useState(false); + const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('logs'), [ + createDerivedIndexPattern, + ]); + + const supportedFields = useMemo(() => { + if (derivedIndexPattern?.fields) { + return derivedIndexPattern.fields.filter(field => { + return (field.type === 'string' || field.type === 'number') && field.searchable; + }); + } else { + return []; + } + }, [derivedIndexPattern]); + + // Set the default expression (disables exhaustive-deps as we only want to run this once on mount) + useEffect(() => { + for (const [key, value] of Object.entries(DEFAULT_EXPRESSION)) { + setAlertParams(key, value); + setHasSetDefaults(true); + } + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + const updateCount = useCallback( + countParams => { + const nextCountParams = { ...alertParams.count, ...countParams }; + setAlertParams('count', nextCountParams); + }, + [alertParams.count, setAlertParams] + ); + + const updateCriterion = useCallback( + (idx, criterionParams) => { + const nextCriteria = alertParams.criteria?.map((criterion, index) => { + return idx === index ? { ...criterion, ...criterionParams } : criterion; + }); + setAlertParams('criteria', nextCriteria ? nextCriteria : []); + }, + [alertParams, setAlertParams] + ); + + const updateTimeSize = useCallback( + (ts: number | undefined) => { + setTimeSize(ts || undefined); + setAlertParams('timeSize', ts); + }, + [setTimeSize, setAlertParams] + ); + + const updateTimeUnit = useCallback( + (tu: string) => { + setTimeUnit(tu as TimeUnit); + setAlertParams('timeUnit', tu); + }, + [setAlertParams] + ); + + const addCriterion = useCallback(() => { + const nextCriteria = alertParams?.criteria + ? [...alertParams.criteria, DEFAULT_CRITERIA] + : [DEFAULT_CRITERIA]; + setAlertParams('criteria', nextCriteria); + }, [alertParams, setAlertParams]); + + const removeCriterion = useCallback( + idx => { + const nextCriteria = alertParams?.criteria?.filter((criterion, index) => { + return index !== idx; + }); + setAlertParams('criteria', nextCriteria); + }, + [alertParams, setAlertParams] + ); + + // Wait until field info has loaded + if (supportedFields.length === 0) return null; + // Wait until the alert param defaults have been set + if (!hasSetDefaults) return null; + + return ( + <> + + + + + + +
+ + + +
+ + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/index.ts b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/index.tsx similarity index 79% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/index.ts rename to x-pack/plugins/infra/public/components/alerting/logs/expression_editor/index.tsx index ea89e91de5046..8b0fd5eb721b3 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/index.ts +++ b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/index.tsx @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { ExplorationDataGrid } from './exploration_data_grid'; +export * from './editor'; diff --git a/x-pack/plugins/infra/public/components/alerting/logs/log_threshold_alert_type.ts b/x-pack/plugins/infra/public/components/alerting/logs/log_threshold_alert_type.ts new file mode 100644 index 0000000000000..18126ec583696 --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/logs/log_threshold_alert_type.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { AlertTypeModel } from '../../../../../triggers_actions_ui/public/types'; +import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID } from '../../../../common/alerting/logs/types'; +import { ExpressionEditor } from './expression_editor'; +import { validateExpression } from './validation'; + +export function getAlertType(): AlertTypeModel { + return { + id: LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, + name: i18n.translate('xpack.infra.logs.alertFlyout.alertName', { + defaultMessage: 'Log threshold', + }), + iconClass: 'bell', + alertParamsExpression: ExpressionEditor, + validate: validateExpression, + defaultActionMessage: i18n.translate( + 'xpack.infra.logs.alerting.threshold.defaultActionMessage', + { + defaultMessage: `\\{\\{context.matchingDocuments\\}\\} log entries have matched the following conditions: \\{\\{context.conditions\\}\\}`, + } + ), + }; +} diff --git a/x-pack/plugins/infra/public/components/alerting/logs/validation.ts b/x-pack/plugins/infra/public/components/alerting/logs/validation.ts new file mode 100644 index 0000000000000..c8c513f57a9d7 --- /dev/null +++ b/x-pack/plugins/infra/public/components/alerting/logs/validation.ts @@ -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'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ValidationResult } from '../../../../../triggers_actions_ui/public/types'; +import { LogDocumentCountAlertParams } from '../../../../common/alerting/logs/types'; + +export function validateExpression({ + count, + criteria, + timeSize, + timeUnit, +}: Partial): ValidationResult { + const validationResult = { errors: {} }; + + // NOTE: In the case of components provided by the Alerting framework the error property names + // must match what they expect. + const errors: { + count: { + value: string[]; + }; + criteria: { + [id: string]: { + field: string[]; + comparator: string[]; + value: string[]; + }; + }; + timeWindowSize: string[]; + timeSizeUnit: string[]; + } = { + count: { + value: [], + }, + criteria: {}, + timeSizeUnit: [], + timeWindowSize: [], + }; + + validationResult.errors = errors; + + // Document count validation + if (typeof count?.value !== 'number') { + errors.count.value.push( + i18n.translate('xpack.infra.logs.alertFlyout.error.documentCountRequired', { + defaultMessage: 'Document count is Required.', + }) + ); + } + + // Time validation + if (!timeSize) { + errors.timeWindowSize.push( + i18n.translate('xpack.infra.logs.alertFlyout.error.timeSizeRequired', { + defaultMessage: 'Time size is Required.', + }) + ); + } + + if (criteria && criteria.length > 0) { + // Criteria validation + criteria.forEach((criterion, idx: number) => { + const id = idx.toString(); + + errors.criteria[id] = { + field: [], + comparator: [], + value: [], + }; + + if (!criterion.field) { + errors.criteria[id].field.push( + i18n.translate('xpack.infra.logs.alertFlyout.error.criterionFieldRequired', { + defaultMessage: 'Field is required.', + }) + ); + } + + if (!criterion.comparator) { + errors.criteria[id].comparator.push( + i18n.translate('xpack.infra.logs.alertFlyout.error.criterionComparatorRequired', { + defaultMessage: 'Comparator is required.', + }) + ); + } + + if (!criterion.value) { + errors.criteria[id].value.push( + i18n.translate('xpack.infra.logs.alertFlyout.error.criterionValueRequired', { + defaultMessage: 'Value is required.', + }) + ); + } + }); + } + + return validationResult; +} diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_actions_column.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_actions_column.tsx index e02346c4e758a..976e4165eb6d5 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_actions_column.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_actions_column.tsx @@ -24,29 +24,49 @@ interface LogEntryActionsColumnProps { isMenuOpen: boolean; onOpenMenu: () => void; onCloseMenu: () => void; - onViewDetails: () => void; + onViewDetails?: () => void; + onViewLogInContext?: () => void; } const MENU_LABEL = i18n.translate('xpack.infra.logEntryItemView.logEntryActionsMenuToolTip', { - defaultMessage: 'View Details', + defaultMessage: 'View actions for line', }); const LOG_DETAILS_LABEL = i18n.translate('xpack.infra.logs.logEntryActionsDetailsButton', { - defaultMessage: 'View actions for line', + defaultMessage: 'View details', }); +const LOG_VIEW_IN_CONTEXT_LABEL = i18n.translate( + 'xpack.infra.lobs.logEntryActionsViewInContextButton', + { + defaultMessage: 'View in context', + } +); + export const LogEntryActionsColumn: React.FC = ({ isHovered, isMenuOpen, onOpenMenu, onCloseMenu, onViewDetails, + onViewLogInContext, }) => { const handleClickViewDetails = useCallback(() => { onCloseMenu(); - onViewDetails(); + + // Function might be `undefined` and the linter doesn't like that. + // eslint-disable-next-line no-unused-expressions + onViewDetails?.(); }, [onCloseMenu, onViewDetails]); + const handleClickViewInContext = useCallback(() => { + onCloseMenu(); + + // Function might be `undefined` and the linter doesn't like that. + // eslint-disable-next-line no-unused-expressions + onViewLogInContext?.(); + }, [onCloseMenu, onViewLogInContext]); + const button = ( = ({ + {onViewLogInContext !== undefined ? ( + + ) : null} diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx index 7d7df796d13ad..5c20df000ae51 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx @@ -5,6 +5,7 @@ */ import React, { memo, useState, useCallback, useMemo } from 'react'; +import { isEmpty } from 'lodash'; import { euiStyled } from '../../../../../observability/public'; import { isTimestampColumn } from '../../../utils/log_entry'; @@ -32,6 +33,7 @@ interface LogEntryRowProps { isHighlighted: boolean; logEntry: LogEntry; openFlyoutWithItem?: (id: string) => void; + openViewLogInContext?: (entry: LogEntry) => void; scale: TextScale; wrap: boolean; } @@ -46,6 +48,7 @@ export const LogEntryRow = memo( isHighlighted, logEntry, openFlyoutWithItem, + openViewLogInContext, scale, wrap, }: LogEntryRowProps) => { @@ -63,6 +66,16 @@ export const LogEntryRow = memo( logEntry.id, ]); + const handleOpenViewLogInContext = useCallback(() => openViewLogInContext?.(logEntry), [ + openViewLogInContext, + logEntry, + ]); + + const hasContext = useMemo(() => !isEmpty(logEntry.context), [logEntry]); + const hasActionFlyoutWithItem = openFlyoutWithItem !== undefined; + const hasActionViewLogInContext = hasContext && openViewLogInContext !== undefined; + const hasActionsMenu = hasActionFlyoutWithItem || hasActionViewLogInContext; + const logEntryColumnsById = useMemo( () => logEntry.columns.reduce<{ @@ -165,18 +178,23 @@ export const LogEntryRow = memo( ); } })} - - - + {hasActionsMenu ? ( + + + + ) : null} ); } diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx index 2c389b47fa6cf..f89aaf12db1bc 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx @@ -26,6 +26,7 @@ import { MeasurableItemView } from './measurable_item_view'; import { VerticalScrollPanel } from './vertical_scroll_panel'; import { useColumnWidths, LogEntryColumnWidths } from './log_entry_column'; import { LogDateRow } from './log_date_row'; +import { LogEntry } from '../../../../common/http_api'; interface ScrollableLogTextStreamViewProps { columnConfigurations: LogColumnConfiguration[]; @@ -50,8 +51,9 @@ interface ScrollableLogTextStreamViewProps { }) => any; loadNewerItems: () => void; reloadItems: () => void; - setFlyoutItem: (id: string) => void; - setFlyoutVisibility: (visible: boolean) => void; + setFlyoutItem?: (id: string) => void; + setFlyoutVisibility?: (visible: boolean) => void; + setContextEntry?: (entry: LogEntry) => void; highlightedItem: string | null; currentHighlightKey: UniqueTimeKey | null; startDateExpression: string; @@ -140,9 +142,16 @@ export class ScrollableLogTextStreamView extends React.PureComponent< lastLoadedTime, updateDateRange, startLiveStreaming, + setFlyoutItem, + setFlyoutVisibility, + setContextEntry, } = this.props; + const { targetId, items, isScrollLocked } = this.state; const hasItems = items.length > 0; + const hasFlyoutAction = !!(setFlyoutItem && setFlyoutVisibility); + const hasContextAction = !!setContextEntry; + return ( {isReloading && (!isStreaming || !hasItems) ? ( @@ -227,7 +236,14 @@ export class ScrollableLogTextStreamView extends React.PureComponent< { - this.props.setFlyoutItem(id); - this.props.setFlyoutVisibility(true); + const { setFlyoutItem, setFlyoutVisibility } = this.props; + + if (setFlyoutItem && setFlyoutVisibility) { + setFlyoutItem(id); + setFlyoutVisibility(true); + } + }; + + private handleOpenViewLogInContext = (entry: LogEntry) => { + const { setContextEntry } = this.props; + if (setContextEntry) { + setContextEntry(entry); + } }; private handleReload = () => { diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx index 69a6abbca4b34..0eb6140c0de84 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx @@ -33,7 +33,7 @@ export const hoveredContentStyle = css` `; export const highlightedContentStyle = css` - background-color: ${props => props.theme.eui.euiFocusBackgroundColor}; + background-color: ${props => props.theme.eui.euiColorHighlight}; `; export const longWrappedContentStyle = css` diff --git a/x-pack/plugins/infra/public/components/toolbar_panel.ts b/x-pack/plugins/infra/public/components/toolbar_panel.ts new file mode 100644 index 0000000000000..65cde03ec98e7 --- /dev/null +++ b/x-pack/plugins/infra/public/components/toolbar_panel.ts @@ -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 { EuiPanel } from '@elastic/eui'; +import { euiStyled } from '../../../observability/public'; + +export const ToolbarPanel = euiStyled(EuiPanel).attrs(() => ({ + grow: false, + paddingSize: 'none', +}))` + border-top: none; + border-right: none; + border-left: none; + border-radius: 0; + padding: ${props => `12px ${props.theme.eui.paddingSizes.m}`}; +`; diff --git a/x-pack/legacy/plugins/rollup/server/lib/is_es_error/index.ts b/x-pack/plugins/infra/public/containers/logs/view_log_in_context/index.ts similarity index 84% rename from x-pack/legacy/plugins/rollup/server/lib/is_es_error/index.ts rename to x-pack/plugins/infra/public/containers/logs/view_log_in_context/index.ts index a9a3c61472d8c..0110c55c7c556 100644 --- a/x-pack/legacy/plugins/rollup/server/lib/is_es_error/index.ts +++ b/x-pack/plugins/infra/public/containers/logs/view_log_in_context/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { isEsError } from './is_es_error'; +export * from './view_log_in_context'; diff --git a/x-pack/plugins/infra/public/containers/logs/view_log_in_context/view_log_in_context.ts b/x-pack/plugins/infra/public/containers/logs/view_log_in_context/view_log_in_context.ts new file mode 100644 index 0000000000000..bc719cbd694e4 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/view_log_in_context/view_log_in_context.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { useState, useEffect, useCallback } from 'react'; +import createContainer from 'constate'; +import { LogEntry } from '../../../../common/http_api'; +import { fetchLogEntries } from '../log_entries/api/fetch_log_entries'; +import { esKuery } from '../../../../../../../src/plugins/data/public'; + +function getQueryFromLogEntry(entry: LogEntry) { + const expression = Object.entries(entry.context).reduce((kuery, [key, value]) => { + const currentExpression = `${key} : "${value}"`; + if (kuery.length > 0) { + return `${kuery} AND ${currentExpression}`; + } else { + return currentExpression; + } + }, ''); + + return JSON.stringify(esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(expression))); +} + +interface ViewLogInContextProps { + sourceId: string; + startTimestamp: number; + endTimestamp: number; +} + +export interface ViewLogInContextState { + entries: LogEntry[]; + isLoading: boolean; + contextEntry?: LogEntry; +} + +interface ViewLogInContextCallbacks { + setContextEntry: (entry?: LogEntry) => void; +} + +export const useViewLogInContext = ( + props: ViewLogInContextProps +): [ViewLogInContextState, ViewLogInContextCallbacks] => { + const [contextEntry, setContextEntry] = useState(); + const [entries, setEntries] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const { startTimestamp, endTimestamp, sourceId } = props; + + const maybeFetchLogs = useCallback(async () => { + if (contextEntry) { + setIsLoading(true); + const { data } = await fetchLogEntries({ + sourceId, + startTimestamp, + endTimestamp, + center: contextEntry.cursor, + query: getQueryFromLogEntry(contextEntry), + }); + setEntries(data.entries); + setIsLoading(false); + } else { + setEntries([]); + setIsLoading(false); + } + }, [contextEntry, startTimestamp, endTimestamp, sourceId]); + + useEffect(() => { + maybeFetchLogs(); + }, [maybeFetchLogs]); + + return [ + { + contextEntry, + entries, + isLoading, + }, + { + setContextEntry, + }, + ]; +}; + +export const ViewLogInContext = createContainer(useViewLogInContext); diff --git a/x-pack/plugins/infra/public/hooks/use_link_props.tsx b/x-pack/plugins/infra/public/hooks/use_link_props.tsx index 8c522bb7fa764..dec8eaae56f41 100644 --- a/x-pack/plugins/infra/public/hooks/use_link_props.tsx +++ b/x-pack/plugins/infra/public/hooks/use_link_props.tsx @@ -26,12 +26,20 @@ interface LinkProps { onClick?: (e: React.MouseEvent | React.MouseEvent) => void; } -export const useLinkProps = ({ app, pathname, hash, search }: LinkDescriptor): LinkProps => { +interface Options { + hrefOnly?: boolean; +} + +export const useLinkProps = ( + { app, pathname, hash, search }: LinkDescriptor, + options: Options = {} +): LinkProps => { validateParams({ app, pathname, hash, search }); const { prompt } = useNavigationWarningPrompt(); const prefixer = usePrefixPathWithBasepath(); const navigateToApp = useKibana().services.application?.navigateToApp; + const { hrefOnly } = options; const encodedSearch = useMemo(() => { return search ? encodeSearch(search) : undefined; @@ -86,7 +94,10 @@ export const useLinkProps = ({ app, pathname, hash, search }: LinkDescriptor): L return { href, - onClick, + // Sometimes it may not be desirable to have onClick call "navigateToApp". + // E.g. the management section of Kibana cannot be successfully deeplinked to via + // "navigateToApp". In those cases we can choose to defer to legacy behaviour. + onClick: hrefOnly ? undefined : onClick, }; }; diff --git a/x-pack/plugins/infra/public/pages/logs/page_content.tsx b/x-pack/plugins/infra/public/pages/logs/page_content.tsx index ed6f06deeef64..dc210406275d8 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_content.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { Route, Switch } from 'react-router-dom'; @@ -21,6 +22,7 @@ import { LogEntryCategoriesPage } from './log_entry_categories'; import { LogEntryRatePage } from './log_entry_rate'; import { LogsSettingsPage } from './settings'; import { StreamPage } from './stream'; +import { AlertDropdown } from '../../components/alerting/logs/alert_dropdown'; export const LogsPageContent: React.FunctionComponent = () => { const uiCapabilities = useKibana().services.application?.capabilities; @@ -65,13 +67,20 @@ export const LogsPageContent: React.FunctionComponent = () => { readOnlyBadge={!uiCapabilities?.logs?.save} /> - + + + + + + + + diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page.tsx index 3e07dcebf112d..aff0ac27c36f8 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page.tsx @@ -10,6 +10,7 @@ import { ColumnarPage } from '../../../components/page'; import { StreamPageContent } from './page_content'; import { StreamPageHeader } from './page_header'; import { LogsPageProviders } from './page_providers'; +import { PageViewLogInContext } from './page_view_log_in_context'; import { useTrackPageview } from '../../../../../observability/public'; export const StreamPage = () => { @@ -21,6 +22,7 @@ export const StreamPage = () => { + ); }; diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx index b6061203a1c72..3208ea2402950 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_logs_content.tsx @@ -27,6 +27,7 @@ import { Source } from '../../../containers/source'; import { LogsToolbar } from './page_toolbar'; import { LogHighlightsState } from '../../../containers/logs/log_highlights'; +import { ViewLogInContext } from '../../../containers/logs/view_log_in_context'; export const LogsPageLogsContent: React.FunctionComponent = () => { const { source, sourceId, version } = useContext(Source.Context); @@ -55,6 +56,9 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { endDateExpression, updateDateRange, } = useContext(LogPositionState.Context); + + const [, { setContextEntry }] = useContext(ViewLogInContext.Context); + return ( <> @@ -104,6 +108,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { wrap={textWrap} setFlyoutItem={setFlyoutId} setFlyoutVisibility={setFlyoutVisibility} + setContextEntry={setContextEntry} highlightedItem={surroundingLogsId ? surroundingLogsId : null} currentHighlightKey={currentHighlightKey} startDateExpression={startDateExpression} diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx index e4ccdaf7c5748..0341dc7c35b33 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_providers.tsx @@ -14,6 +14,7 @@ import { LogFilterState, WithLogFilterUrlState } from '../../../containers/logs/ import { LogEntriesState } from '../../../containers/logs/log_entries'; import { Source } from '../../../containers/source'; +import { ViewLogInContext } from '../../../containers/logs/view_log_in_context'; const LogFilterStateProvider: React.FC = ({ children }) => { const { createDerivedIndexPattern } = useContext(Source.Context); @@ -26,6 +27,25 @@ const LogFilterStateProvider: React.FC = ({ children }) => { ); }; +const ViewLogInContextProvider: React.FC = ({ children }) => { + const { startTimestamp, endTimestamp } = useContext(LogPositionState.Context); + const { sourceId } = useContext(Source.Context); + + if (!startTimestamp || !endTimestamp) { + return null; + } + + return ( + + {children} + + ); +}; + const LogEntriesStateProvider: React.FC = ({ children }) => { const { sourceId } = useContext(Source.Context); const { @@ -91,11 +111,13 @@ export const LogsPageProviders: React.FunctionComponent = ({ children }) => { - - - {children} - - + + + + {children} + + + diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx new file mode 100644 index 0000000000000..fdfc16d6a9bef --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_view_log_in_context.tsx @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useContext, useCallback, useMemo } from 'react'; +import { noop } from 'lodash'; +import { + EuiOverlayMask, + EuiModal, + EuiModalBody, + EuiText, + EuiTextColor, + EuiFlexGroup, + EuiFlexItem, + EuiToolTip, +} from '@elastic/eui'; +import { ViewLogInContext } from '../../../containers/logs/view_log_in_context'; +import { LogEntry } from '../../../../common/http_api'; +import { Source } from '../../../containers/source'; +import { LogViewConfiguration } from '../../../containers/logs/log_view_configuration'; +import { ScrollableLogTextStreamView } from '../../../components/logging/log_text_stream'; +import { useViewportDimensions } from '../../../utils/use_viewport_dimensions'; + +const MODAL_MARGIN = 25; + +export const PageViewLogInContext: React.FC = () => { + const { source } = useContext(Source.Context); + const { textScale, textWrap } = useContext(LogViewConfiguration.Context); + const columnConfigurations = useMemo(() => (source && source.configuration.logColumns) || [], [ + source, + ]); + const [{ contextEntry, entries, isLoading }, { setContextEntry }] = useContext( + ViewLogInContext.Context + ); + const closeModal = useCallback(() => setContextEntry(undefined), [setContextEntry]); + const { width: vw, height: vh } = useViewportDimensions(); + + const streamItems = useMemo( + () => + entries.map(entry => ({ + kind: 'logEntry' as const, + logEntry: entry, + highlights: [], + })), + [entries] + ); + + if (!contextEntry) { + return null; + } + + return ( + + + + + + + + + + + + + ); +}; + +const LogEntryContext: React.FC<{ context: LogEntry['context'] }> = ({ context }) => { + if ('container.id' in context) { + return

Displayed logs are from container {context['container.id']}

; + } + + if ('host.name' in context) { + const shortenedFilePath = + context['log.file.path'].length > 45 + ? context['log.file.path'].slice(0, 20) + '...' + context['log.file.path'].slice(-25) + : context['log.file.path']; + + return ( + +

+ + Displayed logs are from file{' '} + + {shortenedFilePath} + {' '} + and host {context['host.name']} + +

+
+ ); + } + + return null; +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx new file mode 100644 index 0000000000000..f0bc404dc3797 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/dropdown_button.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; +import React, { ReactNode } from 'react'; +import { withTheme, EuiTheme } from '../../../../../../observability/public'; + +interface Props { + label: string; + onClick: () => void; + theme: EuiTheme; + children: ReactNode; +} + +export const DropdownButton = withTheme(({ onClick, label, theme, children }: Props) => { + return ( + + + {label} + + + + {children} + + + + ); +}); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/toolbar.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/filter_bar.tsx similarity index 57% rename from x-pack/plugins/infra/public/pages/metrics/inventory_view/toolbar.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/filter_bar.tsx index d6a87a0197f5f..708d5f7d75907 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/toolbar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/filter_bar.tsx @@ -7,17 +7,13 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; -import { Toolbar } from '../../../components/eui/toolbar'; -import { WaffleTimeControls } from './components/waffle/waffle_time_controls'; -import { WaffleInventorySwitcher } from './components/waffle/waffle_inventory_switcher'; -import { SearchBar } from './components/search_bar'; +import { WaffleTimeControls } from './waffle/waffle_time_controls'; +import { SearchBar } from './search_bar'; +import { ToolbarPanel } from '../../../../components/toolbar_panel'; -export const SnapshotToolbar = () => ( - +export const FilterBar = () => ( + - - - @@ -25,5 +21,5 @@ export const SnapshotToolbar = () => ( - + ); 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 bc8be9862fe63..a71e43874b480 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 @@ -4,20 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { useInterval } from 'react-use'; -import { euiPaletteColorBlind } from '@elastic/eui'; -import { NodesOverview } from './nodes_overview'; -import { Toolbar } from './toolbars/toolbar'; +import { euiPaletteColorBlind, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { convertIntervalToString } from '../../../../utils/convert_interval_to_string'; +import { NodesOverview, calculateBoundsFromNodes } from './nodes_overview'; import { PageContent } from '../../../../components/page'; import { useSnapshot } from '../hooks/use_snaphot'; -import { useInventoryMeta } from '../hooks/use_inventory_meta'; import { useWaffleTimeContext } from '../hooks/use_waffle_time'; import { useWaffleFiltersContext } from '../hooks/use_waffle_filters'; import { useWaffleOptionsContext } from '../hooks/use_waffle_options'; import { useSourceContext } from '../../../../containers/source'; import { InfraFormatterType, InfraWaffleMapGradientLegend } from '../../../../lib/lib'; +import { euiStyled } from '../../../../../../observability/public'; +import { Toolbar } from './toolbars/toolbar'; +import { ViewSwitcher } from './waffle/view_switcher'; +import { SavedViews } from './saved_views'; +import { IntervalLabel } from './waffle/interval_label'; +import { Legend } from './waffle/legend'; +import { createInventoryMetricFormatter } from '../lib/create_inventory_metric_formatter'; const euiVisColorPalette = euiPaletteColorBlind(); @@ -34,7 +40,6 @@ export const Layout = () => { autoBounds, boundsOverride, } = useWaffleOptionsContext(); - const { accounts, regions } = useInventoryMeta(sourceId, nodeType); const { currentTime, jumpToTime, isAutoReloading } = useWaffleTimeContext(); const { filterQueryAsJson, applyFilterQuery } = useWaffleFiltersContext(); const { loading, nodes, reload, interval } = useSnapshot( @@ -72,25 +77,75 @@ export const Layout = () => { isAutoReloading ? 5000 : null ); + const intervalAsString = convertIntervalToString(interval); + const dataBounds = calculateBoundsFromNodes(nodes); + const bounds = autoBounds ? dataBounds : boundsOverride; + const formatter = useCallback(createInventoryMetricFormatter(options.metric), [options.metric]); + return ( <> - - + + + + + + + + + + + + + + + + + + + + + + + + ); }; + +const MainContainer = euiStyled.div` + position: relative; + flex: 1 1 auto; +`; + +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} ${props => props.theme.eui.paddingSizes.s}; + position: absolute; + left: 0; + bottom: 4px; + right: 0; +`; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx index afbfd2a079253..966a327f40bc1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/nodes_overview.tsx @@ -4,31 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { get, max, min } from 'lodash'; -import React from 'react'; +import { max, min } from 'lodash'; +import React, { useCallback } from 'react'; +import { InventoryItemType } from '../../../../../common/inventory_models/types'; import { euiStyled } from '../../../../../../observability/public'; -import { - InfraFormatterType, - InfraWaffleMapBounds, - InfraWaffleMapOptions, -} from '../../../../lib/lib'; -import { createFormatter } from '../../../../utils/formatters'; +import { InfraWaffleMapBounds, InfraWaffleMapOptions, InfraFormatter } from '../../../../lib/lib'; import { NoData } from '../../../../components/empty_states'; import { InfraLoadingPanel } from '../../../../components/loading'; import { Map } from './waffle/map'; -import { ViewSwitcher } from './waffle/view_switcher'; import { TableView } from './table_view'; -import { - SnapshotNode, - SnapshotCustomMetricInputRT, -} from '../../../../../common/http_api/snapshot_api'; -import { convertIntervalToString } from '../../../../utils/convert_interval_to_string'; -import { InventoryItemType } from '../../../../../common/inventory_models/types'; -import { createFormatterForMetric } from '../../metrics_explorer/components/helpers/create_formatter_for_metric'; +import { SnapshotNode } from '../../../../../common/http_api/snapshot_api'; export interface KueryFilterQuery { kind: 'kuery'; @@ -43,74 +30,13 @@ interface Props { reload: () => void; onDrilldown: (filter: KueryFilterQuery) => void; currentTime: number; - onViewChange: (view: string) => void; view: string; boundsOverride: InfraWaffleMapBounds; autoBounds: boolean; - interval: string; -} - -interface MetricFormatter { - formatter: InfraFormatterType; - template: string; - bounds?: { min: number; max: number }; -} - -interface MetricFormatters { - [key: string]: MetricFormatter; + formatter: InfraFormatter; } -const METRIC_FORMATTERS: MetricFormatters = { - ['count']: { formatter: InfraFormatterType.number, template: '{{value}}' }, - ['cpu']: { - formatter: InfraFormatterType.percent, - template: '{{value}}', - }, - ['memory']: { - formatter: InfraFormatterType.percent, - template: '{{value}}', - }, - ['rx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, - ['tx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, - ['logRate']: { - formatter: InfraFormatterType.abbreviatedNumber, - template: '{{value}}/s', - }, - ['diskIOReadBytes']: { - formatter: InfraFormatterType.bytes, - template: '{{value}}/s', - }, - ['diskIOWriteBytes']: { - formatter: InfraFormatterType.bytes, - template: '{{value}}/s', - }, - ['s3BucketSize']: { - formatter: InfraFormatterType.bytes, - template: '{{value}}', - }, - ['s3TotalRequests']: { - formatter: InfraFormatterType.abbreviatedNumber, - template: '{{value}}', - }, - ['s3NumberOfObjects']: { - formatter: InfraFormatterType.abbreviatedNumber, - template: '{{value}}', - }, - ['s3UploadBytes']: { - formatter: InfraFormatterType.bytes, - template: '{{value}}', - }, - ['s3DownloadBytes']: { - formatter: InfraFormatterType.bytes, - template: '{{value}}', - }, - ['sqsOldestMessage']: { - formatter: InfraFormatterType.number, - template: '{{value}} seconds', - }, -}; - -const calculateBoundsFromNodes = (nodes: SnapshotNode[]): InfraWaffleMapBounds => { +export const calculateBoundsFromNodes = (nodes: SnapshotNode[]): InfraWaffleMapBounds => { const maxValues = nodes.map(node => node.metric.max); const minValues = nodes.map(node => node.metric.value); // if there is only one value then we need to set the bottom range to zero for min @@ -122,141 +48,97 @@ const calculateBoundsFromNodes = (nodes: SnapshotNode[]): InfraWaffleMapBounds = return { min: min(minValues) || 0, max: max(maxValues) || 0 }; }; -export const NodesOverview = class extends React.Component { - public static displayName = 'Waffle'; - public render() { - const { - autoBounds, - boundsOverride, - loading, - nodes, - nodeType, - reload, - view, - currentTime, - options, - interval, - } = this.props; - if (loading) { - return ( - - ); - } else if (!loading && nodes && nodes.length === 0) { - return ( - { - reload(); - }} - testString="noMetricsDataPrompt" - /> - ); - } - const dataBounds = calculateBoundsFromNodes(nodes); - const bounds = autoBounds ? dataBounds : boundsOverride; - const intervalAsString = convertIntervalToString(interval); +export const NodesOverview = ({ + autoBounds, + boundsOverride, + loading, + nodes, + nodeType, + reload, + view, + currentTime, + options, + formatter, + onDrilldown, +}: Props) => { + const handleDrilldown = useCallback( + (filter: string) => { + onDrilldown({ + kind: 'kuery', + expression: filter, + }); + return; + }, + [onDrilldown] + ); + + const noData = !loading && nodes && nodes.length === 0; + if (loading) { + return ( + + ); + } else if (noData) { return ( - - - - - - - - -

- -

-
-
-
-
- {view === 'table' ? ( - - - - ) : ( - - - - )} -
+ { + reload(); + }} + testString="noMetricsDataPrompt" + /> ); } + const dataBounds = calculateBoundsFromNodes(nodes); + const bounds = autoBounds ? dataBounds : boundsOverride; - private handleViewChange = (view: string) => this.props.onViewChange(view); - - // TODO: Change this to a real implimentation using the tickFormatter from the prototype as an example. - private formatter = (val: string | number) => { - const { metric } = this.props.options; - if (SnapshotCustomMetricInputRT.is(metric)) { - const formatter = createFormatterForMetric(metric); - return formatter(val); - } - const metricFormatter = get(METRIC_FORMATTERS, metric.type, METRIC_FORMATTERS.count); - if (val == null) { - return ''; - } - const formatter = createFormatter(metricFormatter.formatter, metricFormatter.template); - return formatter(val); - }; - - private handleDrilldown = (filter: string) => { - this.props.onDrilldown({ - kind: 'kuery', - expression: filter, - }); - return; - }; + if (view === 'table') { + return ( + + + + ); + } + return ( + + + + ); }; -const MainContainer = euiStyled.div` - position: relative; - flex: 1 1 auto; -`; - const TableContainer = euiStyled.div` padding: ${props => props.theme.eui.paddingSizes.l}; `; -const ViewSwitcherContainer = euiStyled.div` - padding: ${props => props.theme.eui.paddingSizes.l}; -`; - const MapContainer = euiStyled.div` position: absolute; display: flex; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/save_views.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/saved_views.tsx similarity index 68% rename from x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/save_views.tsx rename to x-pack/plugins/infra/public/pages/metrics/inventory_view/components/saved_views.tsx index eb40ea595662a..356f0598e00d2 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/save_views.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/saved_views.tsx @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { SavedViewsToolbarControls } from '../../../../../components/saved_views/toolbar_control'; -import { inventoryViewSavedObjectType } from '../../../../../../common/saved_objects/inventory_view'; -import { useWaffleViewState } from '../../hooks/use_waffle_view_state'; +import { SavedViewsToolbarControls } from '../../../../components/saved_views/toolbar_control'; +import { inventoryViewSavedObjectType } from '../../../../../common/saved_objects/inventory_view'; +import { useWaffleViewState } from '../hooks/use_waffle_view_state'; export const SavedViews = () => { const { viewState, defaultViewState, onViewChange } = useWaffleViewState(); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx index 3ac9c2c189628..e8485fb812586 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx @@ -6,6 +6,7 @@ import React, { FunctionComponent } from 'react'; import { EuiFlexItem } from '@elastic/eui'; +import { useSourceContext } from '../../../../../containers/source'; import { SnapshotMetricInput, SnapshotGroupBy, @@ -19,7 +20,7 @@ import { InfraGroupByOptions } from '../../../../../lib/lib'; import { IIndexPattern } from '../../../../../../../../../src/plugins/data/public'; import { InventoryItemType } from '../../../../../../common/inventory_models/types'; import { WaffleOptionsState } from '../../hooks/use_waffle_options'; -import { SavedViews } from './save_views'; +import { useInventoryMeta } from '../../hooks/use_inventory_meta'; export interface ToolbarProps extends Omit { @@ -45,9 +46,6 @@ const wrapToolbarItems = ( <> - - - )} @@ -56,10 +54,11 @@ const wrapToolbarItems = ( interface Props { nodeType: InventoryItemType; - regions: string[]; - accounts: InventoryCloudAccount[]; } -export const Toolbar = ({ nodeType, accounts, regions }: Props) => { + +export const Toolbar = ({ nodeType }: Props) => { + const { sourceId } = useSourceContext(); + const { accounts, regions } = useInventoryMeta(sourceId, nodeType); const ToolbarItems = findToolbar(nodeType); return wrapToolbarItems(ToolbarItems, accounts, regions); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx index 86cc0d8ee62e0..ea53122984161 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar_wrapper.tsx @@ -5,14 +5,14 @@ */ import React from 'react'; -import { EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { SnapshotMetricType } from '../../../../../../common/inventory_models/types'; -import { Toolbar } from '../../../../../components/eui/toolbar'; -import { ToolbarProps } from './toolbar'; import { fieldToName } from '../../lib/field_to_display_name'; import { useSourceContext } from '../../../../../containers/source'; import { useWaffleOptionsContext } from '../../hooks/use_waffle_options'; +import { WaffleInventorySwitcher } from '../waffle/waffle_inventory_switcher'; +import { ToolbarProps } from './toolbar'; interface Props { children: (props: Omit) => React.ReactElement; @@ -36,26 +36,27 @@ export const ToolbarWrapper = (props: Props) => { } = useWaffleOptionsContext(); const { createDerivedIndexPattern } = useSourceContext(); return ( - - - {props.children({ - createDerivedIndexPattern, - changeMetric, - changeGroupBy, - changeAccount, - changeRegion, - changeCustomOptions, - customOptions, - groupBy, - metric, - nodeType, - region, - accountId, - customMetrics, - changeCustomMetrics, - })} - - + <> + + + + {props.children({ + createDerivedIndexPattern, + changeMetric, + changeGroupBy, + changeAccount, + changeRegion, + changeCustomOptions, + customOptions, + groupBy, + metric, + nodeType, + region, + accountId, + customMetrics, + changeCustomMetrics, + })} + ); }; 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 new file mode 100644 index 0000000000000..dbbfb0f49c0e9 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/interval_label.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +interface Props { + intervalAsString?: string; +} + +export const IntervalLabel = ({ intervalAsString }: Props) => { + if (!intervalAsString) { + return null; + } + + return ( + +

+ +

+
+ ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx index ccb4cc71924f4..ac699f96a75a6 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend.tsx @@ -53,7 +53,7 @@ export const Legend: React.FC = ({ dataBounds, legend, bounds, formatter const LegendContainer = euiStyled.div` position: absolute; - bottom: 10px; + bottom: 0px; left: 10px; right: 10px; `; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx index 6ec21ad2e1b49..30447e5244241 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/legend_controls.tsx @@ -40,7 +40,7 @@ export const LegendControls = ({ autoBounds, boundsOverride, onChange, dataBound const [draftBounds, setDraftBounds] = useState(autoBounds ? dataBounds : boundsOverride); // should come from bounds prop const buttonComponent = ( = ({ } })} - ); }} diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/index.tsx index 08d5b3e9e0670..f91e9a4034bc2 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/metric_control/index.tsx @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFilterButton, EuiFilterGroup, EuiPopover } from '@elastic/eui'; +import { EuiPopover } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import React, { useState, useCallback } from 'react'; import { IFieldType } from 'src/plugins/data/public'; import { @@ -21,6 +20,7 @@ import { ModeSwitcher } from './mode_switcher'; import { MetricsEditMode } from './metrics_edit_mode'; import { CustomMetricMode } from './types'; import { SnapshotMetricType } from '../../../../../../../common/inventory_models/types'; +import { DropdownButton } from '../../dropdown_button'; interface Props { options: Array<{ text: string; value: string }>; @@ -132,17 +132,13 @@ export const WaffleMetricControls = ({ } const button = ( - - - + + {currentLabel} + ); return ( - + <> - + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx index 78a2cad9ca7ee..76756637eb69e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/view_switcher.tsx @@ -28,7 +28,7 @@ export const ViewSwitcher = ({ view, onChange }: Props) => { label: i18n.translate('xpack.infra.viewSwitcher.tableViewLabel', { defaultMessage: 'Table view', }), - iconType: 'editorUnorderedList', + iconType: 'visTable', }, ]; return ( @@ -37,9 +37,11 @@ export const ViewSwitcher = ({ view, onChange }: Props) => { defaultMessage: 'Switch between table and map view', })} options={buttons} - color="primary" + color="text" + buttonSize="m" idSelected={view} onChange={onChange} + isIconOnly /> ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_accounts_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_accounts_controls.tsx index a8b0cf21bce85..3e4ff1de8291d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_accounts_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_accounts_controls.tsx @@ -4,17 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiContextMenuPanelDescriptor, - EuiFilterButton, - EuiFilterGroup, - EuiPopover, - EuiContextMenu, -} from '@elastic/eui'; +import { EuiContextMenuPanelDescriptor, EuiPopover, EuiContextMenu } from '@elastic/eui'; import React, { useCallback, useState, useMemo } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { InventoryCloudAccount } from '../../../../../../common/http_api/inventory_meta_api'; +import { DropdownButton } from '../dropdown_button'; interface Props { accountId: string; @@ -63,32 +57,26 @@ export const WaffleAccountsControls = (props: Props) => { [options, accountId, changeAccount] ); + const button = ( + + {currentLabel + ? currentLabel.name + : i18n.translate('xpack.infra.waffle.accountAllTitle', { + defaultMessage: 'All', + })} + + ); + return ( - - - - - } - anchorPosition="downLeft" - panelPaddingSize="none" - closePopover={closePopover} - > - - - + + + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx index bc763d2cf9378..c1f406f31e85e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_group_by_controls.tsx @@ -9,8 +9,6 @@ import { EuiContextMenu, EuiContextMenuPanelDescriptor, EuiContextMenuPanelItemDescriptor, - EuiFilterButton, - EuiFilterGroup, EuiPopover, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -22,6 +20,7 @@ import { CustomFieldPanel } from './custom_field_panel'; import { euiStyled } from '../../../../../../../observability/public'; import { InventoryItemType } from '../../../../../../common/inventory_models/types'; import { SnapshotGroupBy } from '../../../../../../common/http_api/snapshot_api'; +import { DropdownButton } from '../dropdown_button'; interface Props { options: Array<{ text: string; field: string; toolTipContent?: string }>; @@ -121,29 +120,31 @@ export const WaffleGroupByControls = class extends React.PureComponent o != null) // In this map the `o && o.field` is totally unnecessary but Typescript is // too stupid to realize that the filter above prevents the next map from being null - .map(o => {o && o.text}) + .map(o => ( + + {o && o.text} + + )) ) : ( ); + const button = ( - - + {buttonBody} - + ); return ( - - - - - + + + ); } diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx index 23e06823f407f..e534c97eda090 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_inventory_switcher.tsx @@ -4,19 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiPopover, - EuiContextMenu, - EuiFilterButton, - EuiFilterGroup, - EuiContextMenuPanelDescriptor, -} from '@elastic/eui'; +import { EuiPopover, EuiContextMenu, EuiContextMenuPanelDescriptor } from '@elastic/eui'; import React, { useCallback, useState, useMemo } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; import { findInventoryModel } from '../../../../../../common/inventory_models'; import { InventoryItemType } from '../../../../../../common/inventory_models/types'; import { useWaffleOptionsContext } from '../../hooks/use_waffle_options'; +import { DropdownButton } from '../dropdown_button'; const getDisplayNameForType = (type: InventoryItemType) => { const inventoryModel = findInventoryModel(type); @@ -120,27 +114,23 @@ export const WaffleInventorySwitcher: React.FC = () => { return getDisplayNameForType(nodeType); }, [nodeType]); + const button = ( + + {selectedText} + + ); + return ( - - - - - } - isOpen={isOpen} - closePopover={closePopover} - panelPaddingSize="none" - withTitle - anchorPosition="downLeft" - > - - - + + + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_region_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_region_controls.tsx index 671e44f42ef6a..9d759424cdc93 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_region_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_region_controls.tsx @@ -4,16 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiContextMenuPanelDescriptor, - EuiFilterButton, - EuiFilterGroup, - EuiPopover, - EuiContextMenu, -} from '@elastic/eui'; +import { EuiContextMenuPanelDescriptor, EuiPopover, EuiContextMenu } from '@elastic/eui'; import React, { useCallback, useState, useMemo } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { DropdownButton } from '../dropdown_button'; interface Props { region?: string; @@ -62,32 +56,25 @@ export const WaffleRegionControls = (props: Props) => { [changeRegion, options, region] ); + const button = ( + + {currentLabel || + i18n.translate('xpack.infra.waffle.region', { + defaultMessage: 'All', + })} + + ); + return ( - - - - - } - anchorPosition="downLeft" - panelPaddingSize="none" - closePopover={closePopover} - > - - - + + + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx index e473aea7a1f0b..3a2c33d1c824c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx @@ -8,7 +8,7 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useContext } from 'react'; -import { SnapshotToolbar } from './toolbar'; +import { FilterBar } from './components/filter_bar'; import { DocumentTitle } from '../../../components/document_title'; import { NoIndices } from '../../../components/empty_states/no_indices'; @@ -56,7 +56,7 @@ export const SnapshotPage = () => { ) : metricIndicesExist ? ( <> - + ) : hasFailedLoadingSource ? ( diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts new file mode 100644 index 0000000000000..acd71e5137694 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_inventory_metric_formatter.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash'; +import { createFormatter } from '../../../../utils/formatters'; +import { InfraFormatterType } from '../../../../lib/lib'; +import { + SnapshotMetricInput, + SnapshotCustomMetricInputRT, +} from '../../../../../common/http_api/snapshot_api'; +import { createFormatterForMetric } from '../../metrics_explorer/components/helpers/create_formatter_for_metric'; + +interface MetricFormatter { + formatter: InfraFormatterType; + template: string; + bounds?: { min: number; max: number }; +} + +interface MetricFormatters { + [key: string]: MetricFormatter; +} + +const METRIC_FORMATTERS: MetricFormatters = { + ['count']: { formatter: InfraFormatterType.number, template: '{{value}}' }, + ['cpu']: { + formatter: InfraFormatterType.percent, + template: '{{value}}', + }, + ['memory']: { + formatter: InfraFormatterType.percent, + template: '{{value}}', + }, + ['rx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, + ['tx']: { formatter: InfraFormatterType.bits, template: '{{value}}/s' }, + ['logRate']: { + formatter: InfraFormatterType.abbreviatedNumber, + template: '{{value}}/s', + }, + ['diskIOReadBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}/s', + }, + ['diskIOWriteBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}/s', + }, + ['s3BucketSize']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + ['s3TotalRequests']: { + formatter: InfraFormatterType.abbreviatedNumber, + template: '{{value}}', + }, + ['s3NumberOfObjects']: { + formatter: InfraFormatterType.abbreviatedNumber, + template: '{{value}}', + }, + ['s3UploadBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + ['s3DownloadBytes']: { + formatter: InfraFormatterType.bytes, + template: '{{value}}', + }, + ['sqsOldestMessage']: { + formatter: InfraFormatterType.number, + template: '{{value}} seconds', + }, +}; + +export const createInventoryMetricFormatter = (metric: SnapshotMetricInput) => ( + val: string | number +) => { + if (SnapshotCustomMetricInputRT.is(metric)) { + const formatter = createFormatterForMetric(metric); + return formatter(val); + } + const metricFormatter = get(METRIC_FORMATTERS, metric.type, METRIC_FORMATTERS.count); + if (val == null) { + return ''; + } + const formatter = createFormatter(metricFormatter.formatter, metricFormatter.template); + return formatter(val); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx index 81971bd31a973..6913f67bad08a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx @@ -17,7 +17,6 @@ import { MetricsExplorerTimeOptions, MetricsExplorerChartOptions, } from '../hooks/use_metrics_explorer_options'; -import { Toolbar } from '../../../../components/eui/toolbar'; import { MetricsExplorerKueryBar } from './kuery_bar'; import { MetricsExplorerMetrics } from './metrics'; import { MetricsExplorerGroupBy } from './group_by'; @@ -28,6 +27,7 @@ import { MetricExplorerViewState } from '../hooks/use_metric_explorer_state'; import { metricsExplorerViewSavedObjectType } from '../../../../../common/saved_objects/metrics_explorer_view'; import { useKibanaUiSetting } from '../../../../utils/use_kibana_ui_setting'; import { mapKibanaQuickRangesToDatePickerRanges } from '../../../../utils/map_timepicker_quickranges_to_datepicker_ranges'; +import { ToolbarPanel } from '../../../../components/toolbar_panel'; interface Props { derivedIndexPattern: IIndexPattern; @@ -65,7 +65,7 @@ export const MetricsExplorerToolbar = ({ const commonlyUsedRanges = mapKibanaQuickRangesToDatePickerRanges(timepickerQuickRanges); return ( - + - + ); }; diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 3b6647b9bfbbe..40366b2a54f24 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -21,7 +21,8 @@ import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/p import { DataEnhancedSetup, DataEnhancedStart } from '../../data_enhanced/public'; import { TriggersAndActionsUIPublicPluginSetup } from '../../../plugins/triggers_actions_ui/public'; -import { getAlertType } from './components/alerting/metrics/metric_threshold_alert_type'; +import { getAlertType as getMetricsAlertType } from './components/alerting/metrics/metric_threshold_alert_type'; +import { getAlertType as getLogsAlertType } from './components/alerting/logs/log_threshold_alert_type'; export type ClientSetup = void; export type ClientStart = void; @@ -52,7 +53,8 @@ export class Plugin setup(core: CoreSetup, pluginsSetup: ClientPluginsSetup) { registerFeatures(pluginsSetup.home); - pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getAlertType()); + pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getMetricsAlertType()); + pluginsSetup.triggers_actions_ui.alertTypeRegistry.register(getLogsAlertType()); core.application.register({ id: 'logs', diff --git a/x-pack/plugins/infra/public/utils/datemath.test.ts b/x-pack/plugins/infra/public/utils/datemath.test.ts index 0f272733c5f97..c8fbe5583db2e 100644 --- a/x-pack/plugins/infra/public/utils/datemath.test.ts +++ b/x-pack/plugins/infra/public/utils/datemath.test.ts @@ -198,7 +198,7 @@ describe('extendDatemath()', () => { }); }); - describe('with a positive Operator', () => { + describe('with a positive operator', () => { it('Halves miliseconds', () => { expect(extendDatemath('now+250ms')).toEqual({ value: 'now+125ms', @@ -307,6 +307,274 @@ describe('extendDatemath()', () => { }); }); }); + + describe('moving after', () => { + describe('with a negative operator', () => { + it('Halves miliseconds', () => { + expect(extendDatemath('now-250ms', 'after')).toEqual({ + value: 'now-125ms', + diffAmount: 125, + diffUnit: 'ms', + }); + }); + + it('Halves seconds', () => { + expect(extendDatemath('now-10s', 'after')).toEqual({ + value: 'now-5s', + diffAmount: 5, + diffUnit: 's', + }); + }); + + it('Halves minutes when the amount is low', () => { + expect(extendDatemath('now-2m', 'after')).toEqual({ + value: 'now-1m', + diffAmount: 1, + diffUnit: 'm', + }); + expect(extendDatemath('now-4m', 'after')).toEqual({ + value: 'now-2m', + diffAmount: 2, + diffUnit: 'm', + }); + expect(extendDatemath('now-6m', 'after')).toEqual({ + value: 'now-3m', + diffAmount: 3, + diffUnit: 'm', + }); + }); + + it('advances minutes in half ammounts when the amount is high', () => { + expect(extendDatemath('now-30m', 'after')).toEqual({ + value: 'now-20m', + diffAmount: 10, + diffUnit: 'm', + }); + }); + + it('advances half an hour when the amount is one hour', () => { + expect(extendDatemath('now-1h', 'after')).toEqual({ + value: 'now-30m', + diffAmount: 30, + diffUnit: 'm', + }); + }); + + it('advances one hour when the amount is one day', () => { + expect(extendDatemath('now-1d', 'after')).toEqual({ + value: 'now-23h', + diffAmount: 1, + diffUnit: 'h', + }); + }); + + it('advances one day when the amount is more than one day', () => { + expect(extendDatemath('now-2d', 'after')).toEqual({ + value: 'now-1d', + diffAmount: 1, + diffUnit: 'd', + }); + expect(extendDatemath('now-3d', 'after')).toEqual({ + value: 'now-2d', + diffAmount: 1, + diffUnit: 'd', + }); + }); + + it('advances one day when the amount is one week', () => { + expect(extendDatemath('now-1w', 'after')).toEqual({ + value: 'now-6d', + diffAmount: 1, + diffUnit: 'd', + }); + }); + + it('advances one week when the amount is more than one week', () => { + expect(extendDatemath('now-2w', 'after')).toEqual({ + value: 'now-1w', + diffAmount: 1, + diffUnit: 'w', + }); + }); + + it('advances one week when the amount is one month', () => { + expect(extendDatemath('now-1M', 'after')).toEqual({ + value: 'now-3w', + diffAmount: 1, + diffUnit: 'w', + }); + }); + + it('advances one month when the amount is more than one month', () => { + expect(extendDatemath('now-2M', 'after')).toEqual({ + value: 'now-1M', + diffAmount: 1, + diffUnit: 'M', + }); + }); + + it('advances one month when the amount is one year', () => { + expect(extendDatemath('now-1y', 'after')).toEqual({ + value: 'now-11M', + diffAmount: 1, + diffUnit: 'M', + }); + }); + + it('advances one year when the amount is in years', () => { + expect(extendDatemath('now-2y', 'after')).toEqual({ + value: 'now-1y', + diffAmount: 1, + diffUnit: 'y', + }); + }); + }); + + describe('with a positive operator', () => { + it('doubles miliseconds', () => { + expect(extendDatemath('now+250ms', 'after')).toEqual({ + value: 'now+500ms', + diffAmount: 250, + diffUnit: 'ms', + }); + }); + + it('normalizes miliseconds', () => { + expect(extendDatemath('now+500ms', 'after')).toEqual({ + value: 'now+1s', + diffAmount: 500, + diffUnit: 'ms', + }); + }); + + it('doubles seconds', () => { + expect(extendDatemath('now+10s', 'after')).toEqual({ + value: 'now+20s', + diffAmount: 10, + diffUnit: 's', + }); + }); + + it('normalizes seconds', () => { + expect(extendDatemath('now+30s', 'after')).toEqual({ + value: 'now+1m', + diffAmount: 30, + diffUnit: 's', + }); + }); + + it('doubles minutes when amount is low', () => { + expect(extendDatemath('now+1m', 'after')).toEqual({ + value: 'now+2m', + diffAmount: 1, + diffUnit: 'm', + }); + expect(extendDatemath('now+2m', 'after')).toEqual({ + value: 'now+4m', + diffAmount: 2, + diffUnit: 'm', + }); + expect(extendDatemath('now+3m', 'after')).toEqual({ + value: 'now+6m', + diffAmount: 3, + diffUnit: 'm', + }); + }); + + it('adds half the minutes when the amount is high', () => { + expect(extendDatemath('now+20m', 'after')).toEqual({ + value: 'now+30m', + diffAmount: 10, + diffUnit: 'm', + }); + }); + + it('Adds half an hour when the amount is one hour', () => { + expect(extendDatemath('now+1h', 'after')).toEqual({ + value: 'now+90m', + diffAmount: 30, + diffUnit: 'm', + }); + }); + + it('Adds one hour when the amount more than one hour', () => { + expect(extendDatemath('now+2h', 'after')).toEqual({ + value: 'now+3h', + diffAmount: 1, + diffUnit: 'h', + }); + }); + + it('Adds one hour when the amount is one day', () => { + expect(extendDatemath('now+1d', 'after')).toEqual({ + value: 'now+25h', + diffAmount: 1, + diffUnit: 'h', + }); + }); + + it('Adds one day when the amount is more than one day', () => { + expect(extendDatemath('now+2d', 'after')).toEqual({ + value: 'now+3d', + diffAmount: 1, + diffUnit: 'd', + }); + expect(extendDatemath('now+3d', 'after')).toEqual({ + value: 'now+4d', + diffAmount: 1, + diffUnit: 'd', + }); + }); + + it('Adds one day when the amount is one week', () => { + expect(extendDatemath('now+1w', 'after')).toEqual({ + value: 'now+8d', + diffAmount: 1, + diffUnit: 'd', + }); + }); + + it('Adds one week when the amount is more than one week', () => { + expect(extendDatemath('now+2w', 'after')).toEqual({ + value: 'now+3w', + diffAmount: 1, + diffUnit: 'w', + }); + }); + + it('Adds one week when the amount is one month', () => { + expect(extendDatemath('now+1M', 'after')).toEqual({ + value: 'now+5w', + diffAmount: 1, + diffUnit: 'w', + }); + }); + + it('Adds one month when the amount is more than one month', () => { + expect(extendDatemath('now+2M', 'after')).toEqual({ + value: 'now+3M', + diffAmount: 1, + diffUnit: 'M', + }); + }); + + it('Adds one month when the amount is one year', () => { + expect(extendDatemath('now+1y', 'after')).toEqual({ + value: 'now+13M', + diffAmount: 1, + diffUnit: 'M', + }); + }); + + it('Adds one year when the amount is in years', () => { + expect(extendDatemath('now+2y', 'after')).toEqual({ + value: 'now+3y', + diffAmount: 1, + diffUnit: 'y', + }); + }); + }); + }); }); describe('convertDate()', () => { diff --git a/x-pack/plugins/infra/public/utils/datemath.ts b/x-pack/plugins/infra/public/utils/datemath.ts index 50a9b6e4f6945..7331a2450956f 100644 --- a/x-pack/plugins/infra/public/utils/datemath.ts +++ b/x-pack/plugins/infra/public/utils/datemath.ts @@ -68,7 +68,8 @@ function extendRelativeDatemath( return undefined; } - const mustIncreaseAmount = operator === '-' && direction === 'before'; + const mustIncreaseAmount = + (operator === '-' && direction === 'before') || (operator === '+' && direction === 'after'); const parsedAmount = parseInt(amount, 10); let newUnit: Unit = unit as Unit; let newAmount: number; diff --git a/x-pack/plugins/infra/public/utils/is_displayable.test.ts b/x-pack/plugins/infra/public/utils/is_displayable.test.ts deleted file mode 100644 index ebd5c07327e9b..0000000000000 --- a/x-pack/plugins/infra/public/utils/is_displayable.test.ts +++ /dev/null @@ -1,65 +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 { isDisplayable } from './is_displayable'; - -describe('isDisplayable()', () => { - test('field that is not displayable', () => { - const field = { - name: 'some.field', - type: 'number', - displayable: false, - }; - expect(isDisplayable(field)).toBe(false); - }); - test('field that is displayable', () => { - const field = { - name: 'some.field', - type: 'number', - displayable: true, - }; - expect(isDisplayable(field)).toBe(true); - }); - test('field that an ecs field', () => { - const field = { - name: '@timestamp', - type: 'date', - displayable: true, - }; - expect(isDisplayable(field)).toBe(true); - }); - test('field that matches same prefix', () => { - const field = { - name: 'system.network.name', - type: 'string', - displayable: true, - }; - expect(isDisplayable(field, ['system.network'])).toBe(true); - }); - test('field that does not matches same prefix', () => { - const field = { - name: 'system.load.1', - type: 'number', - displayable: true, - }; - expect(isDisplayable(field, ['system.network'])).toBe(false); - }); - test('field that is an K8s allowed field but does not match prefix', () => { - const field = { - name: 'kubernetes.namespace', - type: 'string', - displayable: true, - }; - expect(isDisplayable(field, ['kubernetes.pod'])).toBe(true); - }); - test('field that is a Prometheus allowed field but does not match prefix', () => { - const field = { - name: 'prometheus.labels.foo.bar', - type: 'string', - displayable: true, - }; - expect(isDisplayable(field, ['prometheus.metrics'])).toBe(true); - }); -}); diff --git a/x-pack/plugins/infra/public/utils/is_displayable.ts b/x-pack/plugins/infra/public/utils/is_displayable.ts deleted file mode 100644 index 534282e807036..0000000000000 --- a/x-pack/plugins/infra/public/utils/is_displayable.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 { IFieldType } from 'src/plugins/data/public'; -import { startsWith, uniq } from 'lodash'; -import { getAllowedListForPrefix } from '../../common/ecs_allowed_list'; - -interface DisplayableFieldType extends IFieldType { - displayable?: boolean; -} - -const fieldStartsWith = (field: DisplayableFieldType) => (name: string) => - startsWith(field.name, name); - -export const isDisplayable = (field: DisplayableFieldType, additionalPrefixes: string[] = []) => { - // We need to start with at least one prefix, even if it's empty - const prefixes = additionalPrefixes && additionalPrefixes.length ? additionalPrefixes : ['']; - // Create a set of allowed list based on the prefixes - const allowedList = prefixes.reduce((acc, prefix) => { - return uniq([...acc, ...getAllowedListForPrefix(prefix)]); - }, [] as string[]); - // If the field is displayable and part of the allowed list or covered by the prefix - return ( - (field.displayable && prefixes.some(fieldStartsWith(field))) || - allowedList.some(fieldStartsWith(field)) - ); -}; diff --git a/x-pack/plugins/infra/public/utils/use_viewport_dimensions.ts b/x-pack/plugins/infra/public/utils/use_viewport_dimensions.ts new file mode 100644 index 0000000000000..ddaf8fcd31a39 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/use_viewport_dimensions.ts @@ -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 { useState, useEffect } from 'react'; +import { throttle } from 'lodash'; + +interface ViewportDimensions { + width: number; + height: number; +} + +const getViewportWidth = () => + window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; +const getViewportHeight = () => + window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight; + +export function useViewportDimensions(): ViewportDimensions { + const [dimensions, setDimensions] = useState({ + width: getViewportWidth(), + height: getViewportHeight(), + }); + + useEffect(() => { + const updateDimensions = throttle(() => { + setDimensions({ + width: getViewportWidth(), + height: getViewportHeight(), + }); + }, 250); + + window.addEventListener('resize', updateDimensions); + return () => window.removeEventListener('resize', updateDimensions); + }, []); + + return dimensions; +} diff --git a/x-pack/plugins/infra/server/graphql/sources/resolvers.ts b/x-pack/plugins/infra/server/graphql/sources/resolvers.ts index 1fe1431392a38..f880eca933241 100644 --- a/x-pack/plugins/infra/server/graphql/sources/resolvers.ts +++ b/x-pack/plugins/infra/server/graphql/sources/resolvers.ts @@ -93,7 +93,10 @@ export const createSourcesResolvers = ( } => ({ Query: { async source(root, args, { req }) { - const requestedSourceConfiguration = await libs.sources.getSourceConfiguration(req, args.id); + const requestedSourceConfiguration = await libs.sources.getSourceConfiguration( + req.core.savedObjects.client, + args.id + ); return requestedSourceConfiguration; }, diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts new file mode 100644 index 0000000000000..cdec04ab81a8e --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts @@ -0,0 +1,250 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { AlertExecutorOptions, AlertServices } from '../../../../../alerting/server'; +import { + AlertStates, + Comparator, + LogDocumentCountAlertParams, + Criterion, +} from '../../../../common/alerting/logs/types'; +import { InfraBackendLibs } from '../../infra_types'; +import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; +import { InfraSource } from '../../../../common/http_api/source_api'; + +const checkValueAgainstComparatorMap: { + [key: string]: (a: number, b: number) => boolean; +} = { + [Comparator.GT]: (a: number, b: number) => a > b, + [Comparator.GT_OR_EQ]: (a: number, b: number) => a >= b, + [Comparator.LT]: (a: number, b: number) => a < b, + [Comparator.LT_OR_EQ]: (a: number, b: number) => a <= b, +}; + +export const createLogThresholdExecutor = (alertUUID: string, libs: InfraBackendLibs) => + async function({ services, params }: AlertExecutorOptions) { + const { count, criteria } = params as LogDocumentCountAlertParams; + const { alertInstanceFactory, savedObjectsClient, callCluster } = services; + const { sources } = libs; + + const sourceConfiguration = await sources.getSourceConfiguration(savedObjectsClient, 'default'); + const indexPattern = sourceConfiguration.configuration.logAlias; + + const alertInstance = alertInstanceFactory(alertUUID); + + try { + const query = getESQuery( + params as LogDocumentCountAlertParams, + sourceConfiguration.configuration + ); + const result = await getResults(query, indexPattern, callCluster); + + if (checkValueAgainstComparatorMap[count.comparator](result.count, count.value)) { + alertInstance.scheduleActions(FIRED_ACTIONS.id, { + matchingDocuments: result.count, + conditions: createConditionsMessage(criteria), + }); + + alertInstance.replaceState({ + alertState: AlertStates.ALERT, + }); + } else { + alertInstance.replaceState({ + alertState: AlertStates.OK, + }); + } + } catch (e) { + alertInstance.replaceState({ + alertState: AlertStates.ERROR, + }); + + throw new Error(e); + } + }; + +const getESQuery = ( + params: LogDocumentCountAlertParams, + sourceConfiguration: InfraSource['configuration'] +): object => { + const { timeSize, timeUnit, criteria } = params; + const interval = `${timeSize}${timeUnit}`; + const intervalAsSeconds = getIntervalInSeconds(interval); + const to = Date.now(); + const from = to - intervalAsSeconds * 1000; + + const rangeFilters = [ + { + range: { + [sourceConfiguration.fields.timestamp]: { + gte: from, + lte: to, + format: 'epoch_millis', + }, + }, + }, + ]; + + const positiveComparators = getPositiveComparators(); + const negativeComparators = getNegativeComparators(); + const positiveCriteria = criteria.filter(criterion => + positiveComparators.includes(criterion.comparator) + ); + const negativeCriteria = criteria.filter(criterion => + negativeComparators.includes(criterion.comparator) + ); + // Positive assertions (things that "must" match) + const mustFilters = buildFiltersForCriteria(positiveCriteria); + // Negative assertions (things that "must not" match) + const mustNotFilters = buildFiltersForCriteria(negativeCriteria); + + const query = { + query: { + bool: { + filter: [...rangeFilters], + ...(mustFilters.length > 0 && { must: mustFilters }), + ...(mustNotFilters.length > 0 && { must_not: mustNotFilters }), + }, + }, + }; + + return query; +}; + +type SupportedESQueryTypes = 'term' | 'match' | 'match_phrase' | 'range'; +type Filter = { + [key in SupportedESQueryTypes]?: object; +}; + +const buildFiltersForCriteria = (criteria: LogDocumentCountAlertParams['criteria']) => { + let filters: Filter[] = []; + + criteria.forEach(criterion => { + const criterionQuery = buildCriterionQuery(criterion); + if (criterionQuery) { + filters = [...filters, criterionQuery]; + } + }); + return filters; +}; + +const buildCriterionQuery = (criterion: Criterion): Filter | undefined => { + const { field, value, comparator } = criterion; + + const queryType = getQueryMappingForComparator(comparator); + + switch (queryType) { + case 'term': + return { + term: { + [field]: { + value, + }, + }, + }; + break; + case 'match': { + return { + match: { + [field]: value, + }, + }; + } + case 'match_phrase': { + return { + match_phrase: { + [field]: value, + }, + }; + } + case 'range': { + const comparatorToRangePropertyMapping: { + [key: string]: string; + } = { + [Comparator.LT]: 'lt', + [Comparator.LT_OR_EQ]: 'lte', + [Comparator.GT]: 'gt', + [Comparator.GT_OR_EQ]: 'gte', + }; + + const rangeProperty = comparatorToRangePropertyMapping[comparator]; + + return { + range: { + [field]: { + [rangeProperty]: value, + }, + }, + }; + } + default: { + return undefined; + } + } +}; + +const getPositiveComparators = () => { + return [ + Comparator.GT, + Comparator.GT_OR_EQ, + Comparator.LT, + Comparator.LT_OR_EQ, + Comparator.EQ, + Comparator.MATCH, + Comparator.MATCH_PHRASE, + ]; +}; + +const getNegativeComparators = () => { + return [Comparator.NOT_EQ, Comparator.NOT_MATCH, Comparator.NOT_MATCH_PHRASE]; +}; + +const queryMappings: { + [key: string]: string; +} = { + [Comparator.GT]: 'range', + [Comparator.GT_OR_EQ]: 'range', + [Comparator.LT]: 'range', + [Comparator.LT_OR_EQ]: 'range', + [Comparator.EQ]: 'term', + [Comparator.MATCH]: 'match', + [Comparator.MATCH_PHRASE]: 'match_phrase', + [Comparator.NOT_EQ]: 'term', + [Comparator.NOT_MATCH]: 'match', + [Comparator.NOT_MATCH_PHRASE]: 'match_phrase', +}; + +const getQueryMappingForComparator = (comparator: Comparator) => { + return queryMappings[comparator]; +}; + +const getResults = async ( + query: object, + index: string, + callCluster: AlertServices['callCluster'] +) => { + return await callCluster('count', { + body: query, + index, + }); +}; + +const createConditionsMessage = (criteria: LogDocumentCountAlertParams['criteria']) => { + const parts = criteria.map((criterion, index) => { + const { field, comparator, value } = criterion; + return `${index === 0 ? '' : 'and'} ${field} ${comparator} ${value}`; + }); + return parts.join(' '); +}; + +// When the Alerting plugin implements support for multiple action groups, add additional +// action groups here to send different messages, e.g. a recovery notification +export const FIRED_ACTIONS = { + id: 'logs.threshold.fired', + name: i18n.translate('xpack.infra.logs.alerting.threshold.fired', { + defaultMessage: 'Fired', + }), +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts new file mode 100644 index 0000000000000..04207a4233dfd --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.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 uuid from 'uuid'; +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import { PluginSetupContract } from '../../../../../alerting/server'; +import { createLogThresholdExecutor, FIRED_ACTIONS } from './log_threshold_executor'; +import { + LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, + Comparator, +} from '../../../../common/alerting/logs/types'; +import { InfraBackendLibs } from '../../infra_types'; + +const documentCountActionVariableDescription = i18n.translate( + 'xpack.infra.logs.alerting.threshold.documentCountActionVariableDescription', + { + defaultMessage: 'The number of log entries that matched the conditions provided', + } +); + +const conditionsActionVariableDescription = i18n.translate( + 'xpack.infra.logs.alerting.threshold.conditionsActionVariableDescription', + { + defaultMessage: 'The conditions that log entries needed to fulfill', + } +); + +const countSchema = schema.object({ + value: schema.number(), + comparator: schema.oneOf([ + schema.literal(Comparator.GT), + schema.literal(Comparator.LT), + schema.literal(Comparator.GT_OR_EQ), + schema.literal(Comparator.LT_OR_EQ), + schema.literal(Comparator.EQ), + ]), +}); + +const criteriaSchema = schema.object({ + field: schema.string(), + comparator: schema.oneOf([ + schema.literal(Comparator.GT), + schema.literal(Comparator.LT), + schema.literal(Comparator.GT_OR_EQ), + schema.literal(Comparator.LT_OR_EQ), + schema.literal(Comparator.EQ), + schema.literal(Comparator.NOT_EQ), + schema.literal(Comparator.MATCH), + schema.literal(Comparator.NOT_MATCH), + ]), + value: schema.oneOf([schema.number(), schema.string()]), +}); + +export async function registerLogThresholdAlertType( + alertingPlugin: PluginSetupContract, + libs: InfraBackendLibs +) { + if (!alertingPlugin) { + throw new Error( + 'Cannot register log threshold alert type. Both the actions and alerting plugins need to be enabled.' + ); + } + + const alertUUID = uuid.v4(); + + alertingPlugin.registerType({ + id: LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, + name: 'Log threshold', + validate: { + params: schema.object({ + count: countSchema, + criteria: schema.arrayOf(criteriaSchema), + timeUnit: schema.string(), + timeSize: schema.number(), + }), + }, + defaultActionGroupId: FIRED_ACTIONS.id, + actionGroups: [FIRED_ACTIONS], + executor: createLogThresholdExecutor(alertUUID, libs), + actionVariables: { + context: [ + { name: 'matchingDocuments', description: documentCountActionVariableDescription }, + { name: 'conditions', description: conditionsActionVariableDescription }, + ], + }, + }); +} diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts index b697af4fa4c3b..3415ae9873bfb 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { PluginSetupContract } from '../../../../../alerting/server'; import { METRIC_EXPLORER_AGGREGATIONS } from '../../../../common/http_api/metrics_explorer'; import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; +import { InfraBackendLibs } from '../../infra_types'; import { METRIC_THRESHOLD_ALERT_TYPE_ID, Comparator } from './types'; const oneOfLiterals = (arrayOfLiterals: Readonly) => @@ -17,7 +18,10 @@ const oneOfLiterals = (arrayOfLiterals: Readonly) => arrayOfLiterals.includes(value) ? undefined : `must be one of ${arrayOfLiterals.join(' | ')}`, }); -export async function registerMetricThresholdAlertType(alertingPlugin: PluginSetupContract) { +export async function registerMetricThresholdAlertType( + alertingPlugin: PluginSetupContract, + libs: InfraBackendLibs +) { if (!alertingPlugin) { throw new Error( 'Cannot register metric threshold alert type. Both the actions and alerting plugins need to be enabled.' diff --git a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts index 6ec6f31256b78..9760873ff7478 100644 --- a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts @@ -6,13 +6,15 @@ import { PluginSetupContract } from '../../../../alerting/server'; import { registerMetricThresholdAlertType } from './metric_threshold/register_metric_threshold_alert_type'; +import { registerLogThresholdAlertType } from './log_threshold/register_log_threshold_alert_type'; +import { InfraBackendLibs } from '../infra_types'; -const registerAlertTypes = (alertingPlugin: PluginSetupContract) => { +const registerAlertTypes = (alertingPlugin: PluginSetupContract, libs: InfraBackendLibs) => { if (alertingPlugin) { - const registerFns = [registerMetricThresholdAlertType]; + const registerFns = [registerMetricThresholdAlertType, registerLogThresholdAlertType]; registerFns.forEach(fn => { - fn(alertingPlugin); + fn(alertingPlugin, libs); }); } }; diff --git a/x-pack/plugins/infra/server/lib/domains/fields_domain.ts b/x-pack/plugins/infra/server/lib/domains/fields_domain.ts index d2e151ca2c3f5..b6837e5b769a6 100644 --- a/x-pack/plugins/infra/server/lib/domains/fields_domain.ts +++ b/x-pack/plugins/infra/server/lib/domains/fields_domain.ts @@ -21,7 +21,7 @@ export class InfraFieldsDomain { indexType: InfraIndexType ): Promise { const { configuration } = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const includeMetricIndices = [InfraIndexType.ANY, InfraIndexType.METRICS].includes(indexType); diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts index 528b9a69327fa..07bc965dda77a 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts @@ -113,7 +113,7 @@ export class InfraLogEntriesDomain { params: LogEntriesParams ): Promise { const { configuration } = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); @@ -156,14 +156,7 @@ export class InfraLogEntriesDomain { } } ), - context: FIELDS_FROM_CONTEXT.reduce((ctx, field) => { - // Users might have different types here in their mappings. - const value = doc.fields[field]; - if (typeof value === 'string') { - ctx[field] = value; - } - return ctx; - }, {}), + context: getContextFromDoc(doc), }; }); @@ -179,7 +172,7 @@ export class InfraLogEntriesDomain { filterQuery?: LogEntryQuery ): Promise { const { configuration } = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const dateRangeBuckets = await this.adapter.getContainedLogSummaryBuckets( @@ -203,7 +196,7 @@ export class InfraLogEntriesDomain { filterQuery?: LogEntryQuery ): Promise { const { configuration } = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const messageFormattingRules = compileFormattingRules( @@ -352,3 +345,20 @@ const createHighlightQueryDsl = (phrase: string, fields: string[]) => ({ type: 'phrase', }, }); + +const getContextFromDoc = (doc: LogEntryDocument): LogEntry['context'] => { + // Get all context fields, then test for the presence and type of the ones that go together + const containerId = doc.fields['container.id']; + const hostName = doc.fields['host.name']; + const logFilePath = doc.fields['log.file.path']; + + if (typeof containerId === 'string') { + return { 'container.id': containerId }; + } + + if (typeof hostName === 'string' && typeof logFilePath === 'string') { + return { 'host.name': hostName, 'log.file.path': logFilePath }; + } + + return {}; +}; diff --git a/x-pack/plugins/infra/server/lib/source_status.ts b/x-pack/plugins/infra/server/lib/source_status.ts index 1f0845b6b223f..9bb953845e5a1 100644 --- a/x-pack/plugins/infra/server/lib/source_status.ts +++ b/x-pack/plugins/infra/server/lib/source_status.ts @@ -18,7 +18,7 @@ export class InfraSourceStatus { sourceId: string ): Promise { const sourceConfiguration = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const indexNames = await this.adapter.getIndexNames( @@ -32,7 +32,7 @@ export class InfraSourceStatus { sourceId: string ): Promise { const sourceConfiguration = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const indexNames = await this.adapter.getIndexNames( @@ -46,7 +46,7 @@ export class InfraSourceStatus { sourceId: string ): Promise { const sourceConfiguration = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const hasAlias = await this.adapter.hasAlias( @@ -60,7 +60,7 @@ export class InfraSourceStatus { sourceId: string ): Promise { const sourceConfiguration = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const hasAlias = await this.adapter.hasAlias( @@ -74,7 +74,7 @@ export class InfraSourceStatus { sourceId: string ): Promise { const sourceConfiguration = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const hasIndices = await this.adapter.hasIndices( @@ -88,7 +88,7 @@ export class InfraSourceStatus { sourceId: string ): Promise { const sourceConfiguration = await this.libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const hasIndices = await this.adapter.hasIndices( diff --git a/x-pack/plugins/infra/server/lib/sources/sources.test.ts b/x-pack/plugins/infra/server/lib/sources/sources.test.ts index 4a83ca730ff83..57efb0f676b2f 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.test.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.test.ts @@ -29,7 +29,9 @@ describe('the InfraSources lib', () => { }, }); - expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({ + expect( + await sourcesLib.getSourceConfiguration(request.core.savedObjects.client, 'TEST_ID') + ).toMatchObject({ id: 'TEST_ID', version: 'foo', updatedAt: 946684800000, @@ -74,7 +76,9 @@ describe('the InfraSources lib', () => { }, }); - expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({ + expect( + await sourcesLib.getSourceConfiguration(request.core.savedObjects.client, 'TEST_ID') + ).toMatchObject({ id: 'TEST_ID', version: 'foo', updatedAt: 946684800000, @@ -104,7 +108,9 @@ describe('the InfraSources lib', () => { attributes: {}, }); - expect(await sourcesLib.getSourceConfiguration(request, 'TEST_ID')).toMatchObject({ + expect( + await sourcesLib.getSourceConfiguration(request.core.savedObjects.client, 'TEST_ID') + ).toMatchObject({ id: 'TEST_ID', version: 'foo', updatedAt: 946684800000, diff --git a/x-pack/plugins/infra/server/lib/sources/sources.ts b/x-pack/plugins/infra/server/lib/sources/sources.ts index 99e062aa49ccf..0368c7bfd6db8 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.ts @@ -9,7 +9,7 @@ import { failure } from 'io-ts/lib/PathReporter'; import { identity, constant } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import { map, fold } from 'fp-ts/lib/Either'; -import { RequestHandlerContext } from 'src/core/server'; +import { RequestHandlerContext, SavedObjectsClientContract } from 'src/core/server'; import { defaultSourceConfiguration } from './defaults'; import { NotFoundError } from './errors'; import { infraSourceConfigurationSavedObjectType } from './saved_object_mappings'; @@ -37,7 +37,7 @@ export class InfraSources { } public async getSourceConfiguration( - requestContext: RequestHandlerContext, + savedObjectsClient: SavedObjectsClientContract, sourceId: string ): Promise { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); @@ -55,7 +55,7 @@ export class InfraSources { })) .catch(err => err instanceof NotFoundError - ? this.getSavedSourceConfiguration(requestContext, sourceId).then(result => ({ + ? this.getSavedSourceConfiguration(savedObjectsClient, sourceId).then(result => ({ ...result, configuration: mergeSourceConfiguration( staticDefaultSourceConfiguration, @@ -65,7 +65,7 @@ export class InfraSources { : Promise.reject(err) ) .catch(err => - requestContext.core.savedObjects.client.errors.isNotFoundError(err) + savedObjectsClient.errors.isNotFoundError(err) ? Promise.resolve({ id: sourceId, version: undefined, @@ -136,7 +136,10 @@ export class InfraSources { ) { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); - const { configuration, version } = await this.getSourceConfiguration(requestContext, sourceId); + const { configuration, version } = await this.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); const updatedSourceConfigurationAttributes = mergeSourceConfiguration( configuration, @@ -199,10 +202,10 @@ export class InfraSources { } private async getSavedSourceConfiguration( - requestContext: RequestHandlerContext, + savedObjectsClient: SavedObjectsClientContract, sourceId: string ) { - const savedObject = await requestContext.core.savedObjects.client.get( + const savedObject = await savedObjectsClient.get( infraSourceConfigurationSavedObjectType, sourceId ); diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index e3804078604cc..d4dfa60ac67a0 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -147,7 +147,7 @@ export class InfraServerPlugin { ]); initInfraServer(this.libs); - registerAlertTypes(plugins.alerting); + registerAlertTypes(plugins.alerting, this.libs); // Telemetry UsageCollector.registerUsageCollector(plugins.usageCollection); diff --git a/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts b/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts index 7e9b7ada28c8e..687e368736a41 100644 --- a/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts +++ b/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts @@ -39,7 +39,7 @@ export const initInventoryMetaRoute = (libs: InfraBackendLibs) => { ); const { configuration } = await libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const awsMetadata = await getCloudMetadata( diff --git a/x-pack/plugins/infra/server/routes/log_entries/item.ts b/x-pack/plugins/infra/server/routes/log_entries/item.ts index 3a6bdaf3804e3..85dba8f598a89 100644 --- a/x-pack/plugins/infra/server/routes/log_entries/item.ts +++ b/x-pack/plugins/infra/server/routes/log_entries/item.ts @@ -37,8 +37,9 @@ export const initLogEntriesItemRoute = ({ framework, sources, logEntries }: Infr ); const { id, sourceId } = payload; - const sourceConfiguration = (await sources.getSourceConfiguration(requestContext, sourceId)) - .configuration; + const sourceConfiguration = ( + await sources.getSourceConfiguration(requestContext.core.savedObjects.client, sourceId) + ).configuration; const logEntry = await logEntries.getLogItem(requestContext, id, sourceConfiguration); diff --git a/x-pack/plugins/infra/server/routes/metadata/index.ts b/x-pack/plugins/infra/server/routes/metadata/index.ts index c45f191b1130d..fe142aa93dcda 100644 --- a/x-pack/plugins/infra/server/routes/metadata/index.ts +++ b/x-pack/plugins/infra/server/routes/metadata/index.ts @@ -44,7 +44,7 @@ export const initMetadataRoute = (libs: InfraBackendLibs) => { ); const { configuration } = await libs.sources.getSourceConfiguration( - requestContext, + requestContext.core.savedObjects.client, sourceId ); const metricsMetadata = await getMetricMetadata( diff --git a/x-pack/plugins/infra/server/routes/node_details/index.ts b/x-pack/plugins/infra/server/routes/node_details/index.ts index 36906f6f4125b..a457ccac2416c 100644 --- a/x-pack/plugins/infra/server/routes/node_details/index.ts +++ b/x-pack/plugins/infra/server/routes/node_details/index.ts @@ -37,7 +37,10 @@ export const initNodeDetailsRoute = (libs: InfraBackendLibs) => { NodeDetailsRequestRT.decode(request.body), fold(throwErrors(Boom.badRequest), identity) ); - const source = await libs.sources.getSourceConfiguration(requestContext, sourceId); + const source = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); UsageCollector.countNode(nodeType); diff --git a/x-pack/plugins/infra/server/routes/snapshot/index.ts b/x-pack/plugins/infra/server/routes/snapshot/index.ts index e45b9884967d0..d1dc03893a0d9 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/index.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/index.ts @@ -42,7 +42,10 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => { SnapshotRequestRT.decode(request.body), fold(throwErrors(Boom.badRequest), identity) ); - const source = await libs.sources.getSourceConfiguration(requestContext, sourceId); + const source = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); UsageCollector.countNode(nodeType); const options = { filterQuery: parseFilterQuery(filterQuery), diff --git a/x-pack/plugins/infra/server/routes/source/index.ts b/x-pack/plugins/infra/server/routes/source/index.ts index 2f29320d7bb81..62b7fd7ba902f 100644 --- a/x-pack/plugins/infra/server/routes/source/index.ts +++ b/x-pack/plugins/infra/server/routes/source/index.ts @@ -37,7 +37,10 @@ export const initSourceRoute = (libs: InfraBackendLibs) => { try { const { type, sourceId } = request.params; - const source = await libs.sources.getSourceConfiguration(requestContext, sourceId); + const source = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); if (!source) { return response.notFound(); } diff --git a/x-pack/plugins/ingest_manager/README.md b/x-pack/plugins/ingest_manager/README.md index 9cd4821c2a727..0e7abcc3d74a9 100644 --- a/x-pack/plugins/ingest_manager/README.md +++ b/x-pack/plugins/ingest_manager/README.md @@ -1,60 +1,70 @@ # Ingest Manager + ## Plugin - - The plugin is disabled by default. See the TypeScript type for the [the available plugin configuration options](https://github.com/elastic/kibana/blob/master/x-pack/plugins/ingest_manager/common/types/index.ts#L9-L27) - - Setting `xpack.ingestManager.enabled=true` enables the plugin including the EPM and Fleet features. It also adds the `DATASOURCE_API_ROUTES` and `AGENT_CONFIG_API_ROUTES` values in [`common/constants/routes.ts`](./common/constants/routes.ts) - - Adding `--xpack.ingestManager.epm.enabled=false` will disable the EPM API & UI - - Adding `--xpack.ingestManager.fleet.enabled=false` will disable the Fleet API & UI - - [code for adding the routes](https://github.com/elastic/kibana/blob/1f27d349533b1c2865c10c45b2cf705d7416fb36/x-pack/plugins/ingest_manager/server/plugin.ts#L115-L133) - - [Integration tests](server/integration_tests/router.test.ts) - - Both EPM and Fleet require `ingestManager` be enabled. They are not standalone features. + +- The plugin is disabled by default. See the TypeScript type for the [the available plugin configuration options](https://github.com/elastic/kibana/blob/master/x-pack/plugins/ingest_manager/common/types/index.ts#L9-L27) +- Setting `xpack.ingestManager.enabled=true` enables the plugin including the EPM and Fleet features. It also adds the `DATASOURCE_API_ROUTES` and `AGENT_CONFIG_API_ROUTES` values in [`common/constants/routes.ts`](./common/constants/routes.ts) +- Adding `--xpack.ingestManager.epm.enabled=false` will disable the EPM API & UI +- Adding `--xpack.ingestManager.fleet.enabled=false` will disable the Fleet API & UI + - [code for adding the routes](https://github.com/elastic/kibana/blob/1f27d349533b1c2865c10c45b2cf705d7416fb36/x-pack/plugins/ingest_manager/server/plugin.ts#L115-L133) + - [Integration tests](server/integration_tests/router.test.ts) +- Both EPM and Fleet require `ingestManager` be enabled. They are not standalone features. ## Development ### Getting started -See the Kibana docs for [how to set up your dev environment](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#setting-up-your-development-environment), [run Elasticsearch](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#running-elasticsearch), and [start Kibana](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#running-kibana) +See the Kibana docs for [how to set up your dev environment](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#setting-up-your-development-environment), [run Elasticsearch](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#running-elasticsearch), and [start Kibana](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#running-kibana) One common development workflow is: - - Bootstrap Kibana - ``` - yarn kbn bootstrap - ``` - - Start Elasticsearch in one shell - ``` - yarn es snapshot -E xpack.security.authc.api_key.enabled=true - ``` - - Start Kibana in another shell - ``` - yarn start --xpack.ingestManager.enabled=true --no-base-path --xpack.endpoint.enabled=true - ``` + +- Bootstrap Kibana + ``` + yarn kbn bootstrap + ``` +- Start Elasticsearch in one shell + ``` + yarn es snapshot -E xpack.security.authc.api_key.enabled=true + ``` +- Start Kibana in another shell + ``` + yarn start --xpack.ingestManager.enabled=true --no-base-path --xpack.endpoint.enabled=true + ``` This plugin follows the `common`, `server`, `public` structure from the [Architecture Style Guide ](https://github.com/elastic/kibana/blob/master/style_guides/architecture_style_guide.md#file-and-folder-structure). We also follow the pattern of developing feature branches under your personal fork of Kibana. ### API Tests + #### Ingest & Fleet - 1. In one terminal, change to the `x-pack` directory and start the test server with - ``` - node scripts/functional_tests_server.js --config test/api_integration/config.ts - ``` - - 1. in a second terminal, run the tests from the Kibana root directory with - ``` - node scripts/functional_test_runner.js --config x-pack/test/api_integration/config.ts - ``` + +1. In one terminal, change to the `x-pack` directory and start the test server with + + ``` + node scripts/functional_tests_server.js --config test/api_integration/config.js + ``` + +1. in a second terminal, run the tests from the Kibana root directory with + ``` + node scripts/functional_test_runner.js --config x-pack/test/api_integration/config.js + ``` + #### EPM - 1. In one terminal, change to the `x-pack` directory and start the test server with - ``` - node scripts/functional_tests_server.js --config test/epm_api_integration/config.ts - ``` - 1. in a second terminal, run the tests from the Kibana root directory with - ``` - node scripts/functional_test_runner.js --config x-pack/test/epm_api_integration/config.ts - ``` +1. In one terminal, change to the `x-pack` directory and start the test server with + + ``` + node scripts/functional_tests_server.js --config test/epm_api_integration/config.ts + ``` - ### Staying up-to-date with `master` - While we're developing in the `feature-ingest` feature branch, here's is more information on keeping up to date with upstream kibana. +1. in a second terminal, run the tests from the Kibana root directory with + ``` + node scripts/functional_test_runner.js --config x-pack/test/epm_api_integration/config.ts + ``` + +### Staying up-to-date with `master` + +While we're developing in the `feature-ingest` feature branch, here's is more information on keeping up to date with upstream kibana.
merge upstream master into feature-ingest @@ -80,6 +90,7 @@ git push origin ## push your changes to upstream feature branch from the terminal; not GitHub UI git push upstream ``` +
See https://github.com/elastic/kibana/pull/37950 for an example. diff --git a/x-pack/plugins/ingest_manager/common/constants/agent.ts b/x-pack/plugins/ingest_manager/common/constants/agent.ts index 0b462fb4c0319..f3990ba78c539 100644 --- a/x-pack/plugins/ingest_manager/common/constants/agent.ts +++ b/x-pack/plugins/ingest_manager/common/constants/agent.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -export const AGENT_SAVED_OBJECT_TYPE = 'agents'; -export const AGENT_EVENT_SAVED_OBJECT_TYPE = 'agent_events'; -export const AGENT_ACTION_SAVED_OBJECT_TYPE = 'agent_actions'; +export const AGENT_SAVED_OBJECT_TYPE = 'fleet-agents'; +export const AGENT_EVENT_SAVED_OBJECT_TYPE = 'fleet-agent-events'; +export const AGENT_ACTION_SAVED_OBJECT_TYPE = 'fleet-agent-actions'; export const AGENT_TYPE_PERMANENT = 'PERMANENT'; export const AGENT_TYPE_EPHEMERAL = 'EPHEMERAL'; diff --git a/x-pack/plugins/ingest_manager/common/constants/agent_config.ts b/x-pack/plugins/ingest_manager/common/constants/agent_config.ts index 337022e552278..c5067480fb953 100644 --- a/x-pack/plugins/ingest_manager/common/constants/agent_config.ts +++ b/x-pack/plugins/ingest_manager/common/constants/agent_config.ts @@ -5,7 +5,7 @@ */ import { AgentConfigStatus, DefaultPackages } from '../types'; -export const AGENT_CONFIG_SAVED_OBJECT_TYPE = 'agent_configs'; +export const AGENT_CONFIG_SAVED_OBJECT_TYPE = 'ingest-agent-configs'; export const DEFAULT_AGENT_CONFIG = { name: 'Default config', diff --git a/x-pack/plugins/ingest_manager/common/constants/datasource.ts b/x-pack/plugins/ingest_manager/common/constants/datasource.ts index 0ff472b2afeb0..08113cff53bda 100644 --- a/x-pack/plugins/ingest_manager/common/constants/datasource.ts +++ b/x-pack/plugins/ingest_manager/common/constants/datasource.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export const DATASOURCE_SAVED_OBJECT_TYPE = 'datasources'; +export const DATASOURCE_SAVED_OBJECT_TYPE = 'ingest-datasources'; diff --git a/x-pack/plugins/ingest_manager/common/constants/enrollment_api_key.ts b/x-pack/plugins/ingest_manager/common/constants/enrollment_api_key.ts index f4a4bcde2f393..fd28b6632b15c 100644 --- a/x-pack/plugins/ingest_manager/common/constants/enrollment_api_key.ts +++ b/x-pack/plugins/ingest_manager/common/constants/enrollment_api_key.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export const ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE = 'enrollment_api_keys'; +export const ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE = 'fleet-enrollment-api-keys'; diff --git a/x-pack/plugins/ingest_manager/common/constants/epm.ts b/x-pack/plugins/ingest_manager/common/constants/epm.ts index eb72c28e7bf39..4fb259609493d 100644 --- a/x-pack/plugins/ingest_manager/common/constants/epm.ts +++ b/x-pack/plugins/ingest_manager/common/constants/epm.ts @@ -4,5 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export const PACKAGES_SAVED_OBJECT_TYPE = 'epm-package'; +export const PACKAGES_SAVED_OBJECT_TYPE = 'epm-packages'; export const INDEX_PATTERN_SAVED_OBJECT_TYPE = 'index-pattern'; +export const DEFAULT_REGISTRY_URL = 'https://epr.elastic.co'; diff --git a/x-pack/plugins/ingest_manager/common/constants/index.ts b/x-pack/plugins/ingest_manager/common/constants/index.ts index 45d315e6d5664..6a2e559bbbe4f 100644 --- a/x-pack/plugins/ingest_manager/common/constants/index.ts +++ b/x-pack/plugins/ingest_manager/common/constants/index.ts @@ -12,3 +12,4 @@ export * from './datasource'; export * from './epm'; export * from './output'; export * from './enrollment_api_key'; +export * from './settings'; diff --git a/x-pack/plugins/ingest_manager/common/constants/output.ts b/x-pack/plugins/ingest_manager/common/constants/output.ts index 6060a2b63fc8e..ac2d6117be921 100644 --- a/x-pack/plugins/ingest_manager/common/constants/output.ts +++ b/x-pack/plugins/ingest_manager/common/constants/output.ts @@ -5,12 +5,11 @@ */ import { OutputType } from '../types'; -export const OUTPUT_SAVED_OBJECT_TYPE = 'outputs'; +export const OUTPUT_SAVED_OBJECT_TYPE = 'ingest-outputs'; export const DEFAULT_OUTPUT = { name: 'default', is_default: true, type: OutputType.Elasticsearch, hosts: [''], - api_key: '', }; diff --git a/x-pack/plugins/ingest_manager/common/constants/routes.ts b/x-pack/plugins/ingest_manager/common/constants/routes.ts index 616195b32f266..35e3be98e3982 100644 --- a/x-pack/plugins/ingest_manager/common/constants/routes.ts +++ b/x-pack/plugins/ingest_manager/common/constants/routes.ts @@ -5,9 +5,10 @@ */ // Base API paths export const API_ROOT = `/api/ingest_manager`; +export const EPM_API_ROOT = `${API_ROOT}/epm`; +export const DATA_STREAM_API_ROOT = `${API_ROOT}/data_streams`; export const DATASOURCE_API_ROOT = `${API_ROOT}/datasources`; export const AGENT_CONFIG_API_ROOT = `${API_ROOT}/agent_configs`; -export const EPM_API_ROOT = `${API_ROOT}/epm`; export const FLEET_API_ROOT = `${API_ROOT}/fleet`; // EPM API routes @@ -23,6 +24,11 @@ export const EPM_API_ROUTES = { CATEGORIES_PATTERN: `${EPM_API_ROOT}/categories`, }; +// Data stream API routes +export const DATA_STREAM_API_ROUTES = { + LIST_PATTERN: `${DATA_STREAM_API_ROOT}`, +}; + // Datasource API routes export const DATASOURCE_API_ROUTES = { LIST_PATTERN: `${DATASOURCE_API_ROOT}`, @@ -42,6 +48,19 @@ export const AGENT_CONFIG_API_ROUTES = { FULL_INFO_PATTERN: `${AGENT_CONFIG_API_ROOT}/{agentConfigId}/full`, }; +// Output API routes +export const OUTPUT_API_ROUTES = { + LIST_PATTERN: `${API_ROOT}/outputs`, + INFO_PATTERN: `${API_ROOT}/outputs/{outputId}`, + UPDATE_PATTERN: `${API_ROOT}/outputs/{outputId}`, +}; + +// Settings API routes +export const SETTINGS_API_ROUTES = { + INFO_PATTERN: `${API_ROOT}/settings`, + UPDATE_PATTERN: `${API_ROOT}/settings`, +}; + // Agent API routes export const AGENT_API_ROUTES = { LIST_PATTERN: `${FLEET_API_ROOT}/agents`, diff --git a/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/index.ts b/x-pack/plugins/ingest_manager/common/constants/settings.ts similarity index 76% rename from x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/index.ts rename to x-pack/plugins/ingest_manager/common/constants/settings.ts index 0743e443955f4..a9e7f1df4119c 100644 --- a/x-pack/legacy/plugins/rollup/server/lib/license_pre_routing_factory/index.ts +++ b/x-pack/plugins/ingest_manager/common/constants/settings.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { licensePreRoutingFactory } from './license_pre_routing_factory'; +export const GLOBAL_SETTINGS_SAVED_OBJECT_TYPE = 'ingest_manager_settings'; diff --git a/x-pack/plugins/ingest_manager/common/services/agent_status.ts b/x-pack/plugins/ingest_manager/common/services/agent_status.ts index 7bbac55f11937..36e6e84b35bfe 100644 --- a/x-pack/plugins/ingest_manager/common/services/agent_status.ts +++ b/x-pack/plugins/ingest_manager/common/services/agent_status.ts @@ -8,20 +8,22 @@ import { AGENT_TYPE_TEMPORARY, AGENT_POLLING_THRESHOLD_MS, AGENT_TYPE_PERMANENT, + AGENT_SAVED_OBJECT_TYPE, } from '../constants'; export function buildKueryForOnlineAgents() { - return `agents.last_checkin >= now-${(3 * AGENT_POLLING_THRESHOLD_MS) / 1000}s`; + return `${AGENT_SAVED_OBJECT_TYPE}.last_checkin >= now-${(3 * AGENT_POLLING_THRESHOLD_MS) / + 1000}s`; } export function buildKueryForOfflineAgents() { - return `agents.type:${AGENT_TYPE_TEMPORARY} AND agents.last_checkin < now-${(3 * + return `${AGENT_SAVED_OBJECT_TYPE}.type:${AGENT_TYPE_TEMPORARY} AND ${AGENT_SAVED_OBJECT_TYPE}.last_checkin < now-${(3 * AGENT_POLLING_THRESHOLD_MS) / 1000}s`; } export function buildKueryForErrorAgents() { - return `agents.type:${AGENT_TYPE_PERMANENT} AND agents.last_checkin < now-${(4 * + return `${AGENT_SAVED_OBJECT_TYPE}.type:${AGENT_TYPE_PERMANENT} AND ${AGENT_SAVED_OBJECT_TYPE}.last_checkin < now-${(4 * AGENT_POLLING_THRESHOLD_MS) / 1000}s`; } diff --git a/x-pack/plugins/ingest_manager/common/services/routes.ts b/x-pack/plugins/ingest_manager/common/services/routes.ts index f2343b1039151..1a1bd7c65aa25 100644 --- a/x-pack/plugins/ingest_manager/common/services/routes.ts +++ b/x-pack/plugins/ingest_manager/common/services/routes.ts @@ -8,10 +8,13 @@ import { EPM_API_ROUTES, DATASOURCE_API_ROUTES, AGENT_CONFIG_API_ROUTES, + DATA_STREAM_API_ROUTES, FLEET_SETUP_API_ROUTES, AGENT_API_ROUTES, ENROLLMENT_API_KEY_ROUTES, SETUP_API_ROUTE, + OUTPUT_API_ROUTES, + SETTINGS_API_ROUTES, } from '../constants'; export const epmRouteService = { @@ -88,6 +91,12 @@ export const agentConfigRouteService = { }, }; +export const dataStreamRouteService = { + getListPath: () => { + return DATA_STREAM_API_ROUTES.LIST_PATTERN; + }, +}; + export const fleetSetupRouteService = { getFleetSetupPath: () => FLEET_SETUP_API_ROUTES.INFO_PATTERN, postFleetSetupPath: () => FLEET_SETUP_API_ROUTES.CREATE_PATTERN, @@ -105,6 +114,18 @@ export const agentRouteService = { getStatusPath: () => AGENT_API_ROUTES.STATUS_PATTERN, }; +export const outputRoutesService = { + getInfoPath: (outputId: string) => OUTPUT_API_ROUTES.INFO_PATTERN.replace('{outputId}', outputId), + getUpdatePath: (outputId: string) => + OUTPUT_API_ROUTES.UPDATE_PATTERN.replace('{outputId}', outputId), + getListPath: () => OUTPUT_API_ROUTES.LIST_PATTERN, +}; + +export const settingsRoutesService = { + getInfoPath: () => SETTINGS_API_ROUTES.INFO_PATTERN, + getUpdatePath: () => SETTINGS_API_ROUTES.UPDATE_PATTERN, +}; + export const enrollmentAPIKeyRouteService = { getListPath: () => ENROLLMENT_API_KEY_ROUTES.LIST_PATTERN, getCreatePath: () => ENROLLMENT_API_KEY_ROUTES.CREATE_PATTERN, diff --git a/x-pack/plugins/ingest_manager/common/types/index.ts b/x-pack/plugins/ingest_manager/common/types/index.ts index 438db9a25b8ee..748bb14d2d35d 100644 --- a/x-pack/plugins/ingest_manager/common/types/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/index.ts @@ -3,48 +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 { SavedObjectsClientContract } from 'kibana/server'; -import { AgentStatus } from './models'; - export * from './models'; export * from './rest_spec'; -/** - * Service to return the index pattern of EPM packages - */ -export interface ESIndexPatternService { - getESIndexPattern( - savedObjectsClient: SavedObjectsClientContract, - pkgName: string, - datasetPath: string - ): Promise; -} - -/** - * Describes public IngestManager plugin contract returned at the `startup` stage. - */ -export interface IngestManagerStartupContract { - esIndexPatternService: ESIndexPatternService; - agentService: AgentService; -} - -/** - * A service that provides exported functions that return information about an Agent - */ -export interface AgentService { - /** - * Return the status by the Agent's id - * @param soClient - * @param agentId - */ - getAgentStatusById(soClient: SavedObjectsClientContract, agentId: string): Promise; -} - export interface IngestManagerConfigType { enabled: boolean; epm: { enabled: boolean; - registryUrl: string; + registryUrl?: string; }; fleet: { enabled: boolean; diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent.ts b/x-pack/plugins/ingest_manager/common/types/models/agent.ts index 14b2b2e47d17f..fcd3955f3a32f 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent.ts @@ -20,15 +20,18 @@ export interface NewAgentAction { sent_at?: string; } -export type AgentAction = NewAgentAction & { +export interface AgentAction extends NewAgentAction { id: string; agent_id: string; created_at: string; -} & SavedObjectAttributes; +} -export interface AgentActionSOAttributes extends NewAgentAction, SavedObjectAttributes { +export interface AgentActionSOAttributes extends SavedObjectAttributes { + type: 'CONFIG_CHANGE' | 'DATA_DUMP' | 'RESUME' | 'PAUSE'; + sent_at?: string; created_at: string; agent_id: string; + data?: string; } export interface AgentEvent { @@ -64,6 +67,7 @@ interface AgentBase { shared_id?: string; access_api_key_id?: string; default_api_key?: string; + default_api_key_id?: string; config_id?: string; config_revision?: number | null; config_newest_revision?: number; diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/fleet_setup.ts b/x-pack/plugins/ingest_manager/common/types/models/data_stream.ts similarity index 58% rename from x-pack/plugins/ingest_manager/server/types/rest_spec/fleet_setup.ts rename to x-pack/plugins/ingest_manager/common/types/models/data_stream.ts index 2244bcd44043f..7da9bbad1b170 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/fleet_setup.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/data_stream.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -export const GetFleetSetupRequestSchema = {}; - -export const CreateFleetSetupRequestSchema = {}; - -export interface CreateFleetSetupResponse { - isInitialized: boolean; +export interface DataStream { + index: string; + dataset: string; + namespace: string; + type: string; + package: string; + last_activity: string; + size_in_bytes: number; } diff --git a/x-pack/plugins/ingest_manager/common/types/models/index.ts b/x-pack/plugins/ingest_manager/common/types/models/index.ts index 579b510e52daa..2310fdd54a719 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/index.ts @@ -7,6 +7,8 @@ export * from './agent'; export * from './agent_config'; export * from './datasource'; +export * from './data_stream'; export * from './output'; export * from './epm'; export * from './enrollment_api_key'; +export * from './settings'; diff --git a/x-pack/plugins/ingest_manager/common/types/models/settings.ts b/x-pack/plugins/ingest_manager/common/types/models/settings.ts new file mode 100644 index 0000000000000..2921808230b47 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/types/models/settings.ts @@ -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 { SavedObjectAttributes } from 'src/core/public'; + +interface BaseSettings { + agent_auto_upgrade?: boolean; + package_auto_upgrade?: boolean; + kibana_url?: string; + kibana_ca_sha256?: string; +} + +export interface Settings extends BaseSettings { + id: string; +} + +export interface SettingsSOAttributes extends BaseSettings, SavedObjectAttributes {} diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/data_stream.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/data_stream.ts new file mode 100644 index 0000000000000..24f8110562bfc --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/data_stream.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { DataStream } from '../models'; + +export interface GetDataStreamsResponse { + data_streams: DataStream[]; +} diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts index dc1d748a8743a..c4ba8ee595acf 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/fleet_setup.ts @@ -4,16 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface GetFleetSetupRequest {} - -export interface CreateFleetSetupRequest { - body: { - fleet_enroll_username: string; - fleet_enroll_password: string; - }; -} - export interface CreateFleetSetupResponse { isInitialized: boolean; } diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts index abe1bc8e3eddb..763fb7d820b2a 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/index.ts @@ -5,9 +5,12 @@ */ export * from './common'; export * from './datasource'; +export * from './data_stream'; export * from './agent'; export * from './agent_config'; export * from './fleet_setup'; export * from './epm'; export * from './enrollment_api_key'; export * from './install_script'; +export * from './output'; +export * from './settings'; diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/output.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/output.ts new file mode 100644 index 0000000000000..4162060363381 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/output.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Output } from '../models'; + +export interface GetOneOutputResponse { + item: Output; + success: boolean; +} + +export interface GetOneOutputRequest { + params: { + outputId: string; + }; +} + +export interface PutOutputRequest { + params: { + outputId: string; + }; + body: { + hosts?: string[]; + ca_sha256?: string; + }; +} + +export interface PutOutputResponse { + item: Output; + success: boolean; +} + +export interface GetOutputsResponse { + items: Output[]; + total: number; + page: number; + perPage: number; + success: boolean; +} diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/settings.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/settings.ts new file mode 100644 index 0000000000000..c02a5e5878ee9 --- /dev/null +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/settings.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 { Settings } from '../models'; + +export interface GetSettingsResponse { + item: Settings; + success: boolean; +} + +export interface PutSettingsRequest { + body: Partial>; +} + +export interface PutSettingsResponse { + item: Settings; + success: boolean; +} diff --git a/x-pack/plugins/ingest_manager/dev_docs/api/agents_list.md b/x-pack/plugins/ingest_manager/dev_docs/api/agents_list.md index 38f80a8bdc022..fdb54411f8610 100644 --- a/x-pack/plugins/ingest_manager/dev_docs/api/agents_list.md +++ b/x-pack/plugins/ingest_manager/dev_docs/api/agents_list.md @@ -18,5 +18,5 @@ ## Example ```js -GET /api/ingest_manager/fleet/agents?kuery=agents.last_checkin:2019-10-01T13:42:54.323Z +GET /api/ingest_manager/fleet/agents?kuery=fleet-agents.last_checkin:2019-10-01T13:42:54.323Z ``` diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts index bdc8f350f7108..b0b4e79cece79 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/index.ts @@ -7,3 +7,4 @@ export { Loading } from './loading'; export { Error } from './error'; export { Header, HeaderProps } from './header'; export { AlphaMessaging } from './alpha_messaging'; +export * from './settings_flyout'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/search_bar.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/search_bar.tsx index 41c24dadba068..4429b9d8e6b82 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/search_bar.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/search_bar.tsx @@ -8,12 +8,11 @@ import React, { useState, useEffect } from 'react'; import { IFieldType } from 'src/plugins/data/public'; // @ts-ignore import { EuiSuggest, EuiSuggestItemProps } from '@elastic/eui'; -import { useDebounce } from '../hooks'; -import { useStartDeps } from '../hooks/use_deps'; -import { INDEX_NAME } from '../constants'; +import { useDebounce, useStartDeps } from '../hooks'; +import { INDEX_NAME, AGENT_SAVED_OBJECT_TYPE } from '../constants'; const DEBOUNCE_SEARCH_MS = 150; -const HIDDEN_FIELDS = ['agents.actions']; +const HIDDEN_FIELDS = [`${AGENT_SAVED_OBJECT_TYPE}.actions`]; interface Suggestion { label: string; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx new file mode 100644 index 0000000000000..92146e9ee5679 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/settings_flyout.tsx @@ -0,0 +1,240 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 { i18n } from '@kbn/i18n'; +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiSpacer, + EuiButton, + EuiFlyoutFooter, + EuiForm, + EuiFormRow, + EuiFieldText, + EuiRadioGroup, + EuiComboBox, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiText } from '@elastic/eui'; +import { useInput, useComboInput, useCore, useGetSettings, sendPutSettings } from '../hooks'; +import { useGetOutputs, sendPutOutput } from '../hooks/use_request/outputs'; + +interface Props { + onClose: () => void; +} + +function useSettingsForm(outputId: string | undefined) { + const { notifications } = useCore(); + const kibanaUrlInput = useInput(); + const elasticsearchUrlInput = useComboInput([]); + + return { + onSubmit: async () => { + try { + if (!outputId) { + throw new Error('Unable to load outputs'); + } + await sendPutOutput(outputId, { + hosts: elasticsearchUrlInput.value, + }); + await sendPutSettings({ + kibana_url: kibanaUrlInput.value, + }); + } catch (error) { + notifications.toasts.addError(error, { + title: 'Error', + }); + } + notifications.toasts.addSuccess( + i18n.translate('xpack.ingestManager.settings.success.message', { + defaultMessage: 'Settings saved', + }) + ); + }, + inputs: { + kibanaUrl: kibanaUrlInput, + elasticsearchUrl: elasticsearchUrlInput, + }, + }; +} + +export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { + const core = useCore(); + const settingsRequest = useGetSettings(); + const settings = settingsRequest?.data?.item; + const outputsRequest = useGetOutputs(); + const output = outputsRequest.data?.items?.[0]; + const { inputs, onSubmit } = useSettingsForm(output?.id); + + useEffect(() => { + if (output) { + inputs.elasticsearchUrl.setValue(output.hosts || []); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [output]); + + useEffect(() => { + if (settings) { + inputs.kibanaUrl.setValue( + settings.kibana_url || `${window.location.origin}${core.http.basePath.get()}` + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [settings]); + + const body = ( + + {}} + legend={{ + children: ( + +

+ +

+
+ ), + }} + /> + + {}} + legend={{ + children: ( + +

+ +

+
+ ), + }} + /> + + +

+ +

+
+ + + + + + + + + + + + + + + + +
+ ); + + return ( + + + +

+ +

+
+
+ {body} + + + + + + + + + + + + + + +
+ ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts index 282ea8dbee3a2..e9b736e379b58 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/constants/index.ts @@ -3,7 +3,16 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -export { PLUGIN_ID, EPM_API_ROUTES, AGENT_CONFIG_SAVED_OBJECT_TYPE } from '../../../../common'; +export { + PLUGIN_ID, + EPM_API_ROUTES, + AGENT_API_ROUTES, + AGENT_CONFIG_SAVED_OBJECT_TYPE, + AGENT_EVENT_SAVED_OBJECT_TYPE, + AGENT_SAVED_OBJECT_TYPE, + ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + DATASOURCE_SAVED_OBJECT_TYPE, +} from '../../../../common'; export const BASE_PATH = '/app/ingestManager'; export const EPM_PATH = '/epm'; @@ -12,6 +21,7 @@ export const EPM_LIST_INSTALLED_PACKAGES_PATH = `${EPM_PATH}/installed`; export const EPM_DETAIL_VIEW_PATH = `${EPM_PATH}/detail/:pkgkey/:panel?`; export const AGENT_CONFIG_PATH = '/configs'; export const AGENT_CONFIG_DETAILS_PATH = `${AGENT_CONFIG_PATH}/`; +export const DATA_STREAM_PATH = '/data-streams'; export const FLEET_PATH = '/fleet'; export const FLEET_AGENTS_PATH = `${FLEET_PATH}/agents`; export const FLEET_AGENT_DETAIL_PATH = `${FLEET_AGENTS_PATH}/`; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_input.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_input.ts index 4aa0ad7155d2f..c535dc899638d 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_input.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_input.ts @@ -20,5 +20,27 @@ export function useInput(defaultValue = '') { clear: () => { setValue(''); }, + setValue, + }; +} + +export function useComboInput(defaultValue = []) { + const [value, setValue] = React.useState(defaultValue); + + return { + props: { + selectedOptions: value.map((val: string) => ({ label: val })), + onCreateOption: (newVal: any) => { + setValue([...value, newVal]); + }, + onChange: (newVals: any[]) => { + setValue(newVals.map(val => val.label)); + }, + }, + value, + clear: () => { + setValue([]); + }, + setValue, }; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/data_stream.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/data_stream.ts new file mode 100644 index 0000000000000..9acf4b1e17449 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/data_stream.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { useRequest } from './use_request'; +import { dataStreamRouteService } from '../../services'; +import { GetDataStreamsResponse } from '../../types'; + +export const useGetDataStreams = () => { + return useRequest({ + path: dataStreamRouteService.getListPath(), + method: 'get', + }); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts index 5014049407e65..c39d2a5860bf0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/index.ts @@ -6,6 +6,9 @@ export { setHttpClient, sendRequest, useRequest } from './use_request'; export * from './agent_config'; export * from './datasource'; +export * from './data_stream'; export * from './agents'; export * from './enrollment_api_keys'; export * from './epm'; +export * from './outputs'; +export * from './settings'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/outputs.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/outputs.ts new file mode 100644 index 0000000000000..e57256d33ab2f --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/outputs.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 { sendRequest, useRequest } from './use_request'; +import { outputRoutesService } from '../../services'; +import { PutOutputRequest, GetOutputsResponse } from '../../types'; + +export function useGetOutputs() { + return useRequest({ + method: 'get', + path: outputRoutesService.getListPath(), + }); +} + +export function sendPutOutput(outputId: string, body: PutOutputRequest['body']) { + return sendRequest({ + method: 'put', + path: outputRoutesService.getUpdatePath(outputId), + body, + }); +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/settings.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/settings.ts new file mode 100644 index 0000000000000..45e4eccf6625e --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/settings.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 { sendRequest, useRequest } from './use_request'; +import { settingsRoutesService } from '../../services'; +import { PutSettingsResponse, PutSettingsRequest, GetSettingsResponse } from '../../types'; + +export function useGetSettings() { + return useRequest({ + method: 'get', + path: settingsRoutesService.getInfoPath(), + }); +} + +export function sendPutSettings(body: PutSettingsRequest['body']) { + return sendRequest({ + method: 'put', + path: settingsRoutesService.getUpdatePath(), + body, + }); +} 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 f7c2805c6ea7c..6485862830d8a 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 @@ -16,10 +16,10 @@ import { IngestManagerConfigType, IngestManagerStartDeps, } from '../../plugin'; -import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH } from './constants'; +import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH, DATA_STREAM_PATH } from './constants'; import { DefaultLayout, WithoutHeaderLayout } from './layouts'; import { Loading, Error } from './components'; -import { IngestManagerOverview, EPMApp, AgentConfigApp, FleetApp } from './sections'; +import { IngestManagerOverview, EPMApp, AgentConfigApp, FleetApp, DataStreamApp } from './sections'; import { CoreContext, DepsContext, ConfigContext, setHttpClient, useConfig } from './hooks'; import { PackageInstallProvider } from './sections/epm/hooks'; import { sendSetup } from './hooks/use_request/setup'; @@ -98,6 +98,11 @@ const IngestManagerRoutes = ({ ...rest }) => {
+ + + + + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx index 345fd535b8ecc..10245e73520f7 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/layouts/default.tsx @@ -5,12 +5,12 @@ */ import React from 'react'; import styled from 'styled-components'; -import { EuiTabs, EuiTab, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; +import { EuiTabs, EuiTab, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiButtonEmpty } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { Section } from '../sections'; -import { AlphaMessaging } from '../components'; +import { AlphaMessaging, SettingFlyout } from '../components'; import { useLink, useConfig } from '../hooks'; -import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH } from '../constants'; +import { EPM_PATH, FLEET_PATH, AGENT_CONFIG_PATH, DATA_STREAM_PATH } from '../constants'; interface Props { section?: Section; @@ -35,53 +35,79 @@ const Nav = styled.nav` export const DefaultLayout: React.FunctionComponent = ({ section, children }) => { const { epm, fleet } = useConfig(); + + const [isSettingsFlyoutOpen, setIsSettingsFlyoutOpen] = React.useState(false); + return ( - - + {children} + + + ); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_delete_provider.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_delete_provider.tsx index b18349e078766..9ae8369abbd52 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_delete_provider.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/config_delete_provider.tsx @@ -8,6 +8,7 @@ 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 { AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; import { sendDeleteAgentConfigs, useCore, sendRequest } from '../../../hooks'; interface Props { @@ -122,7 +123,7 @@ export const AgentConfigDeleteProvider: React.FunctionComponent = ({ chil path: `/api/ingest_manager/fleet/agents`, method: 'get', query: { - kuery: `agents.config_id : (${agentConfigsToCheck.join(' or ')})`, + kuery: `${AGENT_SAVED_OBJECT_TYPE}.config_id : (${agentConfigsToCheck.join(' or ')})`, }, }); setAgentsCount(data?.total || 0); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/datasource_delete_provider.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/datasource_delete_provider.tsx index 089b0631c2090..df679d33e0324 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/datasource_delete_provider.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/datasource_delete_provider.tsx @@ -9,8 +9,8 @@ import { EuiCallOut, EuiConfirmModal, EuiOverlayMask, EuiSpacer } from '@elastic import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useCore, sendRequest, sendDeleteDatasource, useConfig } from '../../../hooks'; -import { AGENT_API_ROUTES } from '../../../../../../common/constants'; -import { AgentConfig } from '../../../../../../common/types/models'; +import { AGENT_API_ROUTES, AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; +import { AgentConfig } from '../../../types'; interface Props { agentConfig: AgentConfig; @@ -51,7 +51,7 @@ export const DatasourceDeleteProvider: React.FunctionComponent = ({ query: { page: 1, perPage: 1, - kuery: `agents.config_id : ${agentConfig.id}`, + kuery: `${AGENT_SAVED_OBJECT_TYPE}.config_id : ${agentConfig.id}`, }, }); setAgentsCount(data?.total || 0); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx index 0498e814440c7..1ea162252c741 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx @@ -191,7 +191,6 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => { defaultMessage: 'Name', }), width: '20%', - // FIXME: use version once available - see: https://github.com/elastic/kibana/issues/56750 render: (name: string, agentConfig: AgentConfig) => ( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.tsx new file mode 100644 index 0000000000000..7b0641e66fd43 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/index.tsx @@ -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 React from 'react'; +import { HashRouter as Router, Route, Switch } from 'react-router-dom'; +import { DataStreamListPage } from './list_page'; + +export const DataStreamApp: React.FunctionComponent = () => { + return ( + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx new file mode 100644 index 0000000000000..d7a3e933f3bb5 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/data_stream/list_page/index.tsx @@ -0,0 +1,283 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useMemo } from 'react'; +import { + EuiBadge, + EuiButton, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiEmptyPrompt, + EuiInMemoryTable, + EuiTableActionsColumnType, + EuiTableFieldDataColumnType, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; +import { DataStream } from '../../../types'; +import { WithHeaderLayout } from '../../../layouts'; +import { useGetDataStreams, useStartDeps, usePagination } from '../../../hooks'; + +const DataStreamListPageLayout: React.FunctionComponent = ({ children }) => ( + + + +

+ +

+
+
+ + +

+ +

+
+
+
+ } + > + {children} + +); + +export const DataStreamListPage: React.FunctionComponent<{}> = () => { + const { + data: { fieldFormats }, + } = useStartDeps(); + + const { pagination, pageSizeOptions } = usePagination(); + + // Fetch agent configs + const { isLoading, data: dataStreamsData, sendRequest } = useGetDataStreams(); + + // Some configs retrieved, set up table props + const columns = useMemo(() => { + const cols: Array< + EuiTableFieldDataColumnType | EuiTableActionsColumnType + > = [ + { + field: 'dataset', + sortable: true, + width: '25%', + truncateText: true, + name: i18n.translate('xpack.ingestManager.dataStreamList.datasetColumnTitle', { + defaultMessage: 'Dataset', + }), + }, + { + field: 'type', + sortable: true, + truncateText: true, + name: i18n.translate('xpack.ingestManager.dataStreamList.typeColumnTitle', { + defaultMessage: 'Type', + }), + }, + { + field: 'namespace', + sortable: true, + truncateText: true, + name: i18n.translate('xpack.ingestManager.dataStreamList.namespaceColumnTitle', { + defaultMessage: 'Namespace', + }), + render: (namespace: string) => { + return namespace ? {namespace} : ''; + }, + }, + { + field: 'package', + sortable: true, + truncateText: true, + name: i18n.translate('xpack.ingestManager.dataStreamList.integrationColumnTitle', { + defaultMessage: 'Integration', + }), + }, + { + field: 'last_activity', + sortable: true, + width: '25%', + dataType: 'date', + name: i18n.translate('xpack.ingestManager.dataStreamList.lastActivityColumnTitle', { + defaultMessage: 'Last activity', + }), + render: (date: DataStream['last_activity']) => { + try { + const formatter = fieldFormats.getInstance('date'); + return formatter.convert(date); + } catch (e) { + return ; + } + }, + }, + { + field: 'size_in_bytes', + sortable: true, + name: i18n.translate('xpack.ingestManager.dataStreamList.sizeColumnTitle', { + defaultMessage: 'Size', + }), + render: (size: DataStream['size_in_bytes']) => { + try { + const formatter = fieldFormats.getInstance('bytes'); + return formatter.convert(size); + } catch (e) { + return `${size}b`; + } + }, + }, + ]; + return cols; + }, [fieldFormats]); + + const emptyPrompt = useMemo( + () => ( + + + + } + /> + ), + [] + ); + + const filterOptions: { [key: string]: string[] } = { + dataset: [], + type: [], + namespace: [], + package: [], + }; + + if (dataStreamsData && dataStreamsData.data_streams.length) { + dataStreamsData.data_streams.forEach(stream => { + const { dataset, type, namespace, package: pkg } = stream; + if (!filterOptions.dataset.includes(dataset)) { + filterOptions.dataset.push(dataset); + } + if (!filterOptions.type.includes(type)) { + filterOptions.type.push(type); + } + if (!filterOptions.namespace.includes(namespace)) { + filterOptions.namespace.push(namespace); + } + if (!filterOptions.package.includes(pkg)) { + filterOptions.package.push(pkg); + } + }); + } + + return ( + + + ) : dataStreamsData && !dataStreamsData.data_streams.length ? ( + emptyPrompt + ) : ( + + ) + } + items={dataStreamsData ? dataStreamsData.data_streams : []} + itemId="index" + columns={columns} + pagination={{ + initialPageSize: pagination.pageSize, + pageSizeOptions, + }} + sorting={true} + search={{ + toolsRight: [ + sendRequest()}> + + , + ], + box: { + placeholder: i18n.translate( + 'xpack.ingestManager.dataStreamList.searchPlaceholderTitle', + { + defaultMessage: 'Filter data streams', + } + ), + incremental: true, + }, + filters: [ + { + type: 'field_value_selection', + field: 'dataset', + name: i18n.translate('xpack.ingestManager.dataStreamList.datasetColumnTitle', { + defaultMessage: 'Dataset', + }), + multiSelect: 'or', + options: filterOptions.dataset.map(option => ({ + value: option, + name: option, + })), + }, + { + type: 'field_value_selection', + field: 'type', + name: i18n.translate('xpack.ingestManager.dataStreamList.typeColumnTitle', { + defaultMessage: 'Type', + }), + multiSelect: 'or', + options: filterOptions.type.map(option => ({ + value: option, + name: option, + })), + }, + { + type: 'field_value_selection', + field: 'namespace', + name: i18n.translate('xpack.ingestManager.dataStreamList.namespaceColumnTitle', { + defaultMessage: 'Namespace', + }), + multiSelect: 'or', + options: filterOptions.namespace.map(option => ({ + value: option, + name: option, + })), + }, + { + type: 'field_value_selection', + field: 'package', + name: i18n.translate('xpack.ingestManager.dataStreamList.integrationColumnTitle', { + defaultMessage: 'Integration', + }), + multiSelect: 'or', + options: filterOptions.package.map(option => ({ + value: option, + name: option, + })), + }, + ], + }} + /> + + ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx index f947466caf4b0..3589a1a9444e1 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx @@ -8,10 +8,11 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { EuiSpacer } from '@elastic/eui'; -import { useGetPackageInstallStatus } from '../../hooks'; import { InstallStatus, PackageInfo } from '../../../../types'; -import { InstallationButton } from './installation_button'; import { useGetDatasources } from '../../../../hooks'; +import { DATASOURCE_SAVED_OBJECT_TYPE } from '../../../../constants'; +import { useGetPackageInstallStatus } from '../../hooks'; +import { InstallationButton } from './installation_button'; const NoteLabel = () => ( = ({ ag - + 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 c79255104a030..23fe18b82468c 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 @@ -40,7 +40,11 @@ import { SearchBar } from '../../../components/search_bar'; import { AgentHealth } from '../components/agent_health'; import { AgentUnenrollProvider } from '../components/agent_unenroll_provider'; import { AgentStatusKueryHelper } from '../../../services'; -import { FLEET_AGENT_DETAIL_PATH, AGENT_CONFIG_DETAILS_PATH } from '../../../constants'; +import { + FLEET_AGENT_DETAIL_PATH, + AGENT_CONFIG_DETAILS_PATH, + AGENT_SAVED_OBJECT_TYPE, +} from '../../../constants'; const NO_WRAP_TRUNCATE_STYLE: CSSProperties = Object.freeze({ overflow: 'hidden', @@ -180,7 +184,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { if (kuery) { kuery = `(${kuery}) and`; } - kuery = `${kuery} agents.config_id : (${selectedConfigs + kuery = `${kuery} ${AGENT_SAVED_OBJECT_TYPE}.config_id : (${selectedConfigs .map(config => `"${config}"`) .join(' or ')})`; } @@ -394,7 +398,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { }); setSearch(newSearch); }} - fieldPrefix="agents" + fieldPrefix={AGENT_SAVED_OBJECT_TYPE} /> 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 7520f88215efe..c11e3a49c7693 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 @@ -18,6 +18,7 @@ import { EuiText, } from '@elastic/eui'; import { FormattedMessage, FormattedDate } from '@kbn/i18n/react'; +import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../../constants'; import { usePagination, useGetEnrollmentAPIKeys, @@ -29,7 +30,6 @@ import { import { EnrollmentAPIKey } from '../../../types'; import { SearchBar } from '../../../components/search_bar'; import { NewEnrollmentTokenFlyout } from './components/new_enrollment_key_flyout'; -import {} from '@elastic/eui'; import { ConfirmEnrollmentTokenDelete } from './components/confirm_delete_modal'; const NO_WRAP_TRUNCATE_STYLE: CSSProperties = Object.freeze({ @@ -251,7 +251,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { }); setSearch(newSearch); }} - fieldPrefix="enrollment_api_keys" + fieldPrefix={ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE} /> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/index.tsx index c691bb609d435..1f46c4cc820cb 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/index.tsx @@ -6,6 +6,7 @@ export { IngestManagerOverview } from './overview'; export { EPMApp } from './epm'; export { AgentConfigApp } from './agent_config'; +export { DataStreamApp } from './data_stream'; export { FleetApp } from './fleet'; -export type Section = 'overview' | 'epm' | 'agent_config' | 'fleet'; +export type Section = 'overview' | 'epm' | 'agent_config' | 'fleet' | 'data_stream'; 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 5ebd1300baf65..e4791cc816d04 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 @@ -9,11 +9,14 @@ export { getFlattenedObject } from '../../../../../../../src/core/utils'; export { agentConfigRouteService, datasourceRouteService, + dataStreamRouteService, fleetSetupRouteService, agentRouteService, enrollmentAPIKeyRouteService, epmRouteService, setupRouteService, + outputRoutesService, + settingsRoutesService, packageToConfigDatasourceInputs, storedDatasourceToAgentDatasource, AgentStatusKueryHelper, 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 75194d3397f90..2f78ecd1b085e 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 @@ -17,6 +17,8 @@ export { DatasourceInput, DatasourceInputStream, DatasourceConfigRecordEntry, + Output, + DataStream, // API schemas - Agent Config GetAgentConfigsResponse, GetAgentConfigsResponseItem, @@ -30,6 +32,8 @@ export { // API schemas - Datasource CreateDatasourceRequest, CreateDatasourceResponse, + // API schemas - Data Streams + GetDataStreamsResponse, // API schemas - Agents GetAgentsResponse, GetAgentsRequest, @@ -45,6 +49,14 @@ export { GetEnrollmentAPIKeysResponse, GetEnrollmentAPIKeysRequest, GetOneEnrollmentAPIKeyResponse, + // API schemas - Outputs + GetOutputsResponse, + PutOutputRequest, + PutOutputResponse, + // API schemas - Settings + GetSettingsResponse, + PutSettingsRequest, + PutSettingsResponse, // EPM types AssetReference, AssetsGroupedByServiceByType, diff --git a/x-pack/plugins/ingest_manager/server/constants/index.ts b/x-pack/plugins/ingest_manager/server/constants/index.ts index 6ac92ca5d2a91..75c14ffc8fa84 100644 --- a/x-pack/plugins/ingest_manager/server/constants/index.ts +++ b/x-pack/plugins/ingest_manager/server/constants/index.ts @@ -12,13 +12,16 @@ export { // Routes PLUGIN_ID, EPM_API_ROUTES, + DATA_STREAM_API_ROUTES, DATASOURCE_API_ROUTES, AGENT_API_ROUTES, AGENT_CONFIG_API_ROUTES, FLEET_SETUP_API_ROUTES, ENROLLMENT_API_KEY_ROUTES, INSTALL_SCRIPT_API_ROUTES, + OUTPUT_API_ROUTES, SETUP_API_ROUTE, + SETTINGS_API_ROUTES, // Saved object types AGENT_SAVED_OBJECT_TYPE, AGENT_EVENT_SAVED_OBJECT_TYPE, @@ -29,7 +32,9 @@ export { PACKAGES_SAVED_OBJECT_TYPE, INDEX_PATTERN_SAVED_OBJECT_TYPE, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + GLOBAL_SETTINGS_SAVED_OBJECT_TYPE as GLOBAL_SETTINGS_SAVED_OBJET_TYPE, // Defaults DEFAULT_AGENT_CONFIG, DEFAULT_OUTPUT, + DEFAULT_REGISTRY_URL, } from '../../common'; diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index 0f49de0d5fdd1..951ff2337d8c7 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -6,6 +6,12 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext } from 'src/core/server'; import { IngestManagerPlugin } from './plugin'; +export { AgentService, ESIndexPatternService } from './services'; +export { + IngestManagerSetupContract, + IngestManagerSetupDeps, + IngestManagerStartContract, +} from './plugin'; export const config = { exposeToBrowser: { @@ -16,7 +22,7 @@ export const config = { enabled: schema.boolean({ defaultValue: false }), epm: schema.object({ enabled: schema.boolean({ defaultValue: true }), - registryUrl: schema.uri({ defaultValue: 'https://epr-staging.elastic.co' }), + registryUrl: schema.maybe(schema.uri()), }), fleet: schema.object({ enabled: schema.boolean({ defaultValue: true }), @@ -37,13 +43,3 @@ export type IngestManagerConfigType = TypeOf; export const plugin = (initializerContext: PluginInitializerContext) => { return new IngestManagerPlugin(initializerContext); }; - -// Saved object information bootstrapped by legacy `ingest_manager` plugin -// TODO: Remove once saved object mappings can be done from NP -export { savedObjectMappings } from './saved_objects'; -export { - OUTPUT_SAVED_OBJECT_TYPE, - AGENT_CONFIG_SAVED_OBJECT_TYPE, - DATASOURCE_SAVED_OBJECT_TYPE, - PACKAGES_SAVED_OBJECT_TYPE, -} from './constants'; diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index 80e35eadb4603..097825e0b69e1 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -11,11 +11,12 @@ import { Plugin, PluginInitializerContext, SavedObjectsServiceStart, - RecursiveReadonly, } from 'kibana/server'; -import { deepFreeze } from '../../../../src/core/utils'; -import { LicensingPluginSetup } from '../../licensing/server'; -import { EncryptedSavedObjectsPluginStart } from '../../encrypted_saved_objects/server'; +import { LicensingPluginSetup, ILicense } from '../../licensing/server'; +import { + EncryptedSavedObjectsPluginStart, + EncryptedSavedObjectsPluginSetup, +} from '../../encrypted_saved_objects/server'; import { SecurityPluginSetup } from '../../security/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { @@ -28,27 +29,39 @@ import { AGENT_EVENT_SAVED_OBJECT_TYPE, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, } from './constants'; - +import { registerSavedObjects, registerEncryptedSavedObjects } from './saved_objects'; import { registerEPMRoutes, registerDatasourceRoutes, + registerDataStreamRoutes, registerAgentConfigRoutes, registerSetupRoutes, registerAgentRoutes, registerEnrollmentApiKeyRoutes, registerInstallScriptRoutes, + registerOutputRoutes, + registerSettingsRoutes, } from './routes'; -import { IngestManagerConfigType, IngestManagerStartupContract } from '../common'; -import { appContextService, ESIndexPatternSavedObjectService } from './services'; +import { IngestManagerConfigType } from '../common'; +import { + appContextService, + licenseService, + ESIndexPatternSavedObjectService, + ESIndexPatternService, + AgentService, +} from './services'; import { getAgentStatusById } from './services/agents'; export interface IngestManagerSetupDeps { licensing: LicensingPluginSetup; security?: SecurityPluginSetup; features?: FeaturesPluginSetup; + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; } +export type IngestManagerStartDeps = object; + export interface IngestManagerAppContext { encryptedSavedObjects: EncryptedSavedObjectsPluginStart; security?: SecurityPluginSetup; @@ -56,6 +69,8 @@ export interface IngestManagerAppContext { savedObjects: SavedObjectsServiceStart; } +export type IngestManagerSetupContract = void; + const allSavedObjectTypes = [ OUTPUT_SAVED_OBJECT_TYPE, AGENT_CONFIG_SAVED_OBJECT_TYPE, @@ -66,7 +81,23 @@ const allSavedObjectTypes = [ ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, ]; -export class IngestManagerPlugin implements Plugin { +/** + * Describes public IngestManager plugin contract returned at the `startup` stage. + */ +export interface IngestManagerStartContract { + esIndexPatternService: ESIndexPatternService; + agentService: AgentService; +} + +export class IngestManagerPlugin + implements + Plugin< + IngestManagerSetupContract, + IngestManagerStartContract, + IngestManagerSetupDeps, + IngestManagerStartDeps + > { + private licensing$!: Observable; private config$: Observable; private security: SecurityPluginSetup | undefined; @@ -75,10 +106,14 @@ export class IngestManagerPlugin implements Plugin> { + ) { appContextService.start({ encryptedSavedObjects: plugins.encryptedSavedObjects, security: this.security, config$: this.config$, savedObjects: core.savedObjects, }); - return deepFreeze({ + licenseService.start(this.licensing$); + return { esIndexPatternService: new ESIndexPatternSavedObjectService(), agentService: { getAgentStatusById, }, - }); + }; } public async stop() { appContextService.stop(); + licenseService.stop(); } } diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts b/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts index 76247c338a24f..bcb9a7797f26a 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts @@ -10,8 +10,7 @@ import { RequestHandlerContext, SavedObjectsClientContract, } from 'kibana/server'; -import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock'; -import { httpServerMock } from '../../../../../../src/core/server/http/http_server.mocks'; +import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks'; import { ActionsService } from '../../services/agents'; import { AgentAction } from '../../../common/types/models'; import { postNewAgentActionHandlerBuilder } from './actions_handlers'; diff --git a/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts index 67f758c2c1263..42298960cc615 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent_config/handlers.ts @@ -8,6 +8,7 @@ import { RequestHandler } from 'src/core/server'; import bluebird from 'bluebird'; import { appContextService, agentConfigService, datasourceService } from '../../services'; import { listAgents } from '../../services/agents'; +import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { GetAgentConfigsRequestSchema, GetOneAgentConfigRequestSchema, @@ -50,7 +51,7 @@ export const getAgentConfigsHandler: RequestHandler< showInactive: true, perPage: 0, page: 1, - kuery: `agents.config_id:${agentConfig.id}`, + kuery: `${AGENT_SAVED_OBJECT_TYPE}.config_id:${agentConfig.id}`, }).then(({ total: agentTotal }) => (agentConfig.agents = agentTotal)), { concurrency: 10 } ); diff --git a/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts new file mode 100644 index 0000000000000..a24518d644c4c --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/data_streams/handlers.ts @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { RequestHandler } from 'src/core/server'; +import { DataStream } from '../../types'; +import { GetDataStreamsResponse } from '../../../common'; + +const DATA_STREAM_INDEX_PATTERN = 'logs-*-*,metrics-*-*'; + +export const getListHandler: RequestHandler = async (context, request, response) => { + const callCluster = context.core.elasticsearch.dataClient.callAsCurrentUser; + + try { + // Get stats (size on disk) of all potentially matching indices + const { indices: indexStats } = await callCluster('indices.stats', { + index: DATA_STREAM_INDEX_PATTERN, + metric: ['store'], + }); + + // Get all matching indices and info about each + // This returns the top 100,000 indices (as buckets) by last activity + const { + aggregations: { + index: { buckets: indexResults }, + }, + } = await callCluster('search', { + index: DATA_STREAM_INDEX_PATTERN, + body: { + size: 0, + query: { + bool: { + must: [ + { + exists: { + field: 'stream.namespace', + }, + }, + { + exists: { + field: 'stream.dataset', + }, + }, + ], + }, + }, + aggs: { + index: { + terms: { + field: '_index', + size: 100000, + order: { + last_activity: 'desc', + }, + }, + aggs: { + dataset: { + terms: { + field: 'stream.dataset', + size: 1, + }, + }, + namespace: { + terms: { + field: 'stream.namespace', + size: 1, + }, + }, + type: { + terms: { + field: 'stream.type', + size: 1, + }, + }, + package: { + terms: { + field: 'event.module', + size: 1, + }, + }, + last_activity: { + max: { + field: '@timestamp', + }, + }, + }, + }, + }, + }, + }); + + const dataStreams: DataStream[] = (indexResults as any[]).map(result => { + const { + key: indexName, + dataset: { buckets: datasetBuckets }, + namespace: { buckets: namespaceBuckets }, + type: { buckets: typeBuckets }, + package: { buckets: packageBuckets }, + last_activity: { value_as_string: lastActivity }, + } = result; + return { + index: indexName, + dataset: datasetBuckets.length ? datasetBuckets[0].key : '', + namespace: namespaceBuckets.length ? namespaceBuckets[0].key : '', + type: typeBuckets.length ? typeBuckets[0].key : '', + package: packageBuckets.length ? packageBuckets[0].key : '', + last_activity: lastActivity, + size_in_bytes: indexStats[indexName] ? indexStats[indexName].total.store.size_in_bytes : 0, + }; + }); + + const body: GetDataStreamsResponse = { + data_streams: dataStreams, + }; + return response.ok({ + body, + }); + } catch (e) { + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; diff --git a/x-pack/plugins/ingest_manager/server/routes/data_streams/index.ts b/x-pack/plugins/ingest_manager/server/routes/data_streams/index.ts new file mode 100644 index 0000000000000..39502eba89a6a --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/data_streams/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 { IRouter } from 'src/core/server'; +import { PLUGIN_ID, DATA_STREAM_API_ROUTES } from '../../constants'; +import { getListHandler } from './handlers'; + +export const registerRoutes = (router: IRouter) => { + // List of data streams + router.get( + { + path: DATA_STREAM_API_ROUTES.LIST_PATTERN, + validate: false, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + getListHandler + ); +}; diff --git a/x-pack/plugins/ingest_manager/server/routes/index.ts b/x-pack/plugins/ingest_manager/server/routes/index.ts index 33d75f3ab82cd..3ce34d15de46c 100644 --- a/x-pack/plugins/ingest_manager/server/routes/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/index.ts @@ -5,8 +5,11 @@ */ export { registerRoutes as registerAgentConfigRoutes } from './agent_config'; export { registerRoutes as registerDatasourceRoutes } from './datasource'; +export { registerRoutes as registerDataStreamRoutes } from './data_streams'; export { registerRoutes as registerEPMRoutes } from './epm'; export { registerRoutes as registerSetupRoutes } from './setup'; export { registerRoutes as registerAgentRoutes } from './agent'; export { registerRoutes as registerEnrollmentApiKeyRoutes } from './enrollment_api_key'; export { registerRoutes as registerInstallScriptRoutes } from './install_script'; +export { registerRoutes as registerOutputRoutes } from './output'; +export { registerRoutes as registerSettingsRoutes } from './settings'; diff --git a/x-pack/plugins/ingest_manager/server/routes/output/handler.ts b/x-pack/plugins/ingest_manager/server/routes/output/handler.ts new file mode 100644 index 0000000000000..cd35b2a43426c --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/output/handler.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 { RequestHandler } from 'src/core/server'; +import { TypeOf } from '@kbn/config-schema'; +import { GetOneOutputRequestSchema, PutOutputRequestSchema } from '../../types'; +import { GetOneOutputResponse, GetOutputsResponse } from '../../../common'; +import { outputService } from '../../services/output'; + +export const getOutputsHandler: RequestHandler = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + try { + const outputs = await outputService.list(soClient); + + const body: GetOutputsResponse = { + items: outputs.items, + page: outputs.page, + perPage: outputs.perPage, + total: outputs.total, + success: true, + }; + + return response.ok({ body }); + } catch (e) { + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; + +export const getOneOuputHandler: RequestHandler> = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + try { + const output = await outputService.get(soClient, request.params.outputId); + + const body: GetOneOutputResponse = { + item: output, + success: true, + }; + + return response.ok({ body }); + } catch (e) { + if (e.isBoom && e.output.statusCode === 404) { + return response.notFound({ + body: { message: `Output ${request.params.outputId} not found` }, + }); + } + + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; + +export const putOuputHandler: RequestHandler< + TypeOf, + undefined, + TypeOf +> = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + try { + await outputService.update(soClient, request.params.outputId, request.body); + const output = await outputService.get(soClient, request.params.outputId); + + const body: GetOneOutputResponse = { + item: output, + success: true, + }; + + return response.ok({ body }); + } catch (e) { + if (e.isBoom && e.output.statusCode === 404) { + return response.notFound({ + body: { message: `Output ${request.params.outputId} not found` }, + }); + } + + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; diff --git a/x-pack/plugins/ingest_manager/server/routes/output/index.ts b/x-pack/plugins/ingest_manager/server/routes/output/index.ts new file mode 100644 index 0000000000000..139d11dba951a --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/output/index.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from 'src/core/server'; +import { PLUGIN_ID, OUTPUT_API_ROUTES } from '../../constants'; +import { getOneOuputHandler, getOutputsHandler, putOuputHandler } from './handler'; +import { + GetOneOutputRequestSchema, + GetOutputsRequestSchema, + PutOutputRequestSchema, +} from '../../types'; + +export const registerRoutes = (router: IRouter) => { + router.get( + { + path: OUTPUT_API_ROUTES.LIST_PATTERN, + validate: GetOutputsRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + getOutputsHandler + ); + router.get( + { + path: OUTPUT_API_ROUTES.INFO_PATTERN, + validate: GetOneOutputRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + getOneOuputHandler + ); + router.put( + { + path: OUTPUT_API_ROUTES.UPDATE_PATTERN, + validate: PutOutputRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + putOuputHandler + ); +}; diff --git a/x-pack/plugins/ingest_manager/server/routes/settings/index.ts b/x-pack/plugins/ingest_manager/server/routes/settings/index.ts new file mode 100644 index 0000000000000..56e666056e8d0 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/settings/index.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IRouter, RequestHandler } from 'src/core/server'; +import { TypeOf } from '@kbn/config-schema'; +import { PLUGIN_ID, SETTINGS_API_ROUTES } from '../../constants'; +import { PutSettingsRequestSchema, GetSettingsRequestSchema } from '../../types'; + +import { settingsService } from '../../services'; + +export const getSettingsHandler: RequestHandler = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + + try { + const settings = await settingsService.getSettings(soClient); + const body = { + success: true, + item: settings, + }; + return response.ok({ body }); + } catch (e) { + if (e.isBoom && e.output.statusCode === 404) { + return response.notFound({ + body: { message: `Setings not found` }, + }); + } + + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; + +export const putSettingsHandler: RequestHandler< + undefined, + undefined, + TypeOf +> = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + try { + const settings = await settingsService.saveSettings(soClient, request.body); + const body = { + success: true, + item: settings, + }; + return response.ok({ body }); + } catch (e) { + if (e.isBoom && e.output.statusCode === 404) { + return response.notFound({ + body: { message: `Setings not found` }, + }); + } + + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; + +export const registerRoutes = (router: IRouter) => { + router.get( + { + path: SETTINGS_API_ROUTES.INFO_PATTERN, + validate: GetSettingsRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-read`] }, + }, + getSettingsHandler + ); + router.put( + { + path: SETTINGS_API_ROUTES.UPDATE_PATTERN, + validate: PutSettingsRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + putSettingsHandler + ); +}; diff --git a/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts index 5c66f9008e2a3..837e73b966feb 100644 --- a/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/setup/handlers.ts @@ -5,7 +5,7 @@ */ import { RequestHandler } from 'src/core/server'; import { outputService } from '../../services'; -import { CreateFleetSetupResponse } from '../../types'; +import { CreateFleetSetupResponse } from '../../../common'; import { setupIngestManager, setupFleet } from '../../services/setup'; export const getFleetSetupHandler: RequestHandler = async (context, request, response) => { diff --git a/x-pack/plugins/ingest_manager/server/routes/setup/index.ts b/x-pack/plugins/ingest_manager/server/routes/setup/index.ts index a2c641503e825..edc9a0a268161 100644 --- a/x-pack/plugins/ingest_manager/server/routes/setup/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/setup/index.ts @@ -5,7 +5,6 @@ */ import { IRouter } from 'src/core/server'; import { PLUGIN_ID, FLEET_SETUP_API_ROUTES, SETUP_API_ROUTE } from '../../constants'; -import { GetFleetSetupRequestSchema, CreateFleetSetupRequestSchema } from '../../types'; import { getFleetSetupHandler, createFleetSetupHandler, @@ -28,7 +27,7 @@ export const registerRoutes = (router: IRouter) => { router.get( { path: FLEET_SETUP_API_ROUTES.INFO_PATTERN, - validate: GetFleetSetupRequestSchema, + validate: false, options: { tags: [`access:${PLUGIN_ID}-read`] }, }, getFleetSetupHandler @@ -38,7 +37,7 @@ export const registerRoutes = (router: IRouter) => { router.post( { path: FLEET_SETUP_API_ROUTES.CREATE_PATTERN, - validate: CreateFleetSetupRequestSchema, + validate: false, options: { tags: [`access:${PLUGIN_ID}-all`] }, }, createFleetSetupHandler diff --git a/x-pack/plugins/ingest_manager/server/saved_objects.ts b/x-pack/plugins/ingest_manager/server/saved_objects.ts index 0a7229b1f2807..0b130e7b70101 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects.ts @@ -3,6 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +import { SavedObjectsServiceSetup, SavedObjectsType } from 'kibana/server'; +import { EncryptedSavedObjectsPluginSetup } from '../../encrypted_saved_objects/server'; import { OUTPUT_SAVED_OBJECT_TYPE, AGENT_CONFIG_SAVED_OBJECT_TYPE, @@ -12,156 +15,298 @@ import { AGENT_EVENT_SAVED_OBJECT_TYPE, AGENT_ACTION_SAVED_OBJECT_TYPE, ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + GLOBAL_SETTINGS_SAVED_OBJET_TYPE, } from './constants'; /* - * Saved object mappings + * Saved object types and mappings * * Please update typings in `/common/types` if mappings are updated. */ -export const savedObjectMappings = { + +const savedObjectTypes: { [key: string]: SavedObjectsType } = { + [GLOBAL_SETTINGS_SAVED_OBJET_TYPE]: { + name: GLOBAL_SETTINGS_SAVED_OBJET_TYPE, + hidden: false, + namespaceType: 'agnostic', + management: { + importableAndExportable: false, + }, + mappings: { + properties: { + agent_auto_upgrade: { type: 'keyword' }, + package_auto_upgrade: { type: 'keyword' }, + kibana_url: { type: 'keyword' }, + kibana_ca_sha256: { type: 'keyword' }, + }, + }, + }, [AGENT_SAVED_OBJECT_TYPE]: { - properties: { - shared_id: { type: 'keyword' }, - type: { type: 'keyword' }, - active: { type: 'boolean' }, - enrolled_at: { type: 'date' }, - access_api_key_id: { type: 'keyword' }, - version: { type: 'keyword' }, - user_provided_metadata: { type: 'text' }, - local_metadata: { type: 'text' }, - config_id: { type: 'keyword' }, - last_updated: { type: 'date' }, - last_checkin: { type: 'date' }, - config_revision: { type: 'integer' }, - config_newest_revision: { type: 'integer' }, - // FIXME_INGEST https://github.com/elastic/kibana/issues/56554 - default_api_key: { type: 'keyword' }, - updated_at: { type: 'date' }, - current_error_events: { type: 'text' }, + name: AGENT_SAVED_OBJECT_TYPE, + hidden: false, + namespaceType: 'agnostic', + management: { + importableAndExportable: false, + }, + mappings: { + properties: { + shared_id: { type: 'keyword' }, + type: { type: 'keyword' }, + active: { type: 'boolean' }, + enrolled_at: { type: 'date' }, + access_api_key_id: { type: 'keyword' }, + version: { type: 'keyword' }, + user_provided_metadata: { type: 'text' }, + local_metadata: { type: 'text' }, + config_id: { type: 'keyword' }, + last_updated: { type: 'date' }, + last_checkin: { type: 'date' }, + config_revision: { type: 'integer' }, + config_newest_revision: { type: 'integer' }, + default_api_key_id: { type: 'keyword' }, + default_api_key: { type: 'keyword' }, + updated_at: { type: 'date' }, + current_error_events: { type: 'text' }, + }, }, }, [AGENT_ACTION_SAVED_OBJECT_TYPE]: { - properties: { - agent_id: { type: 'keyword' }, - type: { type: 'keyword' }, - // FIXME_INGEST https://github.com/elastic/kibana/issues/56554 - data: { type: 'flattened' }, - sent_at: { type: 'date' }, - created_at: { type: 'date' }, + name: AGENT_ACTION_SAVED_OBJECT_TYPE, + hidden: false, + namespaceType: 'agnostic', + management: { + importableAndExportable: false, + }, + mappings: { + properties: { + agent_id: { type: 'keyword' }, + type: { type: 'keyword' }, + data: { type: 'binary' }, + sent_at: { type: 'date' }, + created_at: { type: 'date' }, + }, }, }, [AGENT_EVENT_SAVED_OBJECT_TYPE]: { - properties: { - type: { type: 'keyword' }, - subtype: { type: 'keyword' }, - agent_id: { type: 'keyword' }, - action_id: { type: 'keyword' }, - config_id: { type: 'keyword' }, - stream_id: { type: 'keyword' }, - timestamp: { type: 'date' }, - message: { type: 'text' }, - payload: { type: 'text' }, - data: { type: 'text' }, + name: AGENT_EVENT_SAVED_OBJECT_TYPE, + hidden: false, + namespaceType: 'agnostic', + management: { + importableAndExportable: false, + }, + mappings: { + properties: { + type: { type: 'keyword' }, + subtype: { type: 'keyword' }, + agent_id: { type: 'keyword' }, + action_id: { type: 'keyword' }, + config_id: { type: 'keyword' }, + stream_id: { type: 'keyword' }, + timestamp: { type: 'date' }, + message: { type: 'text' }, + payload: { type: 'text' }, + data: { type: 'text' }, + }, }, }, [AGENT_CONFIG_SAVED_OBJECT_TYPE]: { - properties: { - id: { type: 'keyword' }, - name: { type: 'text' }, - is_default: { type: 'boolean' }, - namespace: { type: 'keyword' }, - description: { type: 'text' }, - status: { type: 'keyword' }, - datasources: { type: 'keyword' }, - updated_on: { type: 'keyword' }, - updated_by: { type: 'keyword' }, - revision: { type: 'integer' }, + name: AGENT_CONFIG_SAVED_OBJECT_TYPE, + hidden: false, + namespaceType: 'agnostic', + management: { + importableAndExportable: false, + }, + mappings: { + properties: { + id: { type: 'keyword' }, + name: { type: 'text' }, + is_default: { type: 'boolean' }, + namespace: { type: 'keyword' }, + description: { type: 'text' }, + status: { type: 'keyword' }, + datasources: { type: 'keyword' }, + updated_on: { type: 'keyword' }, + updated_by: { type: 'keyword' }, + revision: { type: 'integer' }, + }, }, }, [ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE]: { - properties: { - name: { type: 'keyword' }, - type: { type: 'keyword' }, - // FIXME_INGEST https://github.com/elastic/kibana/issues/56554 - api_key: { type: 'binary' }, - api_key_id: { type: 'keyword' }, - config_id: { type: 'keyword' }, - created_at: { type: 'date' }, - updated_at: { type: 'date' }, - expire_at: { type: 'date' }, - active: { type: 'boolean' }, + name: ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + hidden: false, + namespaceType: 'agnostic', + management: { + importableAndExportable: false, + }, + mappings: { + properties: { + name: { type: 'keyword' }, + type: { type: 'keyword' }, + api_key: { type: 'binary' }, + api_key_id: { type: 'keyword' }, + config_id: { type: 'keyword' }, + created_at: { type: 'date' }, + updated_at: { type: 'date' }, + expire_at: { type: 'date' }, + active: { type: 'boolean' }, + }, }, }, [OUTPUT_SAVED_OBJECT_TYPE]: { - properties: { - name: { type: 'keyword' }, - type: { type: 'keyword' }, - is_default: { type: 'boolean' }, - hosts: { type: 'keyword' }, - ca_sha256: { type: 'keyword' }, - // FIXME_INGEST https://github.com/elastic/kibana/issues/56554 - api_key: { type: 'keyword' }, - fleet_enroll_username: { type: 'binary' }, - fleet_enroll_password: { type: 'binary' }, - config: { type: 'flattened' }, + name: OUTPUT_SAVED_OBJECT_TYPE, + hidden: false, + namespaceType: 'agnostic', + management: { + importableAndExportable: false, + }, + mappings: { + properties: { + name: { type: 'keyword' }, + type: { type: 'keyword' }, + is_default: { type: 'boolean' }, + hosts: { type: 'keyword' }, + ca_sha256: { type: 'keyword' }, + fleet_enroll_username: { type: 'binary' }, + fleet_enroll_password: { type: 'binary' }, + config: { type: 'flattened' }, + }, }, }, [DATASOURCE_SAVED_OBJECT_TYPE]: { - properties: { - name: { type: 'keyword' }, - description: { type: 'text' }, - namespace: { type: 'keyword' }, - config_id: { type: 'keyword' }, - enabled: { type: 'boolean' }, - package: { - properties: { - name: { type: 'keyword' }, - title: { type: 'keyword' }, - version: { type: 'keyword' }, + name: DATASOURCE_SAVED_OBJECT_TYPE, + hidden: false, + namespaceType: 'agnostic', + management: { + importableAndExportable: false, + }, + mappings: { + properties: { + name: { type: 'keyword' }, + description: { type: 'text' }, + namespace: { type: 'keyword' }, + config_id: { type: 'keyword' }, + enabled: { type: 'boolean' }, + package: { + properties: { + name: { type: 'keyword' }, + title: { type: 'keyword' }, + version: { type: 'keyword' }, + }, }, - }, - output_id: { type: 'keyword' }, - inputs: { - type: 'nested', - properties: { - type: { type: 'keyword' }, - enabled: { type: 'boolean' }, - processors: { type: 'keyword' }, - config: { type: 'flattened' }, - streams: { - type: 'nested', - properties: { - id: { type: 'keyword' }, - enabled: { type: 'boolean' }, - dataset: { type: 'keyword' }, - processors: { type: 'keyword' }, - config: { type: 'flattened' }, - pkg_stream: { type: 'flattened' }, + output_id: { type: 'keyword' }, + inputs: { + type: 'nested', + properties: { + type: { type: 'keyword' }, + enabled: { type: 'boolean' }, + processors: { type: 'keyword' }, + config: { type: 'flattened' }, + streams: { + type: 'nested', + properties: { + id: { type: 'keyword' }, + enabled: { type: 'boolean' }, + dataset: { type: 'keyword' }, + processors: { type: 'keyword' }, + config: { type: 'flattened' }, + pkg_stream: { type: 'flattened' }, + }, }, }, }, + revision: { type: 'integer' }, }, - revision: { type: 'integer' }, }, }, [PACKAGES_SAVED_OBJECT_TYPE]: { - properties: { - name: { type: 'keyword' }, - version: { type: 'keyword' }, - internal: { type: 'boolean' }, - removable: { type: 'boolean' }, - es_index_patterns: { - dynamic: false, - type: 'object', - }, - installed: { - type: 'nested', - properties: { - id: { type: 'keyword' }, - type: { type: 'keyword' }, + name: PACKAGES_SAVED_OBJECT_TYPE, + hidden: false, + namespaceType: 'agnostic', + management: { + importableAndExportable: false, + }, + mappings: { + properties: { + name: { type: 'keyword' }, + version: { type: 'keyword' }, + internal: { type: 'boolean' }, + removable: { type: 'boolean' }, + es_index_patterns: { + dynamic: 'false', + type: 'object', + }, + installed: { + type: 'nested', + properties: { + id: { type: 'keyword' }, + type: { type: 'keyword' }, + }, }, }, }, }, }; + +export function registerSavedObjects(savedObjects: SavedObjectsServiceSetup) { + Object.values(savedObjectTypes).forEach(type => { + savedObjects.registerType(type); + }); +} + +export function registerEncryptedSavedObjects( + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup +) { + // Encrypted saved objects + encryptedSavedObjects.registerType({ + type: ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + attributesToEncrypt: new Set(['api_key']), + attributesToExcludeFromAAD: new Set([ + 'name', + 'type', + 'api_key_id', + 'config_id', + 'created_at', + 'updated_at', + 'expire_at', + 'active', + ]), + }); + encryptedSavedObjects.registerType({ + type: OUTPUT_SAVED_OBJECT_TYPE, + attributesToEncrypt: new Set(['fleet_enroll_username', 'fleet_enroll_password']), + attributesToExcludeFromAAD: new Set([ + 'name', + 'type', + 'is_default', + 'hosts', + 'ca_sha256', + 'config', + ]), + }); + encryptedSavedObjects.registerType({ + type: AGENT_SAVED_OBJECT_TYPE, + attributesToEncrypt: new Set(['default_api_key']), + attributesToExcludeFromAAD: new Set([ + 'shared_id', + 'type', + 'active', + 'enrolled_at', + 'access_api_key_id', + 'version', + 'user_provided_metadata', + 'local_metadata', + 'config_id', + 'last_updated', + 'last_checkin', + 'config_revision', + 'config_newest_revision', + 'updated_at', + 'current_error_events', + ]), + }); + encryptedSavedObjects.registerType({ + type: AGENT_ACTION_SAVED_OBJECT_TYPE, + attributesToEncrypt: new Set(['data']), + attributesToExcludeFromAAD: new Set(['agent_id', 'type', 'sent_at', 'created_at']), + }); +} diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config.ts b/x-pack/plugins/ingest_manager/server/services/agent_config.ts index 309ddca3784c2..75bbfc21293c2 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_config.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_config.ts @@ -67,7 +67,7 @@ class AgentConfigService { public async ensureDefaultAgentConfig(soClient: SavedObjectsClientContract) { const configs = await soClient.find({ type: AGENT_CONFIG_SAVED_OBJECT_TYPE, - filter: 'agent_configs.attributes.is_default:true', + filter: `${AGENT_CONFIG_SAVED_OBJECT_TYPE}.attributes.is_default:true`, }); if (configs.total === 0) { @@ -244,7 +244,7 @@ class AgentConfigService { public async getDefaultAgentConfigId(soClient: SavedObjectsClientContract) { const configs = await soClient.find({ type: AGENT_CONFIG_SAVED_OBJECT_TYPE, - filter: 'agent_configs.attributes.is_default:true', + filter: `${AGENT_CONFIG_SAVED_OBJECT_TYPE}.attributes.is_default:true`, }); if (configs.saved_objects.length === 0) { 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 b4c1f09015a69..ae0dedce178a8 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 @@ -5,19 +5,42 @@ */ import Boom from 'boom'; import { SavedObjectsBulkResponse } from 'kibana/server'; -import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { encryptedSavedObjectsMock } from '../../../../../plugins/encrypted_saved_objects/server/mocks'; + import { Agent, AgentAction, AgentActionSOAttributes, AgentEvent, } from '../../../common/types/models'; -import { AGENT_TYPE_PERMANENT } from '../../../common/constants'; +import { AGENT_TYPE_PERMANENT, AGENT_ACTION_SAVED_OBJECT_TYPE } from '../../../common/constants'; import { acknowledgeAgentActions } from './acks'; +import { appContextService } from '../app_context'; +import { IngestManagerAppContext } from '../../plugin'; describe('test agent acks services', () => { it('should succeed on valid and matched actions', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); + const mockStartEncryptedSOClient = encryptedSavedObjectsMock.createStart(); + appContextService.start(({ + encryptedSavedObjects: mockStartEncryptedSOClient, + } as unknown) as IngestManagerAppContext); + + mockStartEncryptedSOClient.getDecryptedAsInternalUser.mockReturnValue( + Promise.resolve({ + id: 'action1', + references: [], + type: AGENT_ACTION_SAVED_OBJECT_TYPE, + attributes: { + type: 'CONFIG_CHANGE', + agent_id: 'id', + 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', + }, + }) + ); mockSavedObjectsClient.bulkGet.mockReturnValue( Promise.resolve({ @@ -25,7 +48,7 @@ describe('test agent acks services', () => { { id: 'action1', references: [], - type: 'agent_actions', + type: AGENT_ACTION_SAVED_OBJECT_TYPE, attributes: { type: 'CONFIG_CHANGE', agent_id: 'id', @@ -114,7 +137,7 @@ describe('test agent acks services', () => { { id: 'action1', references: [], - type: 'agent_actions', + type: AGENT_ACTION_SAVED_OBJECT_TYPE, attributes: { type: 'CONFIG_CHANGE', agent_id: 'id', diff --git a/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts index f2e671c6dbaa8..c739007952389 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts @@ -6,17 +6,17 @@ import { createAgentAction } from './actions'; import { SavedObject } from 'kibana/server'; -import { AgentAction, AgentActionSOAttributes } from '../../../common/types/models'; -import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock'; +import { AgentAction } from '../../../common/types/models'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; describe('test agent actions services', () => { it('should create a new action', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); - const newAgentAction: AgentActionSOAttributes = { + const newAgentAction: Omit = { agent_id: 'agentid', type: 'CONFIG_CHANGE', - data: 'data', + data: { content: 'data' }, sent_at: '2020-03-14T19:45:02.620Z', created_at: '2020-03-14T19:45:02.620Z', }; @@ -31,7 +31,7 @@ describe('test agent actions services', () => { .calls[0][1] as unknown) as AgentAction; expect(createdAction).toBeDefined(); expect(createdAction?.type).toEqual(newAgentAction.type); - expect(createdAction?.data).toEqual(newAgentAction.data); + expect(createdAction?.data).toEqual(JSON.stringify(newAgentAction.data)); expect(createdAction?.sent_at).toEqual(newAgentAction.sent_at); }); }); 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 a8ef0820f8d9f..1bb177e54282d 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/actions.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/actions.ts @@ -8,16 +8,21 @@ import { SavedObjectsClientContract } from 'kibana/server'; import { Agent, AgentAction, AgentActionSOAttributes } from '../../../common/types/models'; import { AGENT_ACTION_SAVED_OBJECT_TYPE } from '../../../common/constants'; import { savedObjectToAgentAction } from './saved_objects'; +import { appContextService } from '../app_context'; export async function createAgentAction( soClient: SavedObjectsClientContract, - newAgentAction: AgentActionSOAttributes + newAgentAction: Omit ): Promise { const so = await soClient.create(AGENT_ACTION_SAVED_OBJECT_TYPE, { ...newAgentAction, + data: newAgentAction.data ? JSON.stringify(newAgentAction.data) : undefined, }); - return savedObjectToAgentAction(so); + const agentAction = savedObjectToAgentAction(so); + agentAction.data = newAgentAction.data; + + return agentAction; } export async function getAgentActionsForCheckin( @@ -29,21 +34,47 @@ export async function getAgentActionsForCheckin( filter: `not ${AGENT_ACTION_SAVED_OBJECT_TYPE}.attributes.sent_at: * and ${AGENT_ACTION_SAVED_OBJECT_TYPE}.attributes.agent_id:${agentId}`, }); - return res.saved_objects.map(savedObjectToAgentAction); + return Promise.all( + res.saved_objects.map(async so => { + // Get decrypted actions + return savedObjectToAgentAction( + await appContextService + .getEncryptedSavedObjects() + .getDecryptedAsInternalUser( + AGENT_ACTION_SAVED_OBJECT_TYPE, + so.id + ) + ); + }) + ); } export async function getAgentActionByIds( soClient: SavedObjectsClientContract, actionIds: string[] ) { - const res = await soClient.bulkGet( - actionIds.map(actionId => ({ - id: actionId, - type: AGENT_ACTION_SAVED_OBJECT_TYPE, - })) - ); + const actions = ( + await soClient.bulkGet( + actionIds.map(actionId => ({ + id: actionId, + type: AGENT_ACTION_SAVED_OBJECT_TYPE, + })) + ) + ).saved_objects.map(savedObjectToAgentAction); - return res.saved_objects.map(savedObjectToAgentAction); + return Promise.all( + actions.map(async action => { + // Get decrypted actions + return savedObjectToAgentAction( + await appContextService + .getEncryptedSavedObjects() + .getDecryptedAsInternalUser( + AGENT_ACTION_SAVED_OBJECT_TYPE, + action.id + ) + ); + }) + ); } export interface ActionsService { diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin.test.ts index ec10ca6e77e05..72a86d7c8158e 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin.test.ts @@ -53,12 +53,12 @@ describe('Agent checkin service', () => { agent_id: 'agent1', type: 'CONFIG_CHANGE', created_at: new Date().toISOString(), - data: JSON.stringify({ + data: { config: { id: 'config1', revision: 2, }, - }), + }, }, ] ); @@ -80,24 +80,24 @@ describe('Agent checkin service', () => { agent_id: 'agent1', type: 'CONFIG_CHANGE', created_at: new Date().toISOString(), - data: JSON.stringify({ + data: { config: { id: 'config2', revision: 2, }, - }), + }, }, { id: 'action1', agent_id: 'agent1', type: 'CONFIG_CHANGE', created_at: new Date().toISOString(), - data: JSON.stringify({ + data: { config: { id: 'config1', revision: 1, }, - }), + }, }, ] ); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts index 2873aad7f691a..c96a81ed9b758 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts @@ -17,6 +17,7 @@ import { agentConfigService } from '../agent_config'; import * as APIKeysService from '../api_keys'; import { AGENT_SAVED_OBJECT_TYPE, AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../constants'; import { getAgentActionsForCheckin, createAgentAction } from './actions'; +import { appContextService } from '../app_context'; export async function agentCheckin( soClient: SavedObjectsClientContract, @@ -27,7 +28,6 @@ export async function agentCheckin( const updateData: { last_checkin: string; default_api_key?: string; - actions?: AgentAction[]; local_metadata?: string; current_error_events?: string; } = { @@ -38,11 +38,17 @@ export async function agentCheckin( // Generate new agent config if config is updated if (agent.config_id && shouldCreateConfigAction(agent, actions)) { + const { + attributes: { default_api_key: defaultApiKey }, + } = await appContextService + .getEncryptedSavedObjects() + .getDecryptedAsInternalUser(AGENT_SAVED_OBJECT_TYPE, agent.id); + const config = await agentConfigService.getFullConfig(soClient, agent.config_id); if (config) { // Assign output API keys // We currently only support default ouput - if (!agent.default_api_key) { + if (!defaultApiKey) { updateData.default_api_key = await APIKeysService.generateOutputApiKey( soClient, 'default', @@ -50,7 +56,7 @@ export async function agentCheckin( ); } // Mutate the config to set the api token for this agent - config.outputs.default.api_key = agent.default_api_key || updateData.default_api_key; + config.outputs.default.api_key = defaultApiKey || updateData.default_api_key; const configChangeAction = await createAgentAction(soClient, { agent_id: agent.id, @@ -62,9 +68,6 @@ export async function agentCheckin( actions.push(configChangeAction); } } - if (localMetadata) { - updateData.local_metadata = JSON.stringify(localMetadata); - } const { updatedErrorEvents } = await processEventsForCheckin(soClient, agent, events); @@ -172,7 +175,7 @@ export function shouldCreateConfigAction(agent: Agent, actions: AgentAction[]): return false; } - const data = JSON.parse(action.data); + const { data } = action; return ( data.config.id === agent.config_id && data.config.revision === agent.config_newest_revision 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 ec270884e62b4..175b92b75aca0 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/crud.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/crud.ts @@ -31,12 +31,17 @@ export async function listAgents( if (kuery && kuery !== '') { // To ensure users dont need to know about SO data structure... - filters.push(kuery.replace(/agents\./g, 'agents.attributes.')); + filters.push( + kuery.replace( + new RegExp(`${AGENT_SAVED_OBJECT_TYPE}\.`, 'g'), + `${AGENT_SAVED_OBJECT_TYPE}.attributes.` + ) + ); } if (showInactive === false) { - const agentActiveCondition = `agents.attributes.active:true AND not agents.attributes.type:${AGENT_TYPE_EPHEMERAL}`; - const recentlySeenEphemeralAgent = `agents.attributes.active:true AND agents.attributes.type:${AGENT_TYPE_EPHEMERAL} AND agents.attributes.last_checkin > ${Date.now() - + 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})`); } 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 707229845531c..2758374eba65f 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/events.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/events.ts @@ -23,7 +23,10 @@ export async function getAgentEvents( type: AGENT_EVENT_SAVED_OBJECT_TYPE, filter: kuery && kuery !== '' - ? kuery.replace(/agent_events\./g, 'agent_events.attributes.') + ? kuery.replace( + new RegExp(`${AGENT_EVENT_SAVED_OBJECT_TYPE}\.`, 'g'), + `${AGENT_EVENT_SAVED_OBJECT_TYPE}.attributes.` + ) : undefined, perPage, page, diff --git a/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts b/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts index aa88520740687..b182662e0fb4e 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts @@ -38,5 +38,6 @@ export function savedObjectToAgentAction(so: SavedObject listAgents(soClient, { - showInactive: true, + showInactive: false, perPage: 0, page: 1, kuery: configId ? kuery - ? `(${kuery}) and (agents.config_id:"${configId}")` - : `agents.config_id:"${configId}"` + ? `(${kuery}) and (${AGENT_SAVED_OBJECT_TYPE}.config_id:"${configId}")` + : `${AGENT_SAVED_OBJECT_TYPE}.config_id:"${configId}"` : kuery, }) ) @@ -91,7 +91,9 @@ export async function getAgentStatusForConfig( async function getEventsCount(soClient: SavedObjectsClientContract, configId?: string) { const { total } = await soClient.find({ type: AGENT_EVENT_SAVED_OBJECT_TYPE, - filter: configId ? `agent_events.attributes.config_id:"${configId}"` : undefined, + filter: configId + ? `${AGENT_EVENT_SAVED_OBJECT_TYPE}.attributes.config_id:"${configId}"` + : undefined, perPage: 0, page: 1, sortField: 'timestamp', 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 7f729b0b531ca..ee7e08d741035 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts @@ -17,11 +17,8 @@ export async function unenrollAgent(soClient: SavedObjectsClientContract, agentI agent.access_api_key_id ? APIKeyService.invalidateAPIKey(soClient, agent.access_api_key_id) : undefined, - agent.default_api_key - ? APIKeyService.invalidateAPIKey( - soClient, - APIKeyService.parseApiKey(agent.default_api_key).apiKeyId - ) + agent.default_api_key_id + ? APIKeyService.invalidateAPIKey(soClient, agent.default_api_key_id) : undefined, ]); await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { diff --git a/x-pack/plugins/ingest_manager/server/services/agents/update.ts b/x-pack/plugins/ingest_manager/server/services/agents/update.ts index 948e518dff5b4..fd57e83d7421e 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/update.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/update.ts @@ -22,7 +22,7 @@ export async function updateAgentsForConfigId( let page = 1; while (hasMore) { const { agents } = await listAgents(soClient, { - kuery: `agents.config_id:"${configId}"`, + kuery: `${AGENT_SAVED_OBJECT_TYPE}.config_id:"${configId}"`, page: page++, perPage: 1000, showInactive: true, @@ -46,7 +46,7 @@ export async function unenrollForConfigId(soClient: SavedObjectsClientContract, let page = 1; while (hasMore) { const { agents } = await listAgents(soClient, { - kuery: `agents.config_id:"${configId}"`, + kuery: `${AGENT_SAVED_OBJECT_TYPE}.config_id:"${configId}"`, page: page++, perPage: 1000, showInactive: true, 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 a6a2db8be4e9d..1ac812c3380cd 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 @@ -10,6 +10,7 @@ import { EnrollmentAPIKey, EnrollmentAPIKeySOAttributes } from '../../types'; import { ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; import { createAPIKey, invalidateAPIKey } from './security'; import { agentConfigService } from '../agent_config'; +import { appContextService } from '../app_context'; export async function listEnrollmentApiKeys( soClient: SavedObjectsClientContract, @@ -30,7 +31,10 @@ export async function listEnrollmentApiKeys( sortOrder: 'DESC', filter: kuery && kuery !== '' - ? kuery.replace(/enrollment_api_keys\./g, 'enrollment_api_keys.attributes.') + ? kuery.replace( + new RegExp(`${ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE}\.`, 'g'), + `${ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE}.attributes.` + ) : undefined, }); @@ -45,9 +49,13 @@ export async function listEnrollmentApiKeys( } export async function getEnrollmentAPIKey(soClient: SavedObjectsClientContract, id: string) { - return savedObjectToEnrollmentApiKey( - await soClient.get(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, id) - ); + const so = await appContextService + .getEncryptedSavedObjects() + .getDecryptedAsInternalUser( + ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + id + ); + return savedObjectToEnrollmentApiKey(so); } /** @@ -75,7 +83,7 @@ export async function deleteEnrollmentApiKeyForConfigId( const { items } = await listEnrollmentApiKeys(soClient, { page: page++, perPage: 100, - kuery: `enrollment_api_keys.config_id:${configId}`, + kuery: `${ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE}.config_id:${configId}`, }); if (items.length === 0) { @@ -120,16 +128,19 @@ export async function generateEnrollmentAPIKey( const apiKey = Buffer.from(`${key.id}:${key.api_key}`).toString('base64'); - return savedObjectToEnrollmentApiKey( - await soClient.create(ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, { + const so = await soClient.create( + ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, + { active: true, api_key_id: key.id, api_key: apiKey, name, config_id: configId, created_at: new Date().toISOString(), - }) + } ); + + return getEnrollmentAPIKey(soClient, so.id); } function savedObjectToEnrollmentApiKey({ diff --git a/x-pack/plugins/ingest_manager/server/services/app_context.ts b/x-pack/plugins/ingest_manager/server/services/app_context.ts index a0a7c8dd7c05a..e917d2edd1309 100644 --- a/x-pack/plugins/ingest_manager/server/services/app_context.ts +++ b/x-pack/plugins/ingest_manager/server/services/app_context.ts @@ -34,6 +34,9 @@ class AppContextService { public stop() {} public getEncryptedSavedObjects() { + if (!this.encryptedSavedObjects) { + throw new Error('Encrypted saved object start service not set.'); + } return this.encryptedSavedObjects; } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts index a96afc5eb7fa5..8e9b920875617 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts @@ -16,11 +16,11 @@ import { RegistrySearchResults, RegistrySearchResult, } from '../../../types'; -import { appContextService } from '../../'; import { cacheGet, cacheSet } from './cache'; import { ArchiveEntry, untarBuffer } from './extract'; import { fetchUrl, getResponse, getResponseStream } from './requests'; import { streamToBuffer } from './streams'; +import { getRegistryUrl } from './registry_url'; export { ArchiveEntry } from './extract'; @@ -32,7 +32,7 @@ export const pkgToPkgKey = ({ name, version }: { name: string; version: string } `${name}-${version}`; export async function fetchList(params?: SearchParams): Promise { - const registryUrl = appContextService.getConfig()?.epm.registryUrl; + const registryUrl = getRegistryUrl(); const url = new URL(`${registryUrl}/search`); if (params && params.category) { url.searchParams.set('category', params.category); @@ -45,7 +45,7 @@ export async function fetchFindLatestPackage( packageName: string, internal: boolean = true ): Promise { - const registryUrl = appContextService.getConfig()?.epm.registryUrl; + const registryUrl = getRegistryUrl(); const url = new URL(`${registryUrl}/search?package=${packageName}&internal=${internal}`); const res = await fetchUrl(url.toString()); const searchResults = JSON.parse(res); @@ -57,17 +57,17 @@ export async function fetchFindLatestPackage( } export async function fetchInfo(pkgName: string, pkgVersion: string): Promise { - const registryUrl = appContextService.getConfig()?.epm.registryUrl; + const registryUrl = getRegistryUrl(); return fetchUrl(`${registryUrl}/package/${pkgName}/${pkgVersion}`).then(JSON.parse); } export async function fetchFile(filePath: string): Promise { - const registryUrl = appContextService.getConfig()?.epm.registryUrl; + const registryUrl = getRegistryUrl(); return getResponse(`${registryUrl}${filePath}`); } export async function fetchCategories(): Promise { - const registryUrl = appContextService.getConfig()?.epm.registryUrl; + const registryUrl = getRegistryUrl(); return fetchUrl(`${registryUrl}/categories`).then(JSON.parse); } @@ -151,7 +151,7 @@ async function getOrFetchArchiveBuffer(pkgName: string, pkgVersion: string): Pro async function fetchArchiveBuffer(pkgName: string, pkgVersion: string): Promise { const { download: archivePath } = await fetchInfo(pkgName, pkgVersion); - const registryUrl = appContextService.getConfig()?.epm.registryUrl; + const registryUrl = getRegistryUrl(); return getResponseStream(`${registryUrl}${archivePath}`).then(streamToBuffer); } 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 new file mode 100644 index 0000000000000..d92d6faf8472e --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/registry_url.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 { DEFAULT_REGISTRY_URL } from '../../../constants'; +import { appContextService, licenseService } from '../../'; + +export const getRegistryUrl = (): string => { + const license = licenseService.getLicenseInformation(); + const customUrl = appContextService.getConfig()?.epm.registryUrl; + + if ( + customUrl && + license && + license.isAvailable && + license.hasAtLeast('gold') && + license.isActive + ) { + return customUrl; + } + + return DEFAULT_REGISTRY_URL; +}; diff --git a/x-pack/plugins/ingest_manager/server/services/es_index_pattern.ts b/x-pack/plugins/ingest_manager/server/services/es_index_pattern.ts index e2c27cdacda2f..fc2fe6d1c40e8 100644 --- a/x-pack/plugins/ingest_manager/server/services/es_index_pattern.ts +++ b/x-pack/plugins/ingest_manager/server/services/es_index_pattern.ts @@ -5,7 +5,7 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; import { getInstallation } from './epm/packages'; -import { ESIndexPatternService } from '../../common/types'; +import { ESIndexPatternService } from '../../server'; export class ESIndexPatternSavedObjectService implements ESIndexPatternService { public async getESIndexPattern( diff --git a/x-pack/plugins/ingest_manager/server/services/index.ts b/x-pack/plugins/ingest_manager/server/services/index.ts index 4dfc3cb58b733..483661b9de915 100644 --- a/x-pack/plugins/ingest_manager/server/services/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/index.ts @@ -3,10 +3,41 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -export { appContextService } from './app_context'; + +import { SavedObjectsClientContract } from 'kibana/server'; +import { AgentStatus } from '../../common/types/models'; +import * as settingsService from './settings'; export { ESIndexPatternSavedObjectService } from './es_index_pattern'; +/** + * Service to return the index pattern of EPM packages + */ +export interface ESIndexPatternService { + getESIndexPattern( + savedObjectsClient: SavedObjectsClientContract, + pkgName: string, + datasetPath: string + ): Promise; +} + +/** + * A service that provides exported functions that return information about an Agent + */ +export interface AgentService { + /** + * Return the status by the Agent's id + * @param soClient + * @param agentId + */ + getAgentStatusById(soClient: SavedObjectsClientContract, agentId: string): Promise; +} + // Saved object services export { datasourceService } from './datasource'; export { agentConfigService } from './agent_config'; export { outputService } from './output'; +export { settingsService }; + +// Plugin services +export { appContextService } from './app_context'; +export { licenseService } from './license'; diff --git a/x-pack/plugins/ingest_manager/server/services/output.ts b/x-pack/plugins/ingest_manager/server/services/output.ts index aebb8188db0cc..395c9af4a4ca2 100644 --- a/x-pack/plugins/ingest_manager/server/services/output.ts +++ b/x-pack/plugins/ingest_manager/server/services/output.ts @@ -14,7 +14,7 @@ class OutputService { public async ensureDefaultOutput(soClient: SavedObjectsClientContract) { const outputs = await soClient.find({ type: OUTPUT_SAVED_OBJECT_TYPE, - filter: 'outputs.attributes.is_default:true', + filter: `${OUTPUT_SAVED_OBJECT_TYPE}.attributes.is_default:true`, }); if (!outputs.saved_objects.length) { @@ -44,7 +44,7 @@ class OutputService { public async getDefaultOutputId(soClient: SavedObjectsClientContract) { const outputs = await soClient.find({ type: OUTPUT_SAVED_OBJECT_TYPE, - filter: 'outputs.attributes.is_default:true', + filter: `${OUTPUT_SAVED_OBJECT_TYPE}.attributes.is_default:true`, }); if (!outputs.saved_objects.length) { @@ -95,6 +95,34 @@ class OutputService { ...outputSO.attributes, }; } + + public async update(soClient: SavedObjectsClientContract, id: string, data: Partial) { + const outputSO = await soClient.update(SAVED_OBJECT_TYPE, id, data); + + if (outputSO.error) { + throw new Error(outputSO.error.message); + } + } + + public async list(soClient: SavedObjectsClientContract) { + const outputs = await soClient.find({ + type: SAVED_OBJECT_TYPE, + page: 1, + perPage: 1000, + }); + + return { + items: outputs.saved_objects.map(outputSO => { + return { + id: outputSO.id, + ...outputSO.attributes, + }; + }), + total: outputs.total, + page: 1, + perPage: 1000, + }; + } } export const outputService = new OutputService(); diff --git a/x-pack/plugins/ingest_manager/server/services/settings.ts b/x-pack/plugins/ingest_manager/server/services/settings.ts new file mode 100644 index 0000000000000..f1c09746d9abd --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/settings.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import Boom from 'boom'; +import { SavedObjectsClientContract } from 'kibana/server'; +import { GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, SettingsSOAttributes, Settings } from '../../common'; + +export async function getSettings(soClient: SavedObjectsClientContract): Promise { + const res = await soClient.find({ + type: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + }); + + if (res.total === 0) { + throw Boom.notFound('Global settings not found'); + } + const settingsSo = res.saved_objects[0]; + return { + id: settingsSo.id, + ...settingsSo.attributes, + }; +} + +export async function saveSettings( + soClient: SavedObjectsClientContract, + newData: Partial> +): Promise { + try { + const settings = await getSettings(soClient); + + const res = await soClient.update( + GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + settings.id, + newData + ); + + return { + id: settings.id, + ...res.attributes, + }; + } catch (e) { + if (e.isBoom && e.output.statusCode === 404) { + const res = await soClient.create( + GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + newData + ); + + return { + id: res.id, + ...res.attributes, + }; + } + + throw e; + } +} diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index 167a24481aba5..206ad76703cf5 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -21,6 +21,8 @@ import { import { getPackageInfo } from './epm/packages'; import { datasourceService } from './datasource'; import { generateEnrollmentAPIKey } from './api_keys'; +import { settingsService } from '.'; +import { appContextService } from './app_context'; const FLEET_ENROLL_USERNAME = 'fleet_enroll'; const FLEET_ENROLL_ROLE = 'fleet_enroll'; @@ -34,6 +36,17 @@ export async function setupIngestManager( ensureInstalledDefaultPackages(soClient, callCluster), outputService.ensureDefaultOutput(soClient), agentConfigService.ensureDefaultAgentConfig(soClient), + settingsService.getSettings(soClient).catch((e: any) => { + if (e.isBoom && e.output.statusCode === 404) { + return settingsService.saveSettings(soClient, { + agent_auto_upgrade: true, + package_auto_upgrade: true, + kibana_url: appContextService.getConfig()?.fleet?.kibana?.host, + }); + } + + return Promise.reject(e); + }), ]); // ensure default packages are added to the default conifg diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx index 105f9039f1e98..a7019ebc0a271 100644 --- a/x-pack/plugins/ingest_manager/server/types/index.tsx +++ b/x-pack/plugins/ingest_manager/server/types/index.tsx @@ -22,6 +22,7 @@ export { AgentConfig, NewAgentConfig, AgentConfigStatus, + DataStream, Output, NewOutput, OutputType, @@ -49,6 +50,8 @@ export { DefaultPackages, TemplateRef, IndexTemplateMappings, + Settings, + SettingsSOAttributes, } from '../../common'; export type CallESAsCurrentUser = ScopedClusterClient['callAsCurrentUser']; diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts index c143cd3b35f91..6976dae38d5f1 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/index.ts @@ -9,5 +9,6 @@ export * from './agent'; export * from './datasource'; export * from './epm'; export * from './enrollment_api_key'; -export * from './fleet_setup'; export * from './install_script'; +export * from './output'; +export * from './settings'; diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/output.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/output.ts new file mode 100644 index 0000000000000..79a7c444dacdb --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/output.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 { schema } from '@kbn/config-schema'; + +export const GetOneOutputRequestSchema = { + params: schema.object({ + outputId: schema.string(), + }), +}; + +export const GetOutputsRequestSchema = {}; + +export const PutOutputRequestSchema = { + params: schema.object({ + outputId: schema.string(), + }), + body: schema.object({ + hosts: schema.maybe(schema.arrayOf(schema.string())), + ca_sha256: schema.maybe(schema.string()), + }), +}; diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/settings.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/settings.ts new file mode 100644 index 0000000000000..8b7500e4a9bd9 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/settings.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { schema } from '@kbn/config-schema'; + +export const GetSettingsRequestSchema = {}; + +export const PutSettingsRequestSchema = { + body: schema.object({ + agent_auto_upgrade: schema.maybe(schema.boolean()), + package_auto_upgrade: schema.maybe(schema.boolean()), + kibana_url: schema.maybe(schema.string()), + kibana_ca_sha256: schema.maybe(schema.string()), + }), +}; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx index 80d33d1b95b61..e75e5fe763d6a 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx @@ -439,7 +439,7 @@ describe('xy_expression', () => { }); test('onElementClick returns correct context data', () => { - const geometry: GeometryValue = { x: 5, y: 1, accessor: 'y1' }; + const geometry: GeometryValue = { x: 5, y: 1, accessor: 'y1', mark: null }; const series = { key: 'spec{d}yAccessor{d}splitAccessors{b-2}', specId: 'd', diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index 00c5e70ad6b8d..b8bad47327f22 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -5,12 +5,14 @@ "configPath": ["xpack", "maps"], "requiredPlugins": [ "inspector", + "licensing", "home", "data", "fileUpload", "uiActions", "navigation", - "visualizations" + "visualizations", + "embeddable" ], "ui": true } diff --git a/x-pack/plugins/maps/public/angular/get_initial_layers.js b/x-pack/plugins/maps/public/angular/get_initial_layers.js index 1eb5dac309f28..f02ded1704533 100644 --- a/x-pack/plugins/maps/public/angular/get_initial_layers.js +++ b/x-pack/plugins/maps/public/angular/get_initial_layers.js @@ -5,27 +5,18 @@ */ import _ from 'lodash'; // Import each layer type, even those not used, to init in registry - import '../layers/sources/wms_source'; - import '../layers/sources/ems_file_source'; - import '../layers/sources/es_search_source'; - -import '../layers/sources/es_pew_pew_source/es_pew_pew_source'; - +import '../layers/sources/es_pew_pew_source'; import '../layers/sources/kibana_regionmap_source'; - import '../layers/sources/es_geo_grid_source'; - import '../layers/sources/xyz_tms_source'; - import { KibanaTilemapSource } from '../layers/sources/kibana_tilemap_source'; - +import { TileLayer } from '../layers/tile_layer'; import { EMSTMSSource } from '../layers/sources/ems_tms_source'; - +import { VectorTileLayer } from '../layers/vector_tile_layer'; import { getInjectedVarFunc } from '../kibana_services'; - import { getKibanaTileMap } from '../meta'; export function getInitialLayers(layerListJSON, initialLayers = []) { @@ -35,18 +26,18 @@ export function getInitialLayers(layerListJSON, initialLayers = []) { const tilemapSourceFromKibana = getKibanaTileMap(); if (_.get(tilemapSourceFromKibana, 'url')) { - const sourceDescriptor = KibanaTilemapSource.createDescriptor(); - const source = new KibanaTilemapSource(sourceDescriptor); - const layer = source.createDefaultLayer(); - return [layer.toLayerDescriptor(), ...initialLayers]; + const layerDescriptor = TileLayer.createDescriptor({ + sourceDescriptor: KibanaTilemapSource.createDescriptor(), + }); + return [layerDescriptor, ...initialLayers]; } const isEmsEnabled = getInjectedVarFunc()('isEmsEnabled', true); if (isEmsEnabled) { - const descriptor = EMSTMSSource.createDescriptor({ isAutoSelect: true }); - const source = new EMSTMSSource(descriptor); - const layer = source.createDefaultLayer(); - return [layer.toLayerDescriptor(), ...initialLayers]; + const layerDescriptor = VectorTileLayer.createDescriptor({ + sourceDescriptor: EMSTMSSource.createDescriptor({ isAutoSelect: true }), + }); + return [layerDescriptor, ...initialLayers]; } return initialLayers; diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/import_editor/view.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/import_editor/view.js index a4fa0d492bf3f..8ebb17ac4fff5 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_addpanel/import_editor/view.js +++ b/x-pack/plugins/maps/public/connected_components/layer_addpanel/import_editor/view.js @@ -4,51 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment } from 'react'; -import { EuiSpacer, EuiPanel, EuiButtonEmpty } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import { EuiPanel } from '@elastic/eui'; import { uploadLayerWizardConfig } from '../../../layers/sources/client_file_source'; -export const ImportEditor = ({ clearSource, isIndexingTriggered, ...props }) => { - const editorProperties = getEditorProperties({ isIndexingTriggered, ...props }); +export const ImportEditor = props => { + const editorProperties = getEditorProperties(props); return ( - - {isIndexingTriggered ? null : ( - - - - - - - )} - - {uploadLayerWizardConfig.renderWizard(editorProperties)} - - + + {uploadLayerWizardConfig.renderWizard(editorProperties)} + ); }; function getEditorProperties({ - inspectorAdapters, + previewLayer, + mapColors, onRemove, - viewLayer, isIndexingTriggered, onIndexReady, importSuccessHandler, importErrorHandler, }) { return { - onPreviewSource: viewLayer, - inspectorAdapters, + previewLayer, + mapColors, onRemove, importSuccessHandler, importErrorHandler, isIndexingTriggered, - addAndViewSource: viewLayer, onIndexReady, }; } diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/index.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/index.js index 24c1f5ced4fe6..a29898f8a2830 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_addpanel/index.js +++ b/x-pack/plugins/maps/public/connected_components/layer_addpanel/index.js @@ -34,12 +34,12 @@ function mapStateToProps(state = {}) { function mapDispatchToProps(dispatch) { return { - viewLayer: async layer => { + previewLayer: async layerDescriptor => { await dispatch(setSelectedLayer(null)); await dispatch(removeTransientLayer()); - dispatch(addLayer(layer.toLayerDescriptor())); - dispatch(setSelectedLayer(layer.getId())); - dispatch(setTransientLayer(layer.getId())); + dispatch(addLayer(layerDescriptor)); + dispatch(setSelectedLayer(layerDescriptor.id)); + dispatch(setTransientLayer(layerDescriptor.id)); }, removeTransientLayer: () => { dispatch(setSelectedLayer(null)); diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_editor/index.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_editor/index.js deleted file mode 100644 index 8937f32d3bf05..0000000000000 --- a/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_editor/index.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 { connect } from 'react-redux'; -import { SourceEditor } from './view'; - -import { getInspectorAdapters } from '../../../reducers/non_serializable_instances'; - -function mapStateToProps(state = {}) { - return { - inspectorAdapters: getInspectorAdapters(state), - }; -} - -const connectedFlyOut = connect(mapStateToProps)(SourceEditor); -export { connectedFlyOut as SourceEditor }; diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_editor/view.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_editor/view.js deleted file mode 100644 index 50312b68277fa..0000000000000 --- a/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_editor/view.js +++ /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 React, { Fragment } from 'react'; -import { EuiSpacer, EuiPanel, EuiButtonEmpty } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export const SourceEditor = ({ - clearSource, - layerWizard, - isIndexingTriggered, - inspectorAdapters, - previewLayer, -}) => { - if (!layerWizard) { - return null; - } - - return ( - - {isIndexingTriggered ? null : ( - - - - - - - )} - - {layerWizard.renderWizard({ onPreviewSource: previewLayer, inspectorAdapters })} - - - ); -}; diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_select/source_select.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_select/source_select.js index 80b05a0fd015b..82df9237e6ed3 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_select/source_select.js +++ b/x-pack/plugins/maps/public/connected_components/layer_addpanel/source_select/source_select.js @@ -7,8 +7,7 @@ import React, { Fragment } from 'react'; import { getLayerWizards } from '../../../layers/layer_wizard_registry'; -import { EuiTitle, EuiSpacer, EuiCard, EuiIcon } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSpacer, EuiCard, EuiIcon } from '@elastic/eui'; import _ from 'lodash'; export function SourceSelect({ updateSourceSelection }) { @@ -38,17 +37,5 @@ export function SourceSelect({ updateSourceSelection }) { ); }); - return ( - - -

- -

-
- {sourceCards} -
- ); + return {sourceCards}; } diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/view.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/view.js index 92fcf01f3901f..127b99d730db5 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_addpanel/view.js +++ b/x-pack/plugins/maps/public/connected_components/layer_addpanel/view.js @@ -4,18 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Component } from 'react'; +import React, { Component, Fragment } from 'react'; import { SourceSelect } from './source_select/source_select'; import { FlyoutFooter } from './flyout_footer'; -import { SourceEditor } from './source_editor'; import { ImportEditor } from './import_editor'; -import { EuiFlexGroup, EuiTitle, EuiFlyoutHeader } from '@elastic/eui'; +import { EuiButtonEmpty, EuiPanel, EuiTitle, EuiFlyoutHeader, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; export class AddLayerPanel extends Component { state = { layerWizard: null, - layer: null, + layerDescriptor: null, // TODO get this from redux store instead of storing locally + isIndexingSource: false, importView: false, layerImportAddReady: false, }; @@ -35,13 +36,9 @@ export class AddLayerPanel extends Component { } _getPanelDescription() { - const { layerWizard, importView, layerImportAddReady } = this.state; + const { importView, layerImportAddReady } = this.state; let panelDescription; - if (!layerWizard) { - panelDescription = i18n.translate('xpack.maps.addLayerPanel.selectSource', { - defaultMessage: 'Select source', - }); - } else if (layerImportAddReady || !importView) { + if (layerImportAddReady || !importView) { panelDescription = i18n.translate('xpack.maps.addLayerPanel.addLayer', { defaultMessage: 'Add layer', }); @@ -53,29 +50,21 @@ export class AddLayerPanel extends Component { return panelDescription; } - _viewLayer = async (source, options = {}) => { + _previewLayer = async (layerDescriptor, isIndexingSource) => { if (!this._isMounted) { return; } - if (!source) { - this.setState({ layer: null }); + if (!layerDescriptor) { + this.setState({ + layerDescriptor: null, + isIndexingSource: false, + }); this.props.removeTransientLayer(); return; } - const styleDescriptor = - this.state.layer && this.state.layer.getCurrentStyle() - ? this.state.layer.getCurrentStyle().getDescriptor() - : null; - const layerInitProps = { - ...options, - style: styleDescriptor, - }; - const newLayer = source.createDefaultLayer(layerInitProps, this.props.mapColors); - if (!this._isMounted) { - return; - } - this.setState({ layer: newLayer }, () => this.props.viewLayer(this.state.layer)); + this.setState({ layerDescriptor, isIndexingSource }); + this.props.previewLayer(layerDescriptor); }; _clearLayerData = ({ keepSourceType = false }) => { @@ -84,7 +73,8 @@ export class AddLayerPanel extends Component { } this.setState({ - layer: null, + layerDescriptor: null, + isIndexingSource: false, ...(!keepSourceType ? { layerWizard: null, importView: false } : {}), }); this.props.removeTransientLayer(); @@ -95,72 +85,75 @@ export class AddLayerPanel extends Component { }; _layerAddHandler = () => { - const { - isIndexingTriggered, - setIndexingTriggered, - selectLayerAndAdd, - resetIndexing, - } = this.props; - const layerSource = this.state.layer.getSource(); - const boolIndexLayer = layerSource.shouldBeIndexed(); - this.setState({ layer: null }); - if (boolIndexLayer && !isIndexingTriggered) { - setIndexingTriggered(); + if (this.state.isIndexingSource && !this.props.isIndexingTriggered) { + this.props.setIndexingTriggered(); } else { - selectLayerAndAdd(); + this.props.selectLayerAndAdd(); if (this.state.importView) { this.setState({ layerImportAddReady: false, }); - resetIndexing(); + this.props.resetIndexing(); } } }; - _renderAddLayerPanel() { - const { layerWizard, importView } = this.state; - if (!layerWizard) { + _renderPanelBody() { + if (!this.state.layerWizard) { return ; } - if (importView) { + + const backButton = this.props.isIndexingTriggered ? null : ( + + + + + + + ); + + if (this.state.importView) { return ( - this._clearLayerData({ keepSourceType: true })} - /> + + {backButton} + this._clearLayerData({ keepSourceType: true })} + /> + ); } - return ( - - ); - } - - _renderFooter(buttonDescription) { - const { importView, layer } = this.state; - const { isIndexingReady, isIndexingSuccess } = this.props; - - const buttonEnabled = importView ? isIndexingReady || isIndexingSuccess : !!layer; return ( - + + {backButton} + + {this.state.layerWizard.renderWizard({ + previewLayer: this._previewLayer, + mapColors: this.props.mapColors, + })} + + ); } - _renderFlyout() { + render() { + if (!this.props.flyoutVisible) { + return null; + } + const panelDescription = this._getPanelDescription(); + const isNextBtnEnabled = this.state.importView + ? this.props.isIndexingReady || this.props.isIndexingSuccess + : !!this.state.layerDescriptor; return ( - +

{panelDescription}

@@ -168,14 +161,16 @@ export class AddLayerPanel extends Component {
-
{this._renderAddLayerPanel()}
+
{this._renderPanelBody()}
- {this._renderFooter(panelDescription)} -
- ); - } - render() { - return this.props.flyoutVisible ? this._renderFlyout() : null; + + + ); } } diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts index be6c3bf978d82..abfaba80c33d1 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts @@ -23,6 +23,7 @@ import { addLayerWithoutDataSync } from '../actions/map_actions'; import { getQueryableUniqueIndexPatternIds } from '../selectors/map_selectors'; import { getInitialLayers } from '../angular/get_initial_layers'; import { mergeInputWithSavedMap } from './merge_input_with_saved_map'; +import '../index.scss'; export class MapEmbeddableFactory implements EmbeddableFactoryDefinition { type = MAP_SAVED_OBJECT_TYPE; diff --git a/x-pack/plugins/maps/public/layers/blended_vector_layer.ts b/x-pack/plugins/maps/public/layers/blended_vector_layer.ts index 1fc3ad203706f..5c486200977d7 100644 --- a/x-pack/plugins/maps/public/layers/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/layers/blended_vector_layer.ts @@ -156,7 +156,7 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { static type = LAYER_TYPE.BLENDED_VECTOR; static createDescriptor( - options: VectorLayerDescriptor, + options: Partial, mapColors: string[] ): VectorLayerDescriptor { const layerDescriptor = VectorLayer.createDescriptor(options, mapColors); diff --git a/x-pack/plugins/maps/public/layers/layer.tsx b/x-pack/plugins/maps/public/layers/layer.tsx index ce48793e1481b..13fe447cec3da 100644 --- a/x-pack/plugins/maps/public/layers/layer.tsx +++ b/x-pack/plugins/maps/public/layers/layer.tsx @@ -62,7 +62,6 @@ export interface ILayer { isLayerLoading(): boolean; hasErrors(): boolean; getErrors(): string; - toLayerDescriptor(): LayerDescriptor; getMbLayerIds(): string[]; ownsMbLayerId(mbLayerId: string): boolean; ownsMbSourceId(mbSourceId: string): boolean; @@ -413,10 +412,6 @@ export class AbstractLayer implements ILayer { : ''; } - toLayerDescriptor(): LayerDescriptor { - return this._descriptor; - } - async syncData(syncContext: SyncContext) { // no-op by default } diff --git a/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts b/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts index cb87aeaa9da3f..633e8c86d8c94 100644 --- a/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts +++ b/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts @@ -6,13 +6,17 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ import { ReactElement } from 'react'; -import { ISource } from './sources/source'; - -export type PreviewSourceHandler = (source: ISource | null) => void; +import { LayerDescriptor } from '../../common/descriptor_types'; export type RenderWizardArguments = { - onPreviewSource: PreviewSourceHandler; - inspectorAdapters: object; + previewLayer: (layerDescriptor: LayerDescriptor | null, isIndexingSource?: boolean) => void; + mapColors: string[]; + // upload arguments + isIndexingTriggered: boolean; + onRemove: () => void; + onIndexReady: () => void; + importSuccessHandler: (indexResponses: unknown) => void; + importErrorHandler: (indexResponses: unknown) => void; }; export type LayerWizard = { diff --git a/x-pack/plugins/maps/public/layers/load_layer_wizards.ts b/x-pack/plugins/maps/public/layers/load_layer_wizards.ts index 49d128257fe20..0b83f3bbdc613 100644 --- a/x-pack/plugins/maps/public/layers/load_layer_wizards.ts +++ b/x-pack/plugins/maps/public/layers/load_layer_wizards.ts @@ -12,7 +12,7 @@ import { esDocumentsLayerWizardConfig } from './sources/es_search_source'; // @ts-ignore import { clustersLayerWizardConfig, heatmapLayerWizardConfig } from './sources/es_geo_grid_source'; // @ts-ignore -import { point2PointLayerWizardConfig } from './sources/es_pew_pew_source/es_pew_pew_source'; +import { point2PointLayerWizardConfig } from './sources/es_pew_pew_source'; // @ts-ignore import { emsBoundariesLayerWizardConfig } from './sources/ems_file_source'; // @ts-ignore diff --git a/x-pack/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js b/x-pack/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js index 36f898f723757..3c9c71d2a1875 100644 --- a/x-pack/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js +++ b/x-pack/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js @@ -5,17 +5,7 @@ */ import { AbstractVectorSource } from '../vector_source'; -import React from 'react'; -import { - ES_GEO_FIELD_TYPE, - SOURCE_TYPES, - DEFAULT_MAX_RESULT_WINDOW, - SCALING_TYPES, -} from '../../../../common/constants'; -import { ClientFileCreateSourceEditor } from './create_client_file_source_editor'; -import { ESSearchSource } from '../es_search_source'; -import uuid from 'uuid/v4'; -import { i18n } from '@kbn/i18n'; +import { SOURCE_TYPES } from '../../../../common/constants'; import { registerSource } from '../source_registry'; export class GeojsonFileSource extends AbstractVectorSource { @@ -66,103 +56,9 @@ export class GeojsonFileSource extends AbstractVectorSource { canFormatFeatureProperties() { return true; } - - shouldBeIndexed() { - return true; - } } -const viewIndexedData = ( - addAndViewSource, - inspectorAdapters, - importSuccessHandler, - importErrorHandler -) => { - return (indexResponses = {}) => { - const { indexDataResp, indexPatternResp } = indexResponses; - - const indexCreationFailed = !(indexDataResp && indexDataResp.success); - const allDocsFailed = indexDataResp.failures.length === indexDataResp.docCount; - const indexPatternCreationFailed = !(indexPatternResp && indexPatternResp.success); - - if (indexCreationFailed || allDocsFailed || indexPatternCreationFailed) { - importErrorHandler(indexResponses); - return; - } - const { fields, id: indexPatternId } = indexPatternResp; - const geoField = fields.find(field => Object.values(ES_GEO_FIELD_TYPE).includes(field.type)); - if (!indexPatternId || !geoField) { - addAndViewSource(null); - } else { - const source = new ESSearchSource( - { - id: uuid(), - indexPatternId, - geoField: geoField.name, - // Only turn on bounds filter for large doc counts - filterByMapBounds: indexDataResp.docCount > DEFAULT_MAX_RESULT_WINDOW, - scalingType: - geoField.type === ES_GEO_FIELD_TYPE.GEO_POINT - ? SCALING_TYPES.CLUSTERS - : SCALING_TYPES.LIMIT, - }, - inspectorAdapters - ); - addAndViewSource(source); - importSuccessHandler(indexResponses); - } - }; -}; - -const previewGeojsonFile = (onPreviewSource, inspectorAdapters) => { - return (geojsonFile, name) => { - if (!geojsonFile) { - onPreviewSource(null); - return; - } - const sourceDescriptor = GeojsonFileSource.createDescriptor(geojsonFile, name); - const source = new GeojsonFileSource(sourceDescriptor, inspectorAdapters); - onPreviewSource(source); - }; -}; - registerSource({ ConstructorFunction: GeojsonFileSource, type: SOURCE_TYPES.GEOJSON_FILE, }); - -export const uploadLayerWizardConfig = { - description: i18n.translate('xpack.maps.source.geojsonFileDescription', { - defaultMessage: 'Index GeoJSON data in Elasticsearch', - }), - icon: 'importAction', - isIndexingSource: true, - renderWizard: ({ - onPreviewSource, - inspectorAdapters, - addAndViewSource, - isIndexingTriggered, - onRemove, - onIndexReady, - importSuccessHandler, - importErrorHandler, - }) => { - return ( - - ); - }, - title: i18n.translate('xpack.maps.source.geojsonFileTitle', { - defaultMessage: 'Upload GeoJSON', - }), -}; diff --git a/x-pack/plugins/maps/public/layers/sources/client_file_source/index.js b/x-pack/plugins/maps/public/layers/sources/client_file_source/index.js index a6a31def4b231..5c2a0afd31885 100644 --- a/x-pack/plugins/maps/public/layers/sources/client_file_source/index.js +++ b/x-pack/plugins/maps/public/layers/sources/client_file_source/index.js @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { GeojsonFileSource, uploadLayerWizardConfig } from './geojson_file_source'; +export { GeojsonFileSource } from './geojson_file_source'; +export { uploadLayerWizardConfig } from './upload_layer_wizard'; diff --git a/x-pack/plugins/maps/public/layers/sources/client_file_source/upload_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/client_file_source/upload_layer_wizard.tsx new file mode 100644 index 0000000000000..2f8aa67d74b52 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/client_file_source/upload_layer_wizard.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { IFieldType } from 'src/plugins/data/public'; +import { + ES_GEO_FIELD_TYPE, + DEFAULT_MAX_RESULT_WINDOW, + SCALING_TYPES, +} from '../../../../common/constants'; +// @ts-ignore +import { ESSearchSource, createDefaultLayerDescriptor } from '../es_search_source'; +import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +// @ts-ignore +import { ClientFileCreateSourceEditor } from './create_client_file_source_editor'; +// @ts-ignore +import { GeojsonFileSource } from './geojson_file_source'; +import { VectorLayer } from '../../vector_layer'; + +export const uploadLayerWizardConfig: LayerWizard = { + description: i18n.translate('xpack.maps.source.geojsonFileDescription', { + defaultMessage: 'Index GeoJSON data in Elasticsearch', + }), + icon: 'importAction', + isIndexingSource: true, + renderWizard: ({ + previewLayer, + mapColors, + isIndexingTriggered, + onRemove, + onIndexReady, + importSuccessHandler, + importErrorHandler, + }: RenderWizardArguments) => { + function previewGeojsonFile(geojsonFile: unknown, name: string) { + if (!geojsonFile) { + previewLayer(null); + return; + } + const sourceDescriptor = GeojsonFileSource.createDescriptor(geojsonFile, name); + const layerDescriptor = VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + // TODO figure out a better way to handle passing this information back to layer_addpanel + previewLayer(layerDescriptor, true); + } + + function viewIndexedData(indexResponses: { + indexDataResp: unknown; + indexPatternResp: unknown; + }) { + const { indexDataResp, indexPatternResp } = indexResponses; + + // @ts-ignore + const indexCreationFailed = !(indexDataResp && indexDataResp.success); + // @ts-ignore + const allDocsFailed = indexDataResp.failures.length === indexDataResp.docCount; + // @ts-ignore + const indexPatternCreationFailed = !(indexPatternResp && indexPatternResp.success); + + if (indexCreationFailed || allDocsFailed || indexPatternCreationFailed) { + importErrorHandler(indexResponses); + return; + } + // @ts-ignore + const { fields, id: indexPatternId } = indexPatternResp; + const geoField = fields.find((field: IFieldType) => + [ES_GEO_FIELD_TYPE.GEO_POINT as string, ES_GEO_FIELD_TYPE.GEO_SHAPE as string].includes( + field.type + ) + ); + if (!indexPatternId || !geoField) { + previewLayer(null); + } else { + const esSearchSourceConfig = { + indexPatternId, + geoField: geoField.name, + // Only turn on bounds filter for large doc counts + // @ts-ignore + filterByMapBounds: indexDataResp.docCount > DEFAULT_MAX_RESULT_WINDOW, + scalingType: + geoField.type === ES_GEO_FIELD_TYPE.GEO_POINT + ? SCALING_TYPES.CLUSTERS + : SCALING_TYPES.LIMIT, + }; + previewLayer(createDefaultLayerDescriptor(esSearchSourceConfig, mapColors)); + importSuccessHandler(indexResponses); + } + } + + return ( + + ); + }, + title: i18n.translate('xpack.maps.source.geojsonFileTitle', { + defaultMessage: 'Upload GeoJSON', + }), +}; diff --git a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx new file mode 100644 index 0000000000000..f31e770df2d95 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { VectorLayer } from '../../vector_layer'; +import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +// @ts-ignore +import { EMSFileCreateSourceEditor } from './create_source_editor'; +// @ts-ignore +import { EMSFileSource, sourceTitle } from './ems_file_source'; + +export const emsBoundariesLayerWizardConfig: LayerWizard = { + description: i18n.translate('xpack.maps.source.emsFileDescription', { + defaultMessage: 'Administrative boundaries from Elastic Maps Service', + }), + icon: 'emsApp', + renderWizard: ({ previewLayer, mapColors }: RenderWizardArguments) => { + const onSourceConfigChange = (sourceConfig: unknown) => { + // @ts-ignore + const sourceDescriptor = EMSFileSource.createDescriptor(sourceConfig); + const layerDescriptor = VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + previewLayer(layerDescriptor); + }; + return ; + }, + title: sourceTitle, +}; diff --git a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js index e8af17b911939..5802a223e4846 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js +++ b/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.js @@ -9,14 +9,13 @@ import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; import React from 'react'; import { SOURCE_TYPES, FIELD_ORIGIN } from '../../../../common/constants'; import { getEMSClient } from '../../../meta'; -import { EMSFileCreateSourceEditor } from './create_source_editor'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { UpdateSourceEditor } from './update_source_editor'; import { EMSFileField } from '../../fields/ems_file_field'; import { registerSource } from '../source_registry'; -const sourceTitle = i18n.translate('xpack.maps.source.emsFileTitle', { +export const sourceTitle = i18n.translate('xpack.maps.source.emsFileTitle', { defaultMessage: 'EMS Boundaries', }); @@ -161,19 +160,3 @@ registerSource({ ConstructorFunction: EMSFileSource, type: SOURCE_TYPES.EMS_FILE, }); - -export const emsBoundariesLayerWizardConfig = { - description: i18n.translate('xpack.maps.source.emsFileDescription', { - defaultMessage: 'Administrative boundaries from Elastic Maps Service', - }), - icon: 'emsApp', - renderWizard: ({ onPreviewSource, inspectorAdapters }) => { - const onSourceConfigChange = sourceConfig => { - const sourceDescriptor = EMSFileSource.createDescriptor(sourceConfig); - const source = new EMSFileSource(sourceDescriptor, inspectorAdapters); - onPreviewSource(source); - }; - return ; - }, - title: sourceTitle, -}; diff --git a/x-pack/plugins/maps/public/layers/sources/ems_file_source/index.js b/x-pack/plugins/maps/public/layers/sources/ems_file_source/index.js index 28fbc04a1a032..e9bf592c6d2b7 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_file_source/index.js +++ b/x-pack/plugins/maps/public/layers/sources/ems_file_source/index.js @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { EMSFileSource, emsBoundariesLayerWizardConfig } from './ems_file_source'; +export { emsBoundariesLayerWizardConfig } from './ems_boundaries_layer_wizard'; +export { EMSFileSource } from './ems_file_source'; diff --git a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx new file mode 100644 index 0000000000000..ced33a0bcf84a --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +// @ts-ignore +import { EMSTMSSource, sourceTitle } from './ems_tms_source'; +// @ts-ignore +import { VectorTileLayer } from '../../vector_tile_layer'; +// @ts-ignore +import { TileServiceSelect } from './tile_service_select'; + +export const emsBaseMapLayerWizardConfig: LayerWizard = { + description: i18n.translate('xpack.maps.source.emsTileDescription', { + defaultMessage: 'Tile map service from Elastic Maps Service', + }), + icon: 'emsApp', + renderWizard: ({ previewLayer }: RenderWizardArguments) => { + const onSourceConfigChange = (sourceConfig: unknown) => { + const layerDescriptor = VectorTileLayer.createDescriptor({ + sourceDescriptor: EMSTMSSource.createDescriptor(sourceConfig), + }); + previewLayer(layerDescriptor); + }; + + return ; + }, + title: sourceTitle, +}; diff --git a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js index 79121c4cdb31f..3bed9b2c09570 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js +++ b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js @@ -7,10 +7,8 @@ import _ from 'lodash'; import React from 'react'; import { AbstractTMSSource } from '../tms_source'; -import { VectorTileLayer } from '../../vector_tile_layer'; import { getEMSClient } from '../../../meta'; -import { TileServiceSelect } from './tile_service_select'; import { UpdateSourceEditor } from './update_source_editor'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; @@ -18,7 +16,7 @@ import { SOURCE_TYPES } from '../../../../common/constants'; import { getInjectedVarFunc, getUiSettings } from '../../../kibana_services'; import { registerSource } from '../source_registry'; -const sourceTitle = i18n.translate('xpack.maps.source.emsTileTitle', { +export const sourceTitle = i18n.translate('xpack.maps.source.emsTileTitle', { defaultMessage: 'EMS Basemaps', }); @@ -84,20 +82,6 @@ export class EMSTMSSource extends AbstractTMSSource { return tmsService; } - _createDefaultLayerDescriptor(options) { - return VectorTileLayer.createDescriptor({ - sourceDescriptor: this._descriptor, - ...options, - }); - } - - createDefaultLayer(options) { - return new VectorTileLayer({ - layerDescriptor: this._createDefaultLayerDescriptor(options), - source: this, - }); - } - async getDisplayName() { try { const emsTMSService = await this._getEMSTMSService(); @@ -150,20 +134,3 @@ registerSource({ ConstructorFunction: EMSTMSSource, type: SOURCE_TYPES.EMS_TMS, }); - -export const emsBaseMapLayerWizardConfig = { - description: i18n.translate('xpack.maps.source.emsTileDescription', { - defaultMessage: 'Tile map service from Elastic Maps Service', - }), - icon: 'emsApp', - renderWizard: ({ onPreviewSource, inspectorAdapters }) => { - const onSourceConfigChange = sourceConfig => { - const descriptor = EMSTMSSource.createDescriptor(sourceConfig); - const source = new EMSTMSSource(descriptor, inspectorAdapters); - onPreviewSource(source); - }; - - return ; - }, - title: sourceTitle, -}; diff --git a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/index.js b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/index.js index 60a4c9b1de891..704bcfd370a85 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/index.js +++ b/x-pack/plugins/maps/public/layers/sources/ems_tms_source/index.js @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { EMSTMSSource, emsBaseMapLayerWizardConfig } from './ems_tms_source'; +export { emsBaseMapLayerWizardConfig } from './ems_base_map_layer_wizard'; +export { EMSTMSSource } from './ems_tms_source'; diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/clusters_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/clusters_layer_wizard.tsx new file mode 100644 index 0000000000000..f9092e64833f1 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/clusters_layer_wizard.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; +// @ts-ignore +import { CreateSourceEditor } from './create_source_editor'; +// @ts-ignore +import { ESGeoGridSource, clustersTitle } from './es_geo_grid_source'; +import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +import { VectorLayer } from '../../vector_layer'; +import { + ESGeoGridSourceDescriptor, + ColorDynamicOptions, + SizeDynamicOptions, +} from '../../../../common/descriptor_types'; +import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults'; +import { VectorStyle } from '../../styles/vector/vector_style'; +import { + COUNT_PROP_NAME, + COLOR_MAP_TYPE, + FIELD_ORIGIN, + RENDER_AS, + VECTOR_STYLES, + STYLE_TYPE, +} from '../../../../common/constants'; +// @ts-ignore +import { COLOR_GRADIENTS } from '../../styles/color_utils'; + +export const clustersLayerWizardConfig: LayerWizard = { + description: i18n.translate('xpack.maps.source.esGridClustersDescription', { + defaultMessage: 'Geospatial data grouped in grids with metrics for each gridded cell', + }), + icon: 'logoElasticsearch', + renderWizard: ({ previewLayer }: RenderWizardArguments) => { + const onSourceConfigChange = (sourceConfig: Partial) => { + if (!sourceConfig) { + previewLayer(null); + return; + } + + const defaultDynamicProperties = getDefaultDynamicProperties(); + const layerDescriptor = VectorLayer.createDescriptor({ + sourceDescriptor: ESGeoGridSource.createDescriptor(sourceConfig), + style: VectorStyle.createDescriptor({ + // @ts-ignore + [VECTOR_STYLES.FILL_COLOR]: { + type: STYLE_TYPE.DYNAMIC, + options: { + ...(defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR]! + .options as ColorDynamicOptions), + field: { + name: COUNT_PROP_NAME, + origin: FIELD_ORIGIN.SOURCE, + }, + color: COLOR_GRADIENTS[0].value, + type: COLOR_MAP_TYPE.ORDINAL, + }, + }, + [VECTOR_STYLES.LINE_COLOR]: { + type: STYLE_TYPE.STATIC, + options: { + color: '#FFF', + }, + }, + [VECTOR_STYLES.LINE_WIDTH]: { + type: STYLE_TYPE.STATIC, + options: { + size: 0, + }, + }, + [VECTOR_STYLES.ICON_SIZE]: { + type: STYLE_TYPE.DYNAMIC, + options: { + ...(defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE]!.options as SizeDynamicOptions), + field: { + name: COUNT_PROP_NAME, + origin: FIELD_ORIGIN.SOURCE, + }, + }, + }, + [VECTOR_STYLES.LABEL_TEXT]: { + type: STYLE_TYPE.DYNAMIC, + options: { + ...defaultDynamicProperties[VECTOR_STYLES.LABEL_TEXT]!.options, + field: { + name: COUNT_PROP_NAME, + origin: FIELD_ORIGIN.SOURCE, + }, + }, + }, + }), + }); + previewLayer(layerDescriptor); + }; + + return ( + + ); + }, + title: clustersTitle, +}; diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js index b9ef13e520bf8..17fad2f2e6453 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js +++ b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js @@ -8,39 +8,27 @@ import React from 'react'; import uuid from 'uuid/v4'; import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; -import { HeatmapLayer } from '../../heatmap_layer'; -import { VectorLayer } from '../../vector_layer'; import { convertCompositeRespToGeoJson, convertRegularRespToGeoJson } from './convert_to_geojson'; -import { VectorStyle } from '../../styles/vector/vector_style'; -import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults'; -import { COLOR_GRADIENTS } from '../../styles/color_utils'; -import { CreateSourceEditor } from './create_source_editor'; import { UpdateSourceEditor } from './update_source_editor'; import { SOURCE_TYPES, DEFAULT_MAX_BUCKETS_LIMIT, - COUNT_PROP_NAME, - COLOR_MAP_TYPE, RENDER_AS, GRID_RESOLUTION, - VECTOR_STYLES, - FIELD_ORIGIN, } from '../../../../common/constants'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { AbstractESAggSource } from '../es_agg_source'; -import { DynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; -import { StaticStyleProperty } from '../../styles/vector/properties/static_style_property'; import { DataRequestAbortError } from '../../util/data_request'; import { registerSource } from '../source_registry'; export const MAX_GEOTILE_LEVEL = 29; -const clustersTitle = i18n.translate('xpack.maps.source.esGridClustersTitle', { +export const clustersTitle = i18n.translate('xpack.maps.source.esGridClustersTitle', { defaultMessage: 'Clusters and grids', }); -const heatmapTitle = i18n.translate('xpack.maps.source.esGridHeatmapTitle', { +export const heatmapTitle = i18n.translate('xpack.maps.source.esGridHeatmapTitle', { defaultMessage: 'Heat map', }); @@ -320,87 +308,6 @@ export class ESGeoGridSource extends AbstractESAggSource { return true; } - _createHeatmapLayerDescriptor(options) { - return HeatmapLayer.createDescriptor({ - sourceDescriptor: this._descriptor, - ...options, - }); - } - - _createVectorLayerDescriptor(options) { - const descriptor = VectorLayer.createDescriptor({ - sourceDescriptor: this._descriptor, - ...options, - }); - - const defaultDynamicProperties = getDefaultDynamicProperties(); - - descriptor.style = VectorStyle.createDescriptor({ - [VECTOR_STYLES.FILL_COLOR]: { - type: DynamicStyleProperty.type, - options: { - ...defaultDynamicProperties[VECTOR_STYLES.FILL_COLOR].options, - field: { - name: COUNT_PROP_NAME, - origin: FIELD_ORIGIN.SOURCE, - }, - color: COLOR_GRADIENTS[0].value, - type: COLOR_MAP_TYPE.ORDINAL, - }, - }, - [VECTOR_STYLES.LINE_COLOR]: { - type: StaticStyleProperty.type, - options: { - color: '#FFF', - }, - }, - [VECTOR_STYLES.LINE_WIDTH]: { - type: StaticStyleProperty.type, - options: { - size: 0, - }, - }, - [VECTOR_STYLES.ICON_SIZE]: { - type: DynamicStyleProperty.type, - options: { - ...defaultDynamicProperties[VECTOR_STYLES.ICON_SIZE].options, - field: { - name: COUNT_PROP_NAME, - origin: FIELD_ORIGIN.SOURCE, - }, - }, - }, - [VECTOR_STYLES.LABEL_TEXT]: { - type: DynamicStyleProperty.type, - options: { - ...defaultDynamicProperties[VECTOR_STYLES.LABEL_TEXT].options, - field: { - name: COUNT_PROP_NAME, - origin: FIELD_ORIGIN.SOURCE, - }, - }, - }, - }); - return descriptor; - } - - createDefaultLayer(options) { - if (this._descriptor.requestType === RENDER_AS.HEATMAP) { - return new HeatmapLayer({ - layerDescriptor: this._createHeatmapLayerDescriptor(options), - source: this, - }); - } - - const layerDescriptor = this._createVectorLayerDescriptor(options); - const style = new VectorStyle(layerDescriptor.style, this); - return new VectorLayer({ - layerDescriptor, - source: this, - style, - }); - } - canFormatFeatureProperties() { return true; } @@ -422,57 +329,3 @@ registerSource({ ConstructorFunction: ESGeoGridSource, type: SOURCE_TYPES.ES_GEO_GRID, }); - -export const clustersLayerWizardConfig = { - description: i18n.translate('xpack.maps.source.esGridClustersDescription', { - defaultMessage: 'Geospatial data grouped in grids with metrics for each gridded cell', - }), - icon: 'logoElasticsearch', - renderWizard: ({ onPreviewSource, inspectorAdapters }) => { - const onSourceConfigChange = sourceConfig => { - if (!sourceConfig) { - onPreviewSource(null); - return; - } - - const sourceDescriptor = ESGeoGridSource.createDescriptor(sourceConfig); - const source = new ESGeoGridSource(sourceDescriptor, inspectorAdapters); - onPreviewSource(source); - }; - - return ( - - ); - }, - title: clustersTitle, -}; - -export const heatmapLayerWizardConfig = { - description: i18n.translate('xpack.maps.source.esGridHeatmapDescription', { - defaultMessage: 'Geospatial data grouped in grids to show density', - }), - icon: 'logoElasticsearch', - renderWizard: ({ onPreviewSource, inspectorAdapters }) => { - const onSourceConfigChange = sourceConfig => { - if (!sourceConfig) { - onPreviewSource(null); - return; - } - - const sourceDescriptor = ESGeoGridSource.createDescriptor(sourceConfig); - const source = new ESGeoGridSource(sourceDescriptor, inspectorAdapters); - onPreviewSource(source); - }; - - return ( - - ); - }, - title: heatmapTitle, -}; diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/heatmap_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/heatmap_layer_wizard.tsx new file mode 100644 index 0000000000000..fee1a81a5c63a --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/heatmap_layer_wizard.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; +// @ts-ignore +import { CreateSourceEditor } from './create_source_editor'; +// @ts-ignore +import { ESGeoGridSource, heatmapTitle } from './es_geo_grid_source'; +import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +// @ts-ignore +import { HeatmapLayer } from '../../heatmap_layer'; +import { ESGeoGridSourceDescriptor } from '../../../../common/descriptor_types'; +import { RENDER_AS } from '../../../../common/constants'; + +export const heatmapLayerWizardConfig: LayerWizard = { + description: i18n.translate('xpack.maps.source.esGridHeatmapDescription', { + defaultMessage: 'Geospatial data grouped in grids to show density', + }), + icon: 'logoElasticsearch', + renderWizard: ({ previewLayer }: RenderWizardArguments) => { + const onSourceConfigChange = (sourceConfig: Partial) => { + if (!sourceConfig) { + previewLayer(null); + return; + } + + const layerDescriptor = HeatmapLayer.createDescriptor({ + sourceDescriptor: ESGeoGridSource.createDescriptor(sourceConfig), + }); + previewLayer(layerDescriptor); + }; + + return ( + + ); + }, + title: heatmapTitle, +}; diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/index.js b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/index.js index c2fa2356b1a3e..2db66ff411627 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/index.js +++ b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/index.js @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export { - ESGeoGridSource, - clustersLayerWizardConfig, - heatmapLayerWizardConfig, -} from './es_geo_grid_source'; +export { clustersLayerWizardConfig } from './clusters_layer_wizard'; +export { ESGeoGridSource } from './es_geo_grid_source'; +export { heatmapLayerWizardConfig } from './heatmap_layer_wizard'; diff --git a/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js index 57e5afb99404b..0d15cff032410 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js @@ -8,29 +8,18 @@ import React from 'react'; import uuid from 'uuid/v4'; import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; -import { VectorLayer } from '../../vector_layer'; -import { CreateSourceEditor } from './create_source_editor'; import { UpdateSourceEditor } from './update_source_editor'; -import { VectorStyle } from '../../styles/vector/vector_style'; -import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults'; import { i18n } from '@kbn/i18n'; -import { - FIELD_ORIGIN, - SOURCE_TYPES, - COUNT_PROP_NAME, - VECTOR_STYLES, -} from '../../../../common/constants'; +import { SOURCE_TYPES } from '../../../../common/constants'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { convertToLines } from './convert_to_lines'; import { AbstractESAggSource } from '../es_agg_source'; -import { DynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; -import { COLOR_GRADIENTS } from '../../styles/color_utils'; import { indexPatterns } from '../../../../../../../src/plugins/data/public'; import { registerSource } from '../source_registry'; const MAX_GEOTILE_LEVEL = 29; -const sourceTitle = i18n.translate('xpack.maps.source.pewPewTitle', { +export const sourceTitle = i18n.translate('xpack.maps.source.pewPewTitle', { defaultMessage: 'Point to point', }); @@ -109,43 +98,6 @@ export class ESPewPewSource extends AbstractESAggSource { ]; } - createDefaultLayer(options) { - const defaultDynamicProperties = getDefaultDynamicProperties(); - const styleDescriptor = VectorStyle.createDescriptor({ - [VECTOR_STYLES.LINE_COLOR]: { - type: DynamicStyleProperty.type, - options: { - ...defaultDynamicProperties[VECTOR_STYLES.LINE_COLOR].options, - field: { - name: COUNT_PROP_NAME, - origin: FIELD_ORIGIN.SOURCE, - }, - color: COLOR_GRADIENTS[0].value, - }, - }, - [VECTOR_STYLES.LINE_WIDTH]: { - type: DynamicStyleProperty.type, - options: { - ...defaultDynamicProperties[VECTOR_STYLES.LINE_WIDTH].options, - field: { - name: COUNT_PROP_NAME, - origin: FIELD_ORIGIN.SOURCE, - }, - }, - }, - }); - - return new VectorLayer({ - layerDescriptor: VectorLayer.createDescriptor({ - ...options, - sourceDescriptor: this._descriptor, - style: styleDescriptor, - }), - source: this, - style: new VectorStyle(styleDescriptor, this), - }); - } - getGeoGridPrecision(zoom) { const targetGeotileLevel = Math.ceil(zoom) + 2; return Math.min(targetGeotileLevel, MAX_GEOTILE_LEVEL); @@ -234,25 +186,3 @@ registerSource({ ConstructorFunction: ESPewPewSource, type: SOURCE_TYPES.ES_PEW_PEW, }); - -export const point2PointLayerWizardConfig = { - description: i18n.translate('xpack.maps.source.pewPewDescription', { - defaultMessage: 'Aggregated data paths between the source and destination', - }), - icon: 'logoElasticsearch', - renderWizard: ({ onPreviewSource, inspectorAdapters }) => { - const onSourceConfigChange = sourceConfig => { - if (!sourceConfig) { - onPreviewSource(null); - return; - } - - const sourceDescriptor = ESPewPewSource.createDescriptor(sourceConfig); - const source = new ESPewPewSource(sourceDescriptor, inspectorAdapters); - onPreviewSource(source); - }; - - return ; - }, - title: sourceTitle, -}; diff --git a/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/index.js b/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/index.js new file mode 100644 index 0000000000000..fabde578085ab --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/index.js @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { ESPewPewSource } from './es_pew_pew_source'; +export { point2PointLayerWizardConfig } from './point_2_point_layer_wizard'; diff --git a/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx new file mode 100644 index 0000000000000..3ad6d64903d4a --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/point_2_point_layer_wizard.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 { i18n } from '@kbn/i18n'; +import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults'; +import { VectorLayer } from '../../vector_layer'; +// @ts-ignore +import { ESPewPewSource, sourceTitle } from './es_pew_pew_source'; +import { VectorStyle } from '../../styles/vector/vector_style'; +import { + FIELD_ORIGIN, + COUNT_PROP_NAME, + VECTOR_STYLES, + STYLE_TYPE, +} from '../../../../common/constants'; +// @ts-ignore +import { COLOR_GRADIENTS } from '../../styles/color_utils'; +// @ts-ignore +import { CreateSourceEditor } from './create_source_editor'; +import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +import { ColorDynamicOptions, SizeDynamicOptions } from '../../../../common/descriptor_types'; + +export const point2PointLayerWizardConfig: LayerWizard = { + description: i18n.translate('xpack.maps.source.pewPewDescription', { + defaultMessage: 'Aggregated data paths between the source and destination', + }), + icon: 'logoElasticsearch', + renderWizard: ({ previewLayer }: RenderWizardArguments) => { + const onSourceConfigChange = (sourceConfig: unknown) => { + if (!sourceConfig) { + previewLayer(null); + return; + } + + const defaultDynamicProperties = getDefaultDynamicProperties(); + const layerDescriptor = VectorLayer.createDescriptor({ + sourceDescriptor: ESPewPewSource.createDescriptor(sourceConfig), + style: VectorStyle.createDescriptor({ + [VECTOR_STYLES.LINE_COLOR]: { + type: STYLE_TYPE.DYNAMIC, + options: { + ...(defaultDynamicProperties[VECTOR_STYLES.LINE_COLOR]! + .options as ColorDynamicOptions), + field: { + name: COUNT_PROP_NAME, + origin: FIELD_ORIGIN.SOURCE, + }, + color: COLOR_GRADIENTS[0].value, + }, + }, + [VECTOR_STYLES.LINE_WIDTH]: { + type: STYLE_TYPE.DYNAMIC, + options: { + ...(defaultDynamicProperties[VECTOR_STYLES.LINE_WIDTH]! + .options as SizeDynamicOptions), + field: { + name: COUNT_PROP_NAME, + origin: FIELD_ORIGIN.SOURCE, + }, + }, + }, + }), + }); + previewLayer(layerDescriptor); + }; + + return ; + }, + title: sourceTitle, +}; diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_documents_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/es_search_source/es_documents_layer_wizard.tsx new file mode 100644 index 0000000000000..4a775dd78f787 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/es_search_source/es_documents_layer_wizard.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; +// @ts-ignore +import { CreateSourceEditor } from './create_source_editor'; +import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +// @ts-ignore +import { ESSearchSource, sourceTitle } from './es_search_source'; +import { BlendedVectorLayer } from '../../blended_vector_layer'; +import { VectorLayer } from '../../vector_layer'; +import { SCALING_TYPES } from '../../../../common/constants'; + +export function createDefaultLayerDescriptor(sourceConfig: unknown, mapColors: string[]) { + const sourceDescriptor = ESSearchSource.createDescriptor(sourceConfig); + + return sourceDescriptor.scalingType === SCALING_TYPES.CLUSTERS + ? BlendedVectorLayer.createDescriptor({ sourceDescriptor }, mapColors) + : VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); +} + +export const esDocumentsLayerWizardConfig: LayerWizard = { + description: i18n.translate('xpack.maps.source.esSearchDescription', { + defaultMessage: 'Vector data from a Kibana index pattern', + }), + icon: 'logoElasticsearch', + renderWizard: ({ previewLayer, mapColors }: RenderWizardArguments) => { + const onSourceConfigChange = (sourceConfig: unknown) => { + if (!sourceConfig) { + previewLayer(null); + return; + } + + previewLayer(createDefaultLayerDescriptor(sourceConfig, mapColors)); + }; + return ; + }, + title: sourceTitle, +}; diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.d.ts b/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.d.ts index c904280a38c85..23e3c759d73c3 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.d.ts +++ b/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.d.ts @@ -8,6 +8,8 @@ import { AbstractESSource } from '../es_source'; import { ESSearchSourceDescriptor } from '../../../../common/descriptor_types'; export class ESSearchSource extends AbstractESSource { + static createDescriptor(sourceConfig: unknown): ESSearchSourceDescriptor; + constructor(sourceDescriptor: Partial, inspectorAdapters: unknown); getFieldNames(): string[]; } diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.js b/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.js index bfbcca1eb3f61..a412c49faceac 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.js +++ b/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.js @@ -6,15 +6,11 @@ import _ from 'lodash'; import React from 'react'; -import uuid from 'uuid/v4'; import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; import { AbstractESSource } from '../es_source'; import { getSearchService } from '../../../kibana_services'; -import { VectorStyle } from '../../styles/vector/vector_style'; -import { VectorLayer } from '../../vector_layer'; import { hitsToGeoJson } from '../../../elasticsearch_geo_utils'; -import { CreateSourceEditor } from './create_source_editor'; import { UpdateSourceEditor } from './update_source_editor'; import { SOURCE_TYPES, @@ -27,14 +23,14 @@ import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; import { getSourceFields } from '../../../index_pattern_util'; import { loadIndexSettings } from './load_index_settings'; -import { BlendedVectorLayer } from '../../blended_vector_layer'; +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'; -const sourceTitle = i18n.translate('xpack.maps.source.esSearchTitle', { +export const sourceTitle = i18n.translate('xpack.maps.source.esSearchTitle', { defaultMessage: 'Documents', }); @@ -71,56 +67,31 @@ function getDocValueAndSourceFields(indexPattern, fieldNames) { export class ESSearchSource extends AbstractESSource { static type = SOURCE_TYPES.ES_SEARCH; + static createDescriptor(descriptor) { + return { + ...descriptor, + id: descriptor.id ? descriptor.id : uuid(), + type: ESSearchSource.type, + indexPatternId: descriptor.indexPatternId, + geoField: descriptor.geoField, + filterByMapBounds: _.get(descriptor, 'filterByMapBounds', DEFAULT_FILTER_BY_MAP_BOUNDS), + tooltipProperties: _.get(descriptor, 'tooltipProperties', []), + sortField: _.get(descriptor, 'sortField', ''), + sortOrder: _.get(descriptor, 'sortOrder', SORT_ORDER.DESC), + scalingType: _.get(descriptor, 'scalingType', SCALING_TYPES.LIMIT), + topHitsSplitField: descriptor.topHitsSplitField, + topHitsSize: _.get(descriptor, 'topHitsSize', 1), + }; + } + constructor(descriptor, inspectorAdapters) { - super( - { - ...descriptor, - id: descriptor.id, - type: ESSearchSource.type, - indexPatternId: descriptor.indexPatternId, - geoField: descriptor.geoField, - filterByMapBounds: _.get(descriptor, 'filterByMapBounds', DEFAULT_FILTER_BY_MAP_BOUNDS), - tooltipProperties: _.get(descriptor, 'tooltipProperties', []), - sortField: _.get(descriptor, 'sortField', ''), - sortOrder: _.get(descriptor, 'sortOrder', SORT_ORDER.DESC), - scalingType: _.get(descriptor, 'scalingType', SCALING_TYPES.LIMIT), - topHitsSplitField: descriptor.topHitsSplitField, - topHitsSize: _.get(descriptor, 'topHitsSize', 1), - }, - inspectorAdapters - ); + super(ESSearchSource.createDescriptor(descriptor), inspectorAdapters); this._tooltipFields = this._descriptor.tooltipProperties.map(property => this.createField({ fieldName: property }) ); } - createDefaultLayer(options, mapColors) { - if (this._descriptor.scalingType === SCALING_TYPES.CLUSTERS) { - const layerDescriptor = BlendedVectorLayer.createDescriptor( - { - sourceDescriptor: this._descriptor, - ...options, - }, - mapColors - ); - const style = new VectorStyle(layerDescriptor.style, this); - return new BlendedVectorLayer({ - layerDescriptor: layerDescriptor, - source: this, - style, - }); - } - - const layerDescriptor = this._createDefaultLayerDescriptor(options, mapColors); - const style = new VectorStyle(layerDescriptor.style, this); - return new VectorLayer({ - layerDescriptor: layerDescriptor, - source: this, - style, - }); - } - createField({ fieldName }) { return new ESDocField({ fieldName, @@ -586,29 +557,3 @@ registerSource({ ConstructorFunction: ESSearchSource, type: SOURCE_TYPES.ES_SEARCH, }); - -export const esDocumentsLayerWizardConfig = { - description: i18n.translate('xpack.maps.source.esSearchDescription', { - defaultMessage: 'Vector data from a Kibana index pattern', - }), - icon: 'logoElasticsearch', - renderWizard: ({ onPreviewSource, inspectorAdapters }) => { - const onSourceConfigChange = sourceConfig => { - if (!sourceConfig) { - onPreviewSource(null); - return; - } - - const source = new ESSearchSource( - { - id: uuid(), - ...sourceConfig, - }, - inspectorAdapters - ); - onPreviewSource(source); - }; - return ; - }, - title: sourceTitle, -}; diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.test.ts b/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.test.ts deleted file mode 100644 index 66cc2ddd85404..0000000000000 --- a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.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. - */ -jest.mock('ui/new_platform'); -jest.mock('../../../kibana_services'); - -import { ESSearchSource } from './es_search_source'; -import { VectorLayer } from '../../vector_layer'; -import { SCALING_TYPES, SOURCE_TYPES } from '../../../../common/constants'; -import { ESSearchSourceDescriptor } from '../../../../common/descriptor_types'; - -const descriptor: ESSearchSourceDescriptor = { - type: SOURCE_TYPES.ES_SEARCH, - id: '1234', - indexPatternId: 'myIndexPattern', - geoField: 'myLocation', - scalingType: SCALING_TYPES.LIMIT, -}; - -describe('ES Search Source', () => { - beforeEach(() => { - require('../../../kibana_services').getUiSettings = () => ({ - get: jest.fn(), - }); - }); - it('should create a vector layer', () => { - const source = new ESSearchSource(descriptor, null); - const layer = source.createDefaultLayer(); - expect(layer instanceof VectorLayer).toEqual(true); - }); -}); diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/index.js b/x-pack/plugins/maps/public/layers/sources/es_search_source/index.js index 2c401ac92567e..6ae327a18b7c2 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_search_source/index.js +++ b/x-pack/plugins/maps/public/layers/sources/es_search_source/index.js @@ -4,4 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export { ESSearchSource, esDocumentsLayerWizardConfig } from './es_search_source'; +export { ESSearchSource } from './es_search_source'; +export { + createDefaultLayerDescriptor, + esDocumentsLayerWizardConfig, +} from './es_documents_layer_wizard'; diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/index.js b/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/index.js index 00c3bfc5f17c6..6e9d7ad1a613b 100644 --- a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/index.js +++ b/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/index.js @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { KibanaRegionmapSource, kibanaRegionMapLayerWizardConfig } from './kibana_regionmap_source'; +export { kibanaRegionMapLayerWizardConfig } from './kibana_regionmap_layer_wizard'; +export { KibanaRegionmapSource } from './kibana_regionmap_source'; diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx new file mode 100644 index 0000000000000..4321501760faf --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +// @ts-ignore +import { KibanaRegionmapSource, sourceTitle } from './kibana_regionmap_source'; +import { VectorLayer } from '../../vector_layer'; +// @ts-ignore +import { CreateSourceEditor } from './create_source_editor'; + +export const kibanaRegionMapLayerWizardConfig: LayerWizard = { + description: i18n.translate('xpack.maps.source.kbnRegionMapDescription', { + defaultMessage: 'Vector data from hosted GeoJSON configured in kibana.yml', + }), + icon: 'logoKibana', + renderWizard: ({ previewLayer, mapColors }: RenderWizardArguments) => { + const onSourceConfigChange = (sourceConfig: unknown) => { + const sourceDescriptor = KibanaRegionmapSource.createDescriptor(sourceConfig); + const layerDescriptor = VectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + previewLayer(layerDescriptor); + }; + + return ; + }, + title: sourceTitle, +}; diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js b/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js index be333f8ee85a4..fb5a2e4f42f1d 100644 --- a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js +++ b/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js @@ -5,8 +5,6 @@ */ import { AbstractVectorSource } from '../vector_source'; -import React from 'react'; -import { CreateSourceEditor } from './create_source_editor'; import { getKibanaRegionList } from '../../../meta'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; @@ -14,7 +12,7 @@ import { FIELD_ORIGIN, SOURCE_TYPES } from '../../../../common/constants'; import { KibanaRegionField } from '../../fields/kibana_region_field'; import { registerSource } from '../source_registry'; -const sourceTitle = i18n.translate('xpack.maps.source.kbnRegionMapTitle', { +export const sourceTitle = i18n.translate('xpack.maps.source.kbnRegionMapTitle', { defaultMessage: 'Configured GeoJSON', }); @@ -101,20 +99,3 @@ registerSource({ ConstructorFunction: KibanaRegionmapSource, type: SOURCE_TYPES.REGIONMAP_FILE, }); - -export const kibanaRegionMapLayerWizardConfig = { - description: i18n.translate('xpack.maps.source.kbnRegionMapDescription', { - defaultMessage: 'Vector data from hosted GeoJSON configured in kibana.yml', - }), - icon: 'logoKibana', - renderWizard: ({ onPreviewSource, inspectorAdapters }) => { - const onSourceConfigChange = sourceConfig => { - const sourceDescriptor = KibanaRegionmapSource.createDescriptor(sourceConfig); - const source = new KibanaRegionmapSource(sourceDescriptor, inspectorAdapters); - onPreviewSource(source); - }; - - return ; - }, - title: sourceTitle, -}; diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/index.js b/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/index.js index 9fd7f088032ca..cc89091605456 100644 --- a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/index.js +++ b/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/index.js @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { KibanaTilemapSource, kibanaBasemapLayerWizardConfig } from './kibana_tilemap_source'; +export { kibanaBasemapLayerWizardConfig } from './kibana_base_map_layer_wizard'; +export { KibanaTilemapSource } from './kibana_tilemap_source'; diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx new file mode 100644 index 0000000000000..aeea2d6084f84 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +// @ts-ignore +import { CreateSourceEditor } from './create_source_editor'; +// @ts-ignore +import { KibanaTilemapSource, sourceTitle } from './kibana_tilemap_source'; +import { TileLayer } from '../../tile_layer'; + +export const kibanaBasemapLayerWizardConfig: LayerWizard = { + description: i18n.translate('xpack.maps.source.kbnTMSDescription', { + defaultMessage: 'Tile map service configured in kibana.yml', + }), + icon: 'logoKibana', + renderWizard: ({ previewLayer }: RenderWizardArguments) => { + const onSourceConfigChange = () => { + const layerDescriptor = TileLayer.createDescriptor({ + sourceDescriptor: KibanaTilemapSource.createDescriptor(), + }); + previewLayer(layerDescriptor); + }; + return ; + }, + title: sourceTitle, +}; diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_tilemap_source.js b/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_tilemap_source.js index bbb653eff32e2..7dc1d454a1c52 100644 --- a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_tilemap_source.js +++ b/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_tilemap_source.js @@ -3,10 +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 React from 'react'; + import { AbstractTMSSource } from '../tms_source'; -import { TileLayer } from '../../tile_layer'; -import { CreateSourceEditor } from './create_source_editor'; import { getKibanaTileMap } from '../../../meta'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel } from '../../../../common/i18n_getters'; @@ -14,7 +12,7 @@ import _ from 'lodash'; import { SOURCE_TYPES } from '../../../../common/constants'; import { registerSource } from '../source_registry'; -const sourceTitle = i18n.translate('xpack.maps.source.kbnTMSTitle', { +export const sourceTitle = i18n.translate('xpack.maps.source.kbnTMSTitle', { defaultMessage: 'Configured Tile Map Service', }); @@ -42,20 +40,6 @@ export class KibanaTilemapSource extends AbstractTMSSource { ]; } - _createDefaultLayerDescriptor(options) { - return TileLayer.createDescriptor({ - sourceDescriptor: this._descriptor, - ...options, - }); - } - - createDefaultLayer(options) { - return new TileLayer({ - layerDescriptor: this._createDefaultLayerDescriptor(options), - source: this, - }); - } - async getUrlTemplate() { const tilemap = getKibanaTileMap(); if (!tilemap.url) { @@ -88,19 +72,3 @@ registerSource({ ConstructorFunction: KibanaTilemapSource, type: SOURCE_TYPES.KIBANA_TILEMAP, }); - -export const kibanaBasemapLayerWizardConfig = { - description: i18n.translate('xpack.maps.source.kbnTMSDescription', { - defaultMessage: 'Tile map service configured in kibana.yml', - }), - icon: 'logoKibana', - renderWizard: ({ onPreviewSource, inspectorAdapters }) => { - const onSourceConfigChange = () => { - const sourceDescriptor = KibanaTilemapSource.createDescriptor(); - const source = new KibanaTilemapSource(sourceDescriptor, inspectorAdapters); - onPreviewSource(source); - }; - return ; - }, - title: sourceTitle, -}; diff --git a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/layer_wizard.tsx index dfdea1489d50c..c94fec3deac67 100644 --- a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/layer_wizard.tsx @@ -12,30 +12,20 @@ import { } from './mvt_single_layer_vector_source_editor'; import { MVTSingleLayerVectorSource, sourceTitle } from './mvt_single_layer_vector_source'; import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; -import { SOURCE_TYPES } from '../../../../common/constants'; +import { TiledVectorLayer } from '../../tiled_vector_layer'; export const mvtVectorSourceWizardConfig: LayerWizard = { description: i18n.translate('xpack.maps.source.mvtVectorSourceWizard', { defaultMessage: 'Vector source wizard', }), icon: 'grid', - renderWizard: ({ onPreviewSource, inspectorAdapters }: RenderWizardArguments) => { - const onSourceConfigChange = ({ - urlTemplate, - layerName, - minSourceZoom, - maxSourceZoom, - }: MVTSingleLayerVectorSourceConfig) => { - const sourceDescriptor = MVTSingleLayerVectorSource.createDescriptor({ - urlTemplate, - layerName, - minSourceZoom, - maxSourceZoom, - type: SOURCE_TYPES.MVT_SINGLE_LAYER, - }); - const source = new MVTSingleLayerVectorSource(sourceDescriptor, inspectorAdapters); - onPreviewSource(source); + renderWizard: ({ previewLayer, mapColors }: RenderWizardArguments) => { + const onSourceConfigChange = (sourceConfig: MVTSingleLayerVectorSourceConfig) => { + const sourceDescriptor = MVTSingleLayerVectorSource.createDescriptor(sourceConfig); + const layerDescriptor = TiledVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + previewLayer(layerDescriptor); }; + return ; }, title: sourceTitle, diff --git a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts index a73cfbdc0d043..58e6e39aaa1f9 100644 --- a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts +++ b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; import { AbstractSource, ImmutableSourceProperty } from '../source'; -import { TiledVectorLayer } from '../../tiled_vector_layer'; import { GeoJsonWithMeta, ITiledSingleLayerVectorSource } from '../vector_source'; import { MAX_ZOOM, MIN_ZOOM, SOURCE_TYPES } from '../../../../common/constants'; import { VECTOR_SHAPE_TYPES } from '../vector_feature_types'; @@ -17,11 +16,10 @@ import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters import { MapExtent, TiledSingleLayerVectorSourceDescriptor, - VectorLayerDescriptor, VectorSourceRequestMeta, VectorSourceSyncMeta, } from '../../../../common/descriptor_types'; -import { VectorLayerArguments } from '../../vector_layer'; +import { MVTSingleLayerVectorSourceConfig } from './mvt_single_layer_vector_source_editor'; export const sourceTitle = i18n.translate( 'xpack.maps.source.MVTSingleLayerVectorSource.sourceTitle', @@ -37,7 +35,7 @@ export class MVTSingleLayerVectorSource extends AbstractSource layerName, minSourceZoom, maxSourceZoom, - }: TiledSingleLayerVectorSourceDescriptor) { + }: MVTSingleLayerVectorSourceConfig) { return { type: SOURCE_TYPES.MVT_SINGLE_LAYER, id: uuid(), @@ -66,22 +64,6 @@ export class MVTSingleLayerVectorSource extends AbstractSource return []; } - createDefaultLayer(options?: Partial): TiledVectorLayer { - const layerDescriptor: Partial = { - sourceDescriptor: this._descriptor, - ...options, - }; - const normalizedLayerDescriptor: VectorLayerDescriptor = TiledVectorLayer.createDescriptor( - layerDescriptor, - [] - ); - const vectorLayerArguments: VectorLayerArguments = { - layerDescriptor: normalizedLayerDescriptor, - source: this, - }; - return new TiledVectorLayer(vectorLayerArguments); - } - getGeoJsonWithMeta( layerName: 'string', searchFilters: unknown[], diff --git a/x-pack/plugins/maps/public/layers/sources/source.ts b/x-pack/plugins/maps/public/layers/sources/source.ts index 1cd84010159ab..af934d7464f61 100644 --- a/x-pack/plugins/maps/public/layers/sources/source.ts +++ b/x-pack/plugins/maps/public/layers/sources/source.ts @@ -13,8 +13,7 @@ import { Adapters } from 'src/plugins/inspector/public'; // @ts-ignore import { copyPersistentState } from '../../reducers/util'; -import { LayerDescriptor, SourceDescriptor } from '../../../common/descriptor_types'; -import { ILayer } from '../layer'; +import { SourceDescriptor } from '../../../common/descriptor_types'; import { IField } from '../fields/field'; import { MAX_ZOOM, MIN_ZOOM } from '../../../common/constants'; @@ -37,7 +36,6 @@ export type PreIndexedShape = { export type FieldFormatter = (value: string | number | null | undefined | boolean) => string; export interface ISource { - createDefaultLayer(options?: Partial): ILayer; destroy(): void; getDisplayName(): Promise; getInspectorAdapters(): Adapters | undefined; @@ -59,7 +57,6 @@ export interface ISource { getIndexPatternIds(): string[]; getQueryableIndexPatternIds(): string[]; getGeoGridPrecision(zoom: number): number; - shouldBeIndexed(): boolean; getPreIndexedShape(): Promise; createFieldFormatter(field: IField): Promise; getValueSuggestions(field: IField, query: string): Promise; @@ -99,10 +96,6 @@ export class AbstractSource implements ISource { return this._inspectorAdapters; } - createDefaultLayer(options?: Partial): ILayer { - throw new Error(`Source#createDefaultLayer not implemented`); - } - async getDisplayName(): Promise { return ''; } @@ -155,10 +148,6 @@ export class AbstractSource implements ISource { return false; } - shouldBeIndexed(): boolean { - return false; - } - isESSource(): boolean { return false; } diff --git a/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.js b/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.js index 509584cbc415a..12e1780d9cad5 100644 --- a/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.js +++ b/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.js @@ -4,9 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { VectorLayer } from '../../vector_layer'; import { TooltipProperty } from '../../tooltips/tooltip_property'; -import { VectorStyle } from '../../styles/vector/vector_style'; import { AbstractSource } from './../source'; import * as topojson from 'topojson-client'; import _ from 'lodash'; @@ -74,30 +72,10 @@ export class AbstractVectorSource extends AbstractSource { return this.createField({ fieldName: name }); } - _createDefaultLayerDescriptor(options, mapColors) { - return VectorLayer.createDescriptor( - { - sourceDescriptor: this._descriptor, - ...options, - }, - mapColors - ); - } - _getTooltipPropertyNames() { return this._tooltipFields.map(field => field.getName()); } - createDefaultLayer(options, mapColors) { - const layerDescriptor = this._createDefaultLayerDescriptor(options, mapColors); - const style = new VectorStyle(layerDescriptor.style, this); - return new VectorLayer({ - layerDescriptor: layerDescriptor, - source: this, - style, - }); - } - isFilterByMapBounds() { return false; } diff --git a/x-pack/plugins/maps/public/layers/sources/wms_source/index.js b/x-pack/plugins/maps/public/layers/sources/wms_source/index.js index daae552a6f772..792e7e862826c 100644 --- a/x-pack/plugins/maps/public/layers/sources/wms_source/index.js +++ b/x-pack/plugins/maps/public/layers/sources/wms_source/index.js @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { WMSSource, wmsLayerWizardConfig } from './wms_source'; +export { wmsLayerWizardConfig } from './wms_layer_wizard'; +export { WMSSource } from './wms_source'; diff --git a/x-pack/plugins/maps/public/layers/sources/wms_source/wms_layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/wms_source/wms_layer_wizard.tsx new file mode 100644 index 0000000000000..fbf5e25c78b17 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/wms_source/wms_layer_wizard.tsx @@ -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 React from 'react'; +import { i18n } from '@kbn/i18n'; +// @ts-ignore +import { WMSCreateSourceEditor } from './wms_create_source_editor'; +// @ts-ignore +import { sourceTitle, WMSSource } from './wms_source'; +import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +import { TileLayer } from '../../tile_layer'; + +export const wmsLayerWizardConfig: LayerWizard = { + description: i18n.translate('xpack.maps.source.wmsDescription', { + defaultMessage: 'Maps from OGC Standard WMS', + }), + icon: 'grid', + renderWizard: ({ previewLayer }: RenderWizardArguments) => { + const onSourceConfigChange = (sourceConfig: unknown) => { + if (!sourceConfig) { + previewLayer(null); + return; + } + + const layerDescriptor = TileLayer.createDescriptor({ + sourceDescriptor: WMSSource.createDescriptor(sourceConfig), + }); + previewLayer(layerDescriptor); + }; + return ; + }, + title: sourceTitle, +}; diff --git a/x-pack/plugins/maps/public/layers/sources/wms_source/wms_source.js b/x-pack/plugins/maps/public/layers/sources/wms_source/wms_source.js index 33f764784124e..cb8f9c34e2b57 100644 --- a/x-pack/plugins/maps/public/layers/sources/wms_source/wms_source.js +++ b/x-pack/plugins/maps/public/layers/sources/wms_source/wms_source.js @@ -4,18 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; - import { AbstractTMSSource } from '../tms_source'; -import { TileLayer } from '../../tile_layer'; -import { WMSCreateSourceEditor } from './wms_create_source_editor'; import { i18n } from '@kbn/i18n'; import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters'; import { WmsClient } from './wms_client'; import { SOURCE_TYPES } from '../../../../common/constants'; import { registerSource } from '../source_registry'; -const sourceTitle = i18n.translate('xpack.maps.source.wmsTitle', { +export const sourceTitle = i18n.translate('xpack.maps.source.wmsTitle', { defaultMessage: 'Web Map Service', }); @@ -52,20 +48,6 @@ export class WMSSource extends AbstractTMSSource { ]; } - _createDefaultLayerDescriptor(options) { - return TileLayer.createDescriptor({ - sourceDescriptor: this._descriptor, - ...options, - }); - } - - createDefaultLayer(options) { - return new TileLayer({ - layerDescriptor: this._createDefaultLayerDescriptor(options), - source: this, - }); - } - async getDisplayName() { return this._descriptor.serviceUrl; } @@ -94,24 +76,3 @@ registerSource({ ConstructorFunction: WMSSource, type: SOURCE_TYPES.WMS, }); - -export const wmsLayerWizardConfig = { - description: i18n.translate('xpack.maps.source.wmsDescription', { - defaultMessage: 'Maps from OGC Standard WMS', - }), - icon: 'grid', - renderWizard: ({ onPreviewSource, inspectorAdapters }) => { - const onSourceConfigChange = sourceConfig => { - if (!sourceConfig) { - onPreviewSource(null); - return; - } - - const sourceDescriptor = WMSSource.createDescriptor(sourceConfig); - const source = new WMSSource(sourceDescriptor, inspectorAdapters); - onPreviewSource(source); - }; - return ; - }, - title: sourceTitle, -}; diff --git a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/layer_wizard.tsx index 8b1ed588c8dd1..e970c75fa7adf 100644 --- a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/layer_wizard.tsx @@ -9,17 +9,19 @@ import React from 'react'; import { XYZTMSEditor, XYZTMSSourceConfig } from './xyz_tms_editor'; import { XYZTMSSource, sourceTitle } from './xyz_tms_source'; import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +import { TileLayer } from '../../tile_layer'; export const tmsLayerWizardConfig: LayerWizard = { description: i18n.translate('xpack.maps.source.ems_xyzDescription', { defaultMessage: 'Tile map service configured in interface', }), icon: 'grid', - renderWizard: ({ onPreviewSource }: RenderWizardArguments) => { + renderWizard: ({ previewLayer }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: XYZTMSSourceConfig) => { - const sourceDescriptor = XYZTMSSource.createDescriptor(sourceConfig); - const source = new XYZTMSSource(sourceDescriptor); - onPreviewSource(source); + const layerDescriptor = TileLayer.createDescriptor({ + sourceDescriptor: XYZTMSSource.createDescriptor(sourceConfig), + }); + previewLayer(layerDescriptor); }; return ; }, diff --git a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.test.ts b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.test.ts index 4031a18bff7cb..b1ba6fc6f6f8e 100644 --- a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.test.ts +++ b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.test.ts @@ -5,8 +5,6 @@ */ import { XYZTMSSource } from './xyz_tms_source'; -import { ILayer } from '../../layer'; -import { TileLayer } from '../../tile_layer'; import { SOURCE_TYPES } from '../../../../common/constants'; import { XYZTMSSourceDescriptor } from '../../../../common/descriptor_types'; @@ -16,12 +14,6 @@ const descriptor: XYZTMSSourceDescriptor = { id: 'foobar', }; describe('xyz Tilemap Source', () => { - it('should create a tile-layer', () => { - const source = new XYZTMSSource(descriptor); - const layer: ILayer = source.createDefaultLayer(); - expect(layer instanceof TileLayer).toEqual(true); - }); - it('should echo url template for url template', async () => { const source = new XYZTMSSource(descriptor); const template = await source.getUrlTemplate(); diff --git a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.ts b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.ts index 77f8d88a8c0ab..b1003d25fb759 100644 --- a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.ts +++ b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.ts @@ -5,15 +5,13 @@ */ import { i18n } from '@kbn/i18n'; -import { TileLayer } from '../../tile_layer'; import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters'; import { SOURCE_TYPES } from '../../../../common/constants'; import { registerSource } from '../source_registry'; import { AbstractTMSSource } from '../tms_source'; -import { LayerDescriptor, XYZTMSSourceDescriptor } from '../../../../common/descriptor_types'; +import { XYZTMSSourceDescriptor } from '../../../../common/descriptor_types'; import { Attribution, ImmutableSourceProperty } from '../source'; import { XYZTMSSourceConfig } from './xyz_tms_editor'; -import { ILayer } from '../../layer'; export const sourceTitle = i18n.translate('xpack.maps.source.ems_xyzTitle', { defaultMessage: 'Tile Map Service', @@ -49,17 +47,6 @@ export class XYZTMSSource extends AbstractTMSSource { ]; } - createDefaultLayer(options?: LayerDescriptor): ILayer { - const layerDescriptor: LayerDescriptor = TileLayer.createDescriptor({ - sourceDescriptor: this._descriptor, - ...options, - }); - return new TileLayer({ - layerDescriptor, - source: this, - }); - } - async getDisplayName(): Promise { return this._descriptor.urlTemplate; } diff --git a/x-pack/plugins/maps/public/layers/tile_layer.test.ts b/x-pack/plugins/maps/public/layers/tile_layer.test.ts index a7e8be9fc4b46..d536b18af4aad 100644 --- a/x-pack/plugins/maps/public/layers/tile_layer.test.ts +++ b/x-pack/plugins/maps/public/layers/tile_layer.test.ts @@ -23,9 +23,6 @@ class MockTileSource extends AbstractTMSSource implements ITMSSource { super(descriptor, {}); this._descriptor = descriptor; } - createDefaultLayer(): ILayer { - throw new Error('not implemented'); - } async getDisplayName(): Promise { return this._descriptor.urlTemplate; diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 74bd305bff963..bdcd14ea98782 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -39,11 +39,15 @@ import { getMapsVisTypeAlias } from './maps_vis_type_alias'; import { registerLayerWizards } from './layers/load_layer_wizards'; import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import { VisualizationsSetup } from '../../../../src/plugins/visualizations/public'; +import { MAP_SAVED_OBJECT_TYPE } from '../common/constants'; +import { MapEmbeddableFactory } from './embeddable'; +import { EmbeddableSetup } from '../../../../src/plugins/embeddable/public'; export interface MapsPluginSetupDependencies { inspector: InspectorSetupContract; home: HomePublicPluginSetup; visualizations: VisualizationsSetup; + embeddable: EmbeddableSetup; } // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface MapsPluginStartDependencies {} @@ -102,12 +106,13 @@ export class MapsPlugin MapsPluginStartDependencies > { public setup(core: CoreSetup, plugins: MapsPluginSetupDependencies) { - const { inspector, home, visualizations } = plugins; + const { inspector, home, visualizations, embeddable } = plugins; bindSetupCoreAndPlugins(core, plugins); inspector.registerView(MapView); home.featureCatalogue.register(featureCatalogueEntry); visualizations.registerAlias(getMapsVisTypeAlias()); + embeddable.registerEmbeddableFactory(MAP_SAVED_OBJECT_TYPE, new MapEmbeddableFactory()); } public start(core: CoreStart, plugins: any) { diff --git a/x-pack/plugins/ml/common/license/index.ts b/x-pack/plugins/ml/common/license/index.ts index e901a9545897b..e87986a26a3bd 100644 --- a/x-pack/plugins/ml/common/license/index.ts +++ b/x-pack/plugins/ml/common/license/index.ts @@ -4,4 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -export { MlLicense, LicenseStatus, MINIMUM_FULL_LICENSE, MINIMUM_LICENSE } from './ml_license'; +export { + MlLicense, + LicenseStatus, + MINIMUM_FULL_LICENSE, + MINIMUM_LICENSE, + isFullLicense, + isMinimumLicense, +} from './ml_license'; diff --git a/x-pack/plugins/ml/common/license/ml_license.ts b/x-pack/plugins/ml/common/license/ml_license.ts index 2a60887310447..25b5b4992b227 100644 --- a/x-pack/plugins/ml/common/license/ml_license.ts +++ b/x-pack/plugins/ml/common/license/ml_license.ts @@ -38,8 +38,8 @@ export class MlLicense { this._isSecurityEnabled = securityIsEnabled; this._hasLicenseExpired = this._license.status === 'expired'; this._isMlEnabled = this._license.getFeature(PLUGIN_ID).isEnabled; - this._isMinimumLicense = this._license.check(PLUGIN_ID, MINIMUM_LICENSE).state === 'valid'; - this._isFullLicense = this._license.check(PLUGIN_ID, MINIMUM_FULL_LICENSE).state === 'valid'; + this._isMinimumLicense = isMinimumLicense(this._license); + this._isFullLicense = isFullLicense(this._license); if (this._initialized === false && postInitFunctions !== undefined) { postInitFunctions.forEach(f => f(this)); @@ -74,3 +74,11 @@ export class MlLicense { return this._isFullLicense; } } + +export function isFullLicense(license: ILicense) { + return license.check(PLUGIN_ID, MINIMUM_FULL_LICENSE).state === 'valid'; +} + +export function isMinimumLicense(license: ILicense) { + return license.check(PLUGIN_ID, MINIMUM_LICENSE).state === 'valid'; +} diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts new file mode 100644 index 0000000000000..2a449c95faa5b --- /dev/null +++ b/x-pack/plugins/ml/common/types/capabilities.ts @@ -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 { KibanaRequest } from 'kibana/server'; + +export const userMlCapabilities = { + // Anomaly Detection + canGetJobs: false, + canGetDatafeeds: false, + // Calendars + canGetCalendars: false, + // File Data Visualizer + canFindFileStructure: false, + // Filters + canGetFilters: false, + // Data Frame Analytics + canGetDataFrameAnalytics: false, +}; + +export const adminMlCapabilities = { + // Anomaly Detection + canCreateJob: false, + canDeleteJob: false, + canOpenJob: false, + canCloseJob: false, + canForecastJob: false, + canStartStopDatafeed: false, + canUpdateJob: false, + canUpdateDatafeed: false, + canPreviewDatafeed: false, + // Calendars + canCreateCalendar: false, + canDeleteCalendar: false, + // Filters + canCreateFilter: false, + canDeleteFilter: false, + // Data Frame Analytics + canDeleteDataFrameAnalytics: false, + canCreateDataFrameAnalytics: false, + canStartStopDataFrameAnalytics: false, +}; + +export type UserMlCapabilities = typeof userMlCapabilities; +export type AdminMlCapabilities = typeof adminMlCapabilities; +export type MlCapabilities = UserMlCapabilities & AdminMlCapabilities; + +export const basicLicenseMlCapabilities = ['canFindFileStructure'] as Array; + +export function getDefaultCapabilities(): MlCapabilities { + return { + ...userMlCapabilities, + ...adminMlCapabilities, + }; +} + +export interface MlCapabilitiesResponse { + capabilities: MlCapabilities; + upgradeInProgress: boolean; + isPlatinumOrTrialLicense: boolean; + mlFeatureEnabledInSpace: boolean; +} + +export type ResolveMlCapabilities = (request: KibanaRequest) => Promise; diff --git a/x-pack/plugins/ml/common/types/privileges.ts b/x-pack/plugins/ml/common/types/privileges.ts deleted file mode 100644 index d9089c751b81b..0000000000000 --- a/x-pack/plugins/ml/common/types/privileges.ts +++ /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. - */ -// - -export interface Privileges { - // Anomaly Detection - canGetJobs: boolean; - canCreateJob: boolean; - canDeleteJob: boolean; - canOpenJob: boolean; - canCloseJob: boolean; - canForecastJob: boolean; - canGetDatafeeds: boolean; - canStartStopDatafeed: boolean; - canUpdateJob: boolean; - canUpdateDatafeed: boolean; - canPreviewDatafeed: boolean; - // Calendars - canGetCalendars: boolean; - canCreateCalendar: boolean; - canDeleteCalendar: boolean; - // Filters - canGetFilters: boolean; - canCreateFilter: boolean; - canDeleteFilter: boolean; - // File Data Visualizer - canFindFileStructure: boolean; - // Data Frame Analytics - canGetDataFrameAnalytics: boolean; - canDeleteDataFrameAnalytics: boolean; - canCreateDataFrameAnalytics: boolean; - canStartStopDataFrameAnalytics: boolean; -} - -export function getDefaultPrivileges(): Privileges { - return { - // Anomaly Detection - canGetJobs: false, - canCreateJob: false, - canDeleteJob: false, - canOpenJob: false, - canCloseJob: false, - canForecastJob: false, - canGetDatafeeds: false, - canStartStopDatafeed: false, - canUpdateJob: false, - canUpdateDatafeed: false, - canPreviewDatafeed: false, - // Calendars - canGetCalendars: false, - canCreateCalendar: false, - canDeleteCalendar: false, - // Filters - canGetFilters: false, - canCreateFilter: false, - canDeleteFilter: false, - // File Data Visualizer - canFindFileStructure: false, - // Data Frame Analytics - canGetDataFrameAnalytics: false, - canDeleteDataFrameAnalytics: false, - canCreateDataFrameAnalytics: false, - canStartStopDataFrameAnalytics: false, - }; -} - -export interface PrivilegesResponse { - capabilities: Privileges; - upgradeInProgress: boolean; - isPlatinumOrTrialLicense: boolean; - mlFeatureEnabledInSpace: boolean; -} diff --git a/x-pack/plugins/ml/public/application/privilege/check_privilege.ts b/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts similarity index 70% rename from x-pack/plugins/ml/public/application/privilege/check_privilege.ts rename to x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts index 4de8c6eb703ff..1ca176d8d09ce 100644 --- a/x-pack/plugins/ml/public/application/privilege/check_privilege.ts +++ b/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts @@ -8,19 +8,19 @@ import { i18n } from '@kbn/i18n'; import { hasLicenseExpired } from '../license'; -import { Privileges, getDefaultPrivileges } from '../../../common/types/privileges'; -import { getPrivileges, getManageMlPrivileges } from './get_privileges'; +import { MlCapabilities, getDefaultCapabilities } from '../../../common/types/capabilities'; +import { getCapabilities, getManageMlCapabilities } from './get_capabilities'; import { ACCESS_DENIED_PATH } from '../management/management_urls'; -let privileges: Privileges = getDefaultPrivileges(); -// manage_ml requires all monitor and admin cluster privileges: https://github.com/elastic/elasticsearch/blob/664a29c8905d8ce9ba8c18aa1ed5c5de93a0eabc/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilege.java#L53 -export function checkGetManagementMlJobs() { +let _capabilities: MlCapabilities = getDefaultCapabilities(); + +export function checkGetManagementMlJobsResolver() { return new Promise<{ mlFeatureEnabledInSpace: boolean }>((resolve, reject) => { - getManageMlPrivileges().then( + getManageMlCapabilities().then( ({ capabilities, isPlatinumOrTrialLicense, mlFeatureEnabledInSpace }) => { - privileges = capabilities; - // Loop through all privileges to ensure they are all set to true. - const isManageML = Object.values(privileges).every(p => p === true); + _capabilities = capabilities; + // Loop through all capabilities to ensure they are all set to true. + const isManageML = Object.values(_capabilities).every(p => p === true); if (isManageML === true && isPlatinumOrTrialLicense === true) { return resolve({ mlFeatureEnabledInSpace }); @@ -33,17 +33,17 @@ export function checkGetManagementMlJobs() { }); } -export function checkGetJobsPrivilege(): Promise { +export function checkGetJobsCapabilitiesResolver(): Promise { return new Promise((resolve, reject) => { - getPrivileges().then(({ capabilities, isPlatinumOrTrialLicense }) => { - privileges = capabilities; + getCapabilities().then(({ capabilities, isPlatinumOrTrialLicense }) => { + _capabilities = capabilities; // the minimum privilege for using ML with a platinum or trial license is being able to get the transforms list. - // all other functionality is controlled by the return privileges object. + // all other functionality is controlled by the return capabilities object. // if the license is basic (isPlatinumOrTrialLicense === false) then do not redirect, // allow the promise to resolve as the separate license check will redirect then user to // a basic feature - if (privileges.canGetJobs || isPlatinumOrTrialLicense === false) { - return resolve(privileges); + if (_capabilities.canGetJobs || isPlatinumOrTrialLicense === false) { + return resolve(_capabilities); } else { window.location.href = '#/access-denied'; return reject(); @@ -52,15 +52,15 @@ export function checkGetJobsPrivilege(): Promise { }); } -export function checkCreateJobsPrivilege(): Promise { +export function checkCreateJobsCapabilitiesResolver(): Promise { return new Promise((resolve, reject) => { - getPrivileges().then(({ capabilities, isPlatinumOrTrialLicense }) => { - privileges = capabilities; + getCapabilities().then(({ capabilities, isPlatinumOrTrialLicense }) => { + _capabilities = capabilities; // if the license is basic (isPlatinumOrTrialLicense === false) then do not redirect, // allow the promise to resolve as the separate license check will redirect then user to // a basic feature - if (privileges.canCreateJob || isPlatinumOrTrialLicense === false) { - return resolve(privileges); + if (_capabilities.canCreateJob || isPlatinumOrTrialLicense === false) { + return resolve(_capabilities); } else { // if the user has no permission to create a job, // redirect them back to the Transforms Management page @@ -71,14 +71,14 @@ export function checkCreateJobsPrivilege(): Promise { }); } -export function checkFindFileStructurePrivilege(): Promise { +export function checkFindFileStructurePrivilegeResolver(): Promise { return new Promise((resolve, reject) => { - getPrivileges().then(({ capabilities }) => { - privileges = capabilities; + getCapabilities().then(({ capabilities }) => { + _capabilities = capabilities; // the minimum privilege for using ML with a basic license is being able to use the datavisualizer. - // all other functionality is controlled by the return privileges object - if (privileges.canFindFileStructure) { - return resolve(privileges); + // all other functionality is controlled by the return _capabilities object + if (_capabilities.canFindFileStructure) { + return resolve(_capabilities); } else { window.location.href = '#/access-denied'; return reject(); @@ -89,14 +89,14 @@ export function checkFindFileStructurePrivilege(): Promise { // check the privilege type and the license to see whether a user has permission to access a feature. // takes the name of the privilege variable as specified in get_privileges.js -export function checkPermission(privilegeType: keyof Privileges) { +export function checkPermission(capability: keyof MlCapabilities) { const licenseHasExpired = hasLicenseExpired(); - return privileges[privilegeType] === true && licenseHasExpired !== true; + return _capabilities[capability] === true && licenseHasExpired !== true; } // create the text for the button's tooltips if the user's license has // expired or if they don't have the privilege to press that button -export function createPermissionFailureMessage(privilegeType: keyof Privileges) { +export function createPermissionFailureMessage(privilegeType: keyof MlCapabilities) { let message = ''; const licenseHasExpired = hasLicenseExpired(); if (licenseHasExpired) { diff --git a/x-pack/plugins/ml/public/application/privilege/get_privileges.ts b/x-pack/plugins/ml/public/application/capabilities/get_capabilities.ts similarity index 68% rename from x-pack/plugins/ml/public/application/privilege/get_privileges.ts rename to x-pack/plugins/ml/public/application/capabilities/get_capabilities.ts index a3811779333d9..c9561ed254544 100644 --- a/x-pack/plugins/ml/public/application/privilege/get_privileges.ts +++ b/x-pack/plugins/ml/public/application/capabilities/get_capabilities.ts @@ -7,12 +7,12 @@ import { ml } from '../services/ml_api_service'; import { setUpgradeInProgress } from '../services/upgrade_service'; -import { PrivilegesResponse } from '../../../common/types/privileges'; +import { MlCapabilitiesResponse } from '../../../common/types/capabilities'; -export function getPrivileges(): Promise { +export function getCapabilities(): Promise { return new Promise((resolve, reject) => { - ml.checkMlPrivileges() - .then((resp: PrivilegesResponse) => { + ml.checkMlCapabilities() + .then((resp: MlCapabilitiesResponse) => { if (resp.upgradeInProgress === true) { setUpgradeInProgress(true); } @@ -24,10 +24,10 @@ export function getPrivileges(): Promise { }); } -export function getManageMlPrivileges(): Promise { +export function getManageMlCapabilities(): Promise { return new Promise((resolve, reject) => { - ml.checkManageMLPrivileges() - .then((resp: PrivilegesResponse) => { + ml.checkManageMLCapabilities() + .then((resp: MlCapabilitiesResponse) => { if (resp.upgradeInProgress === true) { setUpgradeInProgress(true); } diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js index b881bfe4f1fe6..7696143a9ce71 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table.test.js @@ -8,14 +8,14 @@ import React from 'react'; import mockAnomaliesTableData from '../../explorer/__mocks__/mock_anomalies_table_data.json'; import { getColumns } from './anomalies_table_columns'; -jest.mock('../../privilege/check_privilege', () => ({ +jest.mock('../../capabilities/check_capabilities', () => ({ checkPermission: () => false, })); jest.mock('../../license', () => ({ hasLicenseExpired: () => false, })); -jest.mock('../../privilege/get_privileges', () => ({ - getPrivileges: () => {}, +jest.mock('../../capabilities/get_capabilities', () => ({ + getCapabilities: () => {}, })); jest.mock('../../services/field_format_service', () => ({ getFieldFormat: () => {}, diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js index 0c6c959927140..8f79ce4a6c08a 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js @@ -23,7 +23,7 @@ import { DetectorCell } from './detector_cell'; import { EntityCell } from '../entity_cell'; import { InfluencersCell } from './influencers_cell'; import { LinksMenu } from './links_menu'; -import { checkPermission } from '../../privilege/check_privilege'; +import { checkPermission } from '../../capabilities/check_capabilities'; import { mlFieldFormatService } from '../../services/field_format_service'; import { isRuleSupported } from '../../../../common/util/anomaly_utils'; import { formatValue } from '../../formatters/format_value'; diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js index 2a34f12330a75..5d2b8e35fbc0f 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js @@ -17,7 +17,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; -import { checkPermission } from '../../privilege/check_privilege'; +import { checkPermission } from '../../capabilities/check_capabilities'; import { SEARCH_QUERY_LANGUAGE } from '../../../../common/constants/search'; import { isRuleSupported } from '../../../../common/util/anomaly_utils'; import { parseInterval } from '../../../../common/util/parse_interval'; diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/common.test.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.test.ts similarity index 52% rename from x-pack/plugins/transform/public/app/components/pivot_preview/common.test.ts rename to x-pack/plugins/ml/public/application/components/data_grid/common.test.ts index 172256ddb5cee..4bb670ad02dfc 100644 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/common.test.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.test.ts @@ -6,16 +6,7 @@ import { EuiDataGridSorting } from '@elastic/eui'; -import { - getPreviewRequestBody, - PivotAggsConfig, - PivotGroupByConfig, - PIVOT_SUPPORTED_AGGS, - PIVOT_SUPPORTED_GROUP_BY_AGGS, - SimpleQuery, -} from '../../common'; - -import { multiColumnSortFactory, getPivotPreviewDevConsoleStatement } from './common'; +import { multiColumnSortFactory } from './common'; describe('Transform: Define Pivot Common', () => { test('multiColumnSortFactory()', () => { @@ -65,53 +56,4 @@ describe('Transform: Define Pivot Common', () => { { s: 'a', n: 1 }, ]); }); - - test('getPivotPreviewDevConsoleStatement()', () => { - const query: SimpleQuery = { - query_string: { - query: '*', - default_operator: 'AND', - }, - }; - const groupBy: PivotGroupByConfig = { - agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, - field: 'the-group-by-field', - aggName: 'the-group-by-agg-name', - dropDownName: 'the-group-by-drop-down-name', - }; - const agg: PivotAggsConfig = { - agg: PIVOT_SUPPORTED_AGGS.AVG, - field: 'the-agg-field', - aggName: 'the-agg-agg-name', - dropDownName: 'the-agg-drop-down-name', - }; - const request = getPreviewRequestBody('the-index-pattern-title', query, [groupBy], [agg]); - const pivotPreviewDevConsoleStatement = getPivotPreviewDevConsoleStatement(request); - - expect(pivotPreviewDevConsoleStatement).toBe(`POST _transform/_preview -{ - "source": { - "index": [ - "the-index-pattern-title" - ] - }, - "pivot": { - "group_by": { - "the-group-by-agg-name": { - "terms": { - "field": "the-group-by-field" - } - } - }, - "aggregations": { - "the-agg-agg-name": { - "avg": { - "field": "the-agg-field" - } - } - } - } -} -`); - }); }); diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts new file mode 100644 index 0000000000000..d141b68b5d03f --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -0,0 +1,287 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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-timezone'; +import { useEffect, useMemo } from 'react'; + +import { + EuiDataGridCellValueElementProps, + EuiDataGridSorting, + EuiDataGridStyle, +} from '@elastic/eui'; + +import { + IndexPattern, + IFieldType, + ES_FIELD_TYPES, + KBN_FIELD_TYPES, +} from '../../../../../../../src/plugins/data/public'; + +import { + BASIC_NUMERICAL_TYPES, + EXTENDED_NUMERICAL_TYPES, +} from '../../data_frame_analytics/common/fields'; + +import { + FEATURE_IMPORTANCE, + FEATURE_INFLUENCE, + OUTLIER_SCORE, +} from '../../data_frame_analytics/common/constants'; +import { formatHumanReadableDateTimeSeconds } from '../../util/date_utils'; +import { getNestedProperty } from '../../util/object_utils'; +import { mlFieldFormatService } from '../../services/field_format_service'; + +import { DataGridItem, IndexPagination, RenderCellValue } from './types'; + +export const INIT_MAX_COLUMNS = 20; + +export const euiDataGridStyle: EuiDataGridStyle = { + border: 'all', + fontSize: 's', + cellPadding: 's', + stripes: false, + rowHover: 'none', + header: 'shade', +}; + +export const euiDataGridToolbarSettings = { + showColumnSelector: true, + showStyleSelector: false, + showSortSelector: true, + showFullScreenSelector: false, +}; + +export const getFieldsFromKibanaIndexPattern = (indexPattern: IndexPattern): string[] => { + const allFields = indexPattern.fields.map(f => f.name); + const indexPatternFields: string[] = allFields.filter(f => { + if (indexPattern.metaFields.includes(f)) { + return false; + } + + const fieldParts = f.split('.'); + const lastPart = fieldParts.pop(); + if (lastPart === 'keyword' && allFields.includes(fieldParts.join('.'))) { + return false; + } + + return true; + }); + + return indexPatternFields; +}; + +export interface FieldTypes { + [key: string]: ES_FIELD_TYPES; +} + +export const getDataGridSchemasFromFieldTypes = (fieldTypes: FieldTypes, resultsField: string) => { + return Object.keys(fieldTypes).map(field => { + // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] + // To fall back to the default string schema it needs to be undefined. + let schema; + const isSortable = true; + const type = fieldTypes[field]; + + const isNumber = + type !== undefined && (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type)); + if (isNumber) { + schema = 'numeric'; + } + + switch (type) { + case 'date': + schema = 'datetime'; + break; + case 'geo_point': + schema = 'json'; + break; + case 'boolean': + schema = 'boolean'; + break; + } + + if ( + field === `${resultsField}.${OUTLIER_SCORE}` || + field.includes(`${resultsField}.${FEATURE_INFLUENCE}`) + ) { + schema = 'numeric'; + } + + if (field.includes(`${resultsField}.${FEATURE_IMPORTANCE}`)) { + schema = 'json'; + } + + return { id: field, schema, isSortable }; + }); +}; + +export const getDataGridSchemaFromKibanaFieldType = (field: IFieldType | undefined) => { + // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] + // To fall back to the default string schema it needs to be undefined. + let schema; + + switch (field?.type) { + case KBN_FIELD_TYPES.BOOLEAN: + schema = 'boolean'; + break; + case KBN_FIELD_TYPES.DATE: + schema = 'datetime'; + break; + case KBN_FIELD_TYPES.GEO_POINT: + case KBN_FIELD_TYPES.GEO_SHAPE: + schema = 'json'; + break; + case KBN_FIELD_TYPES.NUMBER: + schema = 'numeric'; + break; + } + + return schema; +}; + +export const useRenderCellValue = ( + indexPattern: IndexPattern | undefined, + pagination: IndexPagination, + tableItems: DataGridItem[], + resultsField?: string, + cellPropsCallback?: ( + columnId: string, + cellValue: any, + fullItem: Record, + setCellProps: EuiDataGridCellValueElementProps['setCellProps'] + ) => void +): RenderCellValue => { + const renderCellValue: RenderCellValue = useMemo(() => { + return ({ + rowIndex, + columnId, + setCellProps, + }: { + rowIndex: number; + columnId: string; + setCellProps: EuiDataGridCellValueElementProps['setCellProps']; + }) => { + const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; + + const fullItem = tableItems[adjustedRowIndex]; + + if (fullItem === undefined) { + return null; + } + + if (indexPattern === undefined) { + return null; + } + + let format: any; + + if (indexPattern !== undefined) { + format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, columnId, ''); + } + + function getCellValue(cId: string) { + if (cId.includes(`.${FEATURE_INFLUENCE}.`) && resultsField !== undefined) { + const results = getNestedProperty(tableItems[adjustedRowIndex], resultsField, null); + return results[cId.replace(`${resultsField}.`, '')]; + } + + return tableItems.hasOwnProperty(adjustedRowIndex) + ? getNestedProperty(tableItems[adjustedRowIndex], cId, null) + : null; + } + + const cellValue = getCellValue(columnId); + + // React by default doesn't all us to use a hook in a callback. + // However, this one will be passed on to EuiDataGrid and its docs + // recommend wrapping `setCellProps` in a `useEffect()` hook + // so we're ignoring the linting rule here. + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + if (typeof cellPropsCallback === 'function') { + cellPropsCallback(columnId, cellValue, fullItem, setCellProps); + } + }, [columnId, cellValue]); + + if (typeof cellValue === 'object' && cellValue !== null) { + return JSON.stringify(cellValue); + } + + if (cellValue === undefined || cellValue === null) { + return null; + } + + if (format !== undefined) { + return format.convert(cellValue, 'text'); + } + + if (typeof cellValue === 'string' || cellValue === null) { + return cellValue; + } + + const field = indexPattern.fields.getByName(columnId); + if (field?.type === KBN_FIELD_TYPES.DATE) { + return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); + } + + if (typeof cellValue === 'boolean') { + return cellValue ? 'true' : 'false'; + } + + if (typeof cellValue === 'object' && cellValue !== null) { + return JSON.stringify(cellValue); + } + + return cellValue; + }; + }, [indexPattern?.fields, pagination.pageIndex, pagination.pageSize, tableItems]); + return renderCellValue; +}; + +/** + * Helper to sort an array of objects based on an EuiDataGrid sorting configuration. + * `sortFn()` is recursive to support sorting on multiple columns. + * + * @param sortingColumns - The EUI data grid sorting configuration + * @returns The sorting function which can be used with an array's sort() function. + */ +export const multiColumnSortFactory = (sortingColumns: EuiDataGridSorting['columns']) => { + const isString = (arg: any): arg is string => { + return typeof arg === 'string'; + }; + + const sortFn = (a: any, b: any, sortingColumnIndex = 0): number => { + const sort = sortingColumns[sortingColumnIndex]; + const aValue = getNestedProperty(a, sort.id, null); + const bValue = getNestedProperty(b, sort.id, null); + + if (typeof aValue === 'number' && typeof bValue === 'number') { + if (aValue < bValue) { + return sort.direction === 'asc' ? -1 : 1; + } + if (aValue > bValue) { + return sort.direction === 'asc' ? 1 : -1; + } + } + + if (isString(aValue) && isString(bValue)) { + if (aValue.localeCompare(bValue) === -1) { + return sort.direction === 'asc' ? -1 : 1; + } + if (aValue.localeCompare(bValue) === 1) { + return sort.direction === 'asc' ? 1 : -1; + } + } + + if (sortingColumnIndex + 1 < sortingColumns.length) { + return sortFn(a, b, sortingColumnIndex + 1); + } + + return 0; + }; + + return sortFn; +}; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx new file mode 100644 index 0000000000000..a5b301902cc75 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx @@ -0,0 +1,181 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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, FC } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { + EuiButtonIcon, + EuiCallOut, + EuiCodeBlock, + EuiCopy, + EuiDataGrid, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; + +import { CoreSetup } from 'src/core/public'; + +import { INDEX_STATUS } from '../../data_frame_analytics/common'; + +import { euiDataGridStyle, euiDataGridToolbarSettings } from './common'; +import { UseIndexDataReturnType } from './types'; + +export const DataGridTitle: FC<{ title: string }> = ({ title }) => ( + + {title} + +); + +interface PropsWithoutHeader extends UseIndexDataReturnType { + dataTestSubj: string; + toastNotifications: CoreSetup['notifications']['toasts']; +} + +interface PropsWithHeader extends PropsWithoutHeader { + copyToClipboard: string; + copyToClipboardDescription: string; + title: string; +} + +function isWithHeader(arg: any): arg is PropsWithHeader { + return typeof arg?.title === 'string' && arg?.title !== ''; +} + +type Props = PropsWithHeader | PropsWithoutHeader; + +export const DataGrid: FC = props => { + const { + columns, + dataTestSubj, + errorMessage, + invalidSortingColumnns, + noDataMessage, + onChangeItemsPerPage, + onChangePage, + onSort, + pagination, + setVisibleColumns, + renderCellValue, + rowCount, + sortingColumns, + status, + tableItems: data, + toastNotifications, + visibleColumns, + } = props; + + useEffect(() => { + if (invalidSortingColumnns.length > 0) { + invalidSortingColumnns.forEach(columnId => { + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataGrid.invalidSortingColumnError', { + defaultMessage: `The column '{columnId}' cannot be used for sorting.`, + values: { columnId }, + }) + ); + }); + } + }, [invalidSortingColumnns, toastNotifications]); + + if (status === INDEX_STATUS.LOADED && data.length === 0) { + return ( +
+ {isWithHeader(props) && } + +

+ {i18n.translate('xpack.ml.dataGrid.IndexNoDataCalloutBody', { + defaultMessage: + 'The query for the index returned no results. Please make sure you have sufficient permissions, the index contains documents and your query is not too restrictive.', + })} +

+
+
+ ); + } + + if (noDataMessage !== '') { + return ( +
+ {isWithHeader(props) && } + +

{noDataMessage}

+
+
+ ); + } + + return ( +
+ {isWithHeader(props) && ( + + + + + + + {(copy: () => void) => ( + + )} + + + + )} + {status === INDEX_STATUS.ERROR && ( +
+ + + {errorMessage} + + + +
+ )} + +
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/index.ts b/x-pack/plugins/ml/public/application/components/data_grid/index.ts new file mode 100644 index 0000000000000..2472878d1b0c1 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { + getDataGridSchemasFromFieldTypes, + getDataGridSchemaFromKibanaFieldType, + getFieldsFromKibanaIndexPattern, + multiColumnSortFactory, + useRenderCellValue, +} from './common'; +export { useDataGrid } from './use_data_grid'; +export { DataGrid } from './data_grid'; +export { + DataGridItem, + EsSorting, + RenderCellValue, + SearchResponse7, + UseDataGridReturnType, + UseIndexDataReturnType, +} from './types'; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/types.ts b/x-pack/plugins/ml/public/application/components/data_grid/types.ts new file mode 100644 index 0000000000000..5fa038edf7815 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/types.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 { Dispatch, SetStateAction } from 'react'; +import { SearchResponse } from 'elasticsearch'; + +import { EuiDataGridPaginationProps, EuiDataGridSorting, EuiDataGridColumn } from '@elastic/eui'; + +import { Dictionary } from '../../../../common/types/common'; + +import { INDEX_STATUS } from '../../data_frame_analytics/common/analytics'; + +export type ColumnId = string; +export type DataGridItem = Record; + +export type IndexPagination = Pick; + +export type OnChangeItemsPerPage = (pageSize: any) => void; +export type OnChangePage = (pageIndex: any) => void; +export type OnSort = ( + sc: Array<{ + id: string; + direction: 'asc' | 'desc'; + }> +) => void; + +export type RenderCellValue = ({ + rowIndex, + columnId, + setCellProps, +}: { + rowIndex: number; + columnId: string; + setCellProps: any; +}) => any; + +export type EsSorting = Dictionary<{ + order: 'asc' | 'desc'; +}>; + +// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. +export interface SearchResponse7 extends SearchResponse { + hits: SearchResponse['hits'] & { + total: { + value: number; + relation: string; + }; + }; +} + +export interface UseIndexDataReturnType + extends Pick< + UseDataGridReturnType, + | 'errorMessage' + | 'invalidSortingColumnns' + | 'noDataMessage' + | 'onChangeItemsPerPage' + | 'onChangePage' + | 'onSort' + | 'pagination' + | 'setPagination' + | 'setVisibleColumns' + | 'rowCount' + | 'sortingColumns' + | 'status' + | 'tableItems' + | 'visibleColumns' + > { + columns: EuiDataGridColumn[]; + renderCellValue: RenderCellValue; +} + +export interface UseDataGridReturnType { + errorMessage: string; + invalidSortingColumnns: ColumnId[]; + noDataMessage: string; + onChangeItemsPerPage: OnChangeItemsPerPage; + onChangePage: OnChangePage; + onSort: OnSort; + pagination: IndexPagination; + resetPagination: () => void; + rowCount: number; + setErrorMessage: Dispatch>; + setNoDataMessage: Dispatch>; + setPagination: Dispatch>; + setRowCount: Dispatch>; + setSortingColumns: Dispatch>; + setStatus: Dispatch>; + setTableItems: Dispatch>; + setVisibleColumns: Dispatch>; + sortingColumns: EuiDataGridSorting['columns']; + status: INDEX_STATUS; + tableItems: DataGridItem[]; + visibleColumns: ColumnId[]; +} diff --git a/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts new file mode 100644 index 0000000000000..c7c4f46031b6e --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/use_data_grid.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCallback, useEffect, useState } from 'react'; + +import { EuiDataGridSorting, EuiDataGridColumn } from '@elastic/eui'; + +import { INDEX_STATUS } from '../../data_frame_analytics/common'; + +import { INIT_MAX_COLUMNS } from './common'; +import { + ColumnId, + DataGridItem, + IndexPagination, + OnChangeItemsPerPage, + OnChangePage, + OnSort, + UseDataGridReturnType, +} from './types'; + +export const useDataGrid = ( + columns: EuiDataGridColumn[], + defaultPageSize = 5, + defaultVisibleColumnsCount = INIT_MAX_COLUMNS, + defaultVisibleColumnsFilter?: (id: string) => boolean +): UseDataGridReturnType => { + const defaultPagination: IndexPagination = { pageIndex: 0, pageSize: defaultPageSize }; + + const [noDataMessage, setNoDataMessage] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); + const [status, setStatus] = useState(INDEX_STATUS.UNUSED); + const [rowCount, setRowCount] = useState(0); + const [tableItems, setTableItems] = useState([]); + const [pagination, setPagination] = useState(defaultPagination); + const [sortingColumns, setSortingColumns] = useState([]); + + const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback(pageSize => { + setPagination(p => { + const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); + return { pageIndex, pageSize }; + }); + }, []); + + const onChangePage: OnChangePage = useCallback( + pageIndex => setPagination(p => ({ ...p, pageIndex })), + [] + ); + + const resetPagination = () => setPagination(defaultPagination); + + // Column visibility + const [visibleColumns, setVisibleColumns] = useState([]); + + const columnIds = columns.map(c => c.id); + const filteredColumnIds = + defaultVisibleColumnsFilter !== undefined + ? columnIds.filter(defaultVisibleColumnsFilter) + : columnIds; + const defaultVisibleColumns = filteredColumnIds.splice(0, defaultVisibleColumnsCount); + + useEffect(() => { + setVisibleColumns(defaultVisibleColumns); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [defaultVisibleColumns.join()]); + + const [invalidSortingColumnns, setInvalidSortingColumnns] = useState([]); + + const onSort: OnSort = useCallback( + sc => { + // Check if an unsupported column type for sorting was selected. + const updatedInvalidSortingColumnns = sc.reduce((arr, current) => { + const columnType = columns.find(dgc => dgc.id === current.id); + if (columnType?.schema === 'json') { + arr.push(current.id); + } + return arr; + }, []); + setInvalidSortingColumnns(updatedInvalidSortingColumnns); + if (updatedInvalidSortingColumnns.length === 0) { + setSortingColumns(sc); + } + }, + [columns] + ); + + return { + errorMessage, + invalidSortingColumnns, + noDataMessage, + onChangeItemsPerPage, + onChangePage, + onSort, + pagination, + resetPagination, + rowCount, + setErrorMessage, + setNoDataMessage, + setPagination, + setRowCount, + setSortingColumns, + setStatus, + setTableItems, + setVisibleColumns, + sortingColumns, + status, + tableItems, + visibleColumns, + }; +}; diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js b/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js index f8868ec099985..f259a1f1ffb02 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js @@ -30,7 +30,7 @@ import { import { DetectorDescriptionList } from './components/detector_description_list'; import { ActionsSection } from './actions_section'; -import { checkPermission } from '../../privilege/check_privilege'; +import { checkPermission } from '../../capabilities/check_capabilities'; import { ConditionsSection } from './conditions_section'; import { ScopeSection } from './scope_section'; import { SelectRuleAction } from './select_rule_action'; diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js b/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js index 5c43c558a3333..3e8a17eeb8617 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.test.js @@ -45,7 +45,7 @@ jest.mock('../../services/job_service', () => ({ }, })); jest.mock('../../services/ml_api_service', () => 'ml'); -jest.mock('../../privilege/check_privilege', () => ({ +jest.mock('../../capabilities/check_capabilities', () => ({ checkPermission: () => true, })); diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js b/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js index 8dca6db96d45b..48e0da72f067c 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js @@ -14,7 +14,7 @@ import React from 'react'; import { EuiCallOut, EuiCheckbox, EuiLink, EuiSpacer, EuiTitle } from '@elastic/eui'; import { ScopeExpression } from './scope_expression'; -import { checkPermission } from '../../privilege/check_privilege'; +import { checkPermission } from '../../capabilities/check_capabilities'; import { getScopeFieldDefaults } from './utils'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.test.js b/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.test.js index 3d7861fc7de0a..189fb6a35d3e6 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.test.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.test.js @@ -11,7 +11,7 @@ jest.mock('../../services/job_service.js', () => 'mlJobService'); // The mock is hoisted to the top, so need to prefix the mock function // with 'mock' so it can be used lazily. const mockCheckPermission = jest.fn(() => true); -jest.mock('../../privilege/check_privilege', () => ({ +jest.mock('../../capabilities/check_capabilities', () => ({ checkPermission: privilege => mockCheckPermission(privilege), })); diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts index 475e44af3669c..c65d872212ad6 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/kibana_context.ts @@ -15,7 +15,7 @@ import { LicenseManagementUIPluginSetup } from '../../../../../license_managemen interface StartPlugins { data: DataPublicPluginStart; - security: SecurityPluginSetup; + security?: SecurityPluginSetup; licenseManagement?: LicenseManagementUIPluginSetup; } export type StartServices = CoreStart & StartPlugins; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/text.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts similarity index 54% rename from x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/text.ts rename to x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts index f9faf2ad2e3ca..51b2918012c8d 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/text.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts @@ -4,10 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TagFactory } from '../../../public/lib/tag'; -import { TagStrings as strings } from '../../../i18n'; - -export const text: TagFactory = () => ({ - name: strings.text(), - color: '#D3DAE6', -}); +export const DEFAULT_RESULTS_FIELD = 'ml'; +export const FEATURE_IMPORTANCE = 'feature_importance'; +export const FEATURE_INFLUENCE = 'feature_influence'; +export const OUTLIER_SCORE = 'outlier_score'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/data_grid.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/data_grid.ts deleted file mode 100644 index 2b6d733837562..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/data_grid.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. - */ - -import { EuiDataGridStyle } from '@elastic/eui'; - -export const euiDataGridStyle: EuiDataGridStyle = { - border: 'all', - fontSize: 's', - cellPadding: 's', - stripes: false, - rowHover: 'none', - header: 'shade', -}; - -export const euiDataGridToolbarSettings = { - showColumnSelector: true, - showStyleSelector: false, - showSortSelector: true, - showFullScreenSelector: false, -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts index f165669bdd674..8423bc1b94a09 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts @@ -4,18 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getNestedProperty } from '../../util/object_utils'; import { - DataFrameAnalyticsConfig, getNumTopFeatureImportanceValues, getPredictedFieldName, getDependentVar, getPredictionFieldName, + isClassificationAnalysis, + isOutlierAnalysis, + isRegressionAnalysis, + DataFrameAnalyticsConfig, } from './analytics'; import { Field } from '../../../../common/types/fields'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; import { newJobCapsService } from '../../services/new_job_capabilities_service'; +import { FEATURE_IMPORTANCE, FEATURE_INFLUENCE, OUTLIER_SCORE } from './constants'; + export type EsId = string; export type EsDocSource = Record; export type EsFieldName = string; @@ -42,7 +46,7 @@ export const EXTENDED_NUMERICAL_TYPES = new Set([ ES_FIELD_TYPES.SCALED_FLOAT, ]); -const ML__ID_COPY = 'ml__id_copy'; +export const ML__ID_COPY = 'ml__id_copy'; export const isKeywordAndTextType = (fieldName: string): boolean => { const { fields } = newJobCapsService; @@ -64,32 +68,61 @@ export const isKeywordAndTextType = (fieldName: string): boolean => { }; // Used to sort columns: +// - Anchor on the left ml.outlier_score, ml.is_training, , // - string based columns are moved to the left -// - followed by the outlier_score column -// - feature_influence fields get moved next to the corresponding field column +// - feature_influence/feature_importance fields get moved next to the corresponding field column // - overall fields get sorted alphabetically -export const sortColumns = (obj: EsDocSource, resultsField: string) => (a: string, b: string) => { - const typeofA = typeof obj[a]; - const typeofB = typeof obj[b]; +export const sortExplorationResultsFields = ( + a: string, + b: string, + jobConfig: DataFrameAnalyticsConfig +) => { + const resultsField = jobConfig.dest.results_field; - if (typeofA !== 'string' && typeofB === 'string') { - return 1; - } - if (typeofA === 'string' && typeofB !== 'string') { - return -1; - } - if (typeofA === 'string' && typeofB === 'string') { - return a.localeCompare(b); - } + if (isOutlierAnalysis(jobConfig.analysis)) { + if (a === `${resultsField}.${OUTLIER_SCORE}`) { + return -1; + } - if (a === `${resultsField}.outlier_score`) { - return -1; + if (b === `${resultsField}.${OUTLIER_SCORE}`) { + return 1; + } } - if (b === `${resultsField}.outlier_score`) { - return 1; + if (isClassificationAnalysis(jobConfig.analysis) || isRegressionAnalysis(jobConfig.analysis)) { + const dependentVariable = getDependentVar(jobConfig.analysis); + const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis, true); + + if (a === `${resultsField}.is_training`) { + return -1; + } + if (b === `${resultsField}.is_training`) { + return 1; + } + if (a === predictedField) { + return -1; + } + if (b === predictedField) { + return 1; + } + if (a === dependentVariable || a === dependentVariable.replace(/\.keyword$/, '')) { + return -1; + } + if (b === dependentVariable || b === dependentVariable.replace(/\.keyword$/, '')) { + return 1; + } + + if (a === `${resultsField}.prediction_probability`) { + return -1; + } + if (b === `${resultsField}.prediction_probability`) { + return 1; + } } + const typeofA = typeof a; + const typeofB = typeof b; + const tokensA = a.split('.'); const prefixA = tokensA[0]; const tokensB = b.split('.'); @@ -109,91 +142,6 @@ export const sortColumns = (obj: EsDocSource, resultsField: string) => (a: strin return a.localeCompare(tokensB.join('.')); } - return a.localeCompare(b); -}; - -export const sortRegressionResultsFields = ( - a: string, - b: string, - jobConfig: DataFrameAnalyticsConfig -) => { - const dependentVariable = getDependentVar(jobConfig.analysis); - const resultsField = jobConfig.dest.results_field; - const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis, true); - if (a === `${resultsField}.is_training`) { - return -1; - } - if (b === `${resultsField}.is_training`) { - return 1; - } - if (a === predictedField) { - return -1; - } - if (b === predictedField) { - return 1; - } - if (a === dependentVariable || a === dependentVariable.replace(/\.keyword$/, '')) { - return -1; - } - if (b === dependentVariable || b === dependentVariable.replace(/\.keyword$/, '')) { - return 1; - } - - if (a === `${resultsField}.prediction_probability`) { - return -1; - } - if (b === `${resultsField}.prediction_probability`) { - return 1; - } - - return a.localeCompare(b); -}; - -// Used to sort columns: -// Anchor on the left ml.is_training, , -export const sortRegressionResultsColumns = ( - obj: EsDocSource, - jobConfig: DataFrameAnalyticsConfig -) => (a: string, b: string) => { - const dependentVariable = getDependentVar(jobConfig.analysis); - const resultsField = jobConfig.dest.results_field; - const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis, true); - - const typeofA = typeof obj[a]; - const typeofB = typeof obj[b]; - - if (a === `${resultsField}.is_training`) { - return -1; - } - - if (b === `${resultsField}.is_training`) { - return 1; - } - - if (a === predictedField) { - return -1; - } - - if (b === predictedField) { - return 1; - } - - if (a === dependentVariable) { - return -1; - } - - if (b === dependentVariable) { - return 1; - } - - if (a === `${resultsField}.prediction_probability`) { - return -1; - } - - if (b === `${resultsField}.prediction_probability`) { - return 1; - } - if (typeofA !== 'string' && typeofB === 'string') { return 1; } @@ -204,44 +152,9 @@ export const sortRegressionResultsColumns = ( return a.localeCompare(b); } - const tokensA = a.split('.'); - const prefixA = tokensA[0]; - const tokensB = b.split('.'); - const prefixB = tokensB[0]; - - if (prefixA === resultsField && tokensA.length > 1 && prefixB !== resultsField) { - tokensA.shift(); - tokensA.shift(); - if (tokensA.join('.') === b) return 1; - return tokensA.join('.').localeCompare(b); - } - - if (prefixB === resultsField && tokensB.length > 1 && prefixA !== resultsField) { - tokensB.shift(); - tokensB.shift(); - if (tokensB.join('.') === a) return -1; - return a.localeCompare(tokensB.join('.')); - } - return a.localeCompare(b); }; -export function getFlattenedFields(obj: EsDocSource, resultsField: string): EsFieldName[] { - const flatDocFields: EsFieldName[] = []; - const newDocFields = Object.keys(obj); - newDocFields.forEach(f => { - const fieldValue = getNestedProperty(obj, f); - if (typeof fieldValue !== 'object' || fieldValue === null || Array.isArray(fieldValue)) { - flatDocFields.push(f); - } else { - const innerFields = getFlattenedFields(fieldValue, resultsField); - const flattenedFields = innerFields.map(d => `${f}.${d}`); - flatDocFields.push(...flattenedFields); - } - }); - return flatDocFields.filter(f => f !== ML__ID_COPY); -} - export const getDefaultFieldsFromJobCaps = ( fields: Field[], jobConfig: DataFrameAnalyticsConfig, @@ -259,49 +172,72 @@ export const getDefaultFieldsFromJobCaps = ( return fieldsObj; } - const dependentVariable = getDependentVar(jobConfig.analysis); - const type = newJobCapsService.getFieldById(dependentVariable)?.type; - const predictionFieldName = getPredictionFieldName(jobConfig.analysis); - const numTopFeatureImportanceValues = getNumTopFeatureImportanceValues(jobConfig.analysis); // default is 'ml' const resultsField = jobConfig.dest.results_field; - const defaultPredictionField = `${dependentVariable}_prediction`; - const predictedField = `${resultsField}.${ - predictionFieldName ? predictionFieldName : defaultPredictionField - }`; - const featureImportanceFields = []; - - if ((numTopFeatureImportanceValues ?? 0) > 0) { - featureImportanceFields.push({ - id: `${resultsField}.feature_importance`, - name: `${resultsField}.feature_importance`, - type: KBN_FIELD_TYPES.NUMBER, - }); + const featureInfluenceFields = []; + const allFields: any = []; + let type: ES_FIELD_TYPES | undefined; + let predictedField: string | undefined; + + if (isOutlierAnalysis(jobConfig.analysis)) { + // Only need to add these fields if we didn't use dest index pattern to get the fields + if (needsDestIndexFields === true) { + allFields.push({ + id: `${resultsField}.${OUTLIER_SCORE}`, + name: `${resultsField}.${OUTLIER_SCORE}`, + type: KBN_FIELD_TYPES.NUMBER, + }); + + featureInfluenceFields.push( + ...fields + .filter(d => !jobConfig.analyzed_fields.excludes.includes(d.id)) + .map(d => ({ + id: `${resultsField}.${FEATURE_INFLUENCE}.${d.id}`, + name: `${resultsField}.${FEATURE_INFLUENCE}.${d.name}`, + type: KBN_FIELD_TYPES.NUMBER, + })) + ); + } } - let allFields: any = []; - // Only need to add these fields if we didn't use dest index pattern to get the fields - if (needsDestIndexFields === true) { - allFields.push( - { - id: `${resultsField}.is_training`, - name: `${resultsField}.is_training`, - type: ES_FIELD_TYPES.BOOLEAN, - }, - { id: predictedField, name: predictedField, type } - ); + if (isClassificationAnalysis(jobConfig.analysis) || isRegressionAnalysis(jobConfig.analysis)) { + const dependentVariable = getDependentVar(jobConfig.analysis); + type = newJobCapsService.getFieldById(dependentVariable)?.type; + const predictionFieldName = getPredictionFieldName(jobConfig.analysis); + const numTopFeatureImportanceValues = getNumTopFeatureImportanceValues(jobConfig.analysis); + + const defaultPredictionField = `${dependentVariable}_prediction`; + predictedField = `${resultsField}.${ + predictionFieldName ? predictionFieldName : defaultPredictionField + }`; + + if ((numTopFeatureImportanceValues ?? 0) > 0 && needsDestIndexFields === true) { + featureImportanceFields.push({ + id: `${resultsField}.${FEATURE_IMPORTANCE}`, + name: `${resultsField}.${FEATURE_IMPORTANCE}`, + type: KBN_FIELD_TYPES.UNKNOWN, + }); + } + + // Only need to add these fields if we didn't use dest index pattern to get the fields + if (needsDestIndexFields === true) { + allFields.push( + { + id: `${resultsField}.is_training`, + name: `${resultsField}.is_training`, + type: ES_FIELD_TYPES.BOOLEAN, + }, + { id: predictedField, name: predictedField, type } + ); + } } - allFields.push(...fields, ...featureImportanceFields); + allFields.push(...fields, ...featureImportanceFields, ...featureInfluenceFields); allFields.sort(({ name: a }: { name: string }, { name: b }: { name: string }) => - sortRegressionResultsFields(a, b, jobConfig) + sortExplorationResultsFields(a, b, jobConfig) ); - // Remove feature_importance fields provided by dest index since feature_importance is an array the path is not valid - if (needsDestIndexFields === false) { - allFields = allFields.filter((field: any) => !field.name.includes('.feature_importance.')); - } let selectedFields = allFields.filter( (field: any) => field.name === predictedField || !field.name.includes('.keyword') @@ -317,145 +253,3 @@ export const getDefaultFieldsFromJobCaps = ( depVarType: type, }; }; - -export const getDefaultClassificationFields = ( - docs: EsDoc[], - jobConfig: DataFrameAnalyticsConfig -): EsFieldName[] => { - if (docs.length === 0) { - return []; - } - const resultsField = jobConfig.dest.results_field; - const newDocFields = getFlattenedFields(docs[0]._source, resultsField); - return newDocFields - .filter(k => { - if (k === `${resultsField}.is_training`) { - return true; - } - // predicted value of dependent variable - if (k === getPredictedFieldName(resultsField, jobConfig.analysis, true)) { - return true; - } - // actual value of dependent variable - if (k === getDependentVar(jobConfig.analysis)) { - return true; - } - - if (k === `${resultsField}.prediction_probability`) { - return true; - } - - if (k.split('.')[0] === resultsField) { - return false; - } - - return docs.some(row => row._source[k] !== null); - }) - .sort((a, b) => sortRegressionResultsFields(a, b, jobConfig)) - .slice(0, DEFAULT_REGRESSION_COLUMNS); -}; - -export const getDefaultRegressionFields = ( - docs: EsDoc[], - jobConfig: DataFrameAnalyticsConfig -): EsFieldName[] => { - const resultsField = jobConfig.dest.results_field; - if (docs.length === 0) { - return []; - } - - const newDocFields = getFlattenedFields(docs[0]._source, resultsField); - return newDocFields - .filter(k => { - if (k === `${resultsField}.is_training`) { - return true; - } - // predicted value of dependent variable - if (k === getPredictedFieldName(resultsField, jobConfig.analysis)) { - return true; - } - // actual value of dependent variable - if (k === getDependentVar(jobConfig.analysis)) { - return true; - } - if (k.split('.')[0] === resultsField) { - return false; - } - - return docs.some(row => row._source[k] !== null); - }) - .sort((a, b) => sortRegressionResultsFields(a, b, jobConfig)) - .slice(0, DEFAULT_REGRESSION_COLUMNS); -}; - -export const getDefaultSelectableFields = (docs: EsDoc[], resultsField: string): EsFieldName[] => { - if (docs.length === 0) { - return []; - } - - const newDocFields = getFlattenedFields(docs[0]._source, resultsField); - return newDocFields.filter(k => { - if (k === `${resultsField}.outlier_score`) { - return true; - } - if (k.split('.')[0] === resultsField) { - return false; - } - - return docs.some(row => row._source[k] !== null); - }); -}; - -export const toggleSelectedFieldSimple = ( - selectedFields: EsFieldName[], - column: EsFieldName -): EsFieldName[] => { - const index = selectedFields.indexOf(column); - - if (index === -1) { - selectedFields.push(column); - } else { - selectedFields.splice(index, 1); - } - return selectedFields; -}; -// Fields starting with 'ml' or custom result name not included in newJobCapsService fields so -// need to recreate the field with correct type and add to selected fields -export const toggleSelectedField = ( - selectedFields: Field[], - column: EsFieldName, - resultsField: string, - depVarType?: ES_FIELD_TYPES -): Field[] => { - const index = selectedFields.map(field => field.name).indexOf(column); - if (index === -1) { - const columnField = newJobCapsService.getFieldById(column); - if (columnField !== null) { - selectedFields.push(columnField); - } else { - const resultFieldPattern = `^${resultsField}\.`; - const regex = new RegExp(resultFieldPattern); - const isResultField = column.match(regex) !== null; - let newField; - - if (isResultField && column.includes('is_training')) { - newField = { - id: column, - name: column, - type: ES_FIELD_TYPES.BOOLEAN, - }; - } else if (isResultField && depVarType !== undefined) { - newField = { - id: column, - name: column, - type: depVarType, - }; - } - - if (newField) selectedFields.push(newField); - } - } else { - selectedFields.splice(index, 1); - } - return selectedFields; -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts new file mode 100644 index 0000000000000..87b8c15aeaa78 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.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 { getErrorMessage } from '../../../../common/util/errors'; + +import { EsSorting, SearchResponse7, UseDataGridReturnType } from '../../components/data_grid'; +import { ml } from '../../services/ml_api_service'; + +import { isKeywordAndTextType } from '../common/fields'; +import { SavedSearchQuery } from '../../contexts/ml'; + +import { DataFrameAnalyticsConfig, INDEX_STATUS } from './analytics'; + +export const getIndexData = async ( + jobConfig: DataFrameAnalyticsConfig | undefined, + dataGrid: UseDataGridReturnType, + searchQuery: SavedSearchQuery +) => { + if (jobConfig !== undefined) { + const { + pagination, + setErrorMessage, + setRowCount, + setStatus, + setTableItems, + sortingColumns, + } = dataGrid; + + setErrorMessage(''); + setStatus(INDEX_STATUS.LOADING); + + try { + const sort: EsSorting = sortingColumns + .map(column => { + const { id } = column; + column.id = isKeywordAndTextType(id) ? `${id}.keyword` : id; + return column; + }) + .reduce((s, column) => { + s[column.id] = { order: column.direction }; + return s; + }, {} as EsSorting); + + const { pageIndex, pageSize } = pagination; + const resp: SearchResponse7 = await ml.esSearch({ + index: jobConfig.dest.index, + body: { + query: searchQuery, + from: pageIndex * pageSize, + size: pageSize, + ...(Object.keys(sort).length > 0 ? { sort } : {}), + }, + }); + + setRowCount(resp.hits.total.value); + + const docs = resp.hits.hits.map(d => d._source); + setTableItems(docs); + setStatus(INDEX_STATUS.LOADED); + } catch (e) { + setErrorMessage(getErrorMessage(e)); + setStatus(INDEX_STATUS.ERROR); + } + } +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_fields.ts new file mode 100644 index 0000000000000..12ae4a586e949 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_fields.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 { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; + +import { newJobCapsService } from '../../services/new_job_capabilities_service'; + +import { getDefaultFieldsFromJobCaps, DataFrameAnalyticsConfig } from '../common'; + +export interface FieldTypes { + [key: string]: ES_FIELD_TYPES; +} + +export const getIndexFields = ( + jobConfig: DataFrameAnalyticsConfig | undefined, + needsDestIndexFields: boolean +) => { + const { fields } = newJobCapsService; + if (jobConfig !== undefined) { + const { selectedFields: defaultSelected, docFields } = getDefaultFieldsFromJobCaps( + fields, + jobConfig, + needsDestIndexFields + ); + + const types: FieldTypes = {}; + const allFields: string[] = []; + + docFields.forEach(field => { + types[field.id] = field.type; + allFields.push(field.id); + }); + + return { + defaultSelectedFields: defaultSelected.map(field => field.id), + fieldTypes: types, + tableFields: allFields, + }; + } else { + return { + defaultSelectedFields: [], + fieldTypes: {}, + tableFields: [], + }; + } +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts index 7b76faf613ce8..400902c152c9e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts @@ -30,16 +30,8 @@ export { } from './analytics'; export { - getDefaultSelectableFields, - getDefaultRegressionFields, - getDefaultClassificationFields, getDefaultFieldsFromJobCaps, - getFlattenedFields, - sortColumns, - sortRegressionResultsColumns, - sortRegressionResultsFields, - toggleSelectedField, - toggleSelectedFieldSimple, + sortExplorationResultsFields, EsId, EsDoc, EsDocSource, @@ -47,4 +39,7 @@ export { MAX_COLUMNS, } from './fields'; -export { euiDataGridStyle, euiDataGridToolbarSettings } from './data_grid'; +export { getIndexData } from './get_index_data'; +export { getIndexFields } from './get_index_fields'; + +export { useResultsViewConfig } from './use_results_view_config'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts new file mode 100644 index 0000000000000..0bc9e78207596 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.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 { useEffect, useState } from 'react'; + +import { IndexPattern } from '../../../../../../../src/plugins/data/public'; + +import { getErrorMessage } from '../../../../common/util/errors'; + +import { getIndexPatternIdFromName } from '../../util/index_utils'; +import { ml } from '../../services/ml_api_service'; +import { newJobCapsService } from '../../services/new_job_capabilities_service'; +import { useMlContext } from '../../contexts/ml'; + +import { DataFrameAnalyticsConfig } from '../common'; + +import { isGetDataFrameAnalyticsStatsResponseOk } from '../pages/analytics_management/services/analytics_service/get_analytics'; +import { DATA_FRAME_TASK_STATE } from '../pages/analytics_management/components/analytics_list/common'; + +export const useResultsViewConfig = (jobId: string) => { + const mlContext = useMlContext(); + const [indexPattern, setIndexPattern] = useState(undefined); + const [isInitialized, setIsInitialized] = useState(false); + const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false); + const [jobConfig, setJobConfig] = useState(undefined); + const [jobCapsServiceErrorMessage, setJobCapsServiceErrorMessage] = useState( + undefined + ); + const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined); + const [jobStatus, setJobStatus] = useState(undefined); + + // get analytics configuration, index pattern and field caps + useEffect(() => { + (async function() { + setIsLoadingJobConfig(false); + + try { + const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); + const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); + const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) + ? analyticsStats.data_frame_analytics[0] + : undefined; + + if (stats !== undefined && stats.state) { + setJobStatus(stats.state); + } + + if ( + Array.isArray(analyticsConfigs.data_frame_analytics) && + analyticsConfigs.data_frame_analytics.length > 0 + ) { + const jobConfigUpdate = analyticsConfigs.data_frame_analytics[0]; + + try { + const destIndex = Array.isArray(jobConfigUpdate.dest.index) + ? jobConfigUpdate.dest.index[0] + : jobConfigUpdate.dest.index; + const destIndexPatternId = getIndexPatternIdFromName(destIndex) || destIndex; + let indexP: IndexPattern | undefined; + + try { + indexP = await mlContext.indexPatterns.get(destIndexPatternId); + } catch (e) { + indexP = undefined; + } + + if (indexP === undefined) { + const sourceIndex = jobConfigUpdate.source.index[0]; + const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; + indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); + } + + if (indexP !== undefined) { + await newJobCapsService.initializeFromIndexPattern(indexP, false, false); + setJobConfig(analyticsConfigs.data_frame_analytics[0]); + setIndexPattern(indexP); + setIsInitialized(true); + setIsLoadingJobConfig(false); + } + } catch (e) { + setJobCapsServiceErrorMessage(getErrorMessage(e)); + setIsLoadingJobConfig(false); + } + } + } catch (e) { + setJobConfigErrorMessage(getErrorMessage(e)); + setIsLoadingJobConfig(false); + } + })(); + }, []); + + return { + indexPattern, + isInitialized, + isLoadingJobConfig, + jobCapsServiceErrorMessage, + jobConfig, + jobConfigErrorMessage, + jobStatus, + }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx index 5c151166829ab..ccac9a697210b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx @@ -4,183 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useState, useEffect } from 'react'; -import { EuiCallOut, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { ml } from '../../../../../services/ml_api_service'; -import { DataFrameAnalyticsConfig } from '../../../../common'; -import { EvaluatePanel } from './evaluate_panel'; -import { ResultsTable } from './results_table'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; -import { LoadingPanel } from '../loading_panel'; -import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { useMlContext } from '../../../../../contexts/ml'; -import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; +import React, { FC } from 'react'; -export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => ( - - - {i18n.translate('xpack.ml.dataframe.analytics.classificationExploration.tableJobIdTitle', { - defaultMessage: 'Destination index for classification job ID {jobId}', - values: { jobId }, - })} - - -); +import { i18n } from '@kbn/i18n'; -const jobConfigErrorTitle = i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationFetchError', - { - defaultMessage: - 'Unable to fetch results. An error occurred loading the job configuration data.', - } -); +import { ExplorationPageWrapper } from '../exploration_page_wrapper'; -const jobCapsErrorTitle = i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.jobCapsFetchError', - { - defaultMessage: "Unable to fetch results. An error occurred loading the index's field data.", - } -); +import { EvaluatePanel } from './evaluate_panel'; interface Props { jobId: string; } export const ClassificationExploration: FC = ({ jobId }) => { - const [jobConfig, setJobConfig] = useState(undefined); - const [jobStatus, setJobStatus] = useState(undefined); - const [indexPattern, setIndexPattern] = useState(undefined); - const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false); - const [isInitialized, setIsInitialized] = useState(false); - const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined); - const [jobCapsServiceErrorMessage, setJobCapsServiceErrorMessage] = useState( - undefined - ); - const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const mlContext = useMlContext(); - - const loadJobConfig = async () => { - setIsLoadingJobConfig(true); - try { - const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); - const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); - const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) - ? analyticsStats.data_frame_analytics[0] - : undefined; - - if (stats !== undefined && stats.state) { - setJobStatus(stats.state); - } - - if ( - Array.isArray(analyticsConfigs.data_frame_analytics) && - analyticsConfigs.data_frame_analytics.length > 0 - ) { - setJobConfig(analyticsConfigs.data_frame_analytics[0]); - setIsLoadingJobConfig(false); - } else { - setJobConfigErrorMessage( - i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationNoResultsMessage', - { - defaultMessage: 'No results found.', - } - ) - ); - } - } catch (e) { - if (e.message !== undefined) { - setJobConfigErrorMessage(e.message); - } else { - setJobConfigErrorMessage(JSON.stringify(e)); - } - setIsLoadingJobConfig(false); - } - }; - - useEffect(() => { - loadJobConfig(); - }, []); - - const initializeJobCapsService = async () => { - if (jobConfig !== undefined) { - try { - const destIndex = Array.isArray(jobConfig.dest.index) - ? jobConfig.dest.index[0] - : jobConfig.dest.index; - const destIndexPatternId = getIndexPatternIdFromName(destIndex) || destIndex; - let indexP: IndexPattern | undefined; - - try { - indexP = await mlContext.indexPatterns.get(destIndexPatternId); - } catch (e) { - indexP = undefined; - } - - if (indexP === undefined) { - const sourceIndex = jobConfig.source.index[0]; - const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; - indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); - } - - if (indexP !== undefined) { - setIndexPattern(indexP); - await newJobCapsService.initializeFromIndexPattern(indexP, false, false); - } - setIsInitialized(true); - } catch (e) { - if (e.message !== undefined) { - setJobCapsServiceErrorMessage(e.message); - } else { - setJobCapsServiceErrorMessage(JSON.stringify(e)); - } - } - } - }; - - useEffect(() => { - initializeJobCapsService(); - }, [jobConfig && jobConfig.id]); - - if (jobConfigErrorMessage !== undefined || jobCapsServiceErrorMessage !== undefined) { - return ( - - - - -

{jobConfigErrorMessage ? jobConfigErrorMessage : jobCapsServiceErrorMessage}

-
-
- ); - } - return ( - - {isLoadingJobConfig === true && jobConfig === undefined && } - {isLoadingJobConfig === false && jobConfig !== undefined && isInitialized === true && ( - + - {isLoadingJobConfig === true && jobConfig === undefined && } - {isLoadingJobConfig === false && - jobConfig !== undefined && - indexPattern !== undefined && - isInitialized === true && ( - - )} - + EvaluatePanel={EvaluatePanel} + /> ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration_data_grid.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration_data_grid.tsx deleted file mode 100644 index 424fc002795ca..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration_data_grid.tsx +++ /dev/null @@ -1,135 +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, { Dispatch, FC, SetStateAction, useCallback, useMemo } from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { EuiDataGrid, EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { euiDataGridStyle, euiDataGridToolbarSettings } from '../../../../common'; - -import { mlFieldFormatService } from '../../../../../services/field_format_service'; - -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - -const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; - -type Pagination = Pick; -type TableItem = Record; - -interface ExplorationDataGridProps { - colorRange?: (d: number) => string; - columns: any[]; - indexPattern: IndexPattern; - pagination: Pagination; - resultsField: string; - rowCount: number; - selectedFields: string[]; - setPagination: Dispatch>; - setSelectedFields: Dispatch>; - setSortingColumns: Dispatch>; - sortingColumns: EuiDataGridSorting['columns']; - tableItems: TableItem[]; -} - -export const ClassificationExplorationDataGrid: FC = ({ - columns, - indexPattern, - pagination, - resultsField, - rowCount, - selectedFields, - setPagination, - setSelectedFields, - setSortingColumns, - sortingColumns, - tableItems, -}) => { - const renderCellValue = useMemo(() => { - return ({ rowIndex, columnId }: { rowIndex: number; columnId: string; setCellProps: any }) => { - const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - const fullItem = tableItems[adjustedRowIndex]; - - if (fullItem === undefined) { - return null; - } - - let format: any; - - if (indexPattern !== undefined) { - format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, columnId, ''); - } - - const cellValue = - fullItem.hasOwnProperty(columnId) && fullItem[columnId] !== undefined - ? fullItem[columnId] - : null; - - if (format !== undefined) { - return format.convert(cellValue, 'text'); - } - - if (typeof cellValue === 'string' || cellValue === null) { - return cellValue; - } - - if (typeof cellValue === 'boolean') { - return cellValue ? 'true' : 'false'; - } - - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - - return cellValue; - }; - }, [resultsField, rowCount, tableItems, pagination.pageIndex, pagination.pageSize]); - - const onChangeItemsPerPage = useCallback( - pageSize => { - setPagination(p => { - const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); - return { pageIndex, pageSize }; - }); - }, - [setPagination] - ); - - const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ - setPagination, - ]); - - const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]); - - return ( - - ); -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts deleted file mode 100644 index c8809ca5e471b..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/use_explore_data.ts +++ /dev/null @@ -1,292 +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 { useEffect, useState, Dispatch, SetStateAction } from 'react'; -import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { SearchResponse } from 'elasticsearch'; -import { cloneDeep } from 'lodash'; - -import { SORT_DIRECTION } from '../../../../../components/ml_in_memory_table'; - -import { ml } from '../../../../../services/ml_api_service'; -import { getNestedProperty } from '../../../../../util/object_utils'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { isKeywordAndTextType } from '../../../../common/fields'; -import { Dictionary } from '../../../../../../../common/types/common'; -import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; -import { - defaultSearchQuery, - ResultsSearchQuery, - isResultsSearchBoolQuery, - LoadExploreDataArg, -} from '../../../../common/analytics'; - -import { - getDefaultFieldsFromJobCaps, - getDependentVar, - getFlattenedFields, - getPredictedFieldName, - DataFrameAnalyticsConfig, - EsFieldName, - INDEX_STATUS, -} from '../../../../common'; -import { SavedSearchQuery } from '../../../../../contexts/ml'; - -export type TableItem = Record; -type Pagination = Pick; - -export interface UseExploreDataReturnType { - errorMessage: string; - fieldTypes: { [key: string]: ES_FIELD_TYPES }; - pagination: Pagination; - rowCount: number; - searchQuery: SavedSearchQuery; - selectedFields: EsFieldName[]; - setFilterByIsTraining: Dispatch>; - setPagination: Dispatch>; - setSearchQuery: Dispatch>; - setSelectedFields: Dispatch>; - setSortingColumns: Dispatch>; - sortingColumns: EuiDataGridSorting['columns']; - status: INDEX_STATUS; - tableFields: string[]; - tableItems: TableItem[]; -} - -type EsSorting = Dictionary<{ - order: 'asc' | 'desc'; -}>; - -// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. -interface SearchResponse7 extends SearchResponse { - hits: SearchResponse['hits'] & { - total: { - value: number; - relation: string; - }; - }; -} - -export const useExploreData = ( - jobConfig: DataFrameAnalyticsConfig, - needsDestIndexFields: boolean -): UseExploreDataReturnType => { - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(INDEX_STATUS.UNUSED); - - const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); - const [tableFields, setTableFields] = useState([]); - const [tableItems, setTableItems] = useState([]); - const [fieldTypes, setFieldTypes] = useState<{ [key: string]: ES_FIELD_TYPES }>({}); - const [rowCount, setRowCount] = useState(0); - - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 }); - const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const [filterByIsTraining, setFilterByIsTraining] = useState(undefined); - const [sortingColumns, setSortingColumns] = useState([]); - - const predictedFieldName = getPredictedFieldName( - jobConfig.dest.results_field, - jobConfig.analysis - ); - const dependentVariable = getDependentVar(jobConfig.analysis); - - const getDefaultSelectedFields = () => { - const { fields } = newJobCapsService; - if (selectedFields.length === 0 && jobConfig !== undefined) { - const { selectedFields: defaultSelected, docFields } = getDefaultFieldsFromJobCaps( - fields, - jobConfig, - needsDestIndexFields - ); - - const types: { [key: string]: ES_FIELD_TYPES } = {}; - const allFields: string[] = []; - - docFields.forEach(field => { - types[field.id] = field.type; - allFields.push(field.id); - }); - - setFieldTypes(types); - setSelectedFields(defaultSelected.map(field => field.id)); - setTableFields(allFields); - } - }; - - const loadExploreData = async ({ - filterByIsTraining: isTraining, - searchQuery: incomingQuery, - }: LoadExploreDataArg) => { - if (jobConfig !== undefined) { - setErrorMessage(''); - setStatus(INDEX_STATUS.LOADING); - - try { - const resultsField = jobConfig.dest.results_field; - const searchQueryClone: ResultsSearchQuery = cloneDeep(incomingQuery); - let query: ResultsSearchQuery; - const { pageIndex, pageSize } = pagination; - // If filterByIsTraining is defined - add that in to the final query - const trainingQuery = - isTraining !== undefined - ? { - term: { [`${resultsField}.is_training`]: { value: isTraining } }, - } - : undefined; - - if (JSON.stringify(incomingQuery) === JSON.stringify(defaultSearchQuery)) { - const existsQuery = { - exists: { - field: resultsField, - }, - }; - - query = { - bool: { - must: [existsQuery], - }, - }; - - if (trainingQuery !== undefined && isResultsSearchBoolQuery(query)) { - query.bool.must.push(trainingQuery); - } - } else if (isResultsSearchBoolQuery(searchQueryClone)) { - if (searchQueryClone.bool.must === undefined) { - searchQueryClone.bool.must = []; - } - - searchQueryClone.bool.must.push({ - exists: { - field: resultsField, - }, - }); - - if (trainingQuery !== undefined) { - searchQueryClone.bool.must.push(trainingQuery); - } - - query = searchQueryClone; - } else { - query = searchQueryClone; - } - - const sort: EsSorting = sortingColumns - .map(column => { - const { id } = column; - column.id = isKeywordAndTextType(id) ? `${id}.keyword` : id; - return column; - }) - .reduce((s, column) => { - s[column.id] = { order: column.direction }; - return s; - }, {} as EsSorting); - - const resp: SearchResponse7 = await ml.esSearch({ - index: jobConfig.dest.index, - body: { - query, - from: pageIndex * pageSize, - size: pageSize, - ...(Object.keys(sort).length > 0 ? { sort } : {}), - }, - }); - - setRowCount(resp.hits.total.value); - - const docs = resp.hits.hits; - - if (docs.length === 0) { - setTableItems([]); - setStatus(INDEX_STATUS.LOADED); - return; - } - - // Create a version of the doc's source with flattened field names. - // This avoids confusion later on if a field name has dots in its name - // or is a nested fields when displaying it via EuiInMemoryTable. - const flattenedFields = getFlattenedFields(docs[0]._source, resultsField); - const transformedTableItems = docs.map(doc => { - const item: TableItem = {}; - flattenedFields.forEach(ff => { - item[ff] = getNestedProperty(doc._source, ff); - if (item[ff] === undefined) { - // If the attribute is undefined, it means it was not a nested property - // but had dots in its actual name. This selects the property by its - // full name and assigns it to `item[ff]`. - item[ff] = doc._source[`"${ff}"`]; - } - if (item[ff] === undefined) { - const parts = ff.split('.'); - if (parts[0] === resultsField && parts.length >= 2) { - parts.shift(); - if (doc._source[resultsField] !== undefined) { - item[ff] = doc._source[resultsField][parts.join('.')]; - } - } - } - }); - return item; - }); - - setTableItems(transformedTableItems); - setStatus(INDEX_STATUS.LOADED); - } catch (e) { - if (e.message !== undefined) { - setErrorMessage(e.message); - } else { - setErrorMessage(JSON.stringify(e)); - } - setTableItems([]); - setStatus(INDEX_STATUS.ERROR); - } - } - }; - - useEffect(() => { - getDefaultSelectedFields(); - }, [jobConfig && jobConfig.id]); - - // By default set sorting to descending on the prediction field (`_prediction`). - useEffect(() => { - const sortByField = isKeywordAndTextType(dependentVariable) - ? `${predictedFieldName}.keyword` - : predictedFieldName; - const direction = SORT_DIRECTION.DESC; - - setSortingColumns([{ id: sortByField, direction }]); - }, [jobConfig && jobConfig.id]); - - useEffect(() => { - loadExploreData({ filterByIsTraining, searchQuery }); - }, [ - filterByIsTraining, - jobConfig && jobConfig.id, - pagination, - searchQuery, - selectedFields, - sortingColumns, - ]); - - return { - errorMessage, - fieldTypes, - pagination, - searchQuery, - selectedFields, - rowCount, - setFilterByIsTraining, - setPagination, - setSelectedFields, - setSortingColumns, - setSearchQuery, - sortingColumns, - status, - tableItems, - tableFields, - }; -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/error_callout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/error_callout.tsx index 9765192f0e446..839587c47289a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/error_callout.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/error_callout/error_callout.tsx @@ -15,7 +15,7 @@ interface Props { export const ErrorCallout: FC = ({ error }) => { let errorCallout = ( = ({ error }) => { if (error.includes('index_not_found')) { errorCallout = (

- {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.noIndexCalloutBody', { + {i18n.translate('xpack.ml.dataframe.analytics.errorCallout.noIndexCalloutBody', { defaultMessage: 'The query for the index returned no results. Please make sure the destination index exists and contains documents.', })} @@ -46,16 +46,13 @@ export const ErrorCallout: FC = ({ error }) => { // Job was started but no results have been written yet errorCallout = (

- {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutBody', { + {i18n.translate('xpack.ml.dataframe.analytics.errorCallout.noDataCalloutBody', { defaultMessage: 'The query for the index returned no results. Please make sure the job has completed and the index contains documents.', })} @@ -66,22 +63,16 @@ export const ErrorCallout: FC = ({ error }) => { // query bar syntax is incorrect errorCallout = (

- {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorBody', - { - defaultMessage: - 'The query syntax is invalid and returned no results. Please check the query syntax and try again.', - } - )} + {i18n.translate('xpack.ml.dataframe.analytics.errorCallout.queryParsingErrorBody', { + defaultMessage: + 'The query syntax is invalid and returned no results. Please check the query syntax and try again.', + })}

); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/exploration_data_grid.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/exploration_data_grid.tsx deleted file mode 100644 index e88bc1cd06f95..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_data_grid/exploration_data_grid.tsx +++ /dev/null @@ -1,161 +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, { Dispatch, FC, SetStateAction, useCallback, useMemo } from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { EuiDataGrid, EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { euiDataGridStyle, euiDataGridToolbarSettings } from '../../../../common'; - -import { mlFieldFormatService } from '../../../../../services/field_format_service'; - -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - -const FEATURE_INFLUENCE = 'feature_influence'; -const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; - -type Pagination = Pick; -type TableItem = Record; - -interface ExplorationDataGridProps { - colorRange: (d: number) => string; - columns: any[]; - indexPattern: IndexPattern; - pagination: Pagination; - resultsField: string; - rowCount: number; - selectedFields: string[]; - setPagination: Dispatch>; - setSelectedFields: Dispatch>; - setSortingColumns: Dispatch>; - sortingColumns: EuiDataGridSorting['columns']; - tableItems: TableItem[]; -} - -export const ExplorationDataGrid: FC = ({ - colorRange, - columns, - indexPattern, - pagination, - resultsField, - rowCount, - selectedFields, - setPagination, - setSelectedFields, - setSortingColumns, - sortingColumns, - tableItems, -}) => { - const renderCellValue = useMemo(() => { - return ({ - rowIndex, - columnId, - setCellProps, - }: { - rowIndex: number; - columnId: string; - setCellProps: any; - }) => { - const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - const fullItem = tableItems[adjustedRowIndex]; - - if (fullItem === undefined) { - return null; - } - - let format: any; - - if (indexPattern !== undefined) { - format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, columnId, ''); - } - - const cellValue = - fullItem.hasOwnProperty(columnId) && fullItem[columnId] !== undefined - ? fullItem[columnId] - : null; - - const split = columnId.split('.'); - let backgroundColor; - - // column with feature values get color coded by its corresponding influencer value - if (fullItem[`${resultsField}.${FEATURE_INFLUENCE}.${columnId}`] !== undefined) { - backgroundColor = colorRange(fullItem[`${resultsField}.${FEATURE_INFLUENCE}.${columnId}`]); - } - - // column with influencer values get color coded by its own value - if (split.length > 2 && split[0] === resultsField && split[1] === FEATURE_INFLUENCE) { - backgroundColor = colorRange(cellValue); - } - - if (backgroundColor !== undefined) { - setCellProps({ - style: { backgroundColor }, - }); - } - - if (format !== undefined) { - return format.convert(cellValue, 'text'); - } - - if (typeof cellValue === 'string' || cellValue === null) { - return cellValue; - } - - if (typeof cellValue === 'boolean') { - return cellValue ? 'true' : 'false'; - } - - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - - return cellValue; - }; - }, [resultsField, rowCount, tableItems, pagination.pageIndex, pagination.pageSize]); - - const onChangeItemsPerPage = useCallback( - pageSize => { - setPagination(p => { - const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); - return { pageIndex, pageSize }; - }); - }, - [setPagination] - ); - - const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ - setPagination, - ]); - - const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]); - - return ( - - ); -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx new file mode 100644 index 0000000000000..1986c486974c9 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.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, { FC, useState } from 'react'; + +import { EuiSpacer } from '@elastic/eui'; + +import { useResultsViewConfig, DataFrameAnalyticsConfig } from '../../../../common'; +import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; + +import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; + +import { ExplorationResultsTable } from '../exploration_results_table'; +import { JobConfigErrorCallout } from '../job_config_error_callout'; +import { LoadingPanel } from '../loading_panel'; + +export interface EvaluatePanelProps { + jobConfig: DataFrameAnalyticsConfig; + jobStatus?: DATA_FRAME_TASK_STATE; + searchQuery: ResultsSearchQuery; +} + +interface Props { + jobId: string; + title: string; + EvaluatePanel: FC; +} + +export const ExplorationPageWrapper: FC = ({ jobId, title, EvaluatePanel }) => { + const { + indexPattern, + isInitialized, + isLoadingJobConfig, + jobCapsServiceErrorMessage, + jobConfig, + jobConfigErrorMessage, + jobStatus, + } = useResultsViewConfig(jobId); + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); + + if (jobConfigErrorMessage !== undefined || jobCapsServiceErrorMessage !== undefined) { + return ( + + ); + } + + return ( + <> + {isLoadingJobConfig === true && jobConfig === undefined && } + {isLoadingJobConfig === false && jobConfig !== undefined && isInitialized === true && ( + + )} + + {isLoadingJobConfig === true && jobConfig === undefined && } + {isLoadingJobConfig === false && + jobConfig !== undefined && + indexPattern !== undefined && + isInitialized === true && ( + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/index.ts new file mode 100644 index 0000000000000..bf294a3cd08c9 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { EvaluatePanelProps, ExplorationPageWrapper } from './exploration_page_wrapper'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx similarity index 57% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx index bf63dfe68fe9e..24e5785c6e808 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, FC, useEffect } from 'react'; +import React, { Fragment, FC, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiCallOut, @@ -12,17 +12,15 @@ import { EuiFlexItem, EuiFormRow, EuiPanel, - EuiProgress, EuiSpacer, EuiText, } from '@elastic/eui'; import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; -import { - BASIC_NUMERICAL_TYPES, - EXTENDED_NUMERICAL_TYPES, - sortRegressionResultsFields, -} from '../../../../common/fields'; + +import { DataGrid } from '../../../../../components/data_grid'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; +import { getToastNotifications } from '../../../../../util/dependency_cache'; import { DataFrameAnalyticsConfig, @@ -33,20 +31,20 @@ import { } from '../../../../common'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { useExploreData } from './use_explore_data'; // TableItem -import { ExplorationTitle } from './classification_exploration'; -import { ClassificationExplorationDataGrid } from './classification_exploration_data_grid'; +import { ExplorationTitle } from '../exploration_title'; import { ExplorationQueryBar } from '../exploration_query_bar'; +import { useExplorationResults } from './use_exploration_results'; + const showingDocs = i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.documentsShownHelpText', + 'xpack.ml.dataframe.analytics.explorationResults.documentsShownHelpText', { defaultMessage: 'Showing documents for which predictions exist', } ); const showingFirstDocs = i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.firstDocumentsShownHelpText', + 'xpack.ml.dataframe.analytics.explorationResults.firstDocumentsShownHelpText', { defaultMessage: 'Showing first {searchSize} documents for which predictions exist', values: { searchSize: SEARCH_SIZE }, @@ -58,71 +56,22 @@ interface Props { jobConfig: DataFrameAnalyticsConfig; jobStatus?: DATA_FRAME_TASK_STATE; setEvaluateSearchQuery: React.Dispatch>; + title: string; } -export const ResultsTable: FC = React.memo( - ({ indexPattern, jobConfig, jobStatus, setEvaluateSearchQuery }) => { - const needsDestIndexFields = indexPattern && indexPattern.title === jobConfig.source.index[0]; - const resultsField = jobConfig.dest.results_field; - const { - errorMessage, - fieldTypes, - pagination, - searchQuery, - selectedFields, - rowCount, - setPagination, - setSearchQuery, - setSelectedFields, - setSortingColumns, - sortingColumns, - status, - tableFields, - tableItems, - } = useExploreData(jobConfig, needsDestIndexFields); +export const ExplorationResultsTable: FC = React.memo( + ({ indexPattern, jobConfig, jobStatus, setEvaluateSearchQuery, title }) => { + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); useEffect(() => { setEvaluateSearchQuery(searchQuery); }, [JSON.stringify(searchQuery)]); - const columns = tableFields - .sort((a: any, b: any) => sortRegressionResultsFields(a, b, jobConfig)) - .map((field: any) => { - // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] - // To fall back to the default string schema it needs to be undefined. - let schema; - let isSortable = true; - const type = fieldTypes[field]; - const isNumber = - type !== undefined && - (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type)); - - if (isNumber) { - schema = 'numeric'; - } - - switch (type) { - case 'date': - schema = 'datetime'; - break; - case 'geo_point': - schema = 'json'; - break; - case 'boolean': - schema = 'boolean'; - break; - } + const classificationData = useExplorationResults(indexPattern, jobConfig, searchQuery); + const docFieldsCount = classificationData.columns.length; + const { columns, errorMessage, status, tableItems, visibleColumns } = classificationData; - if (field === `${resultsField}.feature_importance`) { - isSortable = false; - } - - return { id: field, schema, isSortable }; - }); - - const docFieldsCount = tableFields.length; - - if (jobConfig === undefined) { + if (jobConfig === undefined || classificationData === undefined) { return null; } // if it's a searchBar syntax error leave the table visible so they can try again @@ -131,7 +80,7 @@ export const ResultsTable: FC = React.memo( - + {jobStatus !== undefined && ( @@ -156,13 +105,13 @@ export const ResultsTable: FC = React.memo( - + {jobStatus !== undefined && ( @@ -177,11 +126,11 @@ export const ResultsTable: FC = React.memo( {docFieldsCount > MAX_COLUMNS && ( {i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.fieldSelection', + 'xpack.ml.dataframe.analytics.explorationResults.fieldSelection', { defaultMessage: '{selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}} selected', - values: { selectedFieldsLength: selectedFields.length, docFieldsCount }, + values: { selectedFieldsLength: visibleColumns.length, docFieldsCount }, } )} @@ -190,10 +139,6 @@ export const ResultsTable: FC = React.memo( - {status === INDEX_STATUS.LOADING && } - {status !== INDEX_STATUS.LOADING && ( - - )} {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( @@ -213,18 +158,10 @@ export const ResultsTable: FC = React.memo( - diff --git a/x-pack/legacy/plugins/rollup/server/lib/call_with_request_factory/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/index.ts similarity index 77% rename from x-pack/legacy/plugins/rollup/server/lib/call_with_request_factory/index.ts rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/index.ts index 787814d87dff9..19308640c8b02 100644 --- a/x-pack/legacy/plugins/rollup/server/lib/call_with_request_factory/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { callWithRequestFactory } from './call_with_request_factory'; +export { ExplorationResultsTable } from './exploration_results_table'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts new file mode 100644 index 0000000000000..6f9dc694d8172 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.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 { useEffect } from 'react'; + +import { EuiDataGridColumn } from '@elastic/eui'; + +import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; + +import { + getDataGridSchemasFromFieldTypes, + useDataGrid, + useRenderCellValue, + UseIndexDataReturnType, +} from '../../../../../components/data_grid'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; + +import { getIndexData, getIndexFields, DataFrameAnalyticsConfig } from '../../../../common'; +import { DEFAULT_RESULTS_FIELD, FEATURE_IMPORTANCE } from '../../../../common/constants'; +import { sortExplorationResultsFields, ML__ID_COPY } from '../../../../common/fields'; + +export const useExplorationResults = ( + indexPattern: IndexPattern | undefined, + jobConfig: DataFrameAnalyticsConfig | undefined, + searchQuery: SavedSearchQuery +): UseIndexDataReturnType => { + const needsDestIndexFields = + indexPattern !== undefined && indexPattern.title === jobConfig?.source.index[0]; + + const columns: EuiDataGridColumn[] = []; + + if (jobConfig !== undefined) { + const resultsField = jobConfig.dest.results_field; + const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields); + columns.push( + ...getDataGridSchemasFromFieldTypes(fieldTypes, resultsField).sort((a: any, b: any) => + sortExplorationResultsFields(a.id, b.id, jobConfig) + ) + ); + } + + const dataGrid = useDataGrid( + columns, + 25, + // reduce default selected rows from 20 to 8 for performance reasons. + 8, + // by default, hide feature-importance columns and the doc id copy + d => !d.includes(`.${FEATURE_IMPORTANCE}.`) && d !== ML__ID_COPY + ); + + useEffect(() => { + getIndexData(jobConfig, dataGrid, searchQuery); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [jobConfig && jobConfig.id, dataGrid.pagination, searchQuery, dataGrid.sortingColumns]); + + const renderCellValue = useRenderCellValue( + indexPattern, + dataGrid.pagination, + dataGrid.tableItems, + jobConfig?.dest.results_field ?? DEFAULT_RESULTS_FIELD + ); + + return { + ...dataGrid, + columns, + renderCellValue, + }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/exploration_title.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/exploration_title.tsx new file mode 100644 index 0000000000000..f06c88c73df71 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/exploration_title.tsx @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; + +import { EuiTitle } from '@elastic/eui'; + +export const ExplorationTitle: FC<{ title: string }> = ({ title }) => ( + + {title} + +); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/index.ts similarity index 79% rename from x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/index.ts rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/index.ts index a13e678813a00..b34e61b3b5e76 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_title/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { SourceIndexPreview } from './source_index_preview'; +export { ExplorationTitle } from './exploration_title'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/index.ts similarity index 78% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/index.ts rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/index.ts index dd896ca02f7f7..a5991f4325d12 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { useExploreData, TableItem } from './use_explore_data'; +export { JobConfigErrorCallout } from './job_config_error_callout'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/job_config_error_callout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/job_config_error_callout.tsx new file mode 100644 index 0000000000000..945d6654067c0 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/job_config_error_callout/job_config_error_callout.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; + +import { EuiCallOut, EuiPanel, EuiSpacer } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { ExplorationTitle } from '../exploration_title'; + +const jobConfigErrorTitle = i18n.translate('xpack.ml.dataframe.analytics.jobConfig.errorTitle', { + defaultMessage: 'Unable to fetch results. An error occurred loading the job configuration data.', +}); + +const jobCapsErrorTitle = i18n.translate('xpack.ml.dataframe.analytics.jobCaps.errorTitle', { + defaultMessage: "Unable to fetch results. An error occurred loading the index's field data.", +}); + +interface Props { + jobCapsServiceErrorMessage: string | undefined; + jobConfigErrorMessage: string | undefined; + title: string; +} + +export const JobConfigErrorCallout: FC = ({ + jobCapsServiceErrorMessage, + jobConfigErrorMessage, + title, +}) => { + return ( + + + + +

{jobConfigErrorMessage ? jobConfigErrorMessage : jobCapsServiceErrorMessage}

+
+
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/common.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.test.ts similarity index 100% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/common.test.ts rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.test.ts diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts similarity index 51% rename from x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/common.ts rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts index 48db8e34c934e..bfd3dd33995aa 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/common.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/common.ts @@ -4,9 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DataFrameAnalyticsConfig } from '../../../../common'; +import { DataGridItem } from '../../../../../components/data_grid'; -const OUTLIER_SCORE = 'outlier_score'; +import { DataFrameAnalyticsConfig } from '../../../../common'; +import { FEATURE_INFLUENCE, OUTLIER_SCORE } from '../../../../common/constants'; export const getOutlierScoreFieldName = (jobConfig: DataFrameAnalyticsConfig) => `${jobConfig.dest.results_field}.${OUTLIER_SCORE}`; + +export const getFeatureCount = (resultsField: string, tableItems: DataGridItem[] = []) => { + if (tableItems.length === 0) { + return 0; + } + + return Object.keys(tableItems[0]).filter(key => + key.includes(`${resultsField}.${FEATURE_INFLUENCE}.`) + ).length; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx index fdcb7d9d237e3..0154f92576c4a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; +import React, { useState, FC } from 'react'; import { i18n } from '@kbn/i18n'; @@ -15,127 +15,51 @@ import { EuiHorizontalRule, EuiPanel, EuiSpacer, - EuiTitle, } from '@elastic/eui'; import { useColorRange, - ColorRangeLegend, COLOR_RANGE, COLOR_RANGE_SCALE, } from '../../../../../components/color_range_legend'; +import { ColorRangeLegend } from '../../../../../components/color_range_legend'; +import { DataGrid } from '../../../../../components/data_grid'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; +import { getToastNotifications } from '../../../../../util/dependency_cache'; -import { sortColumns, INDEX_STATUS, defaultSearchQuery } from '../../../../common'; +import { defaultSearchQuery, useResultsViewConfig, INDEX_STATUS } from '../../../../common'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; -import { useExploreData, TableItem } from '../../hooks/use_explore_data'; - -import { ExplorationDataGrid } from '../exploration_data_grid'; import { ExplorationQueryBar } from '../exploration_query_bar'; +import { ExplorationTitle } from '../exploration_title'; -const FEATURE_INFLUENCE = 'feature_influence'; +import { getFeatureCount } from './common'; +import { useOutlierData } from './use_outlier_data'; -const ExplorationTitle: FC<{ jobId: string }> = ({ jobId }) => ( - - - {i18n.translate('xpack.ml.dataframe.analytics.exploration.jobIdTitle', { - defaultMessage: 'Outlier detection job ID {jobId}', - values: { jobId }, - })} - - -); +export type TableItem = Record; interface ExplorationProps { jobId: string; } -const getFeatureCount = (resultsField: string, tableItems: TableItem[] = []) => { - if (tableItems.length === 0) { - return 0; - } - - return Object.keys(tableItems[0]).filter(key => - key.includes(`${resultsField}.${FEATURE_INFLUENCE}.`) - ).length; -}; - export const OutlierExploration: FC = React.memo(({ jobId }) => { - const { - errorMessage, - indexPattern, - jobConfig, - jobStatus, - pagination, - searchQuery, - selectedFields, - setPagination, - setSearchQuery, - setSelectedFields, - setSortingColumns, - sortingColumns, - rowCount, - status, - tableFields, - tableItems, - } = useExploreData(jobId); - - const columns = []; - - if ( - jobConfig !== undefined && - indexPattern !== undefined && - selectedFields.length > 0 && - tableItems.length > 0 - ) { - const resultsField = jobConfig.dest.results_field; - const removePrefix = new RegExp(`^${resultsField}\.${FEATURE_INFLUENCE}\.`, 'g'); - columns.push( - ...tableFields.sort(sortColumns(tableItems[0], resultsField)).map(id => { - const idWithoutPrefix = id.replace(removePrefix, ''); - const field = indexPattern.fields.getByName(idWithoutPrefix); - - // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] - // To fall back to the default string schema it needs to be undefined. - let schema; - - switch (field?.type) { - case 'date': - schema = 'datetime'; - break; - case 'geo_point': - schema = 'json'; - break; - case 'number': - schema = 'numeric'; - break; - } - - if (id === `${resultsField}.outlier_score`) { - schema = 'numeric'; - } - - return { id, schema }; - }) - ); - } + const explorationTitle = i18n.translate('xpack.ml.dataframe.analytics.exploration.jobIdTitle', { + defaultMessage: 'Outlier detection job ID {jobId}', + values: { jobId }, + }); - const colorRange = useColorRange( - COLOR_RANGE.BLUE, - COLOR_RANGE_SCALE.INFLUENCER, - jobConfig !== undefined ? getFeatureCount(jobConfig.dest.results_field, tableItems) : 1 - ); + const { indexPattern, jobConfig, jobStatus } = useResultsViewConfig(jobId); + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); + const outlierData = useOutlierData(indexPattern, jobConfig, searchQuery); - if (jobConfig === undefined || indexPattern === undefined) { - return null; - } + const { columns, errorMessage, status, tableItems } = outlierData; // if it's a searchBar syntax error leave the table visible so they can try again if (status === INDEX_STATUS.ERROR && !errorMessage.includes('parsing_exception')) { return ( - + = React.memo(({ jobId }) = ); } - let tableError = - status === INDEX_STATUS.ERROR && errorMessage.includes('parsing_exception') - ? errorMessage - : undefined; - - if (status === INDEX_STATUS.LOADED && tableItems.length === 0 && tableError === undefined) { - tableError = i18n.translate('xpack.ml.dataframe.analytics.exploration.noDataCalloutBody', { - defaultMessage: - 'The query for the index returned no results. Please make sure the index contains documents and your query is not too restrictive.', - }); - } + const colorRange = useColorRange( + COLOR_RANGE.BLUE, + COLOR_RANGE_SCALE.INFLUENCER, + jobConfig !== undefined ? getFeatureCount(jobConfig.dest.results_field, tableItems) : 1 + ); return ( @@ -170,7 +88,7 @@ export const OutlierExploration: FC = React.memo(({ jobId }) = gutterSize="s" > - + {jobStatus !== undefined && ( @@ -179,7 +97,7 @@ export const OutlierExploration: FC = React.memo(({ jobId }) = )}
- {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( + {(columns.length > 0 || searchQuery !== defaultSearchQuery) && indexPattern !== undefined && ( <> @@ -200,19 +118,10 @@ export const OutlierExploration: FC = React.memo(({ jobId }) = {columns.length > 0 && tableItems.length > 0 && ( - )} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.test.ts new file mode 100644 index 0000000000000..91a5ba2db6908 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.test.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DataFrameAnalyticsConfig } from '../../../../common'; + +import { getOutlierScoreFieldName } from './common'; + +describe('Data Frame Analytics: common utils', () => { + test('getOutlierScoreFieldName()', () => { + const jobConfig: DataFrameAnalyticsConfig = { + id: 'the-id', + analysis: { outlier_detection: {} }, + dest: { + index: 'the-dest-index', + results_field: 'the-results-field', + }, + source: { + index: 'the-source-index', + }, + analyzed_fields: { includes: [], excludes: [] }, + model_memory_limit: '50mb', + create_time: 1234, + version: '1.0.0', + }; + + const outlierScoreFieldName = getOutlierScoreFieldName(jobConfig); + + expect(outlierScoreFieldName).toMatch('the-results-field.outlier_score'); + }); +}); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts new file mode 100644 index 0000000000000..0d06bc0d43307 --- /dev/null +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect } from 'react'; + +import { EuiDataGridColumn } from '@elastic/eui'; + +import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; + +import { + useColorRange, + COLOR_RANGE, + COLOR_RANGE_SCALE, +} from '../../../../../components/color_range_legend'; +import { + getDataGridSchemasFromFieldTypes, + useDataGrid, + useRenderCellValue, + UseIndexDataReturnType, +} from '../../../../../components/data_grid'; +import { SavedSearchQuery } from '../../../../../contexts/ml'; + +import { getIndexData, getIndexFields, DataFrameAnalyticsConfig } from '../../../../common'; +import { DEFAULT_RESULTS_FIELD, FEATURE_INFLUENCE } from '../../../../common/constants'; +import { sortExplorationResultsFields, ML__ID_COPY } from '../../../../common/fields'; + +import { getFeatureCount, getOutlierScoreFieldName } from './common'; + +export const useOutlierData = ( + indexPattern: IndexPattern | undefined, + jobConfig: DataFrameAnalyticsConfig | undefined, + searchQuery: SavedSearchQuery +): UseIndexDataReturnType => { + const needsDestIndexFields = + indexPattern !== undefined && indexPattern.title === jobConfig?.source.index[0]; + + const columns: EuiDataGridColumn[] = []; + + if (jobConfig !== undefined && indexPattern !== undefined) { + const resultsField = jobConfig.dest.results_field; + const { fieldTypes } = getIndexFields(jobConfig, needsDestIndexFields); + columns.push( + ...getDataGridSchemasFromFieldTypes(fieldTypes, resultsField).sort((a: any, b: any) => + sortExplorationResultsFields(a.id, b.id, jobConfig) + ) + ); + } + + const dataGrid = useDataGrid( + columns, + 25, + // reduce default selected rows from 20 to 8 for performance reasons. + 8, + // by default, hide feature-influence columns and the doc id copy + d => !d.includes(`.${FEATURE_INFLUENCE}.`) && d !== ML__ID_COPY + ); + + // initialize sorting: reverse sort on outlier score column + useEffect(() => { + if (jobConfig !== undefined) { + dataGrid.setSortingColumns([{ id: getOutlierScoreFieldName(jobConfig), direction: 'desc' }]); + } + }, [jobConfig && jobConfig.id]); + + useEffect(() => { + getIndexData(jobConfig, dataGrid, searchQuery); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [jobConfig && jobConfig.id, dataGrid.pagination, searchQuery, dataGrid.sortingColumns]); + + const colorRange = useColorRange( + COLOR_RANGE.BLUE, + COLOR_RANGE_SCALE.INFLUENCER, + jobConfig !== undefined ? getFeatureCount(jobConfig.dest.results_field, dataGrid.tableItems) : 1 + ); + + const renderCellValue = useRenderCellValue( + indexPattern, + dataGrid.pagination, + dataGrid.tableItems, + jobConfig?.dest.results_field ?? DEFAULT_RESULTS_FIELD, + (columnId, cellValue, fullItem, setCellProps) => { + const resultsField = jobConfig?.dest.results_field ?? ''; + + const split = columnId.split('.'); + let backgroundColor; + + // column with feature values get color coded by its corresponding influencer value + if ( + fullItem[resultsField] !== undefined && + fullItem[resultsField][`${FEATURE_INFLUENCE}.${columnId}`] !== undefined + ) { + backgroundColor = colorRange(fullItem[resultsField][`${FEATURE_INFLUENCE}.${columnId}`]); + } + + // column with influencer values get color coded by its own value + if (split.length > 2 && split[0] === resultsField && split[1] === FEATURE_INFLUENCE) { + backgroundColor = colorRange(cellValue); + } + + if (backgroundColor !== undefined) { + setCellProps({ + style: { backgroundColor }, + }); + } + } + ); + + return { + ...dataGrid, + columns, + renderCellValue, + }; +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx index 6ef6666be5ec6..f6e8e0047671f 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx @@ -235,7 +235,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) href={`${ELASTIC_WEBSITE_URL}guide/en/machine-learning/${DOC_LINK_VERSION}/ml-dfanalytics-evaluate.html#ml-dfanalytics-regression-evaluation`} > {i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.regressionDocsLink', + 'xpack.ml.dataframe.analytics.regressionExploration.regressionDocsLink', { defaultMessage: 'Regression evaluation docs ', } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx index bfeca76a2b1c7..36d91f6f41d44 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx @@ -4,174 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, Fragment, useState, useEffect } from 'react'; -import { EuiCallOut, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { ml } from '../../../../../services/ml_api_service'; -import { DataFrameAnalyticsConfig } from '../../../../common'; -import { EvaluatePanel } from './evaluate_panel'; -import { ResultsTable } from './results_table'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics'; -import { LoadingPanel } from '../loading_panel'; -import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; -import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/common/index_patterns'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { useMlContext } from '../../../../../contexts/ml'; -import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; +import React, { FC } from 'react'; -export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => ( - - - {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.tableJobIdTitle', { - defaultMessage: 'Destination index for regression job ID {jobId}', - values: { jobId }, - })} - - -); +import { i18n } from '@kbn/i18n'; -const jobConfigErrorTitle = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.jobConfigurationFetchError', - { - defaultMessage: - 'Unable to fetch results. An error occurred loading the job configuration data.', - } -); +import { ExplorationPageWrapper } from '../exploration_page_wrapper'; -const jobCapsErrorTitle = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.jobCapsFetchError', - { - defaultMessage: "Unable to fetch results. An error occurred loading the index's field data.", - } -); +import { EvaluatePanel } from './evaluate_panel'; interface Props { jobId: string; } export const RegressionExploration: FC = ({ jobId }) => { - const [jobConfig, setJobConfig] = useState(undefined); - const [jobStatus, setJobStatus] = useState(undefined); - const [indexPattern, setIndexPattern] = useState(undefined); - const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false); - const [isInitialized, setIsInitialized] = useState(false); - const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined); - const [jobCapsServiceErrorMessage, setJobCapsServiceErrorMessage] = useState( - undefined - ); - const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const mlContext = useMlContext(); - - const loadJobConfig = async () => { - setIsLoadingJobConfig(true); - try { - const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); - const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); - const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) - ? analyticsStats.data_frame_analytics[0] - : undefined; - - if (stats !== undefined && stats.state) { - setJobStatus(stats.state); - } - - if ( - Array.isArray(analyticsConfigs.data_frame_analytics) && - analyticsConfigs.data_frame_analytics.length > 0 - ) { - setJobConfig(analyticsConfigs.data_frame_analytics[0]); - setIsLoadingJobConfig(false); - } - } catch (e) { - if (e.message !== undefined) { - setJobConfigErrorMessage(e.message); - } else { - setJobConfigErrorMessage(JSON.stringify(e)); - } - setIsLoadingJobConfig(false); - } - }; - - useEffect(() => { - loadJobConfig(); - }, []); - - const initializeJobCapsService = async () => { - if (jobConfig !== undefined) { - try { - const destIndex = Array.isArray(jobConfig.dest.index) - ? jobConfig.dest.index[0] - : jobConfig.dest.index; - const destIndexPatternId = getIndexPatternIdFromName(destIndex) || destIndex; - let indexP: IIndexPattern | undefined; - - try { - indexP = await mlContext.indexPatterns.get(destIndexPatternId); - } catch (e) { - indexP = undefined; - } - - if (indexP === undefined) { - const sourceIndex = jobConfig.source.index[0]; - const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; - indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); - } - - if (indexP !== undefined) { - setIndexPattern(indexP); - await newJobCapsService.initializeFromIndexPattern(indexP, false, false); - } - setIsInitialized(true); - } catch (e) { - if (e.message !== undefined) { - setJobCapsServiceErrorMessage(e.message); - } else { - setJobCapsServiceErrorMessage(JSON.stringify(e)); - } - } - } - }; - - useEffect(() => { - initializeJobCapsService(); - }, [jobConfig && jobConfig.id]); - - if (jobConfigErrorMessage !== undefined || jobCapsServiceErrorMessage !== undefined) { - return ( - - - - -

{jobConfigErrorMessage ? jobConfigErrorMessage : jobCapsServiceErrorMessage}

-
-
- ); - } - return ( - - {isLoadingJobConfig === true && jobConfig === undefined && } - {isLoadingJobConfig === false && jobConfig !== undefined && isInitialized === true && ( - - )} - - {isLoadingJobConfig === true && jobConfig === undefined && } - {isLoadingJobConfig === false && - jobConfig !== undefined && - indexPattern !== undefined && - isInitialized === true && ( - - )} - + ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration_data_grid.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration_data_grid.tsx deleted file mode 100644 index 0fcb1ed600719..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration_data_grid.tsx +++ /dev/null @@ -1,135 +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, { Dispatch, FC, SetStateAction, useCallback, useMemo } from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { EuiDataGrid, EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { euiDataGridStyle, euiDataGridToolbarSettings } from '../../../../common'; - -import { mlFieldFormatService } from '../../../../../services/field_format_service'; - -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - -const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; - -type Pagination = Pick; -type TableItem = Record; - -interface ExplorationDataGridProps { - colorRange?: (d: number) => string; - columns: any[]; - indexPattern: IndexPattern; - pagination: Pagination; - resultsField: string; - rowCount: number; - selectedFields: string[]; - setPagination: Dispatch>; - setSelectedFields: Dispatch>; - setSortingColumns: Dispatch>; - sortingColumns: EuiDataGridSorting['columns']; - tableItems: TableItem[]; -} - -export const RegressionExplorationDataGrid: FC = ({ - columns, - indexPattern, - pagination, - resultsField, - rowCount, - selectedFields, - setPagination, - setSelectedFields, - setSortingColumns, - sortingColumns, - tableItems, -}) => { - const renderCellValue = useMemo(() => { - return ({ rowIndex, columnId }: { rowIndex: number; columnId: string; setCellProps: any }) => { - const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - const fullItem = tableItems[adjustedRowIndex]; - - if (fullItem === undefined) { - return null; - } - - let format: any; - - if (indexPattern !== undefined) { - format = mlFieldFormatService.getFieldFormatFromIndexPattern(indexPattern, columnId, ''); - } - - const cellValue = - fullItem.hasOwnProperty(columnId) && fullItem[columnId] !== undefined - ? fullItem[columnId] - : null; - - if (format !== undefined) { - return format.convert(cellValue, 'text'); - } - - if (typeof cellValue === 'string' || cellValue === null) { - return cellValue; - } - - if (typeof cellValue === 'boolean') { - return cellValue ? 'true' : 'false'; - } - - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - - return cellValue; - }; - }, [resultsField, rowCount, tableItems, pagination.pageIndex, pagination.pageSize]); - - const onChangeItemsPerPage = useCallback( - pageSize => { - setPagination(p => { - const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); - return { pageIndex, pageSize }; - }); - }, - [setPagination] - ); - - const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ - setPagination, - ]); - - const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]); - - return ( - - ); -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx deleted file mode 100644 index 43fa50b2e4df5..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx +++ /dev/null @@ -1,233 +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, FC, useEffect } from 'react'; - -import { i18n } from '@kbn/i18n'; -import { - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiPanel, - EuiProgress, - EuiSpacer, - EuiText, -} from '@elastic/eui'; - -import { - BASIC_NUMERICAL_TYPES, - EXTENDED_NUMERICAL_TYPES, - sortRegressionResultsFields, -} from '../../../../common/fields'; - -import { - DataFrameAnalyticsConfig, - MAX_COLUMNS, - INDEX_STATUS, - SEARCH_SIZE, - defaultSearchQuery, -} from '../../../../common'; -import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - -import { useExploreData } from './use_explore_data'; -import { ExplorationTitle } from './regression_exploration'; -import { RegressionExplorationDataGrid } from './regression_exploration_data_grid'; -import { ExplorationQueryBar } from '../exploration_query_bar'; - -const showingDocs = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.documentsShownHelpText', - { - defaultMessage: 'Showing documents for which predictions exist', - } -); - -const showingFirstDocs = i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.firstDocumentsShownHelpText', - { - defaultMessage: 'Showing first {searchSize} documents for which predictions exist', - values: { searchSize: SEARCH_SIZE }, - } -); - -interface Props { - indexPattern: IndexPattern; - jobConfig: DataFrameAnalyticsConfig; - jobStatus?: DATA_FRAME_TASK_STATE; - setEvaluateSearchQuery: React.Dispatch>; -} - -export const ResultsTable: FC = React.memo( - ({ indexPattern, jobConfig, jobStatus, setEvaluateSearchQuery }) => { - const needsDestIndexFields = indexPattern && indexPattern.title === jobConfig.source.index[0]; - const resultsField = jobConfig.dest.results_field; - const { - errorMessage, - fieldTypes, - pagination, - searchQuery, - selectedFields, - rowCount, - setPagination, - setSearchQuery, - setSelectedFields, - setSortingColumns, - sortingColumns, - status, - tableFields, - tableItems, - } = useExploreData(jobConfig, needsDestIndexFields); - - useEffect(() => { - setEvaluateSearchQuery(searchQuery); - }, [JSON.stringify(searchQuery)]); - - const columns = tableFields - .sort((a: any, b: any) => sortRegressionResultsFields(a, b, jobConfig)) - .map((field: any) => { - // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] - // To fall back to the default string schema it needs to be undefined. - let schema; - let isSortable = true; - const type = fieldTypes[field]; - const isNumber = - type !== undefined && - (BASIC_NUMERICAL_TYPES.has(type) || EXTENDED_NUMERICAL_TYPES.has(type)); - - if (isNumber) { - schema = 'numeric'; - } - - switch (type) { - case 'date': - schema = 'datetime'; - break; - case 'geo_point': - schema = 'json'; - break; - case 'boolean': - schema = 'boolean'; - break; - } - - if (field === `${resultsField}.feature_importance`) { - isSortable = false; - } - - return { id: field, schema, isSortable }; - }); - - const docFieldsCount = tableFields.length; - - if (jobConfig === undefined) { - return null; - } - // if it's a searchBar syntax error leave the table visible so they can try again - if (status === INDEX_STATUS.ERROR && !errorMessage.includes('parsing_exception')) { - return ( - - - - - - {jobStatus !== undefined && ( - - {getTaskStateBadge(jobStatus)} - - )} - - -

{errorMessage}

-
-
- ); - } - - return ( - - - - - - - - {jobStatus !== undefined && ( - - {getTaskStateBadge(jobStatus)} - - )} - - - - - - {docFieldsCount > MAX_COLUMNS && ( - - {i18n.translate( - 'xpack.ml.dataframe.analytics.regressionExploration.fieldSelection', - { - defaultMessage: - '{selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}} selected', - values: { selectedFieldsLength: selectedFields.length, docFieldsCount }, - } - )} - - )} - - - - - {status === INDEX_STATUS.LOADING && } - {status !== INDEX_STATUS.LOADING && ( - - )} - {(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( - - - - - - - - - - - - - - - - - )} - - ); - } -); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts deleted file mode 100644 index 978aafd10de11..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/use_explore_data.ts +++ /dev/null @@ -1,291 +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 { useEffect, useState, Dispatch, SetStateAction } from 'react'; -import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { SearchResponse } from 'elasticsearch'; -import { cloneDeep } from 'lodash'; - -import { ml } from '../../../../../services/ml_api_service'; -import { getNestedProperty } from '../../../../../util/object_utils'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { SORT_DIRECTION } from '../../../../../components/ml_in_memory_table'; -import { SavedSearchQuery } from '../../../../../contexts/ml'; - -import { - getDefaultFieldsFromJobCaps, - getDependentVar, - getFlattenedFields, - getPredictedFieldName, - DataFrameAnalyticsConfig, - EsFieldName, - INDEX_STATUS, -} from '../../../../common'; -import { Dictionary } from '../../../../../../../common/types/common'; -import { isKeywordAndTextType } from '../../../../common/fields'; -import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; -import { - LoadExploreDataArg, - defaultSearchQuery, - ResultsSearchQuery, - isResultsSearchBoolQuery, -} from '../../../../common/analytics'; - -export type TableItem = Record; -type Pagination = Pick; - -export interface UseExploreDataReturnType { - errorMessage: string; - fieldTypes: { [key: string]: ES_FIELD_TYPES }; - pagination: Pagination; - rowCount: number; - searchQuery: SavedSearchQuery; - selectedFields: EsFieldName[]; - setFilterByIsTraining: Dispatch>; - setPagination: Dispatch>; - setSearchQuery: Dispatch>; - setSelectedFields: Dispatch>; - setSortingColumns: Dispatch>; - sortingColumns: EuiDataGridSorting['columns']; - status: INDEX_STATUS; - tableFields: string[]; - tableItems: TableItem[]; -} - -type EsSorting = Dictionary<{ - order: 'asc' | 'desc'; -}>; - -// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. -interface SearchResponse7 extends SearchResponse { - hits: SearchResponse['hits'] & { - total: { - value: number; - relation: string; - }; - }; -} - -export const useExploreData = ( - jobConfig: DataFrameAnalyticsConfig, - needsDestIndexFields: boolean -): UseExploreDataReturnType => { - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(INDEX_STATUS.UNUSED); - - const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); - const [tableFields, setTableFields] = useState([]); - const [tableItems, setTableItems] = useState([]); - const [fieldTypes, setFieldTypes] = useState<{ [key: string]: ES_FIELD_TYPES }>({}); - const [rowCount, setRowCount] = useState(0); - - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 }); - const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const [filterByIsTraining, setFilterByIsTraining] = useState(undefined); - const [sortingColumns, setSortingColumns] = useState([]); - - const predictedFieldName = getPredictedFieldName( - jobConfig.dest.results_field, - jobConfig.analysis - ); - const dependentVariable = getDependentVar(jobConfig.analysis); - - const getDefaultSelectedFields = () => { - const { fields } = newJobCapsService; - if (selectedFields.length === 0 && jobConfig !== undefined) { - const { selectedFields: defaultSelected, docFields } = getDefaultFieldsFromJobCaps( - fields, - jobConfig, - needsDestIndexFields - ); - - const types: { [key: string]: ES_FIELD_TYPES } = {}; - const allFields: string[] = []; - - docFields.forEach(field => { - types[field.id] = field.type; - allFields.push(field.id); - }); - - setFieldTypes(types); - setSelectedFields(defaultSelected.map(field => field.id)); - setTableFields(allFields); - } - }; - - const loadExploreData = async ({ - filterByIsTraining: isTraining, - searchQuery: incomingQuery, - }: LoadExploreDataArg) => { - if (jobConfig !== undefined) { - setErrorMessage(''); - setStatus(INDEX_STATUS.LOADING); - - try { - const resultsField = jobConfig.dest.results_field; - const searchQueryClone: ResultsSearchQuery = cloneDeep(incomingQuery); - let query: ResultsSearchQuery; - const { pageIndex, pageSize } = pagination; - // If filterByIsTraining is defined - add that in to the final query - const trainingQuery = - isTraining !== undefined - ? { - term: { [`${resultsField}.is_training`]: { value: isTraining } }, - } - : undefined; - - if (JSON.stringify(incomingQuery) === JSON.stringify(defaultSearchQuery)) { - const existsQuery = { - exists: { - field: resultsField, - }, - }; - - query = { - bool: { - must: [existsQuery], - }, - }; - - if (trainingQuery !== undefined && isResultsSearchBoolQuery(query)) { - query.bool.must.push(trainingQuery); - } - } else if (isResultsSearchBoolQuery(searchQueryClone)) { - if (searchQueryClone.bool.must === undefined) { - searchQueryClone.bool.must = []; - } - - searchQueryClone.bool.must.push({ - exists: { - field: resultsField, - }, - }); - - if (trainingQuery !== undefined) { - searchQueryClone.bool.must.push(trainingQuery); - } - - query = searchQueryClone; - } else { - query = searchQueryClone; - } - - const sort: EsSorting = sortingColumns - .map(column => { - const { id } = column; - column.id = isKeywordAndTextType(id) ? `${id}.keyword` : id; - return column; - }) - .reduce((s, column) => { - s[column.id] = { order: column.direction }; - return s; - }, {} as EsSorting); - - const resp: SearchResponse7 = await ml.esSearch({ - index: jobConfig.dest.index, - body: { - query, - from: pageIndex * pageSize, - size: pageSize, - ...(Object.keys(sort).length > 0 ? { sort } : {}), - }, - }); - - setRowCount(resp.hits.total.value); - - const docs = resp.hits.hits; - - if (docs.length === 0) { - setTableItems([]); - setStatus(INDEX_STATUS.LOADED); - return; - } - - // Create a version of the doc's source with flattened field names. - // This avoids confusion later on if a field name has dots in its name - // or is a nested fields when displaying it via EuiInMemoryTable. - const flattenedFields = getFlattenedFields(docs[0]._source, resultsField); - const transformedTableItems = docs.map(doc => { - const item: TableItem = {}; - flattenedFields.forEach(ff => { - item[ff] = getNestedProperty(doc._source, ff); - if (item[ff] === undefined) { - // If the attribute is undefined, it means it was not a nested property - // but had dots in its actual name. This selects the property by its - // full name and assigns it to `item[ff]`. - item[ff] = doc._source[`"${ff}"`]; - } - if (item[ff] === undefined) { - const parts = ff.split('.'); - if (parts[0] === resultsField && parts.length >= 2) { - parts.shift(); - if (doc._source[resultsField] !== undefined) { - item[ff] = doc._source[resultsField][parts.join('.')]; - } - } - } - }); - return item; - }); - - setTableItems(transformedTableItems); - setStatus(INDEX_STATUS.LOADED); - } catch (e) { - if (e.message !== undefined) { - setErrorMessage(e.message); - } else { - setErrorMessage(JSON.stringify(e)); - } - setTableItems([]); - setStatus(INDEX_STATUS.ERROR); - } - } - }; - - useEffect(() => { - getDefaultSelectedFields(); - }, [jobConfig && jobConfig.id]); - - // By default set sorting to descending on the prediction field (`_prediction`). - useEffect(() => { - const sortByField = isKeywordAndTextType(dependentVariable) - ? `${predictedFieldName}.keyword` - : predictedFieldName; - const direction = SORT_DIRECTION.DESC; - - setSortingColumns([{ id: sortByField, direction }]); - }, [jobConfig && jobConfig.id]); - - useEffect(() => { - loadExploreData({ filterByIsTraining, searchQuery }); - }, [ - filterByIsTraining, - jobConfig && jobConfig.id, - pagination, - searchQuery, - selectedFields, - sortingColumns, - ]); - - return { - errorMessage, - fieldTypes, - pagination, - searchQuery, - selectedFields, - rowCount, - setFilterByIsTraining, - setPagination, - setSelectedFields, - setSortingColumns, - setSearchQuery, - sortingColumns, - status, - tableItems, - tableFields, - }; -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts deleted file mode 100644 index a0a9eb8312499..0000000000000 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts +++ /dev/null @@ -1,267 +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 { useEffect, useState, Dispatch, SetStateAction } from 'react'; -import { SearchResponse } from 'elasticsearch'; - -import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; - -import { Dictionary } from '../../../../../../../common/types/common'; - -import { SavedSearchQuery } from '../../../../../contexts/ml'; -import { ml } from '../../../../../services/ml_api_service'; -import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; -import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; -import { getNestedProperty } from '../../../../../util/object_utils'; -import { useMlContext } from '../../../../../contexts/ml'; -import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; - -import { - getDefaultSelectableFields, - getFlattenedFields, - DataFrameAnalyticsConfig, - EsFieldName, - INDEX_STATUS, - MAX_COLUMNS, - defaultSearchQuery, -} from '../../../../common'; -import { isKeywordAndTextType } from '../../../../common/fields'; - -import { getOutlierScoreFieldName } from './common'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; - -export type TableItem = Record; - -type Pagination = Pick; - -interface UseExploreDataReturnType { - errorMessage: string; - indexPattern: IndexPattern | undefined; - jobConfig: DataFrameAnalyticsConfig | undefined; - jobStatus: DATA_FRAME_TASK_STATE | undefined; - pagination: Pagination; - searchQuery: SavedSearchQuery; - selectedFields: EsFieldName[]; - setJobConfig: Dispatch>; - setPagination: Dispatch>; - setSearchQuery: Dispatch>; - setSelectedFields: Dispatch>; - setSortingColumns: Dispatch>; - rowCount: number; - sortingColumns: EuiDataGridSorting['columns']; - status: INDEX_STATUS; - tableFields: string[]; - tableItems: TableItem[]; -} - -type EsSorting = Dictionary<{ - order: 'asc' | 'desc'; -}>; - -// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. -interface SearchResponse7 extends SearchResponse { - hits: SearchResponse['hits'] & { - total: { - value: number; - relation: string; - }; - }; -} - -export const useExploreData = (jobId: string): UseExploreDataReturnType => { - const mlContext = useMlContext(); - - const [indexPattern, setIndexPattern] = useState(undefined); - const [jobConfig, setJobConfig] = useState(undefined); - const [jobStatus, setJobStatus] = useState(undefined); - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(INDEX_STATUS.UNUSED); - - const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]); - const [tableFields, setTableFields] = useState([]); - const [tableItems, setTableItems] = useState([]); - const [rowCount, setRowCount] = useState(0); - - const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 }); - const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); - const [sortingColumns, setSortingColumns] = useState([]); - - // get analytics configuration - useEffect(() => { - (async function() { - const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); - const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); - const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) - ? analyticsStats.data_frame_analytics[0] - : undefined; - - if (stats !== undefined && stats.state) { - setJobStatus(stats.state); - } - - if ( - Array.isArray(analyticsConfigs.data_frame_analytics) && - analyticsConfigs.data_frame_analytics.length > 0 - ) { - setJobConfig(analyticsConfigs.data_frame_analytics[0]); - } - })(); - }, []); - - // get index pattern and field caps - useEffect(() => { - (async () => { - if (jobConfig !== undefined) { - try { - const destIndex = Array.isArray(jobConfig.dest.index) - ? jobConfig.dest.index[0] - : jobConfig.dest.index; - const destIndexPatternId = getIndexPatternIdFromName(destIndex) || destIndex; - let indexP: IndexPattern | undefined; - - try { - indexP = await mlContext.indexPatterns.get(destIndexPatternId); - } catch (e) { - indexP = undefined; - } - - if (indexP === undefined) { - const sourceIndex = jobConfig.source.index[0]; - const sourceIndexPatternId = getIndexPatternIdFromName(sourceIndex) || sourceIndex; - indexP = await mlContext.indexPatterns.get(sourceIndexPatternId); - } - - if (indexP !== undefined) { - setIndexPattern(indexP); - await newJobCapsService.initializeFromIndexPattern(indexP, false, false); - } - } catch (e) { - // eslint-disable-next-line - console.log('Error loading index field data', e); - } - } - })(); - }, [jobConfig && jobConfig.id]); - - // initialize sorting: reverse sort on outlier score column - useEffect(() => { - if (jobConfig !== undefined) { - setSortingColumns([{ id: getOutlierScoreFieldName(jobConfig), direction: 'desc' }]); - } - }, [jobConfig && jobConfig.id]); - - // update data grid data - useEffect(() => { - (async () => { - if (jobConfig !== undefined) { - setErrorMessage(''); - setStatus(INDEX_STATUS.LOADING); - - try { - const resultsField = jobConfig.dest.results_field; - - const sort: EsSorting = sortingColumns - .map(column => { - const { id } = column; - column.id = isKeywordAndTextType(id) ? `${id}.keyword` : id; - return column; - }) - .reduce((s, column) => { - s[column.id] = { order: column.direction }; - return s; - }, {} as EsSorting); - - const { pageIndex, pageSize } = pagination; - const resp: SearchResponse7 = await ml.esSearch({ - index: jobConfig.dest.index, - body: { - query: searchQuery, - from: pageIndex * pageSize, - size: pageSize, - ...(Object.keys(sort).length > 0 ? { sort } : {}), - }, - }); - - setRowCount(resp.hits.total.value); - - const docs = resp.hits.hits; - - if (docs.length === 0) { - setTableItems([]); - setStatus(INDEX_STATUS.LOADED); - return; - } - - if (selectedFields.length === 0) { - const newSelectedFields = getDefaultSelectableFields(docs, resultsField); - setSelectedFields(newSelectedFields.sort().splice(0, MAX_COLUMNS)); - } - - // Create a version of the doc's source with flattened field names. - // This avoids confusion later on if a field name has dots in its name - // or is a nested fields when displaying it via EuiInMemoryTable. - const flattenedFields = getFlattenedFields(docs[0]._source, resultsField); - const transformedTableItems = docs.map(doc => { - const item: TableItem = {}; - flattenedFields.forEach(ff => { - item[ff] = getNestedProperty(doc._source, ff); - if (item[ff] === undefined) { - // If the attribute is undefined, it means it was not a nested property - // but had dots in its actual name. This selects the property by its - // full name and assigns it to `item[ff]`. - item[ff] = doc._source[`"${ff}"`]; - } - if (item[ff] === undefined) { - const parts = ff.split('.'); - if (parts[0] === resultsField && parts.length >= 2) { - parts.shift(); - if (doc._source[resultsField] !== undefined) { - item[ff] = doc._source[resultsField][parts.join('.')]; - } - } - } - }); - return item; - }); - - setTableFields(flattenedFields); - setTableItems(transformedTableItems); - setStatus(INDEX_STATUS.LOADED); - } catch (e) { - if (e.message !== undefined) { - setErrorMessage(e.message); - } else { - setErrorMessage(JSON.stringify(e)); - } - setTableItems([]); - setStatus(INDEX_STATUS.ERROR); - } - } - })(); - }, [jobConfig && jobConfig.id, pagination, searchQuery, selectedFields, sortingColumns]); - - return { - errorMessage, - indexPattern, - jobConfig, - jobStatus, - pagination, - rowCount, - searchQuery, - selectedFields, - setJobConfig, - setPagination, - setSearchQuery, - setSelectedFields, - setSortingColumns, - sortingColumns, - status, - tableFields, - tableItems, - }; -}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx index eb1871c98764b..8c65af1d92959 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_clone.tsx @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n'; import { DeepReadonly } from '../../../../../../../common/types/common'; import { DataFrameAnalyticsConfig, isOutlierAnalysis } from '../../../../common'; import { isClassificationAnalysis, isRegressionAnalysis } from '../../../../common/analytics'; +import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants'; import { CreateAnalyticsFormProps, DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES, @@ -214,7 +215,7 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo }, results_field: { optional: true, - defaultValue: 'ml', + defaultValue: DEFAULT_RESULTS_FIELD, }, }, model_memory_limit: { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx index 6d1db5033863b..3e5224b76329e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.test.tsx @@ -7,13 +7,13 @@ import React from 'react'; import { render } from '@testing-library/react'; -import * as CheckPrivilige from '../../../../../privilege/check_privilege'; +import * as CheckPrivilige from '../../../../../capabilities/check_capabilities'; import { DeleteAction } from './action_delete'; import mockAnalyticsListItem from './__mocks__/analytics_list_item.json'; -jest.mock('../../../../../privilege/check_privilege', () => ({ +jest.mock('../../../../../capabilities/check_capabilities', () => ({ checkPermission: jest.fn(() => false), createPermissionFailureMessage: jest.fn(), })); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx index 47fc84cf450c0..2923938ae68ac 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_delete.tsx @@ -19,7 +19,7 @@ import { deleteAnalytics } from '../../services/analytics_service'; import { checkPermission, createPermissionFailureMessage, -} from '../../../../../privilege/check_privilege'; +} from '../../../../../capabilities/check_capabilities'; import { isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx index 40664a1413845..bbbda85c45e49 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/action_start.tsx @@ -19,7 +19,7 @@ import { startAnalytics } from '../../services/analytics_service'; import { checkPermission, createPermissionFailureMessage, -} from '../../../../../privilege/check_privilege'; +} from '../../../../../capabilities/check_capabilities'; import { DataFrameAnalyticsListRow, isCompletedAnalyticsJob } from './common'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx index 4e19df9ae22a8..72514c91ff58b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx @@ -12,7 +12,7 @@ import { DeepReadonly } from '../../../../../../../common/types/common'; import { checkPermission, createPermissionFailureMessage, -} from '../../../../../privilege/check_privilege'; +} from '../../../../../capabilities/check_capabilities'; import { getAnalysisType, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index d2e5f582d23f6..58bc75bd7309b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -18,7 +18,7 @@ import { } from '@elastic/eui'; import { DataFrameAnalyticsId, useRefreshAnalyticsList } from '../../../../common'; -import { checkPermission } from '../../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../../capabilities/check_capabilities'; import { getTaskStateBadge } from './columns'; import { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx index e5054e8a6ad2c..3c8c3c3b3aa55 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_button/create_analytics_button.tsx @@ -7,7 +7,7 @@ import React, { FC } from 'react'; import { EuiButton, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { createPermissionFailureMessage } from '../../../../../privilege/check_privilege'; +import { createPermissionFailureMessage } from '../../../../../capabilities/check_capabilities'; import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form'; export const CreateAnalyticsButton: FC = props => { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index e121268e65e86..70840a442f6f6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -6,7 +6,7 @@ import { EuiComboBoxOptionOption } from '@elastic/eui'; import { DeepPartial, DeepReadonly } from '../../../../../../../common/types/common'; -import { checkPermission } from '../../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../../../../ml_nodes_check'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/filebeat_config_flyout.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/filebeat_config_flyout.tsx index 30fc74acbabf4..32b51c8b7d4ee 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/filebeat_config_flyout.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/filebeat_config_flyout/filebeat_config_flyout.tsx @@ -55,9 +55,11 @@ export const FilebeatConfigFlyout: FC = ({ } = useMlKibana(); useEffect(() => { - security.authc.getCurrentUser().then(user => { - setUsername(user.username === undefined ? null : user.username); - }); + if (security !== undefined) { + security.authc.getCurrentUser().then(user => { + setUsername(user.username === undefined ? null : user.username); + }); + } }, []); useEffect(() => { diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx index dddf64ce2cfd3..c05ca63df3a75 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/results_links/results_links.tsx @@ -10,7 +10,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiCard, EuiIcon } from '@elastic/eui'; import { ml } from '../../../../services/ml_api_service'; import { isFullLicense } from '../../../../license'; -import { checkPermission } from '../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; import { useMlKibana } from '../../../../contexts/kibana'; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx index 3a37274edbc16..86ffc4a2614b9 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -33,7 +33,7 @@ import { NavigationMenu } from '../../components/navigation_menu'; import { ML_JOB_FIELD_TYPES } from '../../../../common/constants/field_types'; import { SEARCH_QUERY_LANGUAGE } from '../../../../common/constants/search'; import { isFullLicense } from '../../license'; -import { checkPermission } from '../../privilege/check_privilege'; +import { checkPermission } from '../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../ml_nodes_check/check_ml_nodes'; import { FullTimeRangeSelector } from '../../components/full_time_range_selector'; import { mlTimefilterRefresh$ } from '../../services/timefilter_refresh_service'; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js index 92ad842686b05..bb4bed93f922e 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/management.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { checkPermission } from '../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; import { getIndexPatternNames } from '../../../../util/index_utils'; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js index 216c416f30a6b..9406f1b3456cf 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.js @@ -10,7 +10,7 @@ import React, { Component } from 'react'; import { EuiSpacer, EuiCallOut, EuiLoadingSpinner } from '@elastic/eui'; import { mlJobService } from '../../../../services/job_service'; -import { checkPermission } from '../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../capabilities/check_capabilities'; import { ML_DATA_PREVIEW_COUNT } from '../../../../../../common/util/job_utils'; import { MLJobEditor } from '../ml_job_editor'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js index a5509c0f79a36..2e530a66cd83d 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/actions_menu.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { checkPermission } from '../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js index 5f91ba9b6f107..e7b6e3a771a85 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js @@ -22,7 +22,7 @@ import { import { cloneDeep } from 'lodash'; import { ml } from '../../../../../services/ml_api_service'; -import { checkPermission } from '../../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../../capabilities/check_capabilities'; import { GroupList } from './group_list'; import { NewGroupInput } from './new_group_input'; import { mlMessageBarService } from '../../../../../components/messagebar'; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js index 1297ca5b9afd1..fdffa8b38ae04 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { checkPermission } from '../../../../privilege/check_privilege'; +import { checkPermission } from '../../../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../../../ml_nodes_check/check_ml_nodes'; import React from 'react'; diff --git a/x-pack/plugins/ml/public/application/management/index.ts b/x-pack/plugins/ml/public/application/management/index.ts index a6fe9e1d11953..6bc5c9b15074f 100644 --- a/x-pack/plugins/ml/public/application/management/index.ts +++ b/x-pack/plugins/ml/public/application/management/index.ts @@ -25,8 +25,11 @@ export function initManagementSection( ) { const licensing = pluginsSetup.licensing.license$.pipe(take(1)); licensing.subscribe(license => { - if (license.check(PLUGIN_ID, MINIMUM_FULL_LICENSE).state === 'valid') { - const management = pluginsSetup.management; + const management = pluginsSetup.management; + if ( + management !== undefined && + license.check(PLUGIN_ID, MINIMUM_FULL_LICENSE).state === 'valid' + ) { const mlSection = management.sections.register({ id: PLUGIN_ID, title: i18n.translate('xpack.ml.management.mlTitle', { diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index dca1235a96cb6..4a41f3e45001d 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -20,7 +20,7 @@ import { EuiTitle, } from '@elastic/eui'; -import { checkGetManagementMlJobs } from '../../../../privilege/check_privilege'; +import { checkGetManagementMlJobsResolver } from '../../../../capabilities/check_capabilities'; import { getDocLinks } from '../../../../util/dependency_cache'; // @ts-ignore undeclared module @@ -75,7 +75,7 @@ export const JobsListPage: FC<{ I18nContext: CoreStart['i18n']['Context'] }> = ( const check = async () => { try { - const checkPrivilege = await checkGetManagementMlJobs(); + const checkPrivilege = await checkGetManagementMlJobsResolver(); setInitialized(true); setIsMlEnabledInSpace(checkPrivilege.mlFeatureEnabledInSpace); } catch (e) { diff --git a/x-pack/plugins/ml/public/application/overview/overview_page.tsx b/x-pack/plugins/ml/public/application/overview/overview_page.tsx index 7c995c8a568d7..9a852c491ee27 100644 --- a/x-pack/plugins/ml/public/application/overview/overview_page.tsx +++ b/x-pack/plugins/ml/public/application/overview/overview_page.tsx @@ -6,7 +6,7 @@ import React, { Fragment, FC } from 'react'; import { EuiFlexGroup, EuiPage, EuiPageBody } from '@elastic/eui'; -import { checkPermission } from '../privilege/check_privilege'; +import { checkPermission } from '../capabilities/check_capabilities'; import { mlNodesAvailable } from '../ml_nodes_check/check_ml_nodes'; import { NavigationMenu } from '../components/navigation_menu'; import { OverviewSideBar } from './components/sidebar'; diff --git a/x-pack/plugins/ml/public/application/routing/resolvers.ts b/x-pack/plugins/ml/public/application/routing/resolvers.ts index 776f0727389dd..958221df8a636 100644 --- a/x-pack/plugins/ml/public/application/routing/resolvers.ts +++ b/x-pack/plugins/ml/public/application/routing/resolvers.ts @@ -6,7 +6,7 @@ import { loadIndexPatterns, loadSavedSearches } from '../util/index_utils'; import { checkFullLicense } from '../license'; -import { checkGetJobsPrivilege } from '../privilege/check_privilege'; +import { checkGetJobsCapabilitiesResolver } from '../capabilities/check_capabilities'; import { getMlNodeCount } from '../ml_nodes_check/check_ml_nodes'; import { loadMlServerInfo } from '../services/ml_server_info'; @@ -28,6 +28,6 @@ export const basicResolvers = ({ indexPatterns }: BasicResolverDependencies): Re getMlNodeCount, loadMlServerInfo, loadIndexPatterns: () => loadIndexPatterns(indexPatterns), - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, loadSavedSearches, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx index d257a9c080c35..fc2d517b2edb1 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx @@ -16,7 +16,7 @@ import { useResolver } from '../../use_resolver'; import { DatavisualizerSelector } from '../../../datavisualizer'; import { checkBasicLicense } from '../../../license'; -import { checkFindFileStructurePrivilege } from '../../../privilege/check_privilege'; +import { checkFindFileStructurePrivilegeResolver } from '../../../capabilities/check_capabilities'; import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; const breadcrumbs = [ML_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB]; @@ -30,7 +30,7 @@ export const selectorRoute: MlRoute = { const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkBasicLicense, - checkFindFileStructurePrivilege, + checkFindFileStructurePrivilege: checkFindFileStructurePrivilegeResolver, }); return ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx index 174b3e3b4b338..1115a38870821 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx @@ -17,7 +17,7 @@ import { useResolver } from '../../use_resolver'; import { FileDataVisualizerPage } from '../../../datavisualizer/file_based'; import { checkBasicLicense } from '../../../license'; -import { checkFindFileStructurePrivilege } from '../../../privilege/check_privilege'; +import { checkFindFileStructurePrivilegeResolver } from '../../../capabilities/check_capabilities'; import { loadIndexPatterns } from '../../../util/index_utils'; import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; @@ -43,7 +43,7 @@ const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver('', undefined, deps.config, { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), - checkFindFileStructurePrivilege, + checkFindFileStructurePrivilege: checkFindFileStructurePrivilegeResolver, }); return ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx index a3dbc9f97124c..1ec73fced82fe 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx @@ -12,7 +12,7 @@ import { useResolver } from '../../use_resolver'; import { Page } from '../../../datavisualizer/index_based'; import { checkBasicLicense } from '../../../license'; -import { checkGetJobsPrivilege } from '../../../privilege/check_privilege'; +import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; import { loadIndexPatterns } from '../../../util/index_utils'; import { checkMlNodesAvailable } from '../../../ml_nodes_check'; import { DATA_VISUALIZER_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; @@ -39,7 +39,7 @@ const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver(index, savedSearchId, deps.config, { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, checkMlNodesAvailable, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx index 9411b415e4e4d..b630b09b1a46d 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx @@ -13,7 +13,7 @@ import { Page, preConfiguredJobRedirect } from '../../../jobs/new_job/pages/inde import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; import { checkBasicLicense } from '../../../license'; import { loadIndexPatterns } from '../../../util/index_utils'; -import { checkGetJobsPrivilege } from '../../../privilege/check_privilege'; +import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; import { checkMlNodesAvailable } from '../../../ml_nodes_check'; enum MODE { @@ -71,7 +71,7 @@ const PageWrapper: FC = ({ nextStepPath, deps, mode }) = const dataVizResolvers = { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, checkMlNodesAvailable, }; diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx index b1256e21888d9..1c91d7e94b241 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx @@ -15,7 +15,7 @@ import { Page } from '../../../jobs/new_job/pages/new_job'; import { JOB_TYPE } from '../../../../../common/constants/new_job'; import { mlJobService } from '../../../services/job_service'; import { loadNewJobCapabilities } from '../../../services/new_job_capabilities_service'; -import { checkCreateJobsPrivilege } from '../../../privilege/check_privilege'; +import { checkCreateJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; import { ANOMALY_DETECTION_BREADCRUMB, ML_BREADCRUMB } from '../../breadcrumbs'; interface WizardPageProps extends PageProps { @@ -115,7 +115,7 @@ const PageWrapper: FC = ({ location, jobType, deps }) => { const { index, savedSearchId }: Record = parse(location.search, { sort: false }); const { context, results } = useResolver(index, savedSearchId, deps.config, { ...basicResolvers(deps), - privileges: checkCreateJobsPrivilege, + privileges: checkCreateJobsCapabilitiesResolver, jobCaps: () => loadNewJobCapabilities(index, savedSearchId, deps.indexPatterns), existingJobsAndGroups: mlJobService.getJobAndGroupIds, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/overview.tsx b/x-pack/plugins/ml/public/application/routing/routes/overview.tsx index ccb99985cb70c..9b08bbf35c448 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/overview.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/overview.tsx @@ -13,7 +13,7 @@ import { useResolver } from '../use_resolver'; import { OverviewPage } from '../../overview'; import { checkFullLicense } from '../../license'; -import { checkGetJobsPrivilege } from '../../privilege/check_privilege'; +import { checkGetJobsCapabilitiesResolver } from '../../capabilities/check_capabilities'; import { getMlNodeCount } from '../../ml_nodes_check'; import { loadMlServerInfo } from '../../services/ml_server_info'; import { useTimefilter } from '../../contexts/kibana'; @@ -38,7 +38,7 @@ export const overviewRoute: MlRoute = { const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, getMlNodeCount, loadMlServerInfo, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx index 9d5c4e9c0b0a0..e015a3292acc4 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx @@ -17,7 +17,10 @@ import { useResolver } from '../../use_resolver'; import { useTimefilter } from '../../../contexts/kibana'; import { checkFullLicense } from '../../../license'; -import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { + checkGetJobsCapabilitiesResolver, + checkPermission, +} from '../../../capabilities/check_capabilities'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { CalendarsList } from '../../../settings/calendars'; import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; @@ -42,7 +45,7 @@ export const calendarListRoute: MlRoute = { const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, getMlNodeCount, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx index bf039e3bd2354..ebd58120853a9 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx @@ -17,7 +17,10 @@ import { useResolver } from '../../use_resolver'; import { useTimefilter } from '../../../contexts/kibana'; import { checkFullLicense } from '../../../license'; -import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { + checkGetJobsCapabilitiesResolver, + checkPermission, +} from '../../../capabilities/check_capabilities'; import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { NewCalendar } from '../../../settings/calendars'; import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; @@ -74,7 +77,7 @@ const PageWrapper: FC = ({ location, mode, deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, checkMlNodesAvailable, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx index 6839ad833cb06..25bded1a52db1 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx @@ -17,7 +17,10 @@ import { useResolver } from '../../use_resolver'; import { useTimefilter } from '../../../contexts/kibana'; import { checkFullLicense } from '../../../license'; -import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { + checkGetJobsCapabilitiesResolver, + checkPermission, +} from '../../../capabilities/check_capabilities'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { FilterLists } from '../../../settings/filter_lists'; @@ -43,7 +46,7 @@ export const filterListRoute: MlRoute = { const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, getMlNodeCount, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx index 7b8bd6c3c81ac..2f4ccecf2f1a2 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx @@ -17,7 +17,10 @@ import { useResolver } from '../../use_resolver'; import { useTimefilter } from '../../../contexts/kibana'; import { checkFullLicense } from '../../../license'; -import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { + checkGetJobsCapabilitiesResolver, + checkPermission, +} from '../../../capabilities/check_capabilities'; import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { EditFilterList } from '../../../settings/filter_lists'; import { SETTINGS, ML_BREADCRUMB } from '../../breadcrumbs'; @@ -74,7 +77,7 @@ const PageWrapper: FC = ({ location, mode, deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, checkMlNodesAvailable, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx index 10ccc0987fe5d..7cb943c091c4e 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx @@ -16,7 +16,10 @@ import { useResolver } from '../../use_resolver'; import { useTimefilter } from '../../../contexts/kibana'; import { checkFullLicense } from '../../../license'; -import { checkGetJobsPrivilege, checkPermission } from '../../../privilege/check_privilege'; +import { + checkGetJobsCapabilitiesResolver, + checkPermission, +} from '../../../capabilities/check_capabilities'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { Settings } from '../../../settings'; import { ML_BREADCRUMB, SETTINGS } from '../../breadcrumbs'; @@ -32,7 +35,7 @@ export const settingsRoute: MlRoute = { const PageWrapper: FC = ({ deps }) => { const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsPrivilege, + checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, getMlNodeCount, }); diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts index e160126833801..6e3fd08e90e38 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts @@ -15,7 +15,7 @@ import { jobs } from './jobs'; import { fileDatavisualizer } from './datavisualizer'; import { MlServerDefaults, MlServerLimits } from '../../../../common/types/ml_server_info'; -import { PrivilegesResponse } from '../../../../common/types/privileges'; +import { MlCapabilitiesResponse } from '../../../../common/types/capabilities'; import { Calendar, CalendarId, UpdateCalendar } from '../../../../common/types/calendars'; import { Job, @@ -300,18 +300,17 @@ export const ml = { }); }, - checkMlPrivileges() { - return http({ + checkMlCapabilities() { + return http({ path: `${basePath()}/ml_capabilities`, method: 'GET', }); }, - checkManageMLPrivileges() { - return http({ + checkManageMLCapabilities() { + return http({ path: `${basePath()}/ml_capabilities`, method: 'GET', - query: { ignoreSpaces: true }, }); }, diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js index 7e2d6814c0b23..7f5ade64e7f14 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js @@ -7,15 +7,15 @@ jest.mock('../../../components/navigation_menu', () => ({ NavigationMenu: () =>
, })); -jest.mock('../../../privilege/check_privilege', () => ({ +jest.mock('../../../capabilities/check_capabilities', () => ({ checkPermission: () => true, })); jest.mock('../../../license', () => ({ hasLicenseExpired: () => false, isFullLicense: () => false, })); -jest.mock('../../../privilege/get_privileges', () => ({ - getPrivileges: () => {}, +jest.mock('../../../capabilities/get_capabilities', () => ({ + getCapabilities: () => {}, })); jest.mock('../../../ml_nodes_check/check_ml_nodes', () => ({ mlNodesAvailable: () => true, diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js b/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js index 8750927ac1ee7..b2fce2c1474cb 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.test.js @@ -13,15 +13,15 @@ import { CalendarsList } from './calendars_list'; jest.mock('../../../components/navigation_menu', () => ({ NavigationMenu: () =>
, })); -jest.mock('../../../privilege/check_privilege', () => ({ +jest.mock('../../../capabilities/check_capabilities', () => ({ checkPermission: () => true, })); jest.mock('../../../license', () => ({ hasLicenseExpired: () => false, isFullLicense: () => false, })); -jest.mock('../../../privilege/get_privileges', () => ({ - getPrivileges: () => {}, +jest.mock('../../../capabilities/get_capabilities', () => ({ + getCapabilities: () => {}, })); jest.mock('../../../ml_nodes_check/check_ml_nodes', () => ({ mlNodesAvailable: () => true, diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js b/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js index 8b1ab853676cf..0266bc2a55318 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.test.js @@ -8,7 +8,7 @@ // The mock is hoisted to the top, so need to prefix the mock function // with 'mock' so it can be used lazily. const mockCheckPermission = jest.fn(() => true); -jest.mock('../../../../privilege/check_privilege', () => ({ +jest.mock('../../../../capabilities/check_capabilities', () => ({ checkPermission: privilege => mockCheckPermission(privilege), })); jest.mock('../../../../services/ml_api_service', () => 'ml'); diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js b/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js index dbc815b5fc099..c1bcee4acdd37 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/list/filter_lists.test.js @@ -12,7 +12,7 @@ import { FilterLists } from './filter_lists'; jest.mock('../../../components/navigation_menu', () => ({ NavigationMenu: () =>
, })); -jest.mock('../../../privilege/check_privilege', () => ({ +jest.mock('../../../capabilities/check_capabilities', () => ({ checkPermission: () => true, })); diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.test.js b/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.test.js index a5c1c872bfa5a..29b1185ddd4ab 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.test.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.test.js @@ -6,7 +6,7 @@ // Create a mock for the privilege check used within the table to // enable/disable the 'New Filter' button. -jest.mock('../../../privilege/check_privilege', () => ({ +jest.mock('../../../capabilities/check_capabilities', () => ({ checkPermission: () => true, })); jest.mock('../../../services/ml_api_service', () => 'ml'); diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js index c04ac40b962d5..7dd06268f7f8d 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js @@ -32,7 +32,7 @@ import { mlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { checkPermission, createPermissionFailureMessage, -} from '../../../privilege/check_privilege'; +} from '../../../capabilities/check_capabilities'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/plugins/ml/public/application/util/dependency_cache.ts b/x-pack/plugins/ml/public/application/util/dependency_cache.ts index 934a0a5e9ae3a..f8dd2a37dd589 100644 --- a/x-pack/plugins/ml/public/application/util/dependency_cache.ts +++ b/x-pack/plugins/ml/public/application/util/dependency_cache.ts @@ -40,7 +40,7 @@ export interface DependencyCache { savedObjectsClient: SavedObjectsClientContract | null; application: ApplicationStart | null; http: HttpStart | null; - security: SecurityPluginSetup | null; + security: SecurityPluginSetup | undefined | null; i18n: I18nStart | null; urlGenerators: SharePluginStart['urlGenerators'] | null; mlConfig: MlConfigType | null; diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index b51be4d248683..e3b8441db432e 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -32,9 +32,9 @@ export interface MlStartDependencies { share: SharePluginStart; } export interface MlSetupDependencies { - security: SecurityPluginSetup; + security?: SecurityPluginSetup; licensing: LicensingPluginSetup; - management: ManagementSetup; + management?: ManagementSetup; usageCollection: UsageCollectionSetup; licenseManagement?: LicenseManagementUIPluginSetup; home: HomePublicPluginSetup; diff --git a/x-pack/plugins/ml/server/lib/capabilities/__mocks__/ml_capabilities.ts b/x-pack/plugins/ml/server/lib/capabilities/__mocks__/ml_capabilities.ts new file mode 100644 index 0000000000000..8e350b8382276 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/capabilities/__mocks__/ml_capabilities.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + adminMlCapabilities, + userMlCapabilities, + MlCapabilities, + getDefaultCapabilities, +} from '../../../../common/types/capabilities'; + +export function getAdminCapabilities() { + const caps: any = {}; + Object.keys(adminMlCapabilities).forEach(k => { + caps[k] = true; + }); + return { ...getUserCapabilities(), ...caps } as MlCapabilities; +} + +export function getUserCapabilities() { + const caps: any = {}; + Object.keys(userMlCapabilities).forEach(k => { + caps[k] = true; + }); + + return { ...getDefaultCapabilities(), ...caps } as MlCapabilities; +} diff --git a/x-pack/plugins/ml/server/lib/capabilities/capabilities_switcher.ts b/x-pack/plugins/ml/server/lib/capabilities/capabilities_switcher.ts new file mode 100644 index 0000000000000..aef22debf3642 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/capabilities/capabilities_switcher.ts @@ -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 { cloneDeep } from 'lodash'; +import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { CapabilitiesSwitcher, CoreSetup, Logger } from 'src/core/server'; +import { ILicense } from '../../../../licensing/common/types'; +import { isFullLicense, isMinimumLicense } from '../../../common/license'; +import { MlCapabilities, basicLicenseMlCapabilities } from '../../../common/types/capabilities'; + +export const setupCapabilitiesSwitcher = ( + coreSetup: CoreSetup, + license$: Observable, + logger: Logger +) => { + coreSetup.capabilities.registerSwitcher(getSwitcher(license$, logger)); +}; + +function getSwitcher(license$: Observable, logger: Logger): CapabilitiesSwitcher { + return async (request, capabilities) => { + const isAnonymousRequest = !request.route.options.authRequired; + + if (isAnonymousRequest) { + return capabilities; + } + + try { + const license = await license$.pipe(take(1)).toPromise(); + + // full license, leave capabilities as they were + if (isFullLicense(license)) { + return capabilities; + } + + const mlCaps = capabilities.ml as MlCapabilities; + const originalCapabilities = cloneDeep(mlCaps); + + // not full licence, switch off all capabilities + Object.keys(mlCaps).forEach(k => { + mlCaps[k as keyof MlCapabilities] = false; + }); + + // for a basic license, reapply the original capabilities for the basic license features + if (isMinimumLicense(license)) { + basicLicenseMlCapabilities.forEach(c => (mlCaps[c] = originalCapabilities[c])); + } + + return capabilities; + } catch (e) { + logger.debug(`Error updating capabilities for ML based on licensing: ${e}`); + return capabilities; + } + }; +} diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts new file mode 100644 index 0000000000000..5093801d2d184 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.test.ts @@ -0,0 +1,255 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getAdminCapabilities, getUserCapabilities } from './__mocks__/ml_capabilities'; +import { capabilitiesProvider } from './check_capabilities'; +import { MlLicense } from '../../../common/license'; +import { getDefaultCapabilities } from '../../../common/types/capabilities'; + +const mlLicense = { + isSecurityEnabled: () => true, + isFullLicense: () => true, +} as MlLicense; + +const mlLicenseBasic = { + isSecurityEnabled: () => true, + isFullLicense: () => false, +} as MlLicense; + +const mlIsEnabled = async () => true; +const mlIsNotEnabled = async () => false; + +const callWithRequestNonUpgrade = async () => ({ upgrade_mode: false }); +const callWithRequestUpgrade = async () => ({ upgrade_mode: true }); + +describe('check_capabilities', () => { + describe('getCapabilities() - right number of capabilities', () => { + test('kibana capabilities count', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestNonUpgrade, + getAdminCapabilities(), + mlLicense, + mlIsEnabled + ); + const { capabilities } = await getCapabilities(); + const count = Object.keys(capabilities).length; + expect(count).toBe(22); + done(); + }); + }); + + describe('getCapabilities() with security', () => { + test('ml_user capabilities only', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestNonUpgrade, + getUserCapabilities(), + mlLicense, + mlIsEnabled + ); + const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + expect(upgradeInProgress).toBe(false); + expect(mlFeatureEnabledInSpace).toBe(true); + expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canCreateJob).toBe(false); + expect(capabilities.canDeleteJob).toBe(false); + expect(capabilities.canOpenJob).toBe(false); + expect(capabilities.canCloseJob).toBe(false); + expect(capabilities.canForecastJob).toBe(false); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canStartStopDatafeed).toBe(false); + expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canUpdateDatafeed).toBe(false); + expect(capabilities.canPreviewDatafeed).toBe(false); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canCreateCalendar).toBe(false); + expect(capabilities.canDeleteCalendar).toBe(false); + expect(capabilities.canGetFilters).toBe(true); + expect(capabilities.canCreateFilter).toBe(false); + expect(capabilities.canDeleteFilter).toBe(false); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); + expect(capabilities.canCreateDataFrameAnalytics).toBe(false); + expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); + done(); + }); + + test('full capabilities', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestNonUpgrade, + getAdminCapabilities(), + mlLicense, + mlIsEnabled + ); + const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + expect(upgradeInProgress).toBe(false); + expect(mlFeatureEnabledInSpace).toBe(true); + expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canCreateJob).toBe(true); + expect(capabilities.canDeleteJob).toBe(true); + expect(capabilities.canOpenJob).toBe(true); + expect(capabilities.canCloseJob).toBe(true); + expect(capabilities.canForecastJob).toBe(true); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canStartStopDatafeed).toBe(true); + expect(capabilities.canUpdateJob).toBe(true); + expect(capabilities.canUpdateDatafeed).toBe(true); + expect(capabilities.canPreviewDatafeed).toBe(true); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canCreateCalendar).toBe(true); + expect(capabilities.canDeleteCalendar).toBe(true); + expect(capabilities.canGetFilters).toBe(true); + expect(capabilities.canCreateFilter).toBe(true); + expect(capabilities.canDeleteFilter).toBe(true); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canDeleteDataFrameAnalytics).toBe(true); + expect(capabilities.canCreateDataFrameAnalytics).toBe(true); + expect(capabilities.canStartStopDataFrameAnalytics).toBe(true); + done(); + }); + + test('upgrade in progress with full capabilities', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestUpgrade, + getAdminCapabilities(), + mlLicense, + mlIsEnabled + ); + const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + expect(upgradeInProgress).toBe(true); + expect(mlFeatureEnabledInSpace).toBe(true); + expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canCreateJob).toBe(false); + expect(capabilities.canDeleteJob).toBe(false); + expect(capabilities.canOpenJob).toBe(false); + expect(capabilities.canCloseJob).toBe(false); + expect(capabilities.canForecastJob).toBe(false); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canStartStopDatafeed).toBe(false); + expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canUpdateDatafeed).toBe(false); + expect(capabilities.canPreviewDatafeed).toBe(false); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canCreateCalendar).toBe(false); + expect(capabilities.canDeleteCalendar).toBe(false); + expect(capabilities.canGetFilters).toBe(true); + expect(capabilities.canCreateFilter).toBe(false); + expect(capabilities.canDeleteFilter).toBe(false); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); + expect(capabilities.canCreateDataFrameAnalytics).toBe(false); + expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); + done(); + }); + + test('upgrade in progress with partial capabilities', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestUpgrade, + getUserCapabilities(), + mlLicense, + mlIsEnabled + ); + const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + expect(upgradeInProgress).toBe(true); + expect(mlFeatureEnabledInSpace).toBe(true); + expect(capabilities.canGetJobs).toBe(true); + expect(capabilities.canCreateJob).toBe(false); + expect(capabilities.canDeleteJob).toBe(false); + expect(capabilities.canOpenJob).toBe(false); + expect(capabilities.canCloseJob).toBe(false); + expect(capabilities.canForecastJob).toBe(false); + expect(capabilities.canGetDatafeeds).toBe(true); + expect(capabilities.canStartStopDatafeed).toBe(false); + expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canUpdateDatafeed).toBe(false); + expect(capabilities.canPreviewDatafeed).toBe(false); + expect(capabilities.canGetCalendars).toBe(true); + expect(capabilities.canCreateCalendar).toBe(false); + expect(capabilities.canDeleteCalendar).toBe(false); + expect(capabilities.canGetFilters).toBe(true); + expect(capabilities.canCreateFilter).toBe(false); + expect(capabilities.canDeleteFilter).toBe(false); + expect(capabilities.canFindFileStructure).toBe(true); + expect(capabilities.canGetDataFrameAnalytics).toBe(true); + expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); + expect(capabilities.canCreateDataFrameAnalytics).toBe(false); + expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); + done(); + }); + + test('full capabilities, ml disabled in space', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestNonUpgrade, + getDefaultCapabilities(), + mlLicense, + mlIsNotEnabled + ); + const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + expect(upgradeInProgress).toBe(false); + expect(mlFeatureEnabledInSpace).toBe(false); + expect(capabilities.canGetJobs).toBe(false); + expect(capabilities.canCreateJob).toBe(false); + expect(capabilities.canDeleteJob).toBe(false); + expect(capabilities.canOpenJob).toBe(false); + expect(capabilities.canCloseJob).toBe(false); + expect(capabilities.canForecastJob).toBe(false); + expect(capabilities.canGetDatafeeds).toBe(false); + expect(capabilities.canStartStopDatafeed).toBe(false); + expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canUpdateDatafeed).toBe(false); + expect(capabilities.canPreviewDatafeed).toBe(false); + expect(capabilities.canGetCalendars).toBe(false); + expect(capabilities.canCreateCalendar).toBe(false); + expect(capabilities.canDeleteCalendar).toBe(false); + expect(capabilities.canGetFilters).toBe(false); + expect(capabilities.canCreateFilter).toBe(false); + expect(capabilities.canDeleteFilter).toBe(false); + expect(capabilities.canFindFileStructure).toBe(false); + expect(capabilities.canGetDataFrameAnalytics).toBe(false); + expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); + expect(capabilities.canCreateDataFrameAnalytics).toBe(false); + expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); + done(); + }); + }); + + test('full capabilities, basic license, ml disabled in space', async done => { + const { getCapabilities } = capabilitiesProvider( + callWithRequestNonUpgrade, + getDefaultCapabilities(), + mlLicenseBasic, + mlIsNotEnabled + ); + const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getCapabilities(); + expect(upgradeInProgress).toBe(false); + expect(mlFeatureEnabledInSpace).toBe(false); + expect(capabilities.canGetJobs).toBe(false); + expect(capabilities.canCreateJob).toBe(false); + expect(capabilities.canDeleteJob).toBe(false); + expect(capabilities.canOpenJob).toBe(false); + expect(capabilities.canCloseJob).toBe(false); + expect(capabilities.canForecastJob).toBe(false); + expect(capabilities.canGetDatafeeds).toBe(false); + expect(capabilities.canStartStopDatafeed).toBe(false); + expect(capabilities.canUpdateJob).toBe(false); + expect(capabilities.canUpdateDatafeed).toBe(false); + expect(capabilities.canPreviewDatafeed).toBe(false); + expect(capabilities.canGetCalendars).toBe(false); + expect(capabilities.canCreateCalendar).toBe(false); + expect(capabilities.canDeleteCalendar).toBe(false); + expect(capabilities.canGetFilters).toBe(false); + expect(capabilities.canCreateFilter).toBe(false); + expect(capabilities.canDeleteFilter).toBe(false); + expect(capabilities.canFindFileStructure).toBe(false); + expect(capabilities.canGetDataFrameAnalytics).toBe(false); + expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); + expect(capabilities.canCreateDataFrameAnalytics).toBe(false); + expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); + done(); + }); +}); diff --git a/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts new file mode 100644 index 0000000000000..a2ad83c5522de --- /dev/null +++ b/x-pack/plugins/ml/server/lib/capabilities/check_capabilities.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IScopedClusterClient } from 'kibana/server'; +import { + MlCapabilities, + adminMlCapabilities, + MlCapabilitiesResponse, +} from '../../../common/types/capabilities'; +import { upgradeCheckProvider } from './upgrade'; +import { MlLicense } from '../../../common/license'; + +export function capabilitiesProvider( + callAsCurrentUser: IScopedClusterClient['callAsCurrentUser'], + capabilities: MlCapabilities, + mlLicense: MlLicense, + isMlEnabledInSpace: () => Promise +) { + const { isUpgradeInProgress } = upgradeCheckProvider(callAsCurrentUser); + async function getCapabilities(): Promise { + const upgradeInProgress = await isUpgradeInProgress(); + const isPlatinumOrTrialLicense = mlLicense.isFullLicense(); + const mlFeatureEnabledInSpace = await isMlEnabledInSpace(); + + if (upgradeInProgress === true) { + // if an upgrade is in progress, set all admin capabilities to false + disableAdminPrivileges(capabilities); + } + + return { + capabilities, + upgradeInProgress, + isPlatinumOrTrialLicense, + mlFeatureEnabledInSpace, + }; + } + return { getCapabilities }; +} + +function disableAdminPrivileges(capabilities: MlCapabilities) { + Object.keys(adminMlCapabilities).forEach(k => { + capabilities[k as keyof MlCapabilities] = false; + }); +} diff --git a/x-pack/plugins/ml/server/lib/capabilities/index.ts b/x-pack/plugins/ml/server/lib/capabilities/index.ts new file mode 100644 index 0000000000000..b73c6b87f6839 --- /dev/null +++ b/x-pack/plugins/ml/server/lib/capabilities/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { capabilitiesProvider } from './check_capabilities'; +export { setupCapabilitiesSwitcher } from './capabilities_switcher'; diff --git a/x-pack/plugins/ml/server/lib/check_privileges/upgrade.ts b/x-pack/plugins/ml/server/lib/capabilities/upgrade.ts similarity index 100% rename from x-pack/plugins/ml/server/lib/check_privileges/upgrade.ts rename to x-pack/plugins/ml/server/lib/capabilities/upgrade.ts diff --git a/x-pack/plugins/ml/server/lib/check_privileges/__mocks__/call_with_request.ts b/x-pack/plugins/ml/server/lib/check_privileges/__mocks__/call_with_request.ts deleted file mode 100644 index ef82003ec80d6..0000000000000 --- a/x-pack/plugins/ml/server/lib/check_privileges/__mocks__/call_with_request.ts +++ /dev/null @@ -1,154 +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 callWithRequestProvider(testType: string) { - switch (testType) { - case 'partialPrivileges': - return partialPrivileges; - case 'fullPrivileges': - return fullPrivileges; - case 'upgradeWithFullPrivileges': - return upgradeWithFullPrivileges; - case 'upgradeWithPartialPrivileges': - return upgradeWithPartialPrivileges; - - default: - return fullPrivileges; - } -} - -const fullPrivileges = async (action: string, params?: any) => { - switch (action) { - case 'ml.privilegeCheck': - return { - username: 'test2', - has_all_requested: false, - cluster: fullClusterPrivileges, - index: {}, - application: {}, - }; - case 'ml.info': - return { upgrade_mode: false }; - - default: - break; - } -}; - -const partialPrivileges = async (action: string, params?: any) => { - switch (action) { - case 'ml.privilegeCheck': - return { - username: 'test2', - has_all_requested: false, - cluster: partialClusterPrivileges, - index: {}, - application: {}, - }; - case 'ml.info': - return { upgrade_mode: false }; - - default: - break; - } -}; - -const upgradeWithFullPrivileges = async (action: string, params?: any) => { - switch (action) { - case 'ml.privilegeCheck': - return { - username: 'elastic', - has_all_requested: true, - cluster: fullClusterPrivileges, - index: {}, - application: {}, - }; - case 'ml.info': - return { upgrade_mode: true }; - - default: - break; - } -}; - -const upgradeWithPartialPrivileges = async (action: string, params?: any) => { - switch (action) { - case 'ml.privilegeCheck': - return { - username: 'test2', - has_all_requested: false, - cluster: partialClusterPrivileges, - index: {}, - application: {}, - }; - case 'ml.info': - return { upgrade_mode: true }; - - default: - break; - } -}; - -const fullClusterPrivileges = { - 'cluster:admin/xpack/ml/datafeeds/delete': true, - 'cluster:admin/xpack/ml/datafeeds/update': true, - 'cluster:admin/xpack/ml/job/forecast': true, - 'cluster:monitor/xpack/ml/job/stats/get': true, - 'cluster:admin/xpack/ml/filters/delete': true, - 'cluster:admin/xpack/ml/datafeeds/preview': true, - 'cluster:admin/xpack/ml/datafeeds/start': true, - 'cluster:admin/xpack/ml/filters/put': true, - 'cluster:admin/xpack/ml/datafeeds/stop': true, - 'cluster:monitor/xpack/ml/calendars/get': true, - 'cluster:admin/xpack/ml/filters/get': true, - 'cluster:monitor/xpack/ml/datafeeds/get': true, - 'cluster:admin/xpack/ml/filters/update': true, - 'cluster:admin/xpack/ml/calendars/events/post': true, - 'cluster:admin/xpack/ml/job/close': true, - 'cluster:monitor/xpack/ml/datafeeds/stats/get': true, - 'cluster:admin/xpack/ml/calendars/jobs/update': true, - 'cluster:admin/xpack/ml/calendars/put': true, - 'cluster:admin/xpack/ml/calendars/events/delete': true, - 'cluster:admin/xpack/ml/datafeeds/put': true, - 'cluster:admin/xpack/ml/job/open': true, - 'cluster:admin/xpack/ml/job/delete': true, - 'cluster:monitor/xpack/ml/job/get': true, - 'cluster:admin/xpack/ml/job/put': true, - 'cluster:admin/xpack/ml/job/update': true, - 'cluster:admin/xpack/ml/calendars/delete': true, - 'cluster:monitor/xpack/ml/findfilestructure': true, -}; - -// the same as ml_user role -const partialClusterPrivileges = { - 'cluster:admin/xpack/ml/datafeeds/delete': false, - 'cluster:admin/xpack/ml/datafeeds/update': false, - 'cluster:admin/xpack/ml/job/forecast': false, - 'cluster:monitor/xpack/ml/job/stats/get': true, - 'cluster:admin/xpack/ml/filters/delete': false, - 'cluster:admin/xpack/ml/datafeeds/preview': false, - 'cluster:admin/xpack/ml/datafeeds/start': false, - 'cluster:admin/xpack/ml/filters/put': false, - 'cluster:admin/xpack/ml/datafeeds/stop': false, - 'cluster:monitor/xpack/ml/calendars/get': true, - 'cluster:admin/xpack/ml/filters/get': false, - 'cluster:monitor/xpack/ml/datafeeds/get': true, - 'cluster:admin/xpack/ml/filters/update': false, - 'cluster:admin/xpack/ml/calendars/events/post': false, - 'cluster:admin/xpack/ml/job/close': false, - 'cluster:monitor/xpack/ml/datafeeds/stats/get': true, - 'cluster:admin/xpack/ml/calendars/jobs/update': false, - 'cluster:admin/xpack/ml/calendars/put': false, - 'cluster:admin/xpack/ml/calendars/events/delete': false, - 'cluster:admin/xpack/ml/datafeeds/put': false, - 'cluster:admin/xpack/ml/job/open': false, - 'cluster:admin/xpack/ml/job/delete': false, - 'cluster:monitor/xpack/ml/job/get': true, - 'cluster:admin/xpack/ml/job/put': false, - 'cluster:admin/xpack/ml/job/update': false, - 'cluster:admin/xpack/ml/calendars/delete': false, - 'cluster:monitor/xpack/ml/findfilestructure': true, -}; diff --git a/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.test.ts b/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.test.ts deleted file mode 100644 index d8435e9026250..0000000000000 --- a/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.test.ts +++ /dev/null @@ -1,515 +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 { callWithRequestProvider } from './__mocks__/call_with_request'; -import { privilegesProvider } from './check_privileges'; -import { mlPrivileges } from './privileges'; -import { MlLicense } from '../../../common/license'; - -const mlLicenseWithSecurity = { - isSecurityEnabled: () => true, - isFullLicense: () => true, -} as MlLicense; - -const mlLicenseWithOutSecurity = { - isSecurityEnabled: () => false, - isFullLicense: () => true, -} as MlLicense; - -const mlLicenseWithOutSecurityBasicLicense = { - isSecurityEnabled: () => false, - isFullLicense: () => false, -} as MlLicense; - -const mlLicenseWithSecurityBasicLicense = { - isSecurityEnabled: () => true, - isFullLicense: () => false, -} as MlLicense; - -const mlIsEnabled = async () => true; -const mlIsNotEnabled = async () => false; - -describe('check_privileges', () => { - describe('getPrivileges() - right number of capabilities', () => { - test('es capabilities count', async done => { - const count = mlPrivileges.cluster.length; - expect(count).toBe(27); - done(); - }); - - test('kibana capabilities count', async done => { - const callWithRequest = callWithRequestProvider('partialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurity, - mlIsEnabled - ); - const { capabilities } = await getPrivileges(); - const count = Object.keys(capabilities).length; - expect(count).toBe(22); - done(); - }); - }); - - describe('getPrivileges() with security', () => { - test('ml_user capabilities only', async done => { - const callWithRequest = callWithRequestProvider('partialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(true); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('full capabilities', async done => { - const callWithRequest = callWithRequestProvider('fullPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(true); - expect(capabilities.canCreateJob).toBe(true); - expect(capabilities.canDeleteJob).toBe(true); - expect(capabilities.canOpenJob).toBe(true); - expect(capabilities.canCloseJob).toBe(true); - expect(capabilities.canForecastJob).toBe(true); - expect(capabilities.canGetDatafeeds).toBe(true); - expect(capabilities.canStartStopDatafeed).toBe(true); - expect(capabilities.canUpdateJob).toBe(true); - expect(capabilities.canUpdateDatafeed).toBe(true); - expect(capabilities.canPreviewDatafeed).toBe(true); - expect(capabilities.canGetCalendars).toBe(true); - expect(capabilities.canCreateCalendar).toBe(true); - expect(capabilities.canDeleteCalendar).toBe(true); - expect(capabilities.canGetFilters).toBe(true); - expect(capabilities.canCreateFilter).toBe(true); - expect(capabilities.canDeleteFilter).toBe(true); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(true); - expect(capabilities.canCreateDataFrameAnalytics).toBe(true); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(true); - done(); - }); - - test('upgrade in progress with full capabilities', async done => { - const callWithRequest = callWithRequestProvider('upgradeWithFullPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(true); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(true); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(true); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('upgrade in progress with partial capabilities', async done => { - const callWithRequest = callWithRequestProvider('upgradeWithPartialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(true); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(true); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('ml_user capabilities with security with basic license', async done => { - const callWithRequest = callWithRequestProvider('partialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurityBasicLicense, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(false); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(false); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(false); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('full user with security with basic license', async done => { - const callWithRequest = callWithRequestProvider('fullPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurityBasicLicense, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(false); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(false); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(false); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('full capabilities, ml disabled in space', async done => { - const callWithRequest = callWithRequestProvider('fullPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithSecurity, - mlIsNotEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(false); - expect(capabilities.canGetJobs).toBe(false); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(false); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(false); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(false); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - }); - - describe('getPrivileges() without security', () => { - test('ml_user capabilities only', async done => { - const callWithRequest = callWithRequestProvider('partialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithOutSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(true); - expect(capabilities.canCreateJob).toBe(true); - expect(capabilities.canDeleteJob).toBe(true); - expect(capabilities.canOpenJob).toBe(true); - expect(capabilities.canCloseJob).toBe(true); - expect(capabilities.canForecastJob).toBe(true); - expect(capabilities.canGetDatafeeds).toBe(true); - expect(capabilities.canStartStopDatafeed).toBe(true); - expect(capabilities.canUpdateJob).toBe(true); - expect(capabilities.canUpdateDatafeed).toBe(true); - expect(capabilities.canPreviewDatafeed).toBe(true); - expect(capabilities.canGetCalendars).toBe(true); - expect(capabilities.canCreateCalendar).toBe(true); - expect(capabilities.canDeleteCalendar).toBe(true); - expect(capabilities.canGetFilters).toBe(true); - expect(capabilities.canCreateFilter).toBe(true); - expect(capabilities.canDeleteFilter).toBe(true); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(true); - expect(capabilities.canCreateDataFrameAnalytics).toBe(true); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(true); - done(); - }); - - test('upgrade in progress with full capabilities', async done => { - const callWithRequest = callWithRequestProvider('upgradeWithFullPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithOutSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(true); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(true); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(true); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('upgrade in progress with partial capabilities', async done => { - const callWithRequest = callWithRequestProvider('upgradeWithPartialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithOutSecurity, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(true); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(true); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(true); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(true); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(true); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(true); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('ml_user capabilities without security with basic license', async done => { - const callWithRequest = callWithRequestProvider('partialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithOutSecurityBasicLicense, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(false); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(false); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(false); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('full user without security with basic license', async done => { - const callWithRequest = callWithRequestProvider('fullPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithOutSecurityBasicLicense, - mlIsEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(true); - expect(capabilities.canGetJobs).toBe(false); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(false); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(false); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(true); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - - test('ml_user capabilities only, ml disabled in space', async done => { - const callWithRequest = callWithRequestProvider('partialPrivileges'); - const { getPrivileges } = privilegesProvider( - callWithRequest, - mlLicenseWithOutSecurity, - mlIsNotEnabled - ); - const { capabilities, upgradeInProgress, mlFeatureEnabledInSpace } = await getPrivileges(); - expect(upgradeInProgress).toBe(false); - expect(mlFeatureEnabledInSpace).toBe(false); - expect(capabilities.canGetJobs).toBe(false); - expect(capabilities.canCreateJob).toBe(false); - expect(capabilities.canDeleteJob).toBe(false); - expect(capabilities.canOpenJob).toBe(false); - expect(capabilities.canCloseJob).toBe(false); - expect(capabilities.canForecastJob).toBe(false); - expect(capabilities.canGetDatafeeds).toBe(false); - expect(capabilities.canStartStopDatafeed).toBe(false); - expect(capabilities.canUpdateJob).toBe(false); - expect(capabilities.canUpdateDatafeed).toBe(false); - expect(capabilities.canPreviewDatafeed).toBe(false); - expect(capabilities.canGetCalendars).toBe(false); - expect(capabilities.canCreateCalendar).toBe(false); - expect(capabilities.canDeleteCalendar).toBe(false); - expect(capabilities.canGetFilters).toBe(false); - expect(capabilities.canCreateFilter).toBe(false); - expect(capabilities.canDeleteFilter).toBe(false); - expect(capabilities.canFindFileStructure).toBe(false); - expect(capabilities.canGetDataFrameAnalytics).toBe(false); - expect(capabilities.canDeleteDataFrameAnalytics).toBe(false); - expect(capabilities.canCreateDataFrameAnalytics).toBe(false); - expect(capabilities.canStartStopDataFrameAnalytics).toBe(false); - done(); - }); - }); -}); diff --git a/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.ts b/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.ts deleted file mode 100644 index df61ad0111a03..0000000000000 --- a/x-pack/plugins/ml/server/lib/check_privileges/check_privileges.ts +++ /dev/null @@ -1,269 +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 { IScopedClusterClient } from 'kibana/server'; -import { Privileges, getDefaultPrivileges } from '../../../common/types/privileges'; -import { upgradeCheckProvider } from './upgrade'; -import { MlLicense } from '../../../common/license'; - -import { mlPrivileges } from './privileges'; - -type ClusterPrivilege = Record; - -export interface MlCapabilities { - capabilities: Privileges; - upgradeInProgress: boolean; - isPlatinumOrTrialLicense: boolean; - mlFeatureEnabledInSpace: boolean; -} - -export function privilegesProvider( - callAsCurrentUser: IScopedClusterClient['callAsCurrentUser'], - mlLicense: MlLicense, - isMlEnabledInSpace: () => Promise, - ignoreSpaces: boolean = false -) { - const { isUpgradeInProgress } = upgradeCheckProvider(callAsCurrentUser); - async function getPrivileges(): Promise { - // get the default privileges, forced to be false. - const privileges = getDefaultPrivileges(); - - const upgradeInProgress = await isUpgradeInProgress(); - const isSecurityEnabled = mlLicense.isSecurityEnabled(); - - const isPlatinumOrTrialLicense = mlLicense.isFullLicense(); - const mlFeatureEnabledInSpace = await isMlEnabledInSpace(); - - const setGettingPrivileges = isPlatinumOrTrialLicense - ? setFullGettingPrivileges - : setBasicGettingPrivileges; - - const setActionPrivileges = isPlatinumOrTrialLicense - ? setFullActionPrivileges - : setBasicActionPrivileges; - - if (mlFeatureEnabledInSpace === false && ignoreSpaces === false) { - // if ML isn't enabled in the current space, - // return with the default privileges (all false) - return { - capabilities: privileges, - upgradeInProgress, - isPlatinumOrTrialLicense, - mlFeatureEnabledInSpace, - }; - } - - if (isSecurityEnabled === false) { - if (upgradeInProgress === true) { - // if security is disabled and an upgrade in is progress, - // force all "getting" privileges to be true - // leaving all "setting" privileges to be the default false - setGettingPrivileges({}, privileges, true); - } else { - // if no upgrade is in progress, - // get all privileges forced to true - setGettingPrivileges({}, privileges, true); - setActionPrivileges({}, privileges, true); - } - } else { - // security enabled - // load all ml privileges for this user. - const { cluster } = await callAsCurrentUser('ml.privilegeCheck', { body: mlPrivileges }); - setGettingPrivileges(cluster, privileges); - if (upgradeInProgress === false) { - // if an upgrade is in progress, don't apply the "setting" - // privileges. leave them to be the default false. - setActionPrivileges(cluster, privileges); - } - } - return { - capabilities: privileges, - upgradeInProgress, - isPlatinumOrTrialLicense, - mlFeatureEnabledInSpace, - }; - } - return { getPrivileges }; -} - -function setFullGettingPrivileges( - cluster: ClusterPrivilege = {}, - privileges: Privileges, - forceTrue = false -) { - // Anomaly Detection - if ( - forceTrue || - (cluster['cluster:monitor/xpack/ml/job/get'] && - cluster['cluster:monitor/xpack/ml/job/stats/get']) - ) { - privileges.canGetJobs = true; - } - - if ( - forceTrue || - (cluster['cluster:monitor/xpack/ml/datafeeds/get'] && - cluster['cluster:monitor/xpack/ml/datafeeds/stats/get']) - ) { - privileges.canGetDatafeeds = true; - } - - // Calendars - if (forceTrue || cluster['cluster:monitor/xpack/ml/calendars/get']) { - privileges.canGetCalendars = true; - } - - // Filters - if (forceTrue || cluster['cluster:admin/xpack/ml/filters/get']) { - privileges.canGetFilters = true; - } - - // File Data Visualizer - if (forceTrue || cluster['cluster:monitor/xpack/ml/findfilestructure']) { - privileges.canFindFileStructure = true; - } - - // Data Frame Analytics - if ( - forceTrue || - (cluster['cluster:monitor/xpack/ml/job/get'] && - cluster['cluster:monitor/xpack/ml/job/stats/get']) - ) { - privileges.canGetDataFrameAnalytics = true; - } -} - -function setFullActionPrivileges( - cluster: ClusterPrivilege = {}, - privileges: Privileges, - forceTrue = false -) { - // Anomaly Detection - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/job/put'] && - cluster['cluster:admin/xpack/ml/job/open'] && - cluster['cluster:admin/xpack/ml/datafeeds/put']) - ) { - privileges.canCreateJob = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/job/update']) { - privileges.canUpdateJob = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/job/open']) { - privileges.canOpenJob = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/job/close']) { - privileges.canCloseJob = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/job/forecast']) { - privileges.canForecastJob = true; - } - - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/job/delete'] && - cluster['cluster:admin/xpack/ml/datafeeds/delete']) - ) { - privileges.canDeleteJob = true; - } - - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/job/open'] && - cluster['cluster:admin/xpack/ml/datafeeds/start'] && - cluster['cluster:admin/xpack/ml/datafeeds/stop']) - ) { - privileges.canStartStopDatafeed = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/datafeeds/update']) { - privileges.canUpdateDatafeed = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/datafeeds/preview']) { - privileges.canPreviewDatafeed = true; - } - - // Calendars - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/calendars/put'] && - cluster['cluster:admin/xpack/ml/calendars/jobs/update'] && - cluster['cluster:admin/xpack/ml/calendars/events/post']) - ) { - privileges.canCreateCalendar = true; - } - - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/calendars/delete'] && - cluster['cluster:admin/xpack/ml/calendars/events/delete']) - ) { - privileges.canDeleteCalendar = true; - } - - // Filters - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/filters/put'] && - cluster['cluster:admin/xpack/ml/filters/update']) - ) { - privileges.canCreateFilter = true; - } - - if (forceTrue || cluster['cluster:admin/xpack/ml/filters/delete']) { - privileges.canDeleteFilter = true; - } - - // Data Frame Analytics - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/job/put'] && - cluster['cluster:admin/xpack/ml/job/open'] && - cluster['cluster:admin/xpack/ml/datafeeds/put']) - ) { - privileges.canCreateDataFrameAnalytics = true; - } - - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/job/delete'] && - cluster['cluster:admin/xpack/ml/datafeeds/delete']) - ) { - privileges.canDeleteDataFrameAnalytics = true; - } - - if ( - forceTrue || - (cluster['cluster:admin/xpack/ml/job/open'] && - cluster['cluster:admin/xpack/ml/datafeeds/start'] && - cluster['cluster:admin/xpack/ml/datafeeds/stop']) - ) { - privileges.canStartStopDataFrameAnalytics = true; - } -} - -function setBasicGettingPrivileges( - cluster: ClusterPrivilege = {}, - privileges: Privileges, - forceTrue = false -) { - // File Data Visualizer - if (forceTrue || cluster['cluster:monitor/xpack/ml/findfilestructure']) { - privileges.canFindFileStructure = true; - } -} - -function setBasicActionPrivileges( - cluster: ClusterPrivilege = {}, - privileges: Privileges, - forceTrue = false -) {} diff --git a/x-pack/plugins/ml/server/lib/check_privileges/index.ts b/x-pack/plugins/ml/server/lib/check_privileges/index.ts deleted file mode 100644 index 67b435116aa00..0000000000000 --- a/x-pack/plugins/ml/server/lib/check_privileges/index.ts +++ /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 { privilegesProvider, MlCapabilities } from './check_privileges'; diff --git a/x-pack/plugins/ml/server/lib/check_privileges/privileges.ts b/x-pack/plugins/ml/server/lib/check_privileges/privileges.ts deleted file mode 100644 index 9fcd28dd68105..0000000000000 --- a/x-pack/plugins/ml/server/lib/check_privileges/privileges.ts +++ /dev/null @@ -1,37 +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 mlPrivileges = { - cluster: [ - 'cluster:monitor/xpack/ml/job/get', - 'cluster:monitor/xpack/ml/job/stats/get', - 'cluster:monitor/xpack/ml/datafeeds/get', - 'cluster:monitor/xpack/ml/datafeeds/stats/get', - 'cluster:monitor/xpack/ml/calendars/get', - 'cluster:admin/xpack/ml/job/put', - 'cluster:admin/xpack/ml/job/delete', - 'cluster:admin/xpack/ml/job/update', - 'cluster:admin/xpack/ml/job/open', - 'cluster:admin/xpack/ml/job/close', - 'cluster:admin/xpack/ml/job/forecast', - 'cluster:admin/xpack/ml/datafeeds/put', - 'cluster:admin/xpack/ml/datafeeds/delete', - 'cluster:admin/xpack/ml/datafeeds/start', - 'cluster:admin/xpack/ml/datafeeds/stop', - 'cluster:admin/xpack/ml/datafeeds/update', - 'cluster:admin/xpack/ml/datafeeds/preview', - 'cluster:admin/xpack/ml/calendars/put', - 'cluster:admin/xpack/ml/calendars/delete', - 'cluster:admin/xpack/ml/calendars/jobs/update', - 'cluster:admin/xpack/ml/calendars/events/post', - 'cluster:admin/xpack/ml/calendars/events/delete', - 'cluster:admin/xpack/ml/filters/put', - 'cluster:admin/xpack/ml/filters/get', - 'cluster:admin/xpack/ml/filters/update', - 'cluster:admin/xpack/ml/filters/delete', - 'cluster:monitor/xpack/ml/findfilestructure', - ], -}; diff --git a/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts b/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts index 64d750f511f3a..581770e59043f 100644 --- a/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts +++ b/x-pack/plugins/ml/server/models/calendar/calendar_manager.ts @@ -5,7 +5,6 @@ */ import { difference } from 'lodash'; -import Boom from 'boom'; import { IScopedClusterClient } from 'kibana/server'; import { EventManager, CalendarEvent } from './event_manager'; @@ -33,43 +32,31 @@ export class CalendarManager { } async getCalendar(calendarId: string) { - try { - const resp = await this._client('ml.calendars', { - calendarId, - }); - - const calendars = resp.calendars; - if (calendars.length) { - const calendar = calendars[0]; - calendar.events = await this._eventManager.getCalendarEvents(calendarId); - return calendar; - } else { - throw Boom.notFound(`Calendar with the id "${calendarId}" not found`); - } - } catch (error) { - throw Boom.badRequest(error); - } + const resp = await this._client('ml.calendars', { + calendarId, + }); + + const calendars = resp.calendars; + const calendar = calendars[0]; // Endpoint throws a 404 if calendar is not found. + calendar.events = await this._eventManager.getCalendarEvents(calendarId); + return calendar; } async getAllCalendars() { - try { - const calendarsResp = await this._client('ml.calendars'); - - const events: CalendarEvent[] = await this._eventManager.getAllEvents(); - const calendars: Calendar[] = calendarsResp.calendars; - calendars.forEach(cal => (cal.events = [])); - - // loop events and combine with related calendars - events.forEach(event => { - const calendar = calendars.find(cal => cal.calendar_id === event.calendar_id); - if (calendar) { - calendar.events.push(event); - } - }); - return calendars; - } catch (error) { - throw Boom.badRequest(error); - } + const calendarsResp = await this._client('ml.calendars'); + + const events: CalendarEvent[] = await this._eventManager.getAllEvents(); + const calendars: Calendar[] = calendarsResp.calendars; + calendars.forEach(cal => (cal.events = [])); + + // loop events and combine with related calendars + events.forEach(event => { + const calendar = calendars.find(cal => cal.calendar_id === event.calendar_id); + if (calendar) { + calendar.events.push(event); + } + }); + return calendars; } /** @@ -78,12 +65,8 @@ export class CalendarManager { * @returns {Promise<*>} */ async getCalendarsByIds(calendarIds: string) { - try { - const calendars: Calendar[] = await this.getAllCalendars(); - return calendars.filter(calendar => calendarIds.includes(calendar.calendar_id)); - } catch (error) { - throw Boom.badRequest(error); - } + const calendars: Calendar[] = await this.getAllCalendars(); + return calendars.filter(calendar => calendarIds.includes(calendar.calendar_id)); } async newCalendar(calendar: FormCalendar) { @@ -91,75 +74,67 @@ export class CalendarManager { const events = calendar.events; delete calendar.calendarId; delete calendar.events; - try { - await this._client('ml.addCalendar', { - calendarId, - body: calendar, - }); - - if (events.length) { - await this._eventManager.addEvents(calendarId, events); - } + await this._client('ml.addCalendar', { + calendarId, + body: calendar, + }); - // return the newly created calendar - return await this.getCalendar(calendarId); - } catch (error) { - throw Boom.badRequest(error); + if (events.length) { + await this._eventManager.addEvents(calendarId, events); } + + // return the newly created calendar + return await this.getCalendar(calendarId); } async updateCalendar(calendarId: string, calendar: Calendar) { const origCalendar: Calendar = await this.getCalendar(calendarId); - try { - // update job_ids - const jobsToAdd = difference(calendar.job_ids, origCalendar.job_ids); - const jobsToRemove = difference(origCalendar.job_ids, calendar.job_ids); - - // workout the differences between the original events list and the new one - // if an event has no event_id, it must be new - const eventsToAdd = calendar.events.filter( - event => origCalendar.events.find(e => this._eventManager.isEqual(e, event)) === undefined - ); - - // if an event in the original calendar cannot be found, it must have been deleted - const eventsToRemove: CalendarEvent[] = origCalendar.events.filter( - event => calendar.events.find(e => this._eventManager.isEqual(e, event)) === undefined - ); - - // note, both of the loops below could be removed if the add and delete endpoints - // allowed multiple job_ids - - // add all new jobs - if (jobsToAdd.length) { - await this._client('ml.addJobToCalendar', { - calendarId, - jobId: jobsToAdd.join(','), - }); - } - - // remove all removed jobs - if (jobsToRemove.length) { - await this._client('ml.removeJobFromCalendar', { - calendarId, - jobId: jobsToRemove.join(','), - }); - } + // update job_ids + const jobsToAdd = difference(calendar.job_ids, origCalendar.job_ids); + const jobsToRemove = difference(origCalendar.job_ids, calendar.job_ids); + + // workout the differences between the original events list and the new one + // if an event has no event_id, it must be new + const eventsToAdd = calendar.events.filter( + event => origCalendar.events.find(e => this._eventManager.isEqual(e, event)) === undefined + ); + + // if an event in the original calendar cannot be found, it must have been deleted + const eventsToRemove: CalendarEvent[] = origCalendar.events.filter( + event => calendar.events.find(e => this._eventManager.isEqual(e, event)) === undefined + ); + + // note, both of the loops below could be removed if the add and delete endpoints + // allowed multiple job_ids + + // add all new jobs + if (jobsToAdd.length) { + await this._client('ml.addJobToCalendar', { + calendarId, + jobId: jobsToAdd.join(','), + }); + } - // add all new events - if (eventsToAdd.length !== 0) { - await this._eventManager.addEvents(calendarId, eventsToAdd); - } + // remove all removed jobs + if (jobsToRemove.length) { + await this._client('ml.removeJobFromCalendar', { + calendarId, + jobId: jobsToRemove.join(','), + }); + } - // remove all removed events - await Promise.all( - eventsToRemove.map(async event => { - await this._eventManager.deleteEvent(calendarId, event.event_id); - }) - ); - } catch (error) { - throw Boom.badRequest(error); + // add all new events + if (eventsToAdd.length !== 0) { + await this._eventManager.addEvents(calendarId, eventsToAdd); } + // remove all removed events + await Promise.all( + eventsToRemove.map(async event => { + await this._eventManager.deleteEvent(calendarId, event.event_id); + }) + ); + // return the updated calendar return await this.getCalendar(calendarId); } diff --git a/x-pack/plugins/ml/server/models/calendar/event_manager.ts b/x-pack/plugins/ml/server/models/calendar/event_manager.ts index 488839f68b3fe..41240e2695f6f 100644 --- a/x-pack/plugins/ml/server/models/calendar/event_manager.ts +++ b/x-pack/plugins/ml/server/models/calendar/event_manager.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; - import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; export interface CalendarEvent { @@ -23,41 +21,29 @@ export class EventManager { } async getCalendarEvents(calendarId: string) { - try { - const resp = await this._client('ml.events', { calendarId }); + const resp = await this._client('ml.events', { calendarId }); - return resp.events; - } catch (error) { - throw Boom.badRequest(error); - } + return resp.events; } // jobId is optional async getAllEvents(jobId?: string) { const calendarId = GLOBAL_CALENDAR; - try { - const resp = await this._client('ml.events', { - calendarId, - jobId, - }); + const resp = await this._client('ml.events', { + calendarId, + jobId, + }); - return resp.events; - } catch (error) { - throw Boom.badRequest(error); - } + return resp.events; } async addEvents(calendarId: string, events: CalendarEvent[]) { const body = { events }; - try { - return await this._client('ml.addEvent', { - calendarId, - body, - }); - } catch (error) { - throw Boom.badRequest(error); - } + return await this._client('ml.addEvent', { + calendarId, + body, + }); } async deleteEvent(calendarId: string, eventId: string) { diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index c7add12be142c..67e80a3bc44c0 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -7,14 +7,18 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, + CoreStart, Plugin, IScopedClusterClient, + KibanaRequest, Logger, PluginInitializerContext, ICustomClusterClient, + CapabilitiesStart, } from 'kibana/server'; import { PluginsSetup, RouteInitialization } from './types'; import { PLUGIN_ID, PLUGIN_ICON } from '../common/constants/app'; +import { MlCapabilities } from '../common/types/capabilities'; import { elasticsearchJsPlugin } from './client/elasticsearch_ml'; import { initMlTelemetry } from './lib/telemetry'; @@ -41,6 +45,8 @@ import { systemRoutes } from './routes/system'; import { MlLicense } from '../common/license'; import { MlServerLicense } from './lib/license'; import { createSharedServices, SharedServices } from './shared_services'; +import { userMlCapabilities, adminMlCapabilities } from '../common/types/capabilities'; +import { setupCapabilitiesSwitcher } from './lib/capabilities'; declare module 'kibana/server' { interface RequestHandlerContext { @@ -59,6 +65,7 @@ export class MlServerPlugin implements Plugin initSampleDataSets(mlLicense, plugins), ]); + // initialize capabilities switcher to add license filter to ml capabilities + setupCapabilitiesSwitcher(coreSetup, plugins.licensing.license$, this.log); + // Can access via router's handler function 'context' parameter - context.ml.mlClient const mlClient = coreSetup.elasticsearch.createClient(PLUGIN_ID, { plugins: [elasticsearchJsPlugin], @@ -132,6 +145,14 @@ export class MlServerPlugin implements Plugin { + if (this.capabilities === null) { + return null; + } + const capabilities = await this.capabilities.resolveCapabilities(request); + return capabilities.ml as MlCapabilities; + }; + annotationRoutes(routeInit, plugins.security); calendars(routeInit); dataFeedRoutes(routeInit); @@ -144,24 +165,27 @@ export class MlServerPlugin implements Plugin { try { - const ignoreSpaces = request.query && request.query.ignoreSpaces === 'true'; // if spaces is disabled force isMlEnabledInSpace to be true const { isMlEnabledInSpace } = spaces !== undefined ? spacesUtilsProvider(spaces, (request as unknown) as Request) : { isMlEnabledInSpace: async () => true }; - const { getPrivileges } = privilegesProvider( + const mlCapabilities = await resolveMlCapabilities(request); + if (mlCapabilities === null) { + return response.customError(wrapError(new Error('resolveMlCapabilities is not defined'))); + } + + const { getCapabilities } = capabilitiesProvider( context.ml!.mlClient.callAsCurrentUser, + mlCapabilities, mlLicense, - isMlEnabledInSpace, - ignoreSpaces + isMlEnabledInSpace ); return response.ok({ - body: await getPrivileges(), + body: await getCapabilities(), }); } catch (error) { return response.customError(wrapError(error)); diff --git a/x-pack/plugins/ml/server/shared_services/providers/system.ts b/x-pack/plugins/ml/server/shared_services/providers/system.ts index cedf18e80906f..698ac8e6261e5 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/system.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/system.ts @@ -4,23 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { APICaller } from 'kibana/server'; +import { APICaller, KibanaRequest } from 'kibana/server'; import { SearchResponse, SearchParams } from 'elasticsearch'; import { MlServerLicense } from '../../lib/license'; import { CloudSetup } from '../../../../cloud/server'; import { LicenseCheck } from '../license_checks'; -import { spacesUtilsProvider, RequestFacade } from '../../lib/spaces_utils'; +import { spacesUtilsProvider } from '../../lib/spaces_utils'; import { SpacesPluginSetup } from '../../../../spaces/server'; -import { privilegesProvider, MlCapabilities } from '../../lib/check_privileges'; +import { capabilitiesProvider } from '../../lib/capabilities'; import { MlInfoResponse } from '../../../common/types/ml_server_info'; import { ML_RESULTS_INDEX_PATTERN } from '../../../common/constants/index_patterns'; +import { MlCapabilitiesResponse, ResolveMlCapabilities } from '../../../common/types/capabilities'; export interface MlSystemProvider { mlSystemProvider( callAsCurrentUser: APICaller, - request: RequestFacade + request: KibanaRequest ): { - mlCapabilities(ignoreSpaces?: boolean): Promise; + mlCapabilities(): Promise; mlInfo(): Promise; mlSearch(searchParams: SearchParams): Promise>; }; @@ -31,12 +32,13 @@ export function getMlSystemProvider( isFullLicense: LicenseCheck, mlLicense: MlServerLicense, spaces: SpacesPluginSetup | undefined, - cloud: CloudSetup | undefined + cloud: CloudSetup | undefined, + resolveMlCapabilities: ResolveMlCapabilities ): MlSystemProvider { return { - mlSystemProvider(callAsCurrentUser: APICaller, request: RequestFacade) { + mlSystemProvider(callAsCurrentUser: APICaller, request: KibanaRequest) { return { - mlCapabilities(ignoreSpaces?: boolean) { + async mlCapabilities() { isMinimumLicense(); const { isMlEnabledInSpace } = @@ -44,13 +46,18 @@ export function getMlSystemProvider( ? spacesUtilsProvider(spaces, request) : { isMlEnabledInSpace: async () => true }; - const { getPrivileges } = privilegesProvider( + const mlCapabilities = await resolveMlCapabilities(request); + if (mlCapabilities === null) { + throw new Error('resolveMlCapabilities is not defined'); + } + + const { getCapabilities } = capabilitiesProvider( callAsCurrentUser, + mlCapabilities, mlLicense, - isMlEnabledInSpace, - ignoreSpaces + isMlEnabledInSpace ); - return getPrivileges(); + return getCapabilities(); }, async mlInfo(): Promise { isMinimumLicense(); diff --git a/x-pack/plugins/ml/server/shared_services/shared_services.ts b/x-pack/plugins/ml/server/shared_services/shared_services.ts index f08eb5c23b272..f2d20a72444be 100644 --- a/x-pack/plugins/ml/server/shared_services/shared_services.ts +++ b/x-pack/plugins/ml/server/shared_services/shared_services.ts @@ -17,6 +17,7 @@ import { AnomalyDetectorsProvider, getAnomalyDetectorsProvider, } from './providers/anomaly_detectors'; +import { ResolveMlCapabilities } from '../../common/types/capabilities'; export type SharedServices = JobServiceProvider & AnomalyDetectorsProvider & @@ -27,14 +28,22 @@ export type SharedServices = JobServiceProvider & export function createSharedServices( mlLicense: MlServerLicense, spaces: SpacesPluginSetup | undefined, - cloud: CloudSetup + cloud: CloudSetup, + resolveMlCapabilities: ResolveMlCapabilities ): SharedServices { const { isFullLicense, isMinimumLicense } = licenseChecks(mlLicense); return { ...getJobServiceProvider(isFullLicense), ...getAnomalyDetectorsProvider(isFullLicense), - ...getMlSystemProvider(isMinimumLicense, isFullLicense, mlLicense, spaces, cloud), + ...getMlSystemProvider( + isMinimumLicense, + isFullLicense, + mlLicense, + spaces, + cloud, + resolveMlCapabilities + ), ...getModulesProvider(isFullLicense), ...getResultsServiceProvider(isFullLicense), }; diff --git a/x-pack/plugins/ml/server/types.ts b/x-pack/plugins/ml/server/types.ts index ff4d07bd79e42..d4cd61a7fa4f7 100644 --- a/x-pack/plugins/ml/server/types.ts +++ b/x-pack/plugins/ml/server/types.ts @@ -13,6 +13,7 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../features/serve import { LicensingPluginSetup } from '../../licensing/server'; import { SpacesPluginSetup } from '../../spaces/server'; import { MlServerLicense } from './lib/license'; +import { ResolveMlCapabilities } from '../common/types/capabilities'; export interface LicenseCheckResult { isAvailable: boolean; @@ -26,6 +27,11 @@ export interface LicenseCheckResult { export interface SystemRouteDeps { cloud: CloudSetup; spaces?: SpacesPluginSetup; + resolveMlCapabilities: ResolveMlCapabilities; +} + +export interface JobServiceRouteDeps { + resolveMlCapabilities: ResolveMlCapabilities; } export interface PluginsSetup { diff --git a/x-pack/plugins/reporting/public/components/buttons/report_delete_button.tsx b/x-pack/plugins/reporting/public/components/buttons/report_delete_button.tsx index 3f7c210eb3696..e8916ff82e121 100644 --- a/x-pack/plugins/reporting/public/components/buttons/report_delete_button.tsx +++ b/x-pack/plugins/reporting/public/components/buttons/report_delete_button.tsx @@ -83,7 +83,12 @@ export class ReportDeleteButton extends PureComponent { return ( - this.showConfirm()} iconType="trash" color={'danger'}> + this.showConfirm()} + iconType="trash" + color={'danger'} + data-test-subj="deleteReportButton" + > {intl.formatMessage( { id: 'xpack.reporting.listing.table.deleteReportButton', diff --git a/x-pack/legacy/plugins/rollup/README.md b/x-pack/plugins/rollup/README.md similarity index 76% rename from x-pack/legacy/plugins/rollup/README.md rename to x-pack/plugins/rollup/README.md index 3647be38b6a09..b43f4d5981409 100644 --- a/x-pack/legacy/plugins/rollup/README.md +++ b/x-pack/plugins/rollup/README.md @@ -14,7 +14,7 @@ The rest of this doc dives into the implementation details of each of the above ## Create and manage rollup jobs -The most straight forward part of this plugin! A new app called Rollup Jobs is registered in the Management section and follows a typical CRUD UI pattern. This app allows users to create, start, stop, clone, and delete rollup jobs. There is no way to edit an existing rollup job; instead, the UI offers a cloning ability. The client-side portion of this app lives [here](../../../plugins/rollup/public/crud_app) and uses endpoints registered [here](server/routes/api/jobs.js). +The most straight forward part of this plugin! A new app called Rollup Jobs is registered in the Management section and follows a typical CRUD UI pattern. This app allows users to create, start, stop, clone, and delete rollup jobs. There is no way to edit an existing rollup job; instead, the UI offers a cloning ability. The client-side portion of this app lives in [public/crud_app](public/crud_app) and uses endpoints registered in [(server/routes/api/jobs](server/routes/api/jobs). Refer to the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-getting-started.html) to understand rollup indices and how to create rollup jobs. @@ -22,22 +22,22 @@ Refer to the [Elasticsearch documentation](https://www.elastic.co/guide/en/elast Kibana uses index patterns to consume and visualize rollup indices. Typically, Kibana can inspect the indices captured by an index pattern, identify its aggregations and fields, and determine how to consume the data. Rollup indices don't contain this type of information, so we predefine how to consume a rollup index pattern with the type and typeMeta fields on the index pattern saved object. All rollup index patterns have `type` defined as "rollup" and `typeMeta` defined as an object of the index pattern's capabilities. -In the Index Pattern app, the "Create index pattern" button includes a context menu when a rollup index is detected. This menu offers items for creating a standard index pattern and a rollup index pattern. A [rollup config is registered to index pattern creation extension point](../../../plugins/rollup/public/index_pattern_creation/rollup_index_pattern_creation_config.js). The context menu behavior in particular uses the `getIndexPatternCreationOption()` method. When the user chooses to create a rollup index pattern, this config changes the behavior of the index pattern creation wizard: +In the Index Pattern app, the "Create index pattern" button includes a context menu when a rollup index is detected. This menu offers items for creating a standard index pattern and a rollup index pattern. A [rollup config is registered to index pattern creation extension point](public/index_pattern_creation/rollup_index_pattern_creation_config.js). The context menu behavior in particular uses the `getIndexPatternCreationOption()` method. When the user chooses to create a rollup index pattern, this config changes the behavior of the index pattern creation wizard: 1. Adds a `Rollup` badge to rollup indices using `getIndexTags()`. 2. Enforces index pattern rules using `checkIndicesForErrors()`. Rollup index patterns must match **one** rollup index, and optionally, any number of regular indices. A rollup index pattern configured with one or more regular indices is known as a "hybrid" index pattern. This allows the user to visualize historical (rollup) data and live (regular) data in the same visualization. -3. Routes to this plugin's [rollup `_fields_for_wildcard` endpoint](server/routes/api/index_patterns.js), instead of the standard one, using `getFetchForWildcardOptions()`, so that the internal rollup data field names are mapped to the original field names. +3. Routes to this plugin's [rollup `_fields_for_wildcard` endpoint](server/routes/api/index_patterns/register_fields_for_wildcard_route.ts), instead of the standard one, using `getFetchForWildcardOptions()`, so that the internal rollup data field names are mapped to the original field names. 4. Writes additional information about aggregations, fields, histogram interval, and date histogram interval and timezone to the rollup index pattern saved object using `getIndexPatternMappings()`. This collection of information is referred to as its "capabilities". -Once a rollup index pattern is created, it is tagged with `Rollup` in the list of index patterns, and its details page displays capabilities information. This is done by registering [yet another config for the index pattern list](../../../plugins/rollup/public/index_pattern_list/rollup_index_pattern_list_config.js) extension points. +Once a rollup index pattern is created, it is tagged with `Rollup` in the list of index patterns, and its details page displays capabilities information. This is done by registering [yet another config for the index pattern list](public/index_pattern_list/rollup_index_pattern_list_config.js) extension points. ## Create visualizations from rollup index patterns This plugin enables the user to create visualizations from rollup data using the Visualize app, excluding TSVB, Vega, and Timelion. When Visualize sends search requests, this plugin routes the requests to the [Elasticsearch rollup search endpoint](https://www.elastic.co/guide/en/elasticsearch/reference/current/rollup-search.html), which searches the special document structure within rollup indices. The visualization options available to users are based on the capabilities of the rollup index pattern they're visualizing. -Routing to the Elasticsearch rollup search endpoint is done by creating an extension point in Courier, effectively allowing multiple "search strategies" to be registered. A [rollup search strategy](../../../plugins/rollup/public/search/register.js) is registered by this plugin that queries [this plugin's rollup search endpoint](server/routes/api/search.js). +Routing to the Elasticsearch rollup search endpoint is done by creating an extension point in Courier, effectively allowing multiple "search strategies" to be registered. A [rollup search strategy](public/search/register.js) is registered by this plugin that queries [this plugin's rollup search endpoint](server/routes/api/search.js). -Limiting visualization editor options is done by [registering configs](../../../plugins/rollup/public/visualize/index.js) to various vis extension points. These configs use information stored on the rollup index pattern to limit: +Limiting visualization editor options is done by [registering configs](public/visualize/index.js) to various vis extension points. These configs use information stored on the rollup index pattern to limit: * Available aggregation types * Available fields for a particular aggregation * Default and base interval for histogram aggregation @@ -47,6 +47,6 @@ Limiting visualization editor options is done by [registering configs](../../../ In Index Management, similar to system indices, rollup indices are hidden by default. A toggle is provided to show rollup indices and add a badge to the table rows. This is done by using Index Management's extension points. -The toggle and badge are registered on client-side [here](../../../plugins/rollup/public/extend_index_management/index.js). +The toggle and badge are registered on the client-side in [public/extend_index_management](public/extend_index_management). -Additional data needed to filter rollup indices in Index Management is provided with a [data enricher](rollup_data_enricher.js). +Additional data needed to filter rollup indices in Index Management is provided with a [data enricher](rollup_data_enricher.ts). \ No newline at end of file diff --git a/x-pack/plugins/rollup/common/index.ts b/x-pack/plugins/rollup/common/index.ts index aeffa3dc3959f..e94726a6f3d95 100644 --- a/x-pack/plugins/rollup/common/index.ts +++ b/x-pack/plugins/rollup/common/index.ts @@ -4,6 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import { LicenseType } from '../../licensing/common/types'; + +const basicLicense: LicenseType = 'basic'; + +export const PLUGIN = { + ID: 'rollup', + minimumLicenseType: basicLicense, +}; + export const CONFIG_ROLLUPS = 'rollups:enableIndexPatterns'; export const API_BASE_PATH = '/api/rollup'; diff --git a/x-pack/plugins/rollup/kibana.json b/x-pack/plugins/rollup/kibana.json index 8f832f6c6a345..4c7dcb48a4d3f 100644 --- a/x-pack/plugins/rollup/kibana.json +++ b/x-pack/plugins/rollup/kibana.json @@ -4,6 +4,17 @@ "kibanaVersion": "kibana", "server": true, "ui": true, - "optionalPlugins": ["home", "indexManagement", "indexPatternManagement", "usageCollection"], - "requiredPlugins": ["management", "data"] + "requiredPlugins": [ + "indexPatternManagement", + "management", + "licensing", + "data" + ], + "optionalPlugins": [ + "home", + "indexManagement", + "usageCollection", + "visTypeTimeseries" + ], + "configPath": ["xpack", "rollup"] } diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js index 4a55c4679c3d8..eca624e16cb86 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_create/steps_config/index.js @@ -42,7 +42,7 @@ export const stepIds = [ * 1. getDefaultFields: (overrides) => object * 2. fieldValidations * - * See x-pack/plugins/rollup/public/crud_app/services/jobs.js for more information on override's shape + * See rollup/public/crud_app/services/jobs.js for more information on override's shape */ export const stepIdToStepConfigMap = { [STEP_LOGISTICS]: { diff --git a/x-pack/plugins/rollup/public/crud_app/services/track_ui_metric.ts b/x-pack/plugins/rollup/public/crud_app/services/track_ui_metric.ts index aa1cc2dfea323..5d9340a140500 100644 --- a/x-pack/plugins/rollup/public/crud_app/services/track_ui_metric.ts +++ b/x-pack/plugins/rollup/public/crud_app/services/track_ui_metric.ts @@ -16,6 +16,9 @@ export { METRIC_TYPE }; export function trackUserRequest(request: Promise, actionType: string) { // Only track successful actions. return request.then(response => { + // NOTE: METRIC_TYPE.LOADED is probably the wrong metric type here. The correct metric type + // is more likely METRIC_TYPE.APPLICATION_USAGE. This change was introduced in + // https://github.com/elastic/kibana/pull/41113/files#diff-58ac12bdd1a3a05a24e69ff20633c482R20 trackUiMetric(METRIC_TYPE.LOADED, actionType); // We return the response immediately without waiting for the tracking request to resolve, // to avoid adding additional latency. diff --git a/x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/rollup_prompt.js b/x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/rollup_prompt.js index 42c950f0b0d74..9d81abf70a55d 100644 --- a/x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/rollup_prompt.js +++ b/x-pack/plugins/rollup/public/index_pattern_creation/components/rollup_prompt/rollup_prompt.js @@ -5,21 +5,34 @@ */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import { EuiCallOut } from '@elastic/eui'; export const RollupPrompt = () => (

- Kibana's support for rollup index patterns is in beta. You might encounter issues using - these patterns in saved searches, visualizations, and dashboards. They are not supported in - some advanced features, such as Timelion, and Machine Learning. + {i18n.translate( + 'xpack.rollupJobs.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph1Text', + { + defaultMessage: + "Kibana's support for rollup index patterns is in beta. You might encounter issues using " + + 'these patterns in saved searches, visualizations, and dashboards. They are not supported in ' + + 'some advanced features, such as Timelion, and Machine Learning.', + } + )}

- You can match a rollup index pattern against one rollup index and zero or more regular - indices. A rollup index pattern has limited metrics, fields, intervals, and aggregations. A - rollup index is limited to indices that have one job configuration, or multiple jobs with - compatible configurations. + {i18n.translate( + 'xpack.rollupJobs.editRollupIndexPattern.rollupPrompt.betaCalloutParagraph2Text', + { + defaultMessage: + 'You can match a rollup index pattern against one rollup index and zero or more regular ' + + 'indices. A rollup index pattern has limited metrics, fields, intervals, and aggregations. A ' + + 'rollup index is limited to indices that have one job configuration, or multiple jobs with ' + + 'compatible configurations.', + } + )}

); diff --git a/x-pack/legacy/plugins/rollup/server/client/elasticsearch_rollup.ts b/x-pack/plugins/rollup/server/client/elasticsearch_rollup.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/server/client/elasticsearch_rollup.ts rename to x-pack/plugins/rollup/server/client/elasticsearch_rollup.ts diff --git a/x-pack/legacy/plugins/rollup/server/collectors/index.ts b/x-pack/plugins/rollup/server/collectors/index.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/server/collectors/index.ts rename to x-pack/plugins/rollup/server/collectors/index.ts diff --git a/x-pack/legacy/plugins/rollup/server/collectors/register.ts b/x-pack/plugins/rollup/server/collectors/register.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/server/collectors/register.ts rename to x-pack/plugins/rollup/server/collectors/register.ts diff --git a/x-pack/plugins/rollup/server/config.ts b/x-pack/plugins/rollup/server/config.ts new file mode 100644 index 0000000000000..6d02600521c3a --- /dev/null +++ b/x-pack/plugins/rollup/server/config.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), +}); + +export type RollupConfig = TypeOf; diff --git a/x-pack/plugins/rollup/server/index.ts b/x-pack/plugins/rollup/server/index.ts index 4056842453776..78859a959a1e0 100644 --- a/x-pack/plugins/rollup/server/index.ts +++ b/x-pack/plugins/rollup/server/index.ts @@ -4,9 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'src/core/server'; +import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server'; import { RollupPlugin } from './plugin'; +import { configSchema, RollupConfig } from './config'; -export const plugin = (initContext: PluginInitializerContext) => new RollupPlugin(initContext); +export const plugin = (pluginInitializerContext: PluginInitializerContext) => + new RollupPlugin(pluginInitializerContext); -export { RollupSetup } from './plugin'; +export const config: PluginConfigDescriptor = { + schema: configSchema, +}; diff --git a/x-pack/legacy/plugins/rollup/server/lib/__tests__/fixtures/index.js b/x-pack/plugins/rollup/server/lib/__tests__/fixtures/index.js similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/__tests__/fixtures/index.js rename to x-pack/plugins/rollup/server/lib/__tests__/fixtures/index.js diff --git a/x-pack/plugins/rollup/server/lib/__tests__/fixtures/jobs.js b/x-pack/plugins/rollup/server/lib/__tests__/fixtures/jobs.js new file mode 100644 index 0000000000000..c03b7c33abe0a --- /dev/null +++ b/x-pack/plugins/rollup/server/lib/__tests__/fixtures/jobs.js @@ -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. + */ + +export const jobs = [ + { + job_id: 'foo1', + rollup_index: 'foo_rollup', + index_pattern: 'foo-*', + fields: { + node: [ + { + agg: 'terms', + }, + ], + temperature: [ + { + agg: 'min', + }, + { + agg: 'max', + }, + { + agg: 'sum', + }, + ], + timestamp: [ + { + agg: 'date_histogram', + time_zone: 'UTC', + interval: '1h', + delay: '7d', + }, + ], + voltage: [ + { + agg: 'histogram', + interval: 5, + }, + { + agg: 'sum', + }, + ], + }, + }, + { + job_id: 'foo2', + rollup_index: 'foo_rollup', + index_pattern: 'foo-*', + fields: { + host: [ + { + agg: 'terms', + }, + ], + timestamp: [ + { + agg: 'date_histogram', + time_zone: 'UTC', + interval: '1h', + delay: '7d', + }, + ], + voltage: [ + { + agg: 'histogram', + interval: 20, + }, + ], + }, + }, + { + job_id: 'foo3', + rollup_index: 'foo_rollup', + index_pattern: 'foo-*', + fields: { + timestamp: [ + { + agg: 'date_histogram', + time_zone: 'PST', + interval: '1h', + delay: '7d', + }, + ], + voltage: [ + { + agg: 'histogram', + interval: 5, + }, + { + agg: 'sum', + }, + ], + }, + }, +]; diff --git a/x-pack/legacy/plugins/rollup/server/lib/__tests__/jobs_compatibility.js b/x-pack/plugins/rollup/server/lib/__tests__/jobs_compatibility.js similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/__tests__/jobs_compatibility.js rename to x-pack/plugins/rollup/server/lib/__tests__/jobs_compatibility.js diff --git a/x-pack/plugins/rollup/server/lib/format_es_error.ts b/x-pack/plugins/rollup/server/lib/format_es_error.ts new file mode 100644 index 0000000000000..9dde027cd6949 --- /dev/null +++ b/x-pack/plugins/rollup/server/lib/format_es_error.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. + */ + +function extractCausedByChain( + causedBy: Record = {}, + accumulator: string[] = [] +): string[] { + const { reason, caused_by } = causedBy; // eslint-disable-line @typescript-eslint/camelcase + + if (reason) { + accumulator.push(reason); + } + + // eslint-disable-next-line @typescript-eslint/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 + */ +export function wrapEsError( + err: any, + statusCodeToMessageMap: Record = {} +): { message: string; body?: { cause?: string[] }; statusCode: number } { + const { statusCode, response } = err; + + const { + error: { + root_cause = [], // eslint-disable-line @typescript-eslint/camelcase + caused_by = undefined, // eslint-disable-line @typescript-eslint/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 and return it + if (!statusCodeToMessageMap[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; + + return { + message: err.message, + statusCode, + body: { + cause: causedByChain.length ? causedByChain : defaultCause, + }, + }; + } + + // Otherwise, use the custom message to create a Boom error response and + // return it + const message = statusCodeToMessageMap[statusCode]; + return { message, statusCode }; +} + +export function formatEsError(err: any): any { + const { statusCode, message, body } = wrapEsError(err); + return { + statusCode, + body: { + message, + attributes: { + cause: body?.cause, + }, + }, + }; +} diff --git a/x-pack/legacy/plugins/rollup/server/lib/is_es_error/is_es_error.ts b/x-pack/plugins/rollup/server/lib/is_es_error.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/is_es_error/is_es_error.ts rename to x-pack/plugins/rollup/server/lib/is_es_error.ts diff --git a/x-pack/legacy/plugins/rollup/server/lib/jobs_compatibility.ts b/x-pack/plugins/rollup/server/lib/jobs_compatibility.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/jobs_compatibility.ts rename to x-pack/plugins/rollup/server/lib/jobs_compatibility.ts diff --git a/x-pack/legacy/plugins/rollup/server/lib/map_capabilities.ts b/x-pack/plugins/rollup/server/lib/map_capabilities.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/map_capabilities.ts rename to x-pack/plugins/rollup/server/lib/map_capabilities.ts diff --git a/x-pack/legacy/plugins/rollup/server/lib/merge_capabilities_with_fields.ts b/x-pack/plugins/rollup/server/lib/merge_capabilities_with_fields.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/merge_capabilities_with_fields.ts rename to x-pack/plugins/rollup/server/lib/merge_capabilities_with_fields.ts diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/index.ts b/x-pack/plugins/rollup/server/lib/search_strategies/index.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/index.ts rename to x-pack/plugins/rollup/server/lib/search_strategies/index.ts diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/lib/interval_helper.test.js b/x-pack/plugins/rollup/server/lib/search_strategies/lib/interval_helper.test.js similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/lib/interval_helper.test.js rename to x-pack/plugins/rollup/server/lib/search_strategies/lib/interval_helper.test.js diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/lib/interval_helper.ts b/x-pack/plugins/rollup/server/lib/search_strategies/lib/interval_helper.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/lib/interval_helper.ts rename to x-pack/plugins/rollup/server/lib/search_strategies/lib/interval_helper.ts diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.test.js b/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.test.js similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.test.js rename to x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.test.js diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts b/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts similarity index 76% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts rename to x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts index 93c4c1b52140b..333863979ba95 100644 --- a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts +++ b/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts @@ -3,18 +3,19 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { getRollupSearchStrategy } from './rollup_search_strategy'; -import { getRollupSearchRequest } from './rollup_search_request'; -import { getRollupSearchCapabilities } from './rollup_search_capabilities'; + import { AbstractSearchRequest, DefaultSearchCapabilities, AbstractSearchStrategy, -} from '../../../../../../../src/plugins/vis_type_timeseries/server'; -import { RouteDependencies } from '../../types'; +} from '../../../../../../src/plugins/vis_type_timeseries/server'; +import { CallWithRequestFactoryShim } from '../../types'; +import { getRollupSearchStrategy } from './rollup_search_strategy'; +import { getRollupSearchRequest } from './rollup_search_request'; +import { getRollupSearchCapabilities } from './rollup_search_capabilities'; export const registerRollupSearchStrategy = ( - { elasticsearchService }: RouteDependencies, + callWithRequestFactory: CallWithRequestFactoryShim, addSearchStrategy: (searchStrategy: any) => void ) => { const RollupSearchRequest = getRollupSearchRequest(AbstractSearchRequest); @@ -22,8 +23,9 @@ export const registerRollupSearchStrategy = ( const RollupSearchStrategy = getRollupSearchStrategy( AbstractSearchStrategy, RollupSearchRequest, - RollupSearchCapabilities + RollupSearchCapabilities, + callWithRequestFactory ); - addSearchStrategy(new RollupSearchStrategy(elasticsearchService)); + addSearchStrategy(new RollupSearchStrategy()); }; diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.test.js b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.test.js similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.test.js rename to x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.test.js diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.ts b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.ts similarity index 98% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.ts rename to x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.ts index 5a57129aa6039..151afe660847f 100644 --- a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.ts +++ b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_capabilities.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { get, has } from 'lodash'; -import { KibanaRequest } from 'kibana/server'; +import { KibanaRequest } from 'src/core/server'; import { leastCommonInterval, isCalendarInterval } from './lib/interval_helper'; export const getRollupSearchCapabilities = (DefaultSearchCapabilities: any) => diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_request.test.js b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_request.test.js similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_request.test.js rename to x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_request.test.js diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_request.ts b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_request.ts similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_request.ts rename to x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_request.ts diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.test.js b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.test.js similarity index 100% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.test.js rename to x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.test.js diff --git a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts similarity index 84% rename from x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts rename to x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts index 9d5aad2c2d3bc..815fe163411b3 100644 --- a/x-pack/legacy/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts +++ b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import { indexBy, isString } from 'lodash'; -import { ElasticsearchServiceSetup, KibanaRequest } from 'kibana/server'; -import { callWithRequestFactory } from '../call_with_request_factory'; +import { KibanaRequest } from 'src/core/server'; + +import { CallWithRequestFactoryShim } from '../../types'; import { mergeCapabilitiesWithFields } from '../merge_capabilities_with_fields'; import { getCapabilitiesForRollupIndices } from '../map_capabilities'; @@ -20,13 +21,16 @@ const isIndexPatternValid = (indexPattern: string) => export const getRollupSearchStrategy = ( AbstractSearchStrategy: any, RollupSearchRequest: any, - RollupSearchCapabilities: any + RollupSearchCapabilities: any, + callWithRequestFactory: CallWithRequestFactoryShim ) => class RollupSearchStrategy extends AbstractSearchStrategy { name = 'rollup'; - constructor(elasticsearchService: ElasticsearchServiceSetup) { - super(elasticsearchService, callWithRequestFactory, RollupSearchRequest); + constructor() { + // TODO: When vis_type_timeseries and AbstractSearchStrategy are migrated to the NP, it + // shouldn't require elasticsearchService to be injected, and we can remove this null argument. + super(null, callWithRequestFactory, RollupSearchRequest); } getRollupData(req: KibanaRequest, indexPattern: string) { diff --git a/x-pack/plugins/rollup/server/plugin.ts b/x-pack/plugins/rollup/server/plugin.ts index ea6d197e22029..ee9a1844c7468 100644 --- a/x-pack/plugins/rollup/server/plugin.ts +++ b/x-pack/plugins/rollup/server/plugin.ts @@ -4,20 +4,98 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/server'; +declare module 'src/core/server' { + interface RequestHandlerContext { + rollup?: RollupContext; + } +} + +import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { + CoreSetup, + Plugin, + Logger, + KibanaRequest, + PluginInitializerContext, + IScopedClusterClient, + APICaller, + SharedGlobalConfig, +} from 'src/core/server'; import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; -import { CONFIG_ROLLUPS } from '../common'; -export class RollupPlugin implements Plugin { - private readonly initContext: PluginInitializerContext; +import { PLUGIN, CONFIG_ROLLUPS } from '../common'; +import { Dependencies, CallWithRequestFactoryShim } from './types'; +import { registerApiRoutes } from './routes'; +import { License } from './services'; +import { registerRollupUsageCollector } from './collectors'; +import { rollupDataEnricher } from './rollup_data_enricher'; +import { IndexPatternsFetcher } from './shared_imports'; +import { registerRollupSearchStrategy } from './lib/search_strategies'; +import { elasticsearchJsPlugin } from './client/elasticsearch_rollup'; +import { isEsError } from './lib/is_es_error'; +import { formatEsError } from './lib/format_es_error'; +import { getCapabilitiesForRollupIndices } from './lib/map_capabilities'; +import { mergeCapabilitiesWithFields } from './lib/merge_capabilities_with_fields'; + +interface RollupContext { + client: IScopedClusterClient; +} + +export class RollupPlugin implements Plugin { + private readonly logger: Logger; + private readonly globalConfig$: Observable; + private readonly license: License; - constructor(initContext: PluginInitializerContext) { - this.initContext = initContext; + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + this.globalConfig$ = initializerContext.config.legacy.globalConfig$; + this.license = new License(); } - public setup(core: CoreSetup) { - core.uiSettings.register({ + public setup( + { http, uiSettings, elasticsearch }: CoreSetup, + { licensing, indexManagement, visTypeTimeseries, usageCollection }: Dependencies + ) { + this.license.setup( + { + pluginId: PLUGIN.ID, + minimumLicenseType: PLUGIN.minimumLicenseType, + defaultErrorMessage: i18n.translate('xpack.rollupJobs.licenseCheckErrorMessage', { + defaultMessage: 'License check failed', + }), + }, + { + licensing, + logger: this.logger, + } + ); + + // Extend the elasticsearchJs client with additional endpoints. + const esClientConfig = { plugins: [elasticsearchJsPlugin] }; + const rollupEsClient = elasticsearch.createClient('rollup', esClientConfig); + http.registerRouteHandlerContext('rollup', (context, request) => { + return { + client: rollupEsClient.asScoped(request), + }; + }); + + registerApiRoutes({ + router: http.createRouter(), + license: this.license, + lib: { + isEsError, + formatEsError, + getCapabilitiesForRollupIndices, + mergeCapabilitiesWithFields, + }, + sharedImports: { + IndexPatternsFetcher, + }, + }); + + uiSettings.register({ [CONFIG_ROLLUPS]: { name: i18n.translate('xpack.rollupJobs.rollupIndexPatternsTitle', { defaultMessage: 'Enable rollup index patterns', @@ -33,22 +111,34 @@ export class RollupPlugin implements Plugin { }, }); - return { - __legacy: { - config: this.initContext.config, - logger: this.initContext.logger, - }, - }; - } + if (visTypeTimeseries) { + // TODO: When vis_type_timeseries is fully migrated to the NP, it shouldn't require this shim. + const callWithRequestFactoryShim = ( + elasticsearchServiceShim: CallWithRequestFactoryShim, + request: KibanaRequest + ): APICaller => rollupEsClient.asScoped(request).callAsCurrentUser; - public start() {} - public stop() {} -} + const { addSearchStrategy } = visTypeTimeseries; + registerRollupSearchStrategy(callWithRequestFactoryShim, addSearchStrategy); + } + + if (usageCollection) { + this.globalConfig$ + .pipe(first()) + .toPromise() + .then(globalConfig => { + registerRollupUsageCollector(usageCollection, globalConfig.kibana.index); + }) + .catch((e: any) => { + this.logger.warn(`Registering Rollup collector failed: ${e}`); + }); + } + + if (indexManagement && indexManagement.indexDataEnricher) { + indexManagement.indexDataEnricher.add(rollupDataEnricher); + } + } -export interface RollupSetup { - /** @deprecated */ - __legacy: { - config: PluginInitializerContext['config']; - logger: PluginInitializerContext['logger']; - }; + start() {} + stop() {} } diff --git a/x-pack/legacy/plugins/rollup/server/rollup_data_enricher.ts b/x-pack/plugins/rollup/server/rollup_data_enricher.ts similarity index 92% rename from x-pack/legacy/plugins/rollup/server/rollup_data_enricher.ts rename to x-pack/plugins/rollup/server/rollup_data_enricher.ts index ad621f2d9ba80..b06cf971a6460 100644 --- a/x-pack/legacy/plugins/rollup/server/rollup_data_enricher.ts +++ b/x-pack/plugins/rollup/server/rollup_data_enricher.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Index } from '../../../../plugins/index_management/server'; +import { Index } from '../../../plugins/index_management/server'; export const rollupDataEnricher = async (indicesList: Index[], callWithRequest: any) => { if (!indicesList || !indicesList.length) { diff --git a/x-pack/plugins/rollup/server/routes/api/index_patterns/index.ts b/x-pack/plugins/rollup/server/routes/api/index_patterns/index.ts new file mode 100644 index 0000000000000..7bf525ca4aa98 --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/index_patterns/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 { RouteDependencies } from '../../../types'; +import { registerFieldsForWildcardRoute } from './register_fields_for_wildcard_route'; + +export function registerIndexPatternsRoutes(dependencies: RouteDependencies) { + registerFieldsForWildcardRoute(dependencies); +} diff --git a/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts b/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts new file mode 100644 index 0000000000000..32f23314c5259 --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/index_patterns/register_fields_for_wildcard_route.ts @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { indexBy } from 'lodash'; +import { schema } from '@kbn/config-schema'; +import { Field } from '../../../lib/merge_capabilities_with_fields'; +import { RouteDependencies } from '../../../types'; + +const parseMetaFields = (metaFields: string | string[]) => { + let parsedFields: string[] = []; + if (typeof metaFields === 'string') { + parsedFields = JSON.parse(metaFields); + } else { + parsedFields = metaFields; + } + return parsedFields; +}; + +const getFieldsForWildcardRequest = async ( + context: any, + request: any, + response: any, + IndexPatternsFetcher: any +) => { + const { callAsCurrentUser } = context.core.elasticsearch.dataClient; + const indexPatterns = new IndexPatternsFetcher(callAsCurrentUser); + const { pattern, meta_fields: metaFields } = request.query; + + let parsedFields: string[] = []; + try { + parsedFields = parseMetaFields(metaFields); + } catch (error) { + return response.badRequest({ + body: error, + }); + } + + try { + const fields = await indexPatterns.getFieldsForWildcard({ + pattern, + metaFields: parsedFields, + }); + + return response.ok({ + body: { fields }, + headers: { + 'content-type': 'application/json', + }, + }); + } catch (error) { + return response.notFound(); + } +}; + +/** + * Get list of fields for rollup index pattern, in the format of regular index pattern fields + */ +export const registerFieldsForWildcardRoute = ({ + router, + license, + lib: { isEsError, formatEsError, getCapabilitiesForRollupIndices, mergeCapabilitiesWithFields }, + sharedImports: { IndexPatternsFetcher }, +}: RouteDependencies) => { + const querySchema = schema.object({ + pattern: schema.string(), + meta_fields: schema.arrayOf(schema.string(), { + defaultValue: [], + }), + params: schema.string({ + validate(value) { + try { + const params = JSON.parse(value); + const keys = Object.keys(params); + const { rollup_index: rollupIndex } = params; + + if (!rollupIndex) { + return '[request query.params]: "rollup_index" is required'; + } else if (keys.length > 1) { + const invalidParams = keys.filter(key => key !== 'rollup_index'); + return `[request query.params]: ${invalidParams.join(', ')} is not allowed`; + } + } catch (err) { + return '[request query.params]: expected JSON string'; + } + }, + }), + }); + + router.get( + { + path: '/api/index_patterns/rollup/_fields_for_wildcard', + validate: { + query: querySchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { params, meta_fields: metaFields } = request.query; + + try { + // Make call and use field information from response + const { payload } = await getFieldsForWildcardRequest( + context, + request, + response, + IndexPatternsFetcher + ); + const fields = payload.fields; + const parsedParams = JSON.parse(params); + const rollupIndex = parsedParams.rollup_index; + const rollupFields: Field[] = []; + const fieldsFromFieldCapsApi: { [key: string]: any } = indexBy(fields, 'name'); + const rollupIndexCapabilities = getCapabilitiesForRollupIndices( + await context.rollup!.client.callAsCurrentUser('rollup.rollupIndexCapabilities', { + indexPattern: rollupIndex, + }) + )[rollupIndex].aggs; + + // Keep meta fields + metaFields.forEach( + (field: string) => + fieldsFromFieldCapsApi[field] && rollupFields.push(fieldsFromFieldCapsApi[field]) + ); + + const mergedRollupFields = mergeCapabilitiesWithFields( + rollupIndexCapabilities, + fieldsFromFieldCapsApi, + rollupFields + ); + return response.ok({ body: { fields: mergedRollupFields } }); + } catch (err) { + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/rollup/server/routes/api/indices/index.ts b/x-pack/plugins/rollup/server/routes/api/indices/index.ts new file mode 100644 index 0000000000000..0aa5772b56991 --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/indices/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RouteDependencies } from '../../../types'; +import { registerGetRoute } from './register_get_route'; +import { registerValidateIndexPatternRoute } from './register_validate_index_pattern_route'; + +export function registerIndicesRoutes(dependencies: RouteDependencies) { + registerGetRoute(dependencies); + registerValidateIndexPatternRoute(dependencies); +} diff --git a/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts b/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts new file mode 100644 index 0000000000000..3521650c1dc3e --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts @@ -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 { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Returns a list of all rollup index names + */ +export const registerGetRoute = ({ + router, + license, + lib: { isEsError, formatEsError, getCapabilitiesForRollupIndices }, +}: RouteDependencies) => { + router.get( + { + path: addBasePath('/indices'), + validate: false, + }, + license.guardApiRoute(async (context, request, response) => { + try { + const data = await context.rollup!.client.callAsCurrentUser( + 'rollup.rollupIndexCapabilities', + { + indexPattern: '_all', + } + ); + return response.ok({ body: getCapabilitiesForRollupIndices(data) }); + } catch (err) { + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts b/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts new file mode 100644 index 0000000000000..9e22060b9beb7 --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/indices/register_validate_index_pattern_route.ts @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +type NumericField = + | 'long' + | 'integer' + | 'short' + | 'byte' + | 'scaled_float' + | 'double' + | 'float' + | 'half_float'; + +interface FieldCapability { + date?: any; + keyword?: any; + long?: any; + integer?: any; + short?: any; + byte?: any; + double?: any; + float?: any; + half_float?: any; + scaled_float?: any; +} + +interface FieldCapabilities { + fields: FieldCapability[]; +} + +function isNumericField(fieldCapability: FieldCapability) { + const numericTypes = [ + 'long', + 'integer', + 'short', + 'byte', + 'double', + 'float', + 'half_float', + 'scaled_float', + ]; + return numericTypes.some(numericType => fieldCapability[numericType as NumericField] != null); +} + +/** + * Returns information on validity of an index pattern for creating a rollup job: + * - Does the index pattern match any indices? + * - Does the index pattern match rollup indices? + * - Which date fields, numeric fields, and keyword fields are available in the matching indices? + */ +export const registerValidateIndexPatternRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + router.get( + { + path: addBasePath('/index_pattern_validity/{indexPattern}'), + validate: { + params: schema.object({ + indexPattern: schema.string(), + }), + }, + }, + license.guardApiRoute(async (context, request, response) => { + try { + const { indexPattern } = request.params; + const [fieldCapabilities, rollupIndexCapabilities]: [ + FieldCapabilities, + { [key: string]: any } + ] = await Promise.all([ + context.rollup!.client.callAsCurrentUser('rollup.fieldCapabilities', { indexPattern }), + context.rollup!.client.callAsCurrentUser('rollup.rollupIndexCapabilities', { + indexPattern, + }), + ]); + + const doesMatchIndices = Object.entries(fieldCapabilities.fields).length !== 0; + const doesMatchRollupIndices = Object.entries(rollupIndexCapabilities).length !== 0; + + const dateFields: string[] = []; + const numericFields: string[] = []; + const keywordFields: string[] = []; + + const fieldCapabilitiesEntries = Object.entries(fieldCapabilities.fields); + + fieldCapabilitiesEntries.forEach( + ([fieldName, fieldCapability]: [string, FieldCapability]) => { + if (fieldCapability.date) { + dateFields.push(fieldName); + return; + } + + if (isNumericField(fieldCapability)) { + numericFields.push(fieldName); + return; + } + + if (fieldCapability.keyword) { + keywordFields.push(fieldName); + } + } + ); + + const body = { + doesMatchIndices, + doesMatchRollupIndices, + dateFields, + numericFields, + keywordFields, + }; + + return response.ok({ body }); + } catch (err) { + // 404s are still valid results. + if (err.statusCode === 404) { + const notFoundBody = { + doesMatchIndices: false, + doesMatchRollupIndices: false, + dateFields: [], + numericFields: [], + keywordFields: [], + }; + return response.ok({ body: notFoundBody }); + } + + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/index.ts b/x-pack/plugins/rollup/server/routes/api/jobs/index.ts new file mode 100644 index 0000000000000..fe1d1c6109a88 --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/jobs/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 { RouteDependencies } from '../../../types'; +import { registerCreateRoute } from './register_create_route'; +import { registerDeleteRoute } from './register_delete_route'; +import { registerGetRoute } from './register_get_route'; +import { registerStartRoute } from './register_start_route'; +import { registerStopRoute } from './register_stop_route'; + +export function registerJobsRoutes(dependencies: RouteDependencies) { + registerCreateRoute(dependencies); + registerDeleteRoute(dependencies); + registerGetRoute(dependencies); + registerStartRoute(dependencies); + registerStopRoute(dependencies); +} diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.ts new file mode 100644 index 0000000000000..adf8c1da0af0e --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_create_route.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 { schema } from '@kbn/config-schema'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +export const registerCreateRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + router.put( + { + path: addBasePath('/create'), + validate: { + body: schema.object({ + job: schema.object( + { + id: schema.string(), + }, + { unknowns: 'allow' } + ), + }), + }, + }, + license.guardApiRoute(async (context, request, response) => { + try { + const { id, ...rest } = request.body.job; + // Create job. + await context.rollup!.client.callAsCurrentUser('rollup.createJob', { + id, + body: rest, + }); + // Then request the newly created job. + const results = await context.rollup!.client.callAsCurrentUser('rollup.job', { id }); + return response.ok({ body: results.jobs[0] }); + } catch (err) { + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts new file mode 100644 index 0000000000000..32f7b3f35e163 --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_delete_route.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +export const registerDeleteRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + router.post( + { + path: addBasePath('/delete'), + validate: { + body: schema.object({ + jobIds: schema.arrayOf(schema.string()), + }), + }, + }, + license.guardApiRoute(async (context, request, response) => { + try { + const { jobIds } = request.body; + const data = await Promise.all( + jobIds.map((id: string) => + context.rollup!.client.callAsCurrentUser('rollup.deleteJob', { id }) + ) + ).then(() => ({ success: true })); + return response.ok({ body: data }); + } catch (err) { + // There is an issue opened on ES to handle the following error correctly + // https://github.com/elastic/elasticsearch/issues/42908 + // Until then we'll modify the response here. + if (err.response && err.response.includes('Job must be [STOPPED] before deletion')) { + err.status = 400; + err.statusCode = 400; + err.displayName = 'Bad request'; + err.message = JSON.parse(err.response).task_failures[0].reason.reason; + } + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.ts new file mode 100644 index 0000000000000..a8d51f4639fc6 --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_get_route.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 { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +export const registerGetRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + router.get( + { + path: addBasePath('/jobs'), + validate: false, + }, + license.guardApiRoute(async (context, request, response) => { + try { + const data = await context.rollup!.client.callAsCurrentUser('rollup.jobs'); + return response.ok({ body: data }); + } catch (err) { + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts new file mode 100644 index 0000000000000..fb6f2b12ba52e --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_start_route.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +export const registerStartRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + router.post( + { + path: addBasePath('/start'), + validate: { + body: schema.object({ + jobIds: schema.arrayOf(schema.string()), + }), + query: schema.maybe( + schema.object({ + waitForCompletion: schema.maybe(schema.string()), + }) + ), + }, + }, + license.guardApiRoute(async (context, request, response) => { + try { + const { jobIds } = request.body; + + const data = await Promise.all( + jobIds.map((id: string) => + context.rollup!.client.callAsCurrentUser('rollup.startJob', { id }) + ) + ).then(() => ({ success: true })); + return response.ok({ body: data }); + } catch (err) { + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts b/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.ts new file mode 100644 index 0000000000000..118d98e36e03c --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/jobs/register_stop_route.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 { schema } from '@kbn/config-schema'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +export const registerStopRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + router.post( + { + path: addBasePath('/stop'), + validate: { + body: schema.object({ + jobIds: schema.arrayOf(schema.string()), + }), + query: schema.object({ + waitForCompletion: schema.maybe(schema.string()), + }), + }, + }, + license.guardApiRoute(async (context, request, response) => { + try { + const { jobIds } = request.body; + // For our API integration tests we need to wait for the jobs to be stopped + // in order to be able to delete them sequentially. + const { waitForCompletion } = request.query; + const stopRollupJob = (id: string) => + context.rollup!.client.callAsCurrentUser('rollup.stopJob', { + id, + waitForCompletion: waitForCompletion === 'true', + }); + const data = await Promise.all(jobIds.map(stopRollupJob)).then(() => ({ success: true })); + return response.ok({ body: data }); + } catch (err) { + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/rollup/server/routes/api/search/index.ts b/x-pack/plugins/rollup/server/routes/api/search/index.ts new file mode 100644 index 0000000000000..2a2d823e79bc6 --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/search/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 { RouteDependencies } from '../../../types'; +import { registerSearchRoute } from './register_search_route'; + +export function registerSearchRoutes(dependencies: RouteDependencies) { + registerSearchRoute(dependencies); +} diff --git a/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts b/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts new file mode 100644 index 0000000000000..c5c56336def1a --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/api/search/register_search_route.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +export const registerSearchRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + router.post( + { + path: addBasePath('/search'), + validate: { + body: schema.arrayOf( + schema.object({ + index: schema.string(), + query: schema.any(), + }) + ), + }, + }, + license.guardApiRoute(async (context, request, response) => { + try { + const requests = request.body.map(({ index, query }: { index: string; query?: any }) => + context.rollup!.client.callAsCurrentUser('rollup.search', { + index, + rest_total_hits_as_int: true, + body: query, + }) + ); + const data = await Promise.all(requests); + return response.ok({ body: data }); + } catch (err) { + if (isEsError(err)) { + return response.customError({ statusCode: err.statusCode, body: err }); + } + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/rollup/server/routes/index.ts b/x-pack/plugins/rollup/server/routes/index.ts new file mode 100644 index 0000000000000..b25480855b4a2 --- /dev/null +++ b/x-pack/plugins/rollup/server/routes/index.ts @@ -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 { RouteDependencies } from '../types'; + +import { registerIndexPatternsRoutes } from './api/index_patterns'; +import { registerIndicesRoutes } from './api/indices'; +import { registerJobsRoutes } from './api/jobs'; +import { registerSearchRoutes } from './api/search'; + +export function registerApiRoutes(dependencies: RouteDependencies) { + registerIndexPatternsRoutes(dependencies); + registerIndicesRoutes(dependencies); + registerJobsRoutes(dependencies); + registerSearchRoutes(dependencies); +} diff --git a/x-pack/plugins/rollup/server/services/add_base_path.ts b/x-pack/plugins/rollup/server/services/add_base_path.ts new file mode 100644 index 0000000000000..7d7cce3aab334 --- /dev/null +++ b/x-pack/plugins/rollup/server/services/add_base_path.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { API_BASE_PATH } from '../../common'; + +export const addBasePath = (uri: string): string => `${API_BASE_PATH}${uri}`; diff --git a/x-pack/plugins/rollup/server/services/index.ts b/x-pack/plugins/rollup/server/services/index.ts new file mode 100644 index 0000000000000..7f79c4f446546 --- /dev/null +++ b/x-pack/plugins/rollup/server/services/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under 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 { addBasePath } from './add_base_path'; +export { License } from './license'; diff --git a/x-pack/plugins/rollup/server/services/license.ts b/x-pack/plugins/rollup/server/services/license.ts new file mode 100644 index 0000000000000..bfd357867c3e2 --- /dev/null +++ b/x-pack/plugins/rollup/server/services/license.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Logger } from 'src/core/server'; +import { + KibanaRequest, + KibanaResponseFactory, + RequestHandler, + RequestHandlerContext, +} from 'src/core/server'; + +import { LicensingPluginSetup } from '../../../licensing/server'; +import { LicenseType } from '../../../licensing/common/types'; + +export interface LicenseStatus { + isValid: boolean; + message?: string; +} + +interface SetupSettings { + pluginId: string; + minimumLicenseType: LicenseType; + defaultErrorMessage: string; +} + +export class License { + private licenseStatus: LicenseStatus = { + isValid: false, + message: 'Invalid License', + }; + + private _isEsSecurityEnabled: boolean = false; + + setup( + { pluginId, minimumLicenseType, defaultErrorMessage }: SetupSettings, + { licensing, logger }: { licensing: LicensingPluginSetup; logger: Logger } + ) { + licensing.license$.subscribe(license => { + const { state, message } = license.check(pluginId, minimumLicenseType); + const hasRequiredLicense = state === 'valid'; + + // Retrieving security checks the results of GET /_xpack as well as license state, + // so we're also checking whether the security is disabled in elasticsearch.yml. + this._isEsSecurityEnabled = license.getFeature('security').isEnabled; + + if (hasRequiredLicense) { + this.licenseStatus = { isValid: true }; + } else { + this.licenseStatus = { + isValid: false, + message: message || defaultErrorMessage, + }; + if (message) { + logger.info(message); + } + } + }); + } + + guardApiRoute(handler: RequestHandler) { + const license = this; + + return function licenseCheck( + ctx: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory + ) { + const licenseStatus = license.getStatus(); + + if (!licenseStatus.isValid) { + return response.customError({ + body: { + message: licenseStatus.message || '', + }, + statusCode: 403, + }); + } + + return handler(ctx, request, response); + }; + } + + getStatus() { + return this.licenseStatus; + } + + // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility + get isEsSecurityEnabled() { + return this._isEsSecurityEnabled; + } +} diff --git a/x-pack/legacy/plugins/rollup/server/shared_imports.ts b/x-pack/plugins/rollup/server/shared_imports.ts similarity index 75% rename from x-pack/legacy/plugins/rollup/server/shared_imports.ts rename to x-pack/plugins/rollup/server/shared_imports.ts index 941610b97707f..09842f529abed 100644 --- a/x-pack/legacy/plugins/rollup/server/shared_imports.ts +++ b/x-pack/plugins/rollup/server/shared_imports.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { IndexPatternsFetcher } from '../../../../../src/plugins/data/server'; +export { IndexPatternsFetcher } from '../../../../src/plugins/data/server'; diff --git a/x-pack/plugins/rollup/server/types.ts b/x-pack/plugins/rollup/server/types.ts new file mode 100644 index 0000000000000..c21d76400164e --- /dev/null +++ b/x-pack/plugins/rollup/server/types.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter, APICaller, KibanaRequest } from 'src/core/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { VisTypeTimeseriesSetup } from 'src/plugins/vis_type_timeseries/server'; + +import { IndexManagementPluginSetup } from '../../index_management/server'; +import { LicensingPluginSetup } from '../../licensing/server'; +import { License } from './services'; +import { IndexPatternsFetcher } from './shared_imports'; +import { isEsError } from './lib/is_es_error'; +import { formatEsError } from './lib/format_es_error'; +import { getCapabilitiesForRollupIndices } from './lib/map_capabilities'; +import { mergeCapabilitiesWithFields } from './lib/merge_capabilities_with_fields'; + +export interface Dependencies { + indexManagement?: IndexManagementPluginSetup; + visTypeTimeseries?: VisTypeTimeseriesSetup; + usageCollection?: UsageCollectionSetup; + licensing: LicensingPluginSetup; +} + +export interface RouteDependencies { + router: IRouter; + license: License; + lib: { + isEsError: typeof isEsError; + formatEsError: typeof formatEsError; + getCapabilitiesForRollupIndices: typeof getCapabilitiesForRollupIndices; + mergeCapabilitiesWithFields: typeof mergeCapabilitiesWithFields; + }; + sharedImports: { + IndexPatternsFetcher: typeof IndexPatternsFetcher; + }; +} + +// TODO: When vis_type_timeseries is fully migrated to the NP, it shouldn't require this shim. +export type CallWithRequestFactoryShim = ( + elasticsearchServiceShim: CallWithRequestFactoryShim, + request: KibanaRequest +) => APICaller; diff --git a/x-pack/plugins/security/public/authentication/authentication_service.ts b/x-pack/plugins/security/public/authentication/authentication_service.ts index 979f7095cf933..2e73b8cd04482 100644 --- a/x-pack/plugins/security/public/authentication/authentication_service.ts +++ b/x-pack/plugins/security/public/authentication/authentication_service.ts @@ -25,6 +25,11 @@ export interface AuthenticationServiceSetup { * Returns currently authenticated user and throws if current user isn't authenticated. */ getCurrentUser: () => Promise; + + /** + * Determines if API Keys are currently enabled. + */ + areAPIKeysEnabled: () => Promise; } export class AuthenticationService { @@ -37,11 +42,15 @@ export class AuthenticationService { const getCurrentUser = async () => (await http.get('/internal/security/me', { asSystemRequest: true })) as AuthenticatedUser; + const areAPIKeysEnabled = async () => + ((await http.get('/internal/security/api_key/_enabled')) as { apiKeysEnabled: boolean }) + .apiKeysEnabled; + loginApp.create({ application, config, getStartServices, http }); logoutApp.create({ application, http }); loggedOutApp.create({ application, getStartServices, http }); overwrittenSessionApp.create({ application, authc: { getCurrentUser }, getStartServices }); - return { getCurrentUser }; + return { getCurrentUser, areAPIKeysEnabled }; } } diff --git a/x-pack/plugins/security/public/authentication/index.mock.ts b/x-pack/plugins/security/public/authentication/index.mock.ts index c8d77a5b62c6f..dee0a24ab27c2 100644 --- a/x-pack/plugins/security/public/authentication/index.mock.ts +++ b/x-pack/plugins/security/public/authentication/index.mock.ts @@ -9,5 +9,6 @@ import { AuthenticationServiceSetup } from './authentication_service'; export const authenticationMock = { createSetup: (): jest.Mocked => ({ getCurrentUser: jest.fn(), + areAPIKeysEnabled: jest.fn(), }), }; diff --git a/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.ts b/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.ts index 8e0ee73dfb613..213c26d5287dc 100644 --- a/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.ts +++ b/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_app.ts @@ -10,7 +10,7 @@ import { AuthenticationServiceSetup } from '../authentication_service'; interface CreateDeps { application: ApplicationSetup; - authc: AuthenticationServiceSetup; + authc: Pick; getStartServices: StartServicesAccessor; } diff --git a/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_page.tsx b/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_page.tsx index 1093957761d1c..5b77266068ebf 100644 --- a/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_page.tsx +++ b/x-pack/plugins/security/public/authentication/overwritten_session/overwritten_session_page.tsx @@ -14,7 +14,7 @@ import { AuthenticationStatePage } from '../components'; interface Props { basePath: IBasePath; - authc: AuthenticationServiceSetup; + authc: Pick; } export function OverwrittenSessionPage({ authc, basePath }: Props) { diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts index 372b1e56a73c4..a127379d97241 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts @@ -10,6 +10,7 @@ import { ApiKey, ApiKeyToInvalidate } from '../../../common/model'; interface CheckPrivilegesResponse { areApiKeysEnabled: boolean; isAdmin: boolean; + canManage: boolean; } interface InvalidateApiKeysResponse { diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx index ae6ef4aa0fc34..dea04a0eac396 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx @@ -18,7 +18,6 @@ import { APIKeysGridPage } from './api_keys_grid_page'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import { apiKeysAPIClientMock } from '../index.mock'; -const mock403 = () => ({ body: { statusCode: 403 } }); const mock500 = () => ({ body: { error: 'Internal Server Error', message: '', statusCode: 500 } }); const waitForRender = async ( @@ -48,6 +47,7 @@ describe('APIKeysGridPage', () => { apiClientMock.checkPrivileges.mockResolvedValue({ isAdmin: true, areApiKeysEnabled: true, + canManage: true, }); apiClientMock.getApiKeys.mockResolvedValue({ apiKeys: [ @@ -82,6 +82,7 @@ describe('APIKeysGridPage', () => { it('renders a callout when API keys are not enabled', async () => { apiClientMock.checkPrivileges.mockResolvedValue({ isAdmin: true, + canManage: true, areApiKeysEnabled: false, }); @@ -95,7 +96,11 @@ describe('APIKeysGridPage', () => { }); it('renders permission denied if user does not have required permissions', async () => { - apiClientMock.checkPrivileges.mockRejectedValue(mock403()); + apiClientMock.checkPrivileges.mockResolvedValue({ + canManage: false, + isAdmin: false, + areApiKeysEnabled: true, + }); const wrapper = mountWithIntl(); @@ -152,6 +157,7 @@ describe('APIKeysGridPage', () => { beforeEach(() => { apiClientMock.checkPrivileges.mockResolvedValue({ isAdmin: false, + canManage: true, areApiKeysEnabled: true, }); diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx index 698c0d37dbc64..9db09a34d3c3f 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx @@ -26,7 +26,6 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import moment from 'moment-timezone'; -import _ from 'lodash'; import { NotificationsStart } from 'src/core/public'; import { SectionLoading } from '../../../../../../../src/plugins/es_ui_shared/public'; import { ApiKey, ApiKeyToInvalidate } from '../../../../common/model'; @@ -47,10 +46,10 @@ interface State { isLoadingApp: boolean; isLoadingTable: boolean; isAdmin: boolean; + canManage: boolean; areApiKeysEnabled: boolean; apiKeys: ApiKey[]; selectedItems: ApiKey[]; - permissionDenied: boolean; error: any; } @@ -63,9 +62,9 @@ export class APIKeysGridPage extends Component { isLoadingApp: true, isLoadingTable: false, isAdmin: false, + canManage: false, areApiKeysEnabled: false, apiKeys: [], - permissionDenied: false, selectedItems: [], error: undefined, }; @@ -77,19 +76,15 @@ export class APIKeysGridPage extends Component { public render() { const { - permissionDenied, isLoadingApp, isLoadingTable, areApiKeysEnabled, isAdmin, + canManage, error, apiKeys, } = this.state; - if (permissionDenied) { - return ; - } - if (isLoadingApp) { return ( @@ -103,6 +98,10 @@ export class APIKeysGridPage extends Component { ); } + if (!canManage) { + return ; + } + if (error) { const { body: { error: errorTitle, message, statusCode }, @@ -495,26 +494,25 @@ export class APIKeysGridPage extends Component { private async checkPrivileges() { try { - const { isAdmin, areApiKeysEnabled } = await this.props.apiKeysAPIClient.checkPrivileges(); - this.setState({ isAdmin, areApiKeysEnabled }); + const { + isAdmin, + canManage, + areApiKeysEnabled, + } = await this.props.apiKeysAPIClient.checkPrivileges(); + this.setState({ isAdmin, canManage, areApiKeysEnabled }); - if (areApiKeysEnabled) { - this.initiallyLoadApiKeys(); - } else { - // We're done loading and will just show the "Disabled" error. + if (!canManage || !areApiKeysEnabled) { this.setState({ isLoadingApp: false }); - } - } catch (e) { - if (_.get(e, 'body.statusCode') === 403) { - this.setState({ permissionDenied: true, isLoadingApp: false }); } else { - this.props.notifications.toasts.addDanger( - i18n.translate('xpack.security.management.apiKeys.table.fetchingApiKeysErrorMessage', { - defaultMessage: 'Error checking privileges: {message}', - values: { message: _.get(e, 'body.message', '') }, - }) - ); + this.initiallyLoadApiKeys(); } + } catch (e) { + this.props.notifications.toasts.addDanger( + i18n.translate('xpack.security.management.apiKeys.table.fetchingApiKeysErrorMessage', { + defaultMessage: 'Error checking privileges: {message}', + values: { message: e.body?.message ?? '' }, + }) + ); } } diff --git a/x-pack/plugins/security/public/plugin.test.tsx b/x-pack/plugins/security/public/plugin.test.tsx index 122b26378d22b..7c57c4dd997a2 100644 --- a/x-pack/plugins/security/public/plugin.test.tsx +++ b/x-pack/plugins/security/public/plugin.test.tsx @@ -37,7 +37,7 @@ describe('Security Plugin', () => { ) ).toEqual({ __legacyCompat: { logoutUrl: '/some-base-path/logout', tenant: '/some-base-path' }, - authc: { getCurrentUser: expect.any(Function) }, + authc: { getCurrentUser: expect.any(Function), areAPIKeysEnabled: expect.any(Function) }, license: { isEnabled: expect.any(Function), getFeatures: expect.any(Function), @@ -63,7 +63,7 @@ describe('Security Plugin', () => { expect(setupManagementServiceMock).toHaveBeenCalledTimes(1); expect(setupManagementServiceMock).toHaveBeenCalledWith({ - authc: { getCurrentUser: expect.any(Function) }, + authc: { getCurrentUser: expect.any(Function), areAPIKeysEnabled: expect.any(Function) }, license: { isEnabled: expect.any(Function), getFeatures: expect.any(Function), diff --git a/x-pack/plugins/security/server/authentication/api_keys.test.ts b/x-pack/plugins/security/server/authentication/api_keys.test.ts index 836740d0a547f..9f2a628b575d5 100644 --- a/x-pack/plugins/security/server/authentication/api_keys.test.ts +++ b/x-pack/plugins/security/server/authentication/api_keys.test.ts @@ -40,6 +40,82 @@ describe('API Keys', () => { }); }); + describe('areAPIKeysEnabled()', () => { + it('returns false when security feature is disabled', async () => { + mockLicense.isEnabled.mockReturnValue(false); + + const result = await apiKeys.areAPIKeysEnabled(); + expect(result).toEqual(false); + expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); + expect(mockScopedClusterClient.callAsInternalUser).not.toHaveBeenCalled(); + expect(mockClusterClient.callAsInternalUser).not.toHaveBeenCalled(); + }); + + it('returns false when the exception metadata indicates api keys are disabled', async () => { + mockLicense.isEnabled.mockReturnValue(true); + const error = new Error(); + (error as any).body = { + error: { 'disabled.feature': 'api_keys' }, + }; + mockClusterClient.callAsInternalUser.mockRejectedValue(error); + const result = await apiKeys.areAPIKeysEnabled(); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(result).toEqual(false); + }); + + it('returns true when the operation completes without error', async () => { + mockLicense.isEnabled.mockReturnValue(true); + mockClusterClient.callAsInternalUser.mockResolvedValue({}); + const result = await apiKeys.areAPIKeysEnabled(); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(result).toEqual(true); + }); + + it('throws the original error when exception metadata does not indicate that api keys are disabled', async () => { + mockLicense.isEnabled.mockReturnValue(true); + const error = new Error(); + (error as any).body = { + error: { 'disabled.feature': 'something_else' }, + }; + + mockClusterClient.callAsInternalUser.mockRejectedValue(error); + expect(apiKeys.areAPIKeysEnabled()).rejects.toThrowError(error); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); + }); + + it('throws the original error when exception metadata does not contain `disabled.feature`', async () => { + mockLicense.isEnabled.mockReturnValue(true); + const error = new Error(); + (error as any).body = {}; + + mockClusterClient.callAsInternalUser.mockRejectedValue(error); + expect(apiKeys.areAPIKeysEnabled()).rejects.toThrowError(error); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); + }); + + it('throws the original error when exception contains no metadata', async () => { + mockLicense.isEnabled.mockReturnValue(true); + const error = new Error(); + + mockClusterClient.callAsInternalUser.mockRejectedValue(error); + expect(apiKeys.areAPIKeysEnabled()).rejects.toThrowError(error); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1); + }); + + it('calls callCluster with proper parameters', async () => { + mockLicense.isEnabled.mockReturnValue(true); + mockClusterClient.callAsInternalUser.mockResolvedValueOnce({}); + + const result = await apiKeys.areAPIKeysEnabled(); + expect(result).toEqual(true); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('shield.invalidateAPIKey', { + body: { + id: 'kibana-api-key-service-test', + }, + }); + }); + }); + describe('create()', () => { it('returns null when security feature is disabled', async () => { mockLicense.isEnabled.mockReturnValue(false); diff --git a/x-pack/plugins/security/server/authentication/api_keys.ts b/x-pack/plugins/security/server/authentication/api_keys.ts index 9df7219cec334..29ff7e1f69f95 100644 --- a/x-pack/plugins/security/server/authentication/api_keys.ts +++ b/x-pack/plugins/security/server/authentication/api_keys.ts @@ -125,6 +125,35 @@ export class APIKeys { this.license = license; } + /** + * Determines if API Keys are enabled in Elasticsearch. + */ + async areAPIKeysEnabled(): Promise { + if (!this.license.isEnabled()) { + return false; + } + + const id = `kibana-api-key-service-test`; + + this.logger.debug( + `Testing if API Keys are enabled by attempting to invalidate a non-existant key: ${id}` + ); + + try { + await this.clusterClient.callAsInternalUser('shield.invalidateAPIKey', { + body: { + id, + }, + }); + return true; + } catch (e) { + if (this.doesErrorIndicateAPIKeysAreDisabled(e)) { + return false; + } + throw e; + } + } + /** * Tries to create an API key for the current user. * @param request Request instance. @@ -247,6 +276,11 @@ export class APIKeys { return result; } + private doesErrorIndicateAPIKeysAreDisabled(e: Record) { + const disabledFeature = e.body?.error?.['disabled.feature']; + return disabledFeature === 'api_keys'; + } + private getGrantParams(authorizationHeader: HTTPAuthorizationHeader): GrantAPIKeyParams { if (authorizationHeader.scheme.toLowerCase() === 'bearer') { return { diff --git a/x-pack/plugins/security/server/authentication/can_redirect_request.test.ts b/x-pack/plugins/security/server/authentication/can_redirect_request.test.ts index 1c9b936692f9e..9f1d6b27aa9d7 100644 --- a/x-pack/plugins/security/server/authentication/can_redirect_request.test.ts +++ b/x-pack/plugins/security/server/authentication/can_redirect_request.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { httpServerMock } from '../../../../../src/core/server/http/http_server.mocks'; +import { httpServerMock } from 'src/core/server/mocks'; import { canRedirectRequest } from './can_redirect_request'; diff --git a/x-pack/plugins/security/server/authentication/index.mock.ts b/x-pack/plugins/security/server/authentication/index.mock.ts index 8092c1c81017b..9397a7a42b326 100644 --- a/x-pack/plugins/security/server/authentication/index.mock.ts +++ b/x-pack/plugins/security/server/authentication/index.mock.ts @@ -11,6 +11,7 @@ export const authenticationMock = { login: jest.fn(), logout: jest.fn(), isProviderTypeEnabled: jest.fn(), + areAPIKeysEnabled: jest.fn(), createAPIKey: jest.fn(), getCurrentUser: jest.fn(), grantAPIKeyAsInternalUser: jest.fn(), diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index 5d7b49de68d28..d76a5a533d498 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -172,6 +172,7 @@ export async function setupAuthentication({ getSessionInfo: authenticator.getSessionInfo.bind(authenticator), isProviderTypeEnabled: authenticator.isProviderTypeEnabled.bind(authenticator), getCurrentUser, + areAPIKeysEnabled: () => apiKeys.areAPIKeysEnabled(), createAPIKey: (request: KibanaRequest, params: CreateAPIKeyParams) => apiKeys.create(request, params), grantAPIKeyAsInternalUser: (request: KibanaRequest) => apiKeys.grantAsInternalUser(request), diff --git a/x-pack/plugins/security/server/authentication/providers/saml.test.ts b/x-pack/plugins/security/server/authentication/providers/saml.test.ts index a7a43a3031571..ec50ac090f1e7 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.test.ts @@ -315,117 +315,123 @@ describe('SAMLAuthenticationProvider', () => { }); }); - it('redirects to the home page if new SAML Response is for the same user.', async () => { - const request = httpServerMock.createKibanaRequest({ headers: {} }); - const state = { - username: 'user', - accessToken: 'existing-valid-token', - refreshToken: 'existing-valid-refresh-token', - realm: 'test-realm', - }; - const authorization = `Bearer ${state.accessToken}`; - - const user = { username: 'user' }; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); - mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - - mockOptions.client.callAsInternalUser.mockResolvedValue({ - username: 'user', - access_token: 'new-valid-token', - refresh_token: 'new-valid-refresh-token', - }); - - mockOptions.tokens.invalidate.mockResolvedValue(undefined); - - await expect( - provider.login( - request, - { type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' }, - state - ) - ).resolves.toEqual( - AuthenticationResult.redirectTo('/base-path/', { - state: { - username: 'user', - accessToken: 'new-valid-token', - refreshToken: 'new-valid-refresh-token', - realm: 'test-realm', - }, - }) - ); - - expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + for (const [description, response] of [ + ['session is valid', Promise.resolve({ username: 'user' })], + [ + 'session is is expired', + Promise.reject(ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error())), + ], + ] as Array<[string, Promise]>) { + it(`redirects to the home page if new SAML Response is for the same user if ${description}.`, async () => { + const request = httpServerMock.createKibanaRequest({ headers: {} }); + const state = { + username: 'user', + accessToken: 'existing-token', + refreshToken: 'existing-refresh-token', + realm: 'test-realm', + }; + const authorization = `Bearer ${state.accessToken}`; - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( - 'shield.samlAuthenticate', - { - body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, - } - ); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockImplementation(() => response); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); - expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); - expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ - accessToken: state.accessToken, - refreshToken: state.refreshToken, - }); - }); + mockOptions.client.callAsInternalUser.mockResolvedValue({ + username: 'user', + access_token: 'new-valid-token', + refresh_token: 'new-valid-refresh-token', + }); + + mockOptions.tokens.invalidate.mockResolvedValue(undefined); + + await expect( + provider.login( + request, + { type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' }, + state + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo('/base-path/', { + state: { + username: 'user', + accessToken: 'new-valid-token', + refreshToken: 'new-valid-refresh-token', + realm: 'test-realm', + }, + }) + ); - it('redirects to `overwritten_session` if new SAML Response is for the another user.', async () => { - const request = httpServerMock.createKibanaRequest({ headers: {} }); - const state = { - username: 'user', - accessToken: 'existing-valid-token', - refreshToken: 'existing-valid-refresh-token', - realm: 'test-realm', - }; - const authorization = `Bearer ${state.accessToken}`; + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); - const existingUser = { username: 'user' }; - const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); - mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(existingUser); - mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( + 'shield.samlAuthenticate', + { + body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + } + ); - mockOptions.client.callAsInternalUser.mockResolvedValue({ - username: 'new-user', - access_token: 'new-valid-token', - refresh_token: 'new-valid-refresh-token', + expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ + accessToken: state.accessToken, + refreshToken: state.refreshToken, + }); }); - mockOptions.tokens.invalidate.mockResolvedValue(undefined); + it(`redirects to \`overwritten_session\` if new SAML Response is for the another user if ${description}.`, async () => { + const request = httpServerMock.createKibanaRequest({ headers: {} }); + const state = { + username: 'user', + accessToken: 'existing-token', + refreshToken: 'existing-refresh-token', + realm: 'test-realm', + }; + const authorization = `Bearer ${state.accessToken}`; - await expect( - provider.login( - request, - { type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' }, - state - ) - ).resolves.toEqual( - AuthenticationResult.redirectTo('/mock-server-basepath/security/overwritten_session', { - state: { - username: 'new-user', - accessToken: 'new-valid-token', - refreshToken: 'new-valid-refresh-token', - realm: 'test-realm', - }, - }) - ); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.callAsCurrentUser.mockImplementation(() => response); + mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient); + + mockOptions.client.callAsInternalUser.mockResolvedValue({ + username: 'new-user', + access_token: 'new-valid-token', + refresh_token: 'new-valid-refresh-token', + }); + + mockOptions.tokens.invalidate.mockResolvedValue(undefined); + + await expect( + provider.login( + request, + { type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' }, + state + ) + ).resolves.toEqual( + AuthenticationResult.redirectTo('/mock-server-basepath/security/overwritten_session', { + state: { + username: 'new-user', + accessToken: 'new-valid-token', + refreshToken: 'new-valid-refresh-token', + realm: 'test-realm', + }, + }) + ); - expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); + expectAuthenticateCall(mockOptions.client, { headers: { authorization } }); - expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( - 'shield.samlAuthenticate', - { - body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, - } - ); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith( + 'shield.samlAuthenticate', + { + body: { ids: [], content: 'saml-response-xml', realm: 'test-realm' }, + } + ); - expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); - expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ - accessToken: state.accessToken, - refreshToken: state.refreshToken, + expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1); + expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ + accessToken: state.accessToken, + refreshToken: state.refreshToken, + }); }); - }); + } }); describe('User initiated login with captured redirect URL', () => { diff --git a/x-pack/plugins/security/server/authentication/providers/saml.ts b/x-pack/plugins/security/server/authentication/providers/saml.ts index e14d34d1901eb..5c5ec49890901 100644 --- a/x-pack/plugins/security/server/authentication/providers/saml.ts +++ b/x-pack/plugins/security/server/authentication/providers/saml.ts @@ -158,10 +158,14 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider { return await this.loginWithSAMLResponse(request, samlResponse, state); } - if (authenticationResult.succeeded()) { - // If user has been authenticated via session, but request also includes SAML payload - // we should check whether this payload is for the exactly same user and if not - // we'll re-authenticate user and forward to a page with the respective warning. + // If user has been authenticated via session or failed to do so because of expired access token, + // but request also includes SAML payload we should check whether this payload is for the exactly + // same user and if not we'll re-authenticate user and forward to a page with the respective warning. + if ( + authenticationResult.succeeded() || + (authenticationResult.failed() && + Tokens.isAccessTokenExpiredError(authenticationResult.error)) + ) { return await this.loginWithNewSAMLResponse( request, samlResponse, diff --git a/x-pack/plugins/security/server/authentication/tokens.test.ts b/x-pack/plugins/security/server/authentication/tokens.test.ts index 82f29310c04c0..57366183050d7 100644 --- a/x-pack/plugins/security/server/authentication/tokens.test.ts +++ b/x-pack/plugins/security/server/authentication/tokens.test.ts @@ -25,7 +25,7 @@ describe('Tokens', () => { tokens = new Tokens(tokensOptions); }); - it('isAccessTokenExpiredError() returns `true` only if token expired or its document is missing', () => { + it('isAccessTokenExpiredError() returns `true` only if token expired', () => { const nonExpirationErrors = [ {}, new Error(), @@ -91,55 +91,66 @@ describe('Tokens', () => { }); describe('invalidate()', () => { - it('throws if call to delete access token responds with an error', async () => { - const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - - const failureReason = new Error('failed to delete token'); - mockClusterClient.callAsInternalUser.mockImplementation((methodName, args: any) => { - if (args && args.body && args.body.token) { - return Promise.reject(failureReason); - } - - return Promise.resolve({ invalidated_tokens: 1 }); + for (const [description, failureReason] of [ + ['an unknown error', new Error('failed to delete token')], + ['a 404 error without body', { statusCode: 404 }], + ] as Array<[string, object]>) { + it(`throws if call to delete access token responds with ${description}`, async () => { + const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; + + mockClusterClient.callAsInternalUser.mockImplementation((methodName, args: any) => { + if (args && args.body && args.body.token) { + return Promise.reject(failureReason); + } + + return Promise.resolve({ invalidated_tokens: 1 }); + }); + + await expect(tokens.invalidate(tokenPair)).rejects.toBe(failureReason); + + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.deleteAccessToken', + { + body: { token: tokenPair.accessToken }, + } + ); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.deleteAccessToken', + { + body: { refresh_token: tokenPair.refreshToken }, + } + ); }); - await expect(tokens.invalidate(tokenPair)).rejects.toBe(failureReason); - - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { token: tokenPair.accessToken } } - ); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { refresh_token: tokenPair.refreshToken } } - ); - }); - - it('throws if call to delete refresh token responds with an error', async () => { - const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - - const failureReason = new Error('failed to delete token'); - mockClusterClient.callAsInternalUser.mockImplementation((methodName, args: any) => { - if (args && args.body && args.body.refresh_token) { - return Promise.reject(failureReason); - } - - return Promise.resolve({ invalidated_tokens: 1 }); + it(`throws if call to delete refresh token responds with ${description}`, async () => { + const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; + + mockClusterClient.callAsInternalUser.mockImplementation((methodName, args: any) => { + if (args && args.body && args.body.refresh_token) { + return Promise.reject(failureReason); + } + + return Promise.resolve({ invalidated_tokens: 1 }); + }); + + await expect(tokens.invalidate(tokenPair)).rejects.toBe(failureReason); + + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.deleteAccessToken', + { + body: { token: tokenPair.accessToken }, + } + ); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.deleteAccessToken', + { + body: { refresh_token: tokenPair.refreshToken }, + } + ); }); - - await expect(tokens.invalidate(tokenPair)).rejects.toBe(failureReason); - - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { token: tokenPair.accessToken } } - ); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { refresh_token: tokenPair.refreshToken } } - ); - }); + } it('invalidates all provided tokens', async () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; @@ -187,23 +198,35 @@ describe('Tokens', () => { ); }); - it('does not fail if none of the tokens were invalidated', async () => { - const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; - - mockClusterClient.callAsInternalUser.mockResolvedValue({ invalidated_tokens: 0 }); - - await expect(tokens.invalidate(tokenPair)).resolves.toBe(undefined); - - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { token: tokenPair.accessToken } } - ); - expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( - 'shield.deleteAccessToken', - { body: { refresh_token: tokenPair.refreshToken } } - ); - }); + for (const [description, response] of [ + ['none of the tokens were invalidated', Promise.resolve({ invalidated_tokens: 0 })], + [ + '404 error is returned', + Promise.reject({ statusCode: 404, body: { invalidated_tokens: 0 } }), + ], + ] as Array<[string, Promise]>) { + it(`does not fail if ${description}`, async () => { + const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; + + mockClusterClient.callAsInternalUser.mockImplementation(() => response); + + await expect(tokens.invalidate(tokenPair)).resolves.toBe(undefined); + + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(2); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.deleteAccessToken', + { + body: { token: tokenPair.accessToken }, + } + ); + expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.deleteAccessToken', + { + body: { refresh_token: tokenPair.refreshToken }, + } + ); + }); + } it('does not fail if more than one token per access or refresh token were invalidated', async () => { const tokenPair = { accessToken: 'foo', refreshToken: 'bar' }; diff --git a/x-pack/plugins/security/server/authentication/tokens.ts b/x-pack/plugins/security/server/authentication/tokens.ts index ea7b5d5a9ff38..9117c9a679a4a 100644 --- a/x-pack/plugins/security/server/authentication/tokens.ts +++ b/x-pack/plugins/security/server/authentication/tokens.ts @@ -103,8 +103,15 @@ export class Tokens { ).invalidated_tokens; } catch (err) { this.logger.debug(`Failed to invalidate refresh token: ${err.message}`); - // We don't re-throw the error here to have a chance to invalidate access token if it's provided. - invalidationError = err; + + // When using already deleted refresh token, Elasticsearch responds with 404 and a body that + // shows that no tokens were invalidated. + if (getErrorStatusCode(err) === 404 && err.body?.invalidated_tokens === 0) { + invalidatedTokensCount = err.body.invalidated_tokens; + } else { + // We don't re-throw the error here to have a chance to invalidate access token if it's provided. + invalidationError = err; + } } if (invalidatedTokensCount === 0) { @@ -128,7 +135,14 @@ export class Tokens { ).invalidated_tokens; } catch (err) { this.logger.debug(`Failed to invalidate access token: ${err.message}`); - invalidationError = err; + + // When using already deleted access token, Elasticsearch responds with 404 and a body that + // shows that no tokens were invalidated. + if (getErrorStatusCode(err) === 404 && err.body?.invalidated_tokens === 0) { + invalidatedTokensCount = err.body.invalidated_tokens; + } else { + invalidationError = err; + } } if (invalidatedTokensCount === 0) { diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 4767f57de764c..3ce0198273af9 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -69,6 +69,7 @@ describe('Security Plugin', () => { "registerPrivilegesWithCluster": [Function], }, "authc": Object { + "areAPIKeysEnabled": [Function], "createAPIKey": [Function], "getCurrentUser": [Function], "getSessionInfo": [Function], diff --git a/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts b/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts new file mode 100644 index 0000000000000..3c6dc3c0d7bda --- /dev/null +++ b/x-pack/plugins/security/server/routes/api_keys/enabled.test.ts @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../src/core/server'; +import { LicenseCheck } from '../../../../licensing/server'; + +import { httpServerMock } from '../../../../../../src/core/server/mocks'; +import { routeDefinitionParamsMock } from '../index.mock'; +import Boom from 'boom'; +import { defineEnabledApiKeysRoutes } from './enabled'; +import { APIKeys } from '../../authentication/api_keys'; + +interface TestOptions { + licenseCheckResult?: LicenseCheck; + apiResponse?: () => Promise; + asserts: { statusCode: number; result?: Record }; +} + +describe('API keys enabled', () => { + const enabledApiKeysTest = ( + description: string, + { licenseCheckResult = { state: 'valid' }, apiResponse, asserts }: TestOptions + ) => { + test(description, async () => { + const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + + const apiKeys = new APIKeys({ + logger: mockRouteDefinitionParams.logger, + clusterClient: mockRouteDefinitionParams.clusterClient, + license: mockRouteDefinitionParams.license, + }); + + mockRouteDefinitionParams.authc.areAPIKeysEnabled.mockImplementation(() => + apiKeys.areAPIKeysEnabled() + ); + + if (apiResponse) { + mockRouteDefinitionParams.clusterClient.callAsInternalUser.mockImplementation(apiResponse); + } + + defineEnabledApiKeysRoutes(mockRouteDefinitionParams); + const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; + + const headers = { authorization: 'foo' }; + const mockRequest = httpServerMock.createKibanaRequest({ + method: 'get', + path: '/internal/security/api_key/_enabled', + headers, + }); + const mockContext = ({ + licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } }, + } as unknown) as RequestHandlerContext; + + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + expect(response.status).toBe(asserts.statusCode); + expect(response.payload).toEqual(asserts.result); + + if (apiResponse) { + expect(mockRouteDefinitionParams.clusterClient.callAsInternalUser).toHaveBeenCalledWith( + 'shield.invalidateAPIKey', + { + body: { + id: expect.any(String), + }, + } + ); + } else { + expect(mockRouteDefinitionParams.clusterClient.asScoped).not.toHaveBeenCalled(); + } + expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); + }); + }; + + describe('failure', () => { + enabledApiKeysTest('returns result of license checker', { + licenseCheckResult: { state: 'invalid', message: 'test forbidden message' }, + asserts: { statusCode: 403, result: { message: 'test forbidden message' } }, + }); + + const error = Boom.notAcceptable('test not acceptable message'); + enabledApiKeysTest('returns error from cluster client', { + apiResponse: async () => { + throw error; + }, + asserts: { statusCode: 406, result: error }, + }); + }); + + describe('success', () => { + enabledApiKeysTest('returns true if API Keys are enabled', { + apiResponse: async () => ({}), + asserts: { + statusCode: 200, + result: { + apiKeysEnabled: true, + }, + }, + }); + enabledApiKeysTest('returns false if API Keys are disabled', { + apiResponse: async () => { + const error = new Error(); + (error as any).body = { + error: { 'disabled.feature': 'api_keys' }, + }; + throw error; + }, + asserts: { + statusCode: 200, + result: { + apiKeysEnabled: false, + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/security/server/routes/api_keys/enabled.ts b/x-pack/plugins/security/server/routes/api_keys/enabled.ts new file mode 100644 index 0000000000000..2f5b8343bcd89 --- /dev/null +++ b/x-pack/plugins/security/server/routes/api_keys/enabled.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { wrapIntoCustomErrorResponse } from '../../errors'; +import { createLicensedRouteHandler } from '../licensed_route_handler'; +import { RouteDefinitionParams } from '..'; + +export function defineEnabledApiKeysRoutes({ router, authc }: RouteDefinitionParams) { + router.get( + { + path: '/internal/security/api_key/_enabled', + validate: false, + }, + createLicensedRouteHandler(async (context, request, response) => { + try { + const apiKeysEnabled = await authc.areAPIKeysEnabled(); + + return response.ok({ body: { apiKeysEnabled } }); + } catch (error) { + return response.customError(wrapIntoCustomErrorResponse(error)); + } + }) + ); +} diff --git a/x-pack/plugins/security/server/routes/api_keys/index.ts b/x-pack/plugins/security/server/routes/api_keys/index.ts index d75eb1bcbe961..7ac37bbead613 100644 --- a/x-pack/plugins/security/server/routes/api_keys/index.ts +++ b/x-pack/plugins/security/server/routes/api_keys/index.ts @@ -7,9 +7,11 @@ import { defineGetApiKeysRoutes } from './get'; import { defineCheckPrivilegesRoutes } from './privileges'; import { defineInvalidateApiKeysRoutes } from './invalidate'; +import { defineEnabledApiKeysRoutes } from './enabled'; import { RouteDefinitionParams } from '..'; export function defineApiKeysRoutes(params: RouteDefinitionParams) { + defineEnabledApiKeysRoutes(params); defineGetApiKeysRoutes(params); defineCheckPrivilegesRoutes(params); defineInvalidateApiKeysRoutes(params); diff --git a/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts b/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts index 311d50e9eb169..afb67dc3bbfca 100644 --- a/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts +++ b/x-pack/plugins/security/server/routes/api_keys/privileges.test.ts @@ -11,25 +11,53 @@ import { kibanaResponseFactory, RequestHandlerContext } from '../../../../../../ import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../index.mock'; import { defineCheckPrivilegesRoutes } from './privileges'; +import { APIKeys } from '../../authentication/api_keys'; interface TestOptions { licenseCheckResult?: LicenseCheck; - apiResponses?: Array<() => Promise>; - asserts: { statusCode: number; result?: Record; apiArguments?: unknown[][] }; + callAsInternalUserResponses?: Array<() => Promise>; + callAsCurrentUserResponses?: Array<() => Promise>; + asserts: { + statusCode: number; + result?: Record; + callAsInternalUserAPIArguments?: unknown[][]; + callAsCurrentUserAPIArguments?: unknown[][]; + }; } describe('Check API keys privileges', () => { const getPrivilegesTest = ( description: string, - { licenseCheckResult = { state: 'valid' }, apiResponses = [], asserts }: TestOptions + { + licenseCheckResult = { state: 'valid' }, + callAsInternalUserResponses = [], + callAsCurrentUserResponses = [], + asserts, + }: TestOptions ) => { test(description, async () => { const mockRouteDefinitionParams = routeDefinitionParamsMock.create(); + + const apiKeys = new APIKeys({ + logger: mockRouteDefinitionParams.logger, + clusterClient: mockRouteDefinitionParams.clusterClient, + license: mockRouteDefinitionParams.license, + }); + + mockRouteDefinitionParams.authc.areAPIKeysEnabled.mockImplementation(() => + apiKeys.areAPIKeysEnabled() + ); + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); mockRouteDefinitionParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); - for (const apiResponse of apiResponses) { + for (const apiResponse of callAsCurrentUserResponses) { mockScopedClusterClient.callAsCurrentUser.mockImplementationOnce(apiResponse); } + for (const apiResponse of callAsInternalUserResponses) { + mockRouteDefinitionParams.clusterClient.callAsInternalUser.mockImplementationOnce( + apiResponse + ); + } defineCheckPrivilegesRoutes(mockRouteDefinitionParams); const [[, handler]] = mockRouteDefinitionParams.router.get.mock.calls; @@ -48,8 +76,8 @@ describe('Check API keys privileges', () => { expect(response.status).toBe(asserts.statusCode); expect(response.payload).toEqual(asserts.result); - if (Array.isArray(asserts.apiArguments)) { - for (const apiArguments of asserts.apiArguments) { + if (Array.isArray(asserts.callAsCurrentUserAPIArguments)) { + for (const apiArguments of asserts.callAsCurrentUserAPIArguments) { expect(mockRouteDefinitionParams.clusterClient.asScoped).toHaveBeenCalledWith( mockRequest ); @@ -58,6 +86,17 @@ describe('Check API keys privileges', () => { } else { expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); } + + if (Array.isArray(asserts.callAsInternalUserAPIArguments)) { + for (const apiArguments of asserts.callAsInternalUserAPIArguments) { + expect(mockRouteDefinitionParams.clusterClient.callAsInternalUser).toHaveBeenCalledWith( + ...apiArguments + ); + } + } else { + expect(mockRouteDefinitionParams.clusterClient.callAsInternalUser).not.toHaveBeenCalled(); + } + expect(mockContext.licensing.license.check).toHaveBeenCalledWith('security', 'basic'); }); }; @@ -70,16 +109,21 @@ describe('Check API keys privileges', () => { const error = Boom.notAcceptable('test not acceptable message'); getPrivilegesTest('returns error from cluster client', { - apiResponses: [ + callAsCurrentUserResponses: [ async () => { throw error; }, - async () => {}, ], + callAsInternalUserResponses: [async () => {}], asserts: { - apiArguments: [ - ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }], - ['shield.getAPIKeys', { owner: true }], + callAsCurrentUserAPIArguments: [ + [ + 'shield.hasPrivileges', + { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, + ], + ], + callAsInternalUserAPIArguments: [ + ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], ], statusCode: 406, result: error, @@ -89,14 +133,16 @@ describe('Check API keys privileges', () => { describe('success', () => { getPrivilegesTest('returns areApiKeysEnabled and isAdmin', { - apiResponses: [ + callAsCurrentUserResponses: [ async () => ({ username: 'elastic', has_all_requested: true, - cluster: { manage_api_key: true, manage_security: true }, + cluster: { manage_api_key: true, manage_security: true, manage_own_api_key: false }, index: {}, application: {}, }), + ], + callAsInternalUserResponses: [ async () => ({ api_keys: [ { @@ -112,71 +158,108 @@ describe('Check API keys privileges', () => { }), ], asserts: { - apiArguments: [ - ['shield.getAPIKeys', { owner: true }], - ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }], + callAsCurrentUserAPIArguments: [ + [ + 'shield.hasPrivileges', + { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, + ], + ], + callAsInternalUserAPIArguments: [ + ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], ], statusCode: 200, - result: { areApiKeysEnabled: true, isAdmin: true }, + result: { areApiKeysEnabled: true, isAdmin: true, canManage: true }, }, }); getPrivilegesTest( - 'returns areApiKeysEnabled=false when getAPIKeys error message includes "api keys are not enabled"', + 'returns areApiKeysEnabled=false when API Keys are disabled in Elasticsearch', { - apiResponses: [ + callAsCurrentUserResponses: [ async () => ({ username: 'elastic', has_all_requested: true, - cluster: { manage_api_key: true, manage_security: true }, + cluster: { manage_api_key: true, manage_security: true, manage_own_api_key: true }, index: {}, application: {}, }), + ], + callAsInternalUserResponses: [ async () => { - throw Boom.unauthorized('api keys are not enabled'); + const error = new Error(); + (error as any).body = { + error: { + 'disabled.feature': 'api_keys', + }, + }; + throw error; }, ], asserts: { - apiArguments: [ - ['shield.getAPIKeys', { owner: true }], - ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }], + callAsCurrentUserAPIArguments: [ + [ + 'shield.hasPrivileges', + { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, + ], + ], + callAsInternalUserAPIArguments: [ + ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], ], statusCode: 200, - result: { areApiKeysEnabled: false, isAdmin: true }, + result: { areApiKeysEnabled: false, isAdmin: true, canManage: true }, }, } ); getPrivilegesTest('returns isAdmin=false when user has insufficient privileges', { - apiResponses: [ + callAsCurrentUserResponses: [ async () => ({ username: 'elastic', has_all_requested: true, - cluster: { manage_api_key: false, manage_security: false }, + cluster: { manage_api_key: false, manage_security: false, manage_own_api_key: false }, index: {}, application: {}, }), - async () => ({ - api_keys: [ - { - id: 'si8If24B1bKsmSLTAhJV', - name: 'my-api-key', - creation: 1574089261632, - expiration: 1574175661632, - invalidated: false, - username: 'elastic', - realm: 'reserved', - }, + ], + callAsInternalUserResponses: [async () => ({})], + asserts: { + callAsCurrentUserAPIArguments: [ + [ + 'shield.hasPrivileges', + { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, ], + ], + callAsInternalUserAPIArguments: [ + ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], + ], + statusCode: 200, + result: { areApiKeysEnabled: true, isAdmin: false, canManage: false }, + }, + }); + + getPrivilegesTest('returns canManage=true when user can manage their own API Keys', { + callAsCurrentUserResponses: [ + async () => ({ + username: 'elastic', + has_all_requested: true, + cluster: { manage_api_key: false, manage_security: false, manage_own_api_key: true }, + index: {}, + application: {}, }), ], + callAsInternalUserResponses: [async () => ({})], asserts: { - apiArguments: [ - ['shield.getAPIKeys', { owner: true }], - ['shield.hasPrivileges', { body: { cluster: ['manage_security', 'manage_api_key'] } }], + callAsCurrentUserAPIArguments: [ + [ + 'shield.hasPrivileges', + { body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] } }, + ], + ], + callAsInternalUserAPIArguments: [ + ['shield.invalidateAPIKey', { body: { id: expect.any(String) } }], ], statusCode: 200, - result: { areApiKeysEnabled: true, isAdmin: false }, + result: { areApiKeysEnabled: true, isAdmin: false, canManage: true }, }, }); }); diff --git a/x-pack/plugins/security/server/routes/api_keys/privileges.ts b/x-pack/plugins/security/server/routes/api_keys/privileges.ts index 216d1ef1bf4a4..9cccb96752772 100644 --- a/x-pack/plugins/security/server/routes/api_keys/privileges.ts +++ b/x-pack/plugins/security/server/routes/api_keys/privileges.ts @@ -8,7 +8,11 @@ import { wrapIntoCustomErrorResponse } from '../../errors'; import { createLicensedRouteHandler } from '../licensed_route_handler'; import { RouteDefinitionParams } from '..'; -export function defineCheckPrivilegesRoutes({ router, clusterClient }: RouteDefinitionParams) { +export function defineCheckPrivilegesRoutes({ + router, + clusterClient, + authc, +}: RouteDefinitionParams) { router.get( { path: '/internal/security/api_key/privileges', @@ -20,26 +24,25 @@ export function defineCheckPrivilegesRoutes({ router, clusterClient }: RouteDefi const [ { - cluster: { manage_security: manageSecurity, manage_api_key: manageApiKey }, + cluster: { + manage_security: manageSecurity, + manage_api_key: manageApiKey, + manage_own_api_key: manageOwnApiKey, + }, }, - { areApiKeysEnabled }, + areApiKeysEnabled, ] = await Promise.all([ scopedClusterClient.callAsCurrentUser('shield.hasPrivileges', { - body: { cluster: ['manage_security', 'manage_api_key'] }, + body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] }, }), - scopedClusterClient.callAsCurrentUser('shield.getAPIKeys', { owner: true }).then( - // If the API returns a truthy result that means it's enabled. - result => ({ areApiKeysEnabled: !!result }), - // This is a brittle dependency upon message. Tracked by https://github.com/elastic/elasticsearch/issues/47759. - e => - e.message.includes('api keys are not enabled') - ? Promise.resolve({ areApiKeysEnabled: false }) - : Promise.reject(e) - ), + authc.areAPIKeysEnabled(), ]); + const isAdmin = manageSecurity || manageApiKey; + const canManage = manageSecurity || manageApiKey || manageOwnApiKey; + return response.ok({ - body: { areApiKeysEnabled, isAdmin: manageSecurity || manageApiKey }, + body: { areApiKeysEnabled, isAdmin, canManage }, }); } catch (error) { return response.customError(wrapIntoCustomErrorResponse(error)); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts index 6244a4cc64e68..e8d778bddadc2 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { loggerMock } from 'src/core/server/logging/logger.mock'; +import { loggingServiceMock } from 'src/core/server/mocks'; import { getResult } from '../routes/__mocks__/request_responses'; import { rulesNotificationAlertType } from './rules_notification_alert_type'; import { buildSignalsSearchQuery } from './build_signals_query'; @@ -15,12 +15,12 @@ jest.mock('./build_signals_query'); describe('rules_notification_alert_type', () => { let payload: NotificationExecutorOptions; let alert: ReturnType; - let logger: ReturnType; + let logger: ReturnType; let alertServices: AlertServicesMock; beforeEach(() => { alertServices = alertsMock.createAlertServices(); - logger = loggerMock.create(); + logger = loggingServiceMock.createLogger(); payload = { alertId: '1111', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.test.ts index 4fce037b483d5..0c9ccf069b3b6 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/notifications/types.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { loggerMock } from 'src/core/server/logging/logger.mock'; +import { loggingServiceMock } from 'src/core/server/mocks'; import { getNotificationResult, getResult } from '../routes/__mocks__/request_responses'; import { isAlertTypes, isNotificationAlertExecutor } from './types'; import { rulesNotificationAlertType } from './rules_notification_alert_type'; @@ -20,7 +20,9 @@ describe('types', () => { it('isNotificationAlertExecutor should return true it passed object is NotificationAlertTypeDefinition type', () => { expect( - isNotificationAlertExecutor(rulesNotificationAlertType({ logger: loggerMock.create() })) + isNotificationAlertExecutor( + rulesNotificationAlertType({ logger: loggingServiceMock.createLogger() }) + ) ).toEqual(true); }); }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index 7eecc5cb9bad0..0c7f0839f8daf 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -5,7 +5,7 @@ */ import moment from 'moment'; -import { loggerMock } from 'src/core/server/logging/logger.mock'; +import { loggingServiceMock } from 'src/core/server/mocks'; import { getResult, getMlResult } from '../routes/__mocks__/request_responses'; import { signalRulesAlertType } from './signal_rule_alert_type'; import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; @@ -70,13 +70,13 @@ describe('rules_notification_alert_type', () => { }; let payload: jest.Mocked; let alert: ReturnType; - let logger: ReturnType; + let logger: ReturnType; let alertServices: AlertServicesMock; let ruleStatusService: Record; beforeEach(() => { alertServices = alertsMock.createAlertServices(); - logger = loggerMock.create(); + logger = loggingServiceMock.createLogger(); ruleStatusService = { success: jest.fn(), find: jest.fn(), diff --git a/x-pack/plugins/siem/server/utils/build_validation/route_validation.test.ts b/x-pack/plugins/siem/server/utils/build_validation/route_validation.test.ts index 866dbe9480ca5..8a1e4271303a6 100644 --- a/x-pack/plugins/siem/server/utils/build_validation/route_validation.test.ts +++ b/x-pack/plugins/siem/server/utils/build_validation/route_validation.test.ts @@ -6,7 +6,7 @@ import { buildRouteValidation } from './route_validation'; import * as rt from 'io-ts'; -import { RouteValidationResultFactory } from '../../../../../../src/core/server/http'; +import { RouteValidationResultFactory } from 'src/core/server'; describe('buildRouteValidation', () => { const schema = rt.exact( diff --git a/x-pack/plugins/task_manager/server/config.test.ts b/x-pack/plugins/task_manager/server/config.test.ts index f7962f7011f34..8e877f696a2fc 100644 --- a/x-pack/plugins/task_manager/server/config.test.ts +++ b/x-pack/plugins/task_manager/server/config.test.ts @@ -7,7 +7,7 @@ import { configSchema } from './config'; describe('config validation', () => { test('task manager defaults', () => { - const config: Record = {}; + const config: Record = {}; expect(configSchema.validate(config)).toMatchInlineSnapshot(` Object { "enabled": true, @@ -21,7 +21,7 @@ describe('config validation', () => { }); test('the ElastiSearch Tasks index cannot be used for task manager', () => { - const config: Record = { + const config: Record = { index: '.tasks', }; expect(() => { diff --git a/x-pack/plugins/task_manager/server/create_task_manager.ts b/x-pack/plugins/task_manager/server/create_task_manager.ts index 9ff97bbcc17e6..7ab6acba7976d 100644 --- a/x-pack/plugins/task_manager/server/create_task_manager.ts +++ b/x-pack/plugins/task_manager/server/create_task_manager.ts @@ -12,9 +12,10 @@ import { } from '../../../../src/core/server'; import { TaskManager } from './task_manager'; import { Logger } from './types'; +import { TaskManagerConfig } from './config'; export interface LegacyDeps { - config: any; + config: unknown; elasticsearch: Pick; savedObjectsRepository: ISavedObjectsRepository; savedObjectsSerializer: SavedObjectsSerializer; @@ -33,7 +34,7 @@ export function createTaskManager( ) { return new TaskManager({ taskManagerId: core.uuid.getInstanceUuid(), - config, + config: config as TaskManagerConfig, savedObjectsRepository, serializer: savedObjectsSerializer, callAsInternalUser, diff --git a/x-pack/plugins/task_manager/server/lib/middleware.test.ts b/x-pack/plugins/task_manager/server/lib/middleware.test.ts index 3aa39eb3db513..abf69e726262f 100644 --- a/x-pack/plugins/task_manager/server/lib/middleware.test.ts +++ b/x-pack/plugins/task_manager/server/lib/middleware.test.ts @@ -28,9 +28,9 @@ const getMockConcreteTaskInstance = () => { scheduledAt: Date; startedAt: Date | null; retryAt: Date | null; - state: any; + state: unknown; taskType: string; - params: any; + params: unknown; ownerId: string | null; } = { id: 'hy8o99o83', @@ -47,7 +47,7 @@ const getMockConcreteTaskInstance = () => { params: { abc: 'def' }, ownerId: null, }; - return concrete; + return (concrete as unknown) as ConcreteTaskInstance; }; const getMockRunContext = (runTask: ConcreteTaskInstance) => ({ taskInstance: runTask, @@ -95,7 +95,7 @@ describe('addMiddlewareToChain', () => { await middlewareChain .beforeSave({ taskInstance: getMockTaskInstance() }) - .then((saveOpts: any) => { + .then((saveOpts: unknown) => { expect(saveOpts).toMatchInlineSnapshot(` Object { "taskInstance": Object { diff --git a/x-pack/plugins/task_manager/server/lib/sanitize_task_definitions.test.ts b/x-pack/plugins/task_manager/server/lib/sanitize_task_definitions.test.ts index da64befe28673..650eb36347c86 100644 --- a/x-pack/plugins/task_manager/server/lib/sanitize_task_definitions.test.ts +++ b/x-pack/plugins/task_manager/server/lib/sanitize_task_definitions.test.ts @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { RunContext } from '../task'; +import { RunContext, TaskDictionary, TaskDefinition } from '../task'; import { sanitizeTaskDefinitions } from './sanitize_task_definitions'; interface Opts { @@ -14,7 +14,7 @@ interface Opts { const getMockTaskDefinitions = (opts: Opts) => { const { numTasks } = opts; - const tasks: any = {}; + const tasks: Record = {}; for (let i = 0; i < numTasks; i++) { const type = `test_task_type_${i}`; @@ -35,7 +35,7 @@ const getMockTaskDefinitions = (opts: Opts) => { }, }; } - return tasks; + return (tasks as unknown) as TaskDictionary; }; describe('sanitizeTaskDefinitions', () => { diff --git a/x-pack/plugins/task_manager/server/plugin.ts b/x-pack/plugins/task_manager/server/plugin.ts index e837fcd9c0dec..a70fbdb18c30b 100644 --- a/x-pack/plugins/task_manager/server/plugin.ts +++ b/x-pack/plugins/task_manager/server/plugin.ts @@ -35,7 +35,7 @@ export class TaskManagerPlugin this.currentConfig = {} as TaskManagerConfig; } - public setup(core: CoreSetup, plugins: any): TaskManagerSetupContract { + public setup(core: CoreSetup, plugins: unknown): TaskManagerSetupContract { const logger = this.initContext.logger.get('taskManager'); const config$ = this.initContext.config.create(); return { diff --git a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts index 8f7cc47f936b2..4fd4da3d83a36 100644 --- a/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts +++ b/x-pack/plugins/task_manager/server/queries/mark_available_tasks_as_claimed.ts @@ -55,6 +55,7 @@ export const IdleTaskWithExpiredRunAt: MustCondition = }; // TODO: Fix query clauses to support this +// eslint-disable-next-line @typescript-eslint/no-explicit-any export const InactiveTasks: BoolClauseWithAnyCondition = { bool: { must_not: [ diff --git a/x-pack/plugins/task_manager/server/task.ts b/x-pack/plugins/task_manager/server/task.ts index 48e87582ce3fe..e806a866ff0b7 100644 --- a/x-pack/plugins/task_manager/server/task.ts +++ b/x-pack/plugins/task_manager/server/task.ts @@ -28,7 +28,7 @@ type Require = Omit & Required Promise; +export type ElasticJs = (action: string, args: unknown) => Promise; /** * The run context is passed into a task's run function as its sole argument. @@ -61,12 +61,12 @@ export interface RunResult { * The state which will be passed to the next run of this task (if this is a * recurring task). See the RunContext type definition for more details. */ - state: Record; + state: Record; } export interface SuccessfulRunResult { runAt?: Date; - state?: Record; + state?: Record; } export interface FailedRunResult extends SuccessfulRunResult { @@ -237,6 +237,9 @@ export interface TaskInstance { * A task-specific set of parameters, used by the task's run function to tailor * its work. This is generally user-input, such as { sms: '333-444-2222' }. */ + // we allow any here as unknown will break current use in other plugins + // this can be fixed by supporting generics in the future + // eslint-disable-next-line @typescript-eslint/no-explicit-any params: Record; /** @@ -244,6 +247,9 @@ export interface TaskInstance { * run. If there was no previous run, or if the previous run did not return * any state, this will be the empy object: {} */ + // we allow any here as unknown will break current use in other plugins + // this can be fixed by supporting generics in the future + // eslint-disable-next-line @typescript-eslint/no-explicit-any state: Record; /** @@ -336,6 +342,9 @@ export interface ConcreteTaskInstance extends TaskInstance { * run. If there was no previous run, or if the previous run did not return * any state, this will be the empy object: {} */ + // we allow any here as unknown will break current use in other plugins + // this can be fixed by supporting generics in the future + // eslint-disable-next-line @typescript-eslint/no-explicit-any state: Record; /** @@ -343,3 +352,15 @@ export interface ConcreteTaskInstance extends TaskInstance { */ ownerId: string | null; } + +export type SerializedConcreteTaskInstance = Omit< + ConcreteTaskInstance, + 'state' | 'params' | 'scheduledAt' | 'startedAt' | 'retryAt' | 'runAt' +> & { + state: string; + params: string; + scheduledAt: string; + startedAt: string | null; + retryAt: string | null; + runAt: string; +}; diff --git a/x-pack/plugins/task_manager/server/task_events.ts b/x-pack/plugins/task_manager/server/task_events.ts index 063ac2499471f..b17a3636c1730 100644 --- a/x-pack/plugins/task_manager/server/task_events.ts +++ b/x-pack/plugins/task_manager/server/task_events.ts @@ -68,16 +68,18 @@ export function asTaskRunRequestEvent( } export function isTaskMarkRunningEvent( - taskEvent: TaskEvent + taskEvent: TaskEvent ): taskEvent is TaskMarkRunning { return taskEvent.type === TaskEventType.TASK_MARK_RUNNING; } -export function isTaskRunEvent(taskEvent: TaskEvent): taskEvent is TaskRun { +export function isTaskRunEvent(taskEvent: TaskEvent): taskEvent is TaskRun { return taskEvent.type === TaskEventType.TASK_RUN; } -export function isTaskClaimEvent(taskEvent: TaskEvent): taskEvent is TaskClaim { +export function isTaskClaimEvent(taskEvent: TaskEvent): taskEvent is TaskClaim { return taskEvent.type === TaskEventType.TASK_CLAIM; } -export function isTaskRunRequestEvent(taskEvent: TaskEvent): taskEvent is TaskRunRequest { +export function isTaskRunRequestEvent( + taskEvent: TaskEvent +): taskEvent is TaskRunRequest { return taskEvent.type === TaskEventType.TASK_RUN_REQUEST; } diff --git a/x-pack/plugins/task_manager/server/task_manager.test.ts b/x-pack/plugins/task_manager/server/task_manager.test.ts index 3d48ce18c9d6a..3f3b14a791f24 100644 --- a/x-pack/plugins/task_manager/server/task_manager.test.ts +++ b/x-pack/plugins/task_manager/server/task_manager.test.ts @@ -25,6 +25,7 @@ import { SavedObjectsSerializer, SavedObjectTypeRegistry } from '../../../../src import { mockLogger } from './test_utils'; import { asErr, asOk } from './lib/result_type'; import { ConcreteTaskInstance, TaskLifecycleResult, TaskStatus } from './task'; +import { Middleware } from './lib/middleware'; const savedObjectsClient = savedObjectsRepositoryMock.create(); const serializer = new SavedObjectsSerializer(new SavedObjectTypeRegistry()); @@ -247,20 +248,20 @@ describe('TaskManager', () => { test('allows middleware registration before starting', () => { const client = new TaskManager(taskManagerOpts); - const middleware = { - beforeSave: async (saveOpts: any) => saveOpts, - beforeRun: async (runOpts: any) => runOpts, - beforeMarkRunning: async (runOpts: any) => runOpts, + const middleware: Middleware = { + beforeSave: jest.fn(async saveOpts => saveOpts), + beforeRun: jest.fn(async runOpts => runOpts), + beforeMarkRunning: jest.fn(async runOpts => runOpts), }; expect(() => client.addMiddleware(middleware)).not.toThrow(); }); test('disallows middleware registration after starting', async () => { const client = new TaskManager(taskManagerOpts); - const middleware = { - beforeSave: async (saveOpts: any) => saveOpts, - beforeRun: async (runOpts: any) => runOpts, - beforeMarkRunning: async (runOpts: any) => runOpts, + const middleware: Middleware = { + beforeSave: jest.fn(async saveOpts => saveOpts), + beforeRun: jest.fn(async runOpts => runOpts), + beforeMarkRunning: jest.fn(async runOpts => runOpts), }; client.start(); diff --git a/x-pack/plugins/task_manager/server/task_manager.ts b/x-pack/plugins/task_manager/server/task_manager.ts index a7c67d190e72e..24ceea0fe71ef 100644 --- a/x-pack/plugins/task_manager/server/task_manager.ts +++ b/x-pack/plugins/task_manager/server/task_manager.ts @@ -43,6 +43,7 @@ import { TaskLifecycle, TaskLifecycleResult, TaskStatus, + ElasticJs, } from './task'; import { createTaskPoller, PollingError, PollingErrorType } from './task_poller'; import { TaskPool } from './task_pool'; @@ -129,7 +130,7 @@ export class TaskManager { this.store = new TaskStore({ serializer: opts.serializer, savedObjectsRepository: opts.savedObjectsRepository, - callCluster: opts.callAsInternalUser, + callCluster: (opts.callAsInternalUser as unknown) as ElasticJs, index: opts.config.index, maxAttempts: opts.config.max_attempts, definitions: this.definitions, @@ -273,7 +274,7 @@ export class TaskManager { */ public async schedule( taskInstance: TaskInstanceWithDeprecatedFields, - options?: any + options?: object ): Promise { await this.waitUntilStarted(); const { taskInstance: modifiedTask } = await this.middleware.beforeSave({ @@ -308,7 +309,7 @@ export class TaskManager { */ public async ensureScheduled( taskInstance: TaskInstanceWithId, - options?: any + options?: object ): Promise { try { return await this.schedule(taskInstance, options); diff --git a/x-pack/plugins/task_manager/server/task_pool.test.ts b/x-pack/plugins/task_manager/server/task_pool.test.ts index fb87b6290a3da..dee4d80b97917 100644 --- a/x-pack/plugins/task_manager/server/task_pool.test.ts +++ b/x-pack/plugins/task_manager/server/task_pool.test.ts @@ -9,6 +9,7 @@ import { TaskPool, TaskPoolRunResult } from './task_pool'; import { mockLogger, resolvable, sleep } from './test_utils'; import { asOk } from './lib/result_type'; import { SavedObjectsErrorHelpers } from '../../../../src/core/server'; +import moment from 'moment'; describe('TaskPool', () => { test('occupiedWorkers are a sum of running tasks', async () => { @@ -190,14 +191,16 @@ describe('TaskPool', () => { }); test('run cancels expired tasks prior to running new tasks', async () => { + const logger = mockLogger(); const pool = new TaskPool({ maxWorkers: 2, - logger: mockLogger(), + logger, }); const expired = resolvable(); const shouldRun = sinon.spy(() => Promise.resolve()); const shouldNotRun = sinon.spy(() => Promise.resolve()); + const now = new Date(); const result = await pool.run([ { ...mockTask(), @@ -207,6 +210,16 @@ describe('TaskPool', () => { await sleep(10); return asOk({ state: {} }); }, + get expiration() { + return now; + }, + get startedAt() { + // 5 and a half minutes + return moment(now) + .subtract(5, 'm') + .subtract(30, 's') + .toDate(); + }, cancel: shouldRun, }, { @@ -231,6 +244,10 @@ describe('TaskPool', () => { expect(pool.occupiedWorkers).toEqual(2); expect(pool.availableWorkers).toEqual(0); + + expect(logger.warn).toHaveBeenCalledWith( + `Cancelling task TaskType "shooooo" as it expired at ${now.toISOString()} after running for 05m 30s (with timeout set at 5m).` + ); }); test('logs if cancellation errors', async () => { @@ -285,6 +302,20 @@ describe('TaskPool', () => { markTaskAsRunning: jest.fn(async () => true), run: mockRun(), toString: () => `TaskType "shooooo"`, + get expiration() { + return new Date(); + }, + get startedAt() { + return new Date(); + }, + get definition() { + return { + type: '', + title: '', + timeout: '5m', + createTaskRunner: jest.fn(), + }; + }, }; } }); diff --git a/x-pack/plugins/task_manager/server/task_pool.ts b/x-pack/plugins/task_manager/server/task_pool.ts index 8999fb48680ce..bd0de86551aaa 100644 --- a/x-pack/plugins/task_manager/server/task_pool.ts +++ b/x-pack/plugins/task_manager/server/task_pool.ts @@ -8,7 +8,9 @@ * This module contains the logic that ensures we don't run too many * tasks at once in a given Kibana instance. */ +import moment, { Duration } from 'moment'; import { performance } from 'perf_hooks'; +import { padLeft } from 'lodash'; import { Logger } from './types'; import { TaskRunner } from './task_runner'; import { isTaskSavedObjectNotFoundError } from './lib/is_task_not_found_error'; @@ -148,7 +150,19 @@ export class TaskPool { private cancelExpiredTasks() { for (const task of this.running) { if (task.isExpired) { - this.logger.debug(`Cancelling expired task ${task.toString()}.`); + this.logger.warn( + `Cancelling task ${task.toString()} as it expired at ${task.expiration.toISOString()}${ + task.startedAt + ? ` after running for ${durationAsString( + moment.duration( + moment(new Date()) + .utc() + .diff(task.startedAt) + ) + )}` + : `` + }${task.definition.timeout ? ` (with timeout set at ${task.definition.timeout})` : ``}.` + ); this.cancelTask(task); } } @@ -169,3 +183,8 @@ function partitionListByCount(list: T[], count: number): [T[], T[]] { const listInCount = list.splice(0, count); return [listInCount, list]; } + +function durationAsString(duration: Duration): string { + const [m, s] = [duration.minutes(), duration.seconds()].map(value => padLeft(`${value}`, 2, '0')); + return `${m}m ${s}s`; +} diff --git a/x-pack/plugins/task_manager/server/task_runner.test.ts b/x-pack/plugins/task_manager/server/task_runner.test.ts index 3f0132105347e..07247dcb1da47 100644 --- a/x-pack/plugins/task_manager/server/task_runner.test.ts +++ b/x-pack/plugins/task_manager/server/task_runner.test.ts @@ -9,10 +9,11 @@ import sinon from 'sinon'; import { minutesFromNow } from './lib/intervals'; import { asOk, asErr } from './lib/result_type'; import { TaskEvent, asTaskRunEvent, asTaskMarkRunningEvent } from './task_events'; -import { ConcreteTaskInstance, TaskStatus } from './task'; +import { ConcreteTaskInstance, TaskStatus, TaskDictionary, TaskDefinition } from './task'; import { TaskManagerRunner } from './task_runner'; import { mockLogger } from './test_utils'; import { SavedObjectsErrorHelpers } from '../../../../src/core/server'; +import moment from 'moment'; let fakeTimer: sinon.SinonFakeTimers; @@ -113,6 +114,60 @@ describe('TaskManagerRunner', () => { expect(instance.runAt.getTime()).toBeLessThanOrEqual(minutesFromNow(10).getTime()); }); + test('expiration returns time after which timeout will have elapsed from start', async () => { + const now = moment(); + const { runner } = testOpts({ + instance: { + schedule: { interval: '10m' }, + status: TaskStatus.Running, + startedAt: now.toDate(), + }, + definitions: { + bar: { + timeout: `1m`, + createTaskRunner: () => ({ + async run() { + return; + }, + }), + }, + }, + }); + + await runner.run(); + + expect(runner.isExpired).toBe(false); + expect(runner.expiration).toEqual(now.add(1, 'm').toDate()); + }); + + test('runDuration returns duration which has elapsed since start', async () => { + const now = moment() + .subtract(30, 's') + .toDate(); + const { runner } = testOpts({ + instance: { + schedule: { interval: '10m' }, + status: TaskStatus.Running, + startedAt: now, + }, + definitions: { + bar: { + timeout: `1m`, + createTaskRunner: () => ({ + async run() { + return; + }, + }), + }, + }, + }); + + await runner.run(); + + expect(runner.isExpired).toBe(false); + expect(runner.startedAt).toEqual(now); + }); + test('reschedules tasks that return a runAt', async () => { const runAt = minutesFromNow(_.random(1, 10)); const { runner, store } = testOpts({ @@ -208,7 +263,7 @@ describe('TaskManagerRunner', () => { expect(logger.warn).not.toHaveBeenCalled(); }); - test('warns if cancel is called on a non-cancellable task', async () => { + test('debug logs if cancel is called on a non-cancellable task', async () => { const { runner, logger } = testOpts({ definitions: { bar: { @@ -223,10 +278,7 @@ describe('TaskManagerRunner', () => { await runner.cancel(); await promise; - expect(logger.warn).toHaveBeenCalledTimes(1); - expect(logger.warn.mock.calls[0][0]).toMatchInlineSnapshot( - `"The task bar \\"foo\\" is not cancellable."` - ); + expect(logger.debug).toHaveBeenCalledWith(`The task bar "foo" is not cancellable.`); }); test('sets startedAt, status, attempts and retryAt when claiming a task', async () => { @@ -854,8 +906,8 @@ describe('TaskManagerRunner', () => { interface TestOpts { instance?: Partial; - definitions?: any; - onTaskEvent?: (event: TaskEvent) => void; + definitions?: unknown; + onTaskEvent?: (event: TaskEvent) => void; } function testOpts(opts: TestOpts) { @@ -904,7 +956,7 @@ describe('TaskManagerRunner', () => { title: 'Bar!', createTaskRunner, }, - }), + }) as TaskDictionary, onTaskEvent: opts.onTaskEvent, }); @@ -918,7 +970,7 @@ describe('TaskManagerRunner', () => { }; } - async function testReturn(result: any, shouldBeValid: boolean) { + async function testReturn(result: unknown, shouldBeValid: boolean) { const { runner, logger } = testOpts({ definitions: { bar: { @@ -939,11 +991,11 @@ describe('TaskManagerRunner', () => { } } - function allowsReturnType(result: any) { + function allowsReturnType(result: unknown) { return testReturn(result, true); } - function disallowsReturnType(result: any) { + function disallowsReturnType(result: unknown) { return testReturn(result, false); } }); diff --git a/x-pack/plugins/task_manager/server/task_runner.ts b/x-pack/plugins/task_manager/server/task_runner.ts index 682885aaa0b1c..7a9fa0c45e15f 100644 --- a/x-pack/plugins/task_manager/server/task_runner.ts +++ b/x-pack/plugins/task_manager/server/task_runner.ts @@ -39,6 +39,9 @@ const EMPTY_RUN_RESULT: SuccessfulRunResult = {}; export interface TaskRunner { isExpired: boolean; + expiration: Date; + startedAt: Date | null; + definition: TaskDefinition; cancel: CancelFunction; markTaskAsRunning: () => Promise; run: () => Promise>; @@ -129,11 +132,25 @@ export class TaskManagerRunner implements TaskRunner { return this.definitions[this.taskType]; } + /** + * Gets the time at which this task will expire. + */ + public get expiration() { + return intervalFromDate(this.instance.startedAt!, this.definition.timeout)!; + } + + /** + * Gets the duration of the current task run + */ + public get startedAt() { + return this.instance.startedAt; + } + /** * Gets whether or not this task has run longer than its expiration setting allows. */ public get isExpired() { - return intervalFromDate(this.instance.startedAt!, this.definition.timeout)! < new Date(); + return this.expiration < new Date(); } /** @@ -261,12 +278,12 @@ export class TaskManagerRunner implements TaskRunner { */ public async cancel() { const { task } = this; - if (task && task.cancel) { + if (task?.cancel) { this.task = undefined; return task.cancel(); } - this.logger.warn(`The task ${this} is not cancellable.`); + this.logger.debug(`The task ${this} is not cancellable.`); } private validateResult(result?: RunResult | void): Result { @@ -392,7 +409,7 @@ export class TaskManagerRunner implements TaskRunner { attempts, addDuration, }: { - error: any; + error: Error; attempts: number; addDuration?: string; }): Date | null { diff --git a/x-pack/plugins/task_manager/server/task_store.test.ts b/x-pack/plugins/task_manager/server/task_store.test.ts index 4ecefcb7984eb..6524ea212e7c5 100644 --- a/x-pack/plugins/task_manager/server/task_store.test.ts +++ b/x-pack/plugins/task_manager/server/task_store.test.ts @@ -15,11 +15,17 @@ import { TaskInstance, TaskStatus, TaskLifecycleResult, + SerializedConcreteTaskInstance, + ConcreteTaskInstance, } from './task'; import { StoreOpts, OwnershipClaimingOpts, TaskStore, SearchOpts } from './task_store'; -import { savedObjectsRepositoryMock } from '../../../../src/core/server/mocks'; -import { SavedObjectsSerializer, SavedObjectTypeRegistry } from '../../../../src/core/server'; -import { SavedObjectsErrorHelpers } from '../../../../src/core/server/saved_objects/service/lib/errors'; +import { savedObjectsRepositoryMock } from 'src/core/server/mocks'; +import { + SavedObjectsSerializer, + SavedObjectTypeRegistry, + SavedObjectAttributes, + SavedObjectsErrorHelpers, +} from 'src/core/server'; import { asTaskClaimEvent, TaskEvent } from './task_events'; import { asOk, asErr } from './lib/result_type'; @@ -47,6 +53,7 @@ const serializer = new SavedObjectsSerializer(new SavedObjectTypeRegistry()); beforeEach(() => jest.resetAllMocks()); const mockedDate = new Date('2019-02-12T21:01:22.479Z'); +// eslint-disable-next-line @typescript-eslint/no-explicit-any (global as any).Date = class Date { constructor() { return mockedDate; @@ -58,9 +65,9 @@ const mockedDate = new Date('2019-02-12T21:01:22.479Z'); describe('TaskStore', () => { describe('schedule', () => { - async function testSchedule(task: TaskInstance) { + async function testSchedule(task: unknown) { const callCluster = jest.fn(); - savedObjectsClient.create.mockImplementation(async (type: string, attributes: any) => ({ + savedObjectsClient.create.mockImplementation(async (type: string, attributes: unknown) => ({ id: 'testid', type, attributes, @@ -76,7 +83,7 @@ describe('TaskStore', () => { definitions: taskDefinitions, savedObjectsRepository: savedObjectsClient, }); - const result = await store.schedule(task); + const result = await store.schedule(task as TaskInstance); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); @@ -149,14 +156,16 @@ describe('TaskStore', () => { test('sets runAt to now if not specified', async () => { await testSchedule({ taskType: 'dernstraight', params: {}, state: {} }); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); - const attributes: any = savedObjectsClient.create.mock.calls[0][1]; + const attributes = savedObjectsClient.create.mock + .calls[0][1] as SerializedConcreteTaskInstance; expect(new Date(attributes.runAt as string).getTime()).toEqual(mockedDate.getTime()); }); test('ensures params and state are not null', async () => { - await testSchedule({ taskType: 'yawn' } as any); + await testSchedule({ taskType: 'yawn' }); expect(savedObjectsClient.create).toHaveBeenCalledTimes(1); - const attributes: any = savedObjectsClient.create.mock.calls[0][1]; + const attributes = savedObjectsClient.create.mock + .calls[0][1] as SerializedConcreteTaskInstance; expect(attributes.params).toEqual('{}'); expect(attributes.state).toEqual('{}'); }); @@ -169,8 +178,8 @@ describe('TaskStore', () => { }); describe('fetch', () => { - async function testFetch(opts?: SearchOpts, hits: any[] = []) { - const callCluster = sinon.spy(async (name: string, params?: any) => ({ hits: { hits } })); + async function testFetch(opts?: SearchOpts, hits: unknown[] = []) { + const callCluster = sinon.spy(async (name: string, params?: unknown) => ({ hits: { hits } })); const store = new TaskStore({ index: 'tasky', taskManagerId: '', @@ -229,11 +238,11 @@ describe('TaskStore', () => { claimingOpts, }: { opts: Partial; - hits?: any[]; + hits?: unknown[]; claimingOpts: OwnershipClaimingOpts; }) { const versionConflicts = 2; - const callCluster = sinon.spy(async (name: string, params?: any) => + const callCluster = sinon.spy(async (name: string, params?: unknown) => name === 'updateByQuery' ? { total: hits.length + versionConflicts, @@ -266,7 +275,7 @@ describe('TaskStore', () => { } test('it returns normally with no tasks when the index does not exist.', async () => { - const callCluster = sinon.spy(async (name: string, params?: any) => ({ + const callCluster = sinon.spy(async (name: string, params?: unknown) => ({ total: 0, updated: 0, })); @@ -745,7 +754,7 @@ if (doc['task.runAt'].size()!=0) { }; savedObjectsClient.update.mockImplementation( - async (type: string, id: string, attributes: any) => { + async (type: string, id: string, attributes: SavedObjectAttributes) => { return { id, type, @@ -1017,7 +1026,7 @@ if (doc['task.runAt'].size()!=0) { test('emits an event when a task is succesfully claimed by id', async done => { const { taskManagerId, runAt, tasks } = generateTasks(); - const callCluster = sinon.spy(async (name: string, params?: any) => + const callCluster = sinon.spy(async (name: string, params?: unknown) => name === 'updateByQuery' ? { total: tasks.length, @@ -1036,9 +1045,9 @@ if (doc['task.runAt'].size()!=0) { }); const sub = store.events - .pipe(filter((event: TaskEvent) => event.id === 'aaa')) + .pipe(filter((event: TaskEvent) => event.id === 'aaa')) .subscribe({ - next: (event: TaskEvent) => { + next: (event: TaskEvent) => { expect(event).toMatchObject( asTaskClaimEvent( 'aaa', @@ -1074,7 +1083,7 @@ if (doc['task.runAt'].size()!=0) { test('emits an event when a task is succesfully by scheduling', async done => { const { taskManagerId, runAt, tasks } = generateTasks(); - const callCluster = sinon.spy(async (name: string, params?: any) => + const callCluster = sinon.spy(async (name: string, params?: unknown) => name === 'updateByQuery' ? { total: tasks.length, @@ -1093,9 +1102,9 @@ if (doc['task.runAt'].size()!=0) { }); const sub = store.events - .pipe(filter((event: TaskEvent) => event.id === 'bbb')) + .pipe(filter((event: TaskEvent) => event.id === 'bbb')) .subscribe({ - next: (event: TaskEvent) => { + next: (event: TaskEvent) => { expect(event).toMatchObject( asTaskClaimEvent( 'bbb', @@ -1131,7 +1140,7 @@ if (doc['task.runAt'].size()!=0) { test('emits an event when the store fails to claim a required task by id', async done => { const { taskManagerId, tasks } = generateTasks(); - const callCluster = sinon.spy(async (name: string, params?: any) => + const callCluster = sinon.spy(async (name: string, params?: unknown) => name === 'updateByQuery' ? { total: tasks.length, @@ -1150,9 +1159,9 @@ if (doc['task.runAt'].size()!=0) { }); const sub = store.events - .pipe(filter((event: TaskEvent) => event.id === 'ccc')) + .pipe(filter((event: TaskEvent) => event.id === 'ccc')) .subscribe({ - next: (event: TaskEvent) => { + next: (event: TaskEvent) => { expect(event).toMatchObject( asTaskClaimEvent('ccc', asErr(new Error(`failed to claim task 'ccc'`))) ); diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index 0e487386eb04d..01299615c7d49 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -9,11 +9,11 @@ */ import apm from 'elastic-apm-node'; import { Subject, Observable } from 'rxjs'; -import { omit, difference } from 'lodash'; +import { omit, difference, defaults } from 'lodash'; +import { SearchResponse, UpdateDocumentByQueryResponse } from 'elasticsearch'; import { SavedObject, - SavedObjectAttributes, SavedObjectsSerializer, SavedObjectsRawDoc, ISavedObjectsRepository, @@ -29,6 +29,7 @@ import { TaskInstance, TaskLifecycle, TaskLifecycleResult, + SerializedConcreteTaskInstance, } from './task'; import { TaskClaim, asTaskClaimEvent } from './task_events'; @@ -71,7 +72,7 @@ export interface SearchOpts { query?: object; size?: number; seq_no_primary_term?: boolean; - search_after?: any[]; + search_after?: unknown[]; } export interface UpdateByQuerySearchOpts extends SearchOpts { @@ -166,7 +167,7 @@ export class TaskStore { ); } - const savedObject = await this.savedObjectsRepository.create( + const savedObject = await this.savedObjectsRepository.create( 'task', taskInstanceToAttributes(taskInstance), { id: taskInstance.id, refresh: false } @@ -314,17 +315,21 @@ export class TaskStore { * @returns {Promise} */ public async update(doc: ConcreteTaskInstance): Promise { - const updatedSavedObject = await this.savedObjectsRepository.update( - 'task', - doc.id, - taskInstanceToAttributes(doc), - { - refresh: false, - version: doc.version, - } - ); + const attributes = taskInstanceToAttributes(doc); + const updatedSavedObject = await this.savedObjectsRepository.update< + SerializedConcreteTaskInstance + >('task', doc.id, attributes, { + refresh: false, + version: doc.version, + }); - return savedObjectToConcreteTaskInstance(updatedSavedObject); + return savedObjectToConcreteTaskInstance( + // The SavedObjects update api forces a Partial on the `attributes` on the response, + // but actually returns the whole object that is passed to it, so as we know we're + // passing in the whole object, this is safe to do. + // This is far from ideal, but unless we change the SavedObjectsClient this is the best we can do + { ...updatedSavedObject, attributes: defaults(updatedSavedObject.attributes, attributes) } + ); } /** @@ -377,12 +382,12 @@ export class TaskStore { }, }); - const rawDocs = result.hits.hits; + const rawDocs = (result as SearchResponse).hits.hits; return { docs: (rawDocs as SavedObjectsRawDoc[]) .map(doc => this.serializer.rawToSavedObject(doc)) - .map(doc => omit(doc, 'namespace') as SavedObject) + .map(doc => omit(doc, 'namespace') as SavedObject) .map(savedObjectToConcreteTaskInstance), }; } @@ -404,7 +409,7 @@ export class TaskStore { }, }); - const { total, updated, version_conflicts } = result; + const { total, updated, version_conflicts } = result as UpdateDocumentByQueryResponse; return { total, updated, @@ -413,7 +418,7 @@ export class TaskStore { } } -function taskInstanceToAttributes(doc: TaskInstance): SavedObjectAttributes { +function taskInstanceToAttributes(doc: TaskInstance): SerializedConcreteTaskInstance { return { ...omit(doc, 'id', 'version'), params: JSON.stringify(doc.params || {}), @@ -428,8 +433,7 @@ function taskInstanceToAttributes(doc: TaskInstance): SavedObjectAttributes { } export function savedObjectToConcreteTaskInstance( - // TODO: define saved object type - savedObject: Omit, 'references'> + savedObject: Omit, 'references'> ): ConcreteTaskInstance { return { ...savedObject.attributes, @@ -437,8 +441,8 @@ export function savedObjectToConcreteTaskInstance( version: savedObject.version, scheduledAt: new Date(savedObject.attributes.scheduledAt), runAt: new Date(savedObject.attributes.runAt), - startedAt: savedObject.attributes.startedAt && new Date(savedObject.attributes.startedAt), - retryAt: savedObject.attributes.retryAt && new Date(savedObject.attributes.retryAt), + startedAt: savedObject.attributes.startedAt ? new Date(savedObject.attributes.startedAt) : null, + retryAt: savedObject.attributes.retryAt ? new Date(savedObject.attributes.retryAt) : null, state: parseJSONField(savedObject.attributes.state, 'state', savedObject.id), params: parseJSONField(savedObject.attributes.params, 'params', savedObject.id), }; diff --git a/x-pack/plugins/task_manager/server/test_utils/index.ts b/x-pack/plugins/task_manager/server/test_utils/index.ts index 719ccadbe33dd..3dfc53672b46f 100644 --- a/x-pack/plugins/task_manager/server/test_utils/index.ts +++ b/x-pack/plugins/task_manager/server/test_utils/index.ts @@ -33,11 +33,11 @@ interface Resolvable { */ export function resolvable(): PromiseLike & Resolvable { let resolve: () => void; - const result = new Promise(r => (resolve = r)) as any; - - result.resolve = () => nativeTimeout(resolve, 0); - - return result; + return Object.assign(new Promise(r => (resolve = r)), { + resolve() { + return nativeTimeout(resolve, 0); + }, + }); } /** diff --git a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts index 0db11ffa061c0..9d8106a1366d6 100644 --- a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts +++ b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +// actual mocks export const expandLiteralStrings = jest.fn(); export const XJsonMode = jest.fn(); export const useRequest = jest.fn(() => ({ @@ -11,5 +12,20 @@ export const useRequest = jest.fn(() => ({ error: null, data: undefined, })); -export { mlInMemoryTableBasicFactory } from '../../../ml/public/application/components/ml_in_memory_table'; -export const SORT_DIRECTION = { ASC: 'asc' }; + +// just passing through the reimports +export { getErrorMessage } from '../../../ml/common/util/errors'; +export { + getDataGridSchemaFromKibanaFieldType, + getFieldsFromKibanaIndexPattern, + multiColumnSortFactory, + useDataGrid, + useRenderCellValue, + DataGrid, + EsSorting, + RenderCellValue, + SearchResponse7, + UseDataGridReturnType, + UseIndexDataReturnType, +} from '../../../ml/public/application/components/data_grid'; +export { INDEX_STATUS } from '../../../ml/public/application/data_frame_analytics/common'; diff --git a/x-pack/plugins/transform/public/app/common/data_grid.test.ts b/x-pack/plugins/transform/public/app/common/data_grid.test.ts new file mode 100644 index 0000000000000..0e5ecb5d3b214 --- /dev/null +++ b/x-pack/plugins/transform/public/app/common/data_grid.test.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + getPreviewRequestBody, + PivotAggsConfig, + PivotGroupByConfig, + PIVOT_SUPPORTED_AGGS, + PIVOT_SUPPORTED_GROUP_BY_AGGS, + SimpleQuery, +} from '../common'; + +import { getIndexDevConsoleStatement, getPivotPreviewDevConsoleStatement } from './data_grid'; + +describe('Transform: Data Grid', () => { + test('getPivotPreviewDevConsoleStatement()', () => { + const query: SimpleQuery = { + query_string: { + query: '*', + default_operator: 'AND', + }, + }; + const groupBy: PivotGroupByConfig = { + agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, + field: 'the-group-by-field', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-group-by-drop-down-name', + }; + const agg: PivotAggsConfig = { + agg: PIVOT_SUPPORTED_AGGS.AVG, + field: 'the-agg-field', + aggName: 'the-agg-agg-name', + dropDownName: 'the-agg-drop-down-name', + }; + const request = getPreviewRequestBody('the-index-pattern-title', query, [groupBy], [agg]); + const pivotPreviewDevConsoleStatement = getPivotPreviewDevConsoleStatement(request); + + expect(pivotPreviewDevConsoleStatement).toBe(`POST _transform/_preview +{ + "source": { + "index": [ + "the-index-pattern-title" + ] + }, + "pivot": { + "group_by": { + "the-group-by-agg-name": { + "terms": { + "field": "the-group-by-field" + } + } + }, + "aggregations": { + "the-agg-agg-name": { + "avg": { + "field": "the-agg-field" + } + } + } + } +} +`); + }); +}); + +describe('Transform: Index Preview Common', () => { + test('getIndexDevConsoleStatement()', () => { + const query: SimpleQuery = { + query_string: { + query: '*', + default_operator: 'AND', + }, + }; + const indexPreviewDevConsoleStatement = getIndexDevConsoleStatement( + query, + 'the-index-pattern-title' + ); + + expect(indexPreviewDevConsoleStatement).toBe(`GET the-index-pattern-title/_search +{ + "query": { + "query_string": { + "query": "*", + "default_operator": "AND" + } + } +} +`); + }); +}); diff --git a/x-pack/plugins/transform/public/app/common/data_grid.ts b/x-pack/plugins/transform/public/app/common/data_grid.ts index 0e9cceefb3156..cf9ba5d6f5853 100644 --- a/x-pack/plugins/transform/public/app/common/data_grid.ts +++ b/x-pack/plugins/transform/public/app/common/data_grid.ts @@ -4,22 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiDataGridStyle } from '@elastic/eui'; +import { PivotQuery } from './request'; +import { PreviewRequestBody } from './transform'; export const INIT_MAX_COLUMNS = 20; -export const euiDataGridStyle: EuiDataGridStyle = { - border: 'all', - fontSize: 's', - cellPadding: 's', - stripes: false, - rowHover: 'highlight', - header: 'shade', +export const getPivotPreviewDevConsoleStatement = (request: PreviewRequestBody) => { + return `POST _transform/_preview\n${JSON.stringify(request, null, 2)}\n`; }; -export const euiDataGridToolbarSettings = { - showColumnSelector: true, - showStyleSelector: false, - showSortSelector: true, - showFullScreenSelector: false, +export const getIndexDevConsoleStatement = (query: PivotQuery, indexPatternTitle: string) => { + return `GET ${indexPatternTitle}/_search\n${JSON.stringify( + { + query, + }, + null, + 2 + )}\n`; }; diff --git a/x-pack/plugins/transform/public/app/common/index.ts b/x-pack/plugins/transform/public/app/common/index.ts index daeddaa801828..009c8c7a2a9f5 100644 --- a/x-pack/plugins/transform/public/app/common/index.ts +++ b/x-pack/plugins/transform/public/app/common/index.ts @@ -5,7 +5,11 @@ */ export { AggName, isAggName } from './aggregations'; -export { euiDataGridStyle, euiDataGridToolbarSettings, INIT_MAX_COLUMNS } from './data_grid'; +export { + getIndexDevConsoleStatement, + getPivotPreviewDevConsoleStatement, + INIT_MAX_COLUMNS, +} from './data_grid'; export { getDefaultSelectableFields, getFlattenedFields, diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/common.ts b/x-pack/plugins/transform/public/app/components/pivot_preview/common.ts deleted file mode 100644 index 498c3a3ac60af..0000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/common.ts +++ /dev/null @@ -1,60 +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 { EuiDataGridSorting } from '@elastic/eui'; - -import { getNestedProperty } from '../../../../common/utils/object_utils'; - -import { PreviewRequestBody } from '../../common'; - -/** - * Helper to sort an array of objects based on an EuiDataGrid sorting configuration. - * `sortFn()` is recursive to support sorting on multiple columns. - * - * @param sortingColumns - The EUI data grid sorting configuration - * @returns The sorting function which can be used with an array's sort() function. - */ -export const multiColumnSortFactory = (sortingColumns: EuiDataGridSorting['columns']) => { - const isString = (arg: any): arg is string => { - return typeof arg === 'string'; - }; - - const sortFn = (a: any, b: any, sortingColumnIndex = 0): number => { - const sort = sortingColumns[sortingColumnIndex]; - const aValue = getNestedProperty(a, sort.id, null); - const bValue = getNestedProperty(b, sort.id, null); - - if (typeof aValue === 'number' && typeof bValue === 'number') { - if (aValue < bValue) { - return sort.direction === 'asc' ? -1 : 1; - } - if (aValue > bValue) { - return sort.direction === 'asc' ? 1 : -1; - } - } - - if (isString(aValue) && isString(bValue)) { - if (aValue.localeCompare(bValue) === -1) { - return sort.direction === 'asc' ? -1 : 1; - } - if (aValue.localeCompare(bValue) === 1) { - return sort.direction === 'asc' ? 1 : -1; - } - } - - if (sortingColumnIndex + 1 < sortingColumns.length) { - return sortFn(a, b, sortingColumnIndex + 1); - } - - return 0; - }; - - return sortFn; -}; - -export const getPivotPreviewDevConsoleStatement = (request: PreviewRequestBody) => { - return `POST _transform/_preview\n${JSON.stringify(request, null, 2)}\n`; -}; diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.test.tsx b/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.test.tsx deleted file mode 100644 index 5ed50eaab46ba..0000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.test.tsx +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { render, wait } from '@testing-library/react'; -import '@testing-library/jest-dom/extend-expect'; - -import { - getPivotQuery, - PivotAggsConfig, - PivotGroupByConfig, - PIVOT_SUPPORTED_AGGS, - PIVOT_SUPPORTED_GROUP_BY_AGGS, -} from '../../common'; - -import { PivotPreview } from './pivot_preview'; - -jest.mock('../../../shared_imports'); -jest.mock('../../../app/app_dependencies'); - -describe('Transform: ', () => { - // Using the async/await wait()/done() pattern to avoid act() errors. - test('Minimal initialization', async done => { - // Arrange - const groupBy: PivotGroupByConfig = { - agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, - field: 'the-group-by-field', - aggName: 'the-group-by-agg-name', - dropDownName: 'the-group-by-drop-down-name', - }; - const agg: PivotAggsConfig = { - agg: PIVOT_SUPPORTED_AGGS.AVG, - field: 'the-agg-field', - aggName: 'the-group-by-agg-name', - dropDownName: 'the-group-by-drop-down-name', - }; - const props = { - aggs: { 'the-agg-name': agg }, - groupBy: { 'the-group-by-name': groupBy }, - indexPatternTitle: 'the-index-pattern-title', - query: getPivotQuery('the-query'), - }; - - const { getByText } = render(); - - // Act - // Assert - expect(getByText('Transform pivot preview')).toBeInTheDocument(); - await wait(); - done(); - }); -}); diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.tsx b/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.tsx deleted file mode 100644 index c50df0366d698..0000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.tsx +++ /dev/null @@ -1,345 +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 moment from 'moment-timezone'; -import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { - EuiButtonIcon, - EuiCallOut, - EuiCodeBlock, - EuiCopy, - EuiDataGrid, - EuiDataGridSorting, - EuiFlexGroup, - EuiFlexItem, - EuiProgress, - EuiTitle, -} from '@elastic/eui'; - -import { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/common'; - -import { dictionaryToArray } from '../../../../common/types/common'; -import { formatHumanReadableDateTimeSeconds } from '../../../../common/utils/date_utils'; -import { getNestedProperty } from '../../../../common/utils/object_utils'; - -import { - euiDataGridStyle, - euiDataGridToolbarSettings, - EsFieldName, - PreviewRequestBody, - PivotAggsConfigDict, - PivotGroupByConfig, - PivotGroupByConfigDict, - PivotQuery, - INIT_MAX_COLUMNS, -} from '../../common'; -import { SearchItems } from '../../hooks/use_search_items'; - -import { getPivotPreviewDevConsoleStatement, multiColumnSortFactory } from './common'; -import { PIVOT_PREVIEW_STATUS, usePivotPreviewData } from './use_pivot_preview_data'; - -function sortColumns(groupByArr: PivotGroupByConfig[]) { - return (a: string, b: string) => { - // make sure groupBy fields are always most left columns - if (groupByArr.some(d => d.aggName === a) && groupByArr.some(d => d.aggName === b)) { - return a.localeCompare(b); - } - if (groupByArr.some(d => d.aggName === a)) { - return -1; - } - if (groupByArr.some(d => d.aggName === b)) { - return 1; - } - return a.localeCompare(b); - }; -} - -interface PreviewTitleProps { - previewRequest: PreviewRequestBody; -} - -const PreviewTitle: FC = ({ previewRequest }) => { - const euiCopyText = i18n.translate('xpack.transform.pivotPreview.copyClipboardTooltip', { - defaultMessage: 'Copy Dev Console statement of the pivot preview to the clipboard.', - }); - - return ( - - - - - {i18n.translate('xpack.transform.pivotPreview.PivotPreviewTitle', { - defaultMessage: 'Transform pivot preview', - })} - - - - - - {(copy: () => void) => ( - - )} - - - - ); -}; - -interface ErrorMessageProps { - message: string; -} - -const ErrorMessage: FC = ({ message }) => ( - - {message} - -); - -interface PivotPreviewProps { - aggs: PivotAggsConfigDict; - groupBy: PivotGroupByConfigDict; - indexPatternTitle: SearchItems['indexPattern']['title']; - query: PivotQuery; - showHeader?: boolean; -} - -const defaultPagination = { pageIndex: 0, pageSize: 5 }; - -export const PivotPreview: FC = React.memo( - ({ aggs, groupBy, indexPatternTitle, query, showHeader = true }) => { - const { - previewData: data, - previewMappings, - errorMessage, - previewRequest, - status, - } = usePivotPreviewData(indexPatternTitle, query, aggs, groupBy); - const groupByArr = dictionaryToArray(groupBy); - - // Filters mapping properties of type `object`, which get returned for nested field parents. - const columnKeys = Object.keys(previewMappings.properties).filter( - key => previewMappings.properties[key].type !== 'object' - ); - columnKeys.sort(sortColumns(groupByArr)); - - // Column visibility - const [visibleColumns, setVisibleColumns] = useState([]); - - useEffect(() => { - setVisibleColumns(columnKeys.splice(0, INIT_MAX_COLUMNS)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [columnKeys.join()]); - - const [pagination, setPagination] = useState(defaultPagination); - - // Reset pagination if data changes. This is to avoid ending up with an empty table - // when for example the user selected a page that is not available with the updated data. - useEffect(() => { - setPagination(defaultPagination); - }, [data.length]); - - // EuiDataGrid State - const dataGridColumns = columnKeys.map(id => { - const field = previewMappings.properties[id]; - - // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] - // To fall back to the default string schema it needs to be undefined. - let schema; - - switch (field?.type) { - case ES_FIELD_TYPES.GEO_POINT: - case ES_FIELD_TYPES.GEO_SHAPE: - schema = 'json'; - break; - case ES_FIELD_TYPES.BOOLEAN: - schema = 'boolean'; - break; - case ES_FIELD_TYPES.DATE: - case ES_FIELD_TYPES.DATE_NANOS: - schema = 'datetime'; - break; - case ES_FIELD_TYPES.BYTE: - case ES_FIELD_TYPES.DOUBLE: - case ES_FIELD_TYPES.FLOAT: - case ES_FIELD_TYPES.HALF_FLOAT: - case ES_FIELD_TYPES.INTEGER: - case ES_FIELD_TYPES.LONG: - case ES_FIELD_TYPES.SCALED_FLOAT: - case ES_FIELD_TYPES.SHORT: - schema = 'numeric'; - break; - // keep schema undefined for text based columns - case ES_FIELD_TYPES.KEYWORD: - case ES_FIELD_TYPES.TEXT: - break; - } - - return { id, schema }; - }); - - const onChangeItemsPerPage = useCallback( - pageSize => { - setPagination(p => { - const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); - return { pageIndex, pageSize }; - }); - }, - [setPagination] - ); - - const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ - setPagination, - ]); - - // Sorting config - const [sortingColumns, setSortingColumns] = useState([]); - const onSort = useCallback(sc => setSortingColumns(sc), [setSortingColumns]); - - if (sortingColumns.length > 0) { - data.sort(multiColumnSortFactory(sortingColumns)); - } - - const pageData = data.slice( - pagination.pageIndex * pagination.pageSize, - (pagination.pageIndex + 1) * pagination.pageSize - ); - - const renderCellValue = useMemo(() => { - return ({ - rowIndex, - columnId, - setCellProps, - }: { - rowIndex: number; - columnId: string; - setCellProps: any; - }) => { - const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - const cellValue = pageData.hasOwnProperty(adjustedRowIndex) - ? getNestedProperty(pageData[adjustedRowIndex], columnId, null) - : null; - - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - - if (cellValue === undefined || cellValue === null) { - return null; - } - - if ( - [ES_FIELD_TYPES.DATE, ES_FIELD_TYPES.DATE_NANOS].includes( - previewMappings.properties[columnId].type - ) - ) { - return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); - } - - if (previewMappings.properties[columnId].type === ES_FIELD_TYPES.BOOLEAN) { - return cellValue ? 'true' : 'false'; - } - - return cellValue; - }; - }, [pageData, pagination.pageIndex, pagination.pageSize, previewMappings.properties]); - - if (status === PIVOT_PREVIEW_STATUS.ERROR) { - return ( -
- - - - -
- ); - } - - if (data.length === 0) { - let noDataMessage = i18n.translate( - 'xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody', - { - defaultMessage: - 'The preview request did not return any data. Please ensure the optional query returns data and that values exist for the field used by group-by and aggregation fields.', - } - ); - - const aggsArr = dictionaryToArray(aggs); - if (aggsArr.length === 0 || groupByArr.length === 0) { - noDataMessage = i18n.translate( - 'xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody', - { - defaultMessage: 'Please choose at least one group-by field and aggregation.', - } - ); - } - - return ( -
- - -

{noDataMessage}

-
-
- ); - } - - if (columnKeys.length === 0) { - return null; - } - - return ( -
- {showHeader && ( - <> - -
- {status === PIVOT_PREVIEW_STATUS.LOADING && } - {status !== PIVOT_PREVIEW_STATUS.LOADING && ( - - )} -
- - )} - {dataGridColumns.length > 0 && data.length > 0 && ( - - )} -
- ); - } -); diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.test.tsx b/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.test.tsx deleted file mode 100644 index 8d09d06b1c731..0000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.test.tsx +++ /dev/null @@ -1,69 +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, { FC } from 'react'; -import ReactDOM from 'react-dom'; - -import { SimpleQuery } from '../../common'; -import { - PIVOT_PREVIEW_STATUS, - usePivotPreviewData, - UsePivotPreviewDataReturnType, -} from './use_pivot_preview_data'; - -jest.mock('../../hooks/use_api'); - -type Callback = () => void; -interface TestHookProps { - callback: Callback; -} - -const TestHook: FC = ({ callback }) => { - callback(); - return null; -}; - -const testHook = (callback: Callback) => { - const container = document.createElement('div'); - document.body.appendChild(container); - ReactDOM.render(, container); -}; - -const query: SimpleQuery = { - query_string: { - query: '*', - default_operator: 'AND', - }, -}; - -let pivotPreviewObj: UsePivotPreviewDataReturnType; - -describe('usePivotPreviewData', () => { - test('indexPattern not defined', () => { - testHook(() => { - pivotPreviewObj = usePivotPreviewData('the-title', query, {}, {}); - }); - - expect(pivotPreviewObj.errorMessage).toBe(''); - expect(pivotPreviewObj.status).toBe(PIVOT_PREVIEW_STATUS.UNUSED); - expect(pivotPreviewObj.previewData).toEqual([]); - }); - - test('indexPattern set triggers loading', () => { - testHook(() => { - pivotPreviewObj = usePivotPreviewData('the-title', query, {}, {}); - }); - - expect(pivotPreviewObj.errorMessage).toBe(''); - // ideally this should be LOADING instead of UNUSED but jest/enzyme/hooks doesn't - // trigger that state upate yet. - expect(pivotPreviewObj.status).toBe(PIVOT_PREVIEW_STATUS.UNUSED); - expect(pivotPreviewObj.previewData).toEqual([]); - }); - - // TODO add more tests to check data retrieved via `api.esSearch()`. - // This needs more investigation in regards to jest/enzyme's React Hooks support. -}); diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.ts b/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.ts deleted file mode 100644 index 83fa7ba189ff0..0000000000000 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/use_pivot_preview_data.ts +++ /dev/null @@ -1,91 +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 { useEffect, useState } from 'react'; - -import { dictionaryToArray } from '../../../../common/types/common'; -import { useApi } from '../../hooks/use_api'; - -import { IndexPattern } from '../../../../../../../src/plugins/data/public'; - -import { - getPreviewRequestBody, - PreviewRequestBody, - PivotAggsConfigDict, - PivotGroupByConfigDict, - PivotQuery, - PreviewData, - PreviewMappings, -} from '../../common'; - -export enum PIVOT_PREVIEW_STATUS { - UNUSED, - LOADING, - LOADED, - ERROR, -} - -export interface UsePivotPreviewDataReturnType { - errorMessage: string; - status: PIVOT_PREVIEW_STATUS; - previewData: PreviewData; - previewMappings: PreviewMappings; - previewRequest: PreviewRequestBody; -} - -export const usePivotPreviewData = ( - indexPatternTitle: IndexPattern['title'], - query: PivotQuery, - aggs: PivotAggsConfigDict, - groupBy: PivotGroupByConfigDict -): UsePivotPreviewDataReturnType => { - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(PIVOT_PREVIEW_STATUS.UNUSED); - const [previewData, setPreviewData] = useState([]); - const [previewMappings, setPreviewMappings] = useState({ properties: {} }); - const api = useApi(); - - const aggsArr = dictionaryToArray(aggs); - const groupByArr = dictionaryToArray(groupBy); - - const previewRequest = getPreviewRequestBody(indexPatternTitle, query, groupByArr, aggsArr); - - const getPreviewData = async () => { - if (aggsArr.length === 0 || groupByArr.length === 0) { - setPreviewData([]); - return; - } - - setErrorMessage(''); - setStatus(PIVOT_PREVIEW_STATUS.LOADING); - - try { - const resp = await api.getTransformsPreview(previewRequest); - setPreviewData(resp.preview); - setPreviewMappings(resp.generated_dest_index.mappings); - setStatus(PIVOT_PREVIEW_STATUS.LOADED); - } catch (e) { - setErrorMessage(JSON.stringify(e, null, 2)); - setPreviewData([]); - setPreviewMappings({ properties: {} }); - setStatus(PIVOT_PREVIEW_STATUS.ERROR); - } - }; - - useEffect(() => { - getPreviewData(); - // custom comparison - /* eslint-disable react-hooks/exhaustive-deps */ - }, [ - indexPatternTitle, - JSON.stringify(aggsArr), - JSON.stringify(groupByArr), - JSON.stringify(query), - /* eslint-enable react-hooks/exhaustive-deps */ - ]); - - return { errorMessage, status, previewData, previewMappings, previewRequest }; -}; diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx new file mode 100644 index 0000000000000..4ca536e3c115d --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.test.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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, wait } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import '@testing-library/jest-dom/extend-expect'; + +import { CoreSetup } from 'src/core/public'; + +import { DataGrid, UseIndexDataReturnType, INDEX_STATUS } from '../../shared_imports'; + +import { SimpleQuery } from '../common'; + +import { SearchItems } from './use_search_items'; +import { useIndexData } from './use_index_data'; + +jest.mock('../../shared_imports'); +jest.mock('../app_dependencies'); +jest.mock('./use_api'); + +const query: SimpleQuery = { + query_string: { + query: '*', + default_operator: 'AND', + }, +}; + +describe('Transform: useIndexData()', () => { + test('indexPattern set triggers loading', async done => { + const { result, waitForNextUpdate } = renderHook(() => + useIndexData( + ({ + id: 'the-id', + title: 'the-title', + fields: [], + } as unknown) as SearchItems['indexPattern'], + query + ) + ); + const IndexObj: UseIndexDataReturnType = result.current; + + await waitForNextUpdate(); + + expect(IndexObj.errorMessage).toBe(''); + expect(IndexObj.status).toBe(INDEX_STATUS.LOADING); + expect(IndexObj.tableItems).toEqual([]); + done(); + }); +}); + +describe('Transform: with useIndexData()', () => { + // Using the async/await wait()/done() pattern to avoid act() errors. + test('Minimal initialization', async done => { + // Arrange + const indexPattern = { + title: 'the-index-pattern-title', + fields: [] as any[], + } as SearchItems['indexPattern']; + + const Wrapper = () => { + const props = { + ...useIndexData(indexPattern, { match_all: {} }), + copyToClipboard: 'the-copy-to-clipboard-code', + copyToClipboardDescription: 'the-copy-to-clipboard-description', + dataTestSubj: 'the-data-test-subj', + title: 'the-index-preview-title', + toastNotifications: {} as CoreSetup['notifications']['toasts'], + }; + + return ; + }; + const { getByText } = render(); + + // Act + // Assert + expect(getByText('the-index-preview-title')).toBeInTheDocument(); + await wait(); + done(); + }); +}); diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts new file mode 100644 index 0000000000000..ec5a4d244c152 --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect } from 'react'; + +import { + getDataGridSchemaFromKibanaFieldType, + getFieldsFromKibanaIndexPattern, + getErrorMessage, + useDataGrid, + useRenderCellValue, + EsSorting, + SearchResponse7, + UseIndexDataReturnType, + INDEX_STATUS, +} from '../../shared_imports'; + +import { isDefaultQuery, matchAllQuery, PivotQuery } from '../common'; + +import { SearchItems } from './use_search_items'; +import { useApi } from './use_api'; + +type IndexSearchResponse = SearchResponse7; + +export const useIndexData = ( + indexPattern: SearchItems['indexPattern'], + query: PivotQuery +): UseIndexDataReturnType => { + const api = useApi(); + + const indexPatternFields = getFieldsFromKibanaIndexPattern(indexPattern); + + // EuiDataGrid State + const columns = [ + ...indexPatternFields.map(id => { + const field = indexPattern.fields.getByName(id); + const schema = getDataGridSchemaFromKibanaFieldType(field); + return { id, schema }; + }), + ]; + + const dataGrid = useDataGrid(columns); + + const { + pagination, + resetPagination, + setErrorMessage, + setRowCount, + setStatus, + setTableItems, + sortingColumns, + tableItems, + } = dataGrid; + + useEffect(() => { + resetPagination(); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(query)]); + + const getIndexData = async function() { + setErrorMessage(''); + setStatus(INDEX_STATUS.LOADING); + + const sort: EsSorting = sortingColumns.reduce((s, column) => { + s[column.id] = { order: column.direction }; + return s; + }, {} as EsSorting); + + const esSearchRequest = { + index: indexPattern.title, + body: { + // Instead of using the default query (`*`), fall back to a more efficient `match_all` query. + query: isDefaultQuery(query) ? matchAllQuery : query, + from: pagination.pageIndex * pagination.pageSize, + size: pagination.pageSize, + ...(Object.keys(sort).length > 0 ? { sort } : {}), + }, + }; + + try { + const resp: IndexSearchResponse = await api.esSearch(esSearchRequest); + + const docs = resp.hits.hits.map(d => d._source); + + setRowCount(resp.hits.total.value); + setTableItems(docs); + setStatus(INDEX_STATUS.LOADED); + } catch (e) { + setErrorMessage(getErrorMessage(e)); + setStatus(INDEX_STATUS.ERROR); + } + }; + + useEffect(() => { + getIndexData(); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [indexPattern.title, JSON.stringify([query, pagination, sortingColumns])]); + + const renderCellValue = useRenderCellValue(indexPattern, pagination, tableItems); + + return { + ...dataGrid, + columns, + renderCellValue, + }; +}; diff --git a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts new file mode 100644 index 0000000000000..ff7ca5d42b5f7 --- /dev/null +++ b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts @@ -0,0 +1,240 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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-timezone'; +import { useEffect, useMemo, useState } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/common'; + +import { dictionaryToArray } from '../../../common/types/common'; +import { formatHumanReadableDateTimeSeconds } from '../../../common/utils/date_utils'; +import { getNestedProperty } from '../../../common/utils/object_utils'; + +import { + getErrorMessage, + multiColumnSortFactory, + useDataGrid, + RenderCellValue, + UseIndexDataReturnType, + INDEX_STATUS, +} from '../../shared_imports'; + +import { + getPreviewRequestBody, + PivotAggsConfigDict, + PivotGroupByConfigDict, + PivotGroupByConfig, + PivotQuery, + PreviewMappings, +} from '../common'; + +import { SearchItems } from './use_search_items'; +import { useApi } from './use_api'; + +function sortColumns(groupByArr: PivotGroupByConfig[]) { + return (a: string, b: string) => { + // make sure groupBy fields are always most left columns + if (groupByArr.some(d => d.aggName === a) && groupByArr.some(d => d.aggName === b)) { + return a.localeCompare(b); + } + if (groupByArr.some(d => d.aggName === a)) { + return -1; + } + if (groupByArr.some(d => d.aggName === b)) { + return 1; + } + return a.localeCompare(b); + }; +} + +export const usePivotData = ( + indexPatternTitle: SearchItems['indexPattern']['title'], + query: PivotQuery, + aggs: PivotAggsConfigDict, + groupBy: PivotGroupByConfigDict +): UseIndexDataReturnType => { + const [previewMappings, setPreviewMappings] = useState({ properties: {} }); + const api = useApi(); + + const aggsArr = dictionaryToArray(aggs); + const groupByArr = dictionaryToArray(groupBy); + + // Filters mapping properties of type `object`, which get returned for nested field parents. + const columnKeys = Object.keys(previewMappings.properties).filter( + key => previewMappings.properties[key].type !== 'object' + ); + columnKeys.sort(sortColumns(groupByArr)); + + // EuiDataGrid State + const columns = columnKeys.map(id => { + const field = previewMappings.properties[id]; + + // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] + // To fall back to the default string schema it needs to be undefined. + let schema; + + switch (field?.type) { + case ES_FIELD_TYPES.GEO_POINT: + case ES_FIELD_TYPES.GEO_SHAPE: + schema = 'json'; + break; + case ES_FIELD_TYPES.BOOLEAN: + schema = 'boolean'; + break; + case ES_FIELD_TYPES.DATE: + case ES_FIELD_TYPES.DATE_NANOS: + schema = 'datetime'; + break; + case ES_FIELD_TYPES.BYTE: + case ES_FIELD_TYPES.DOUBLE: + case ES_FIELD_TYPES.FLOAT: + case ES_FIELD_TYPES.HALF_FLOAT: + case ES_FIELD_TYPES.INTEGER: + case ES_FIELD_TYPES.LONG: + case ES_FIELD_TYPES.SCALED_FLOAT: + case ES_FIELD_TYPES.SHORT: + schema = 'numeric'; + break; + // keep schema undefined for text based columns + case ES_FIELD_TYPES.KEYWORD: + case ES_FIELD_TYPES.TEXT: + break; + } + + return { id, schema }; + }); + + const dataGrid = useDataGrid(columns); + + const { + pagination, + resetPagination, + setErrorMessage, + setNoDataMessage, + setRowCount, + setStatus, + setTableItems, + sortingColumns, + tableItems, + } = dataGrid; + + const previewRequest = getPreviewRequestBody(indexPatternTitle, query, groupByArr, aggsArr); + + const getPreviewData = async () => { + if (aggsArr.length === 0 || groupByArr.length === 0) { + setTableItems([]); + setRowCount(0); + setNoDataMessage( + i18n.translate('xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody', { + defaultMessage: 'Please choose at least one group-by field and aggregation.', + }) + ); + return; + } + + setErrorMessage(''); + setNoDataMessage(''); + setStatus(INDEX_STATUS.LOADING); + + try { + const resp = await api.getTransformsPreview(previewRequest); + setTableItems(resp.preview); + setRowCount(resp.preview.length); + setPreviewMappings(resp.generated_dest_index.mappings); + setStatus(INDEX_STATUS.LOADED); + + if (resp.preview.length === 0) { + setNoDataMessage( + i18n.translate('xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody', { + defaultMessage: + 'The preview request did not return any data. Please ensure the optional query returns data and that values exist for the field used by group-by and aggregation fields.', + }) + ); + } + } catch (e) { + setErrorMessage(getErrorMessage(e)); + setTableItems([]); + setRowCount(0); + setPreviewMappings({ properties: {} }); + setStatus(INDEX_STATUS.ERROR); + } + }; + + useEffect(() => { + resetPagination(); + // custom comparison + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [JSON.stringify(query)]); + + useEffect(() => { + getPreviewData(); + // custom comparison + /* eslint-disable react-hooks/exhaustive-deps */ + }, [ + indexPatternTitle, + JSON.stringify(aggsArr), + JSON.stringify(groupByArr), + JSON.stringify(query), + /* eslint-enable react-hooks/exhaustive-deps */ + ]); + + if (sortingColumns.length > 0) { + tableItems.sort(multiColumnSortFactory(sortingColumns)); + } + + const pageData = tableItems.slice( + pagination.pageIndex * pagination.pageSize, + (pagination.pageIndex + 1) * pagination.pageSize + ); + + const renderCellValue: RenderCellValue = useMemo(() => { + return ({ + rowIndex, + columnId, + setCellProps, + }: { + rowIndex: number; + columnId: string; + setCellProps: any; + }) => { + const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; + + const cellValue = pageData.hasOwnProperty(adjustedRowIndex) + ? getNestedProperty(pageData[adjustedRowIndex], columnId, null) + : null; + + if (typeof cellValue === 'object' && cellValue !== null) { + return JSON.stringify(cellValue); + } + + if (cellValue === undefined || cellValue === null) { + return null; + } + + if ( + [ES_FIELD_TYPES.DATE, ES_FIELD_TYPES.DATE_NANOS].includes( + previewMappings.properties[columnId].type + ) + ) { + return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); + } + + if (previewMappings.properties[columnId].type === ES_FIELD_TYPES.BOOLEAN) { + return cellValue ? 'true' : 'false'; + } + + return cellValue; + }; + }, [pageData, pagination.pageIndex, pagination.pageSize, previewMappings.properties]); + + return { + ...dataGrid, + columns, + renderCellValue, + }; +}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap deleted file mode 100644 index b668c7d8e4a69..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/__snapshots__/expanded_row.test.tsx.snap +++ /dev/null @@ -1,71 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Transform: Test against strings, objects and arrays. 1`] = ` - - - - name - : - - - - the-name -    - - - - - nested.inner1 - : - - - - the-inner-1 -    - - - - - nested.inner2 - : - - - - the-inner-2 -    - - - - - arrayString - : - - - - ["the-array-string-1","the-array-string-2"] -    - - - - - arrayObject - : - - - - [{"object1":"the-object-1"},{"object2":"the-objects-2"}] -    - - - -`; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.test.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.test.ts deleted file mode 100644 index d3bf81bba2e56..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.test.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 { SimpleQuery } from '../../../../common'; - -import { getSourceIndexDevConsoleStatement } from './common'; - -describe('Transform: Source Index Preview Common', () => { - test('getSourceIndexDevConsoleStatement()', () => { - const query: SimpleQuery = { - query_string: { - query: '*', - default_operator: 'AND', - }, - }; - const sourceIndexPreviewDevConsoleStatement = getSourceIndexDevConsoleStatement( - query, - 'the-index-pattern-title' - ); - - expect(sourceIndexPreviewDevConsoleStatement).toBe(`GET the-index-pattern-title/_search -{ - "query": { - "query_string": { - "query": "*", - "default_operator": "AND" - } - } -} -`); - }); -}); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.ts deleted file mode 100644 index c34675463bf8b..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/common.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 { PivotQuery } from '../../../../common'; - -export const getSourceIndexDevConsoleStatement = (query: PivotQuery, indexPatternTitle: string) => { - return `GET ${indexPatternTitle}/_search\n${JSON.stringify( - { - query, - }, - null, - 2 - )}\n`; -}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.test.tsx deleted file mode 100644 index ddd1a1482fd35..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.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 { shallow } from 'enzyme'; -import React from 'react'; - -import { getNestedProperty } from '../../../../../../common/utils/object_utils'; -import { getFlattenedFields } from '../../../../common'; - -import { ExpandedRow } from './expanded_row'; - -describe('Transform: ', () => { - test('Test against strings, objects and arrays.', () => { - const source = { - name: 'the-name', - nested: { - inner1: 'the-inner-1', - inner2: 'the-inner-2', - }, - arrayString: ['the-array-string-1', 'the-array-string-2'], - arrayObject: [{ object1: 'the-object-1' }, { object2: 'the-objects-2' }], - } as Record; - - const flattenedSource = getFlattenedFields(source).reduce((p, c) => { - p[c] = getNestedProperty(source, c); - if (p[c] === undefined) { - p[c] = source[`"${c}"`]; - } - return p; - }, {} as Record); - - const props = { - item: { - _id: 'the-id', - _source: flattenedSource, - }, - }; - - const wrapper = shallow(); - - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.tsx deleted file mode 100644 index 9b83a3e5da8a8..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/expanded_row.tsx +++ /dev/null @@ -1,22 +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 { EuiBadge, EuiText } from '@elastic/eui'; - -import { EsDoc } from '../../../../common'; - -export const ExpandedRow: React.FC<{ item: EsDoc }> = ({ item }) => ( - - {Object.entries(item._source).map(([k, value]) => ( - - {k}: - {typeof value === 'string' ? value : JSON.stringify(value)}   - - ))} - -); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.test.tsx deleted file mode 100644 index 32f6ff9490a0f..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.test.tsx +++ /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 React from 'react'; -import { render, wait } from '@testing-library/react'; -import '@testing-library/jest-dom/extend-expect'; - -import { getPivotQuery } from '../../../../common'; -import { SearchItems } from '../../../../hooks/use_search_items'; - -import { SourceIndexPreview } from './source_index_preview'; - -jest.mock('../../../../../shared_imports'); -jest.mock('../../../../../app/app_dependencies'); - -describe('Transform: ', () => { - // Using the async/await wait()/done() pattern to avoid act() errors. - test('Minimal initialization', async done => { - // Arrange - const props = { - indexPattern: { - title: 'the-index-pattern-title', - fields: [] as any[], - } as SearchItems['indexPattern'], - query: getPivotQuery('the-query'), - }; - const { getByText } = render(); - - // Act - // Assert - expect(getByText(`Source index ${props.indexPattern.title}`)).toBeInTheDocument(); - await wait(); - done(); - }); -}); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx deleted file mode 100644 index bcdeb7ddb0d36..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx +++ /dev/null @@ -1,293 +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 moment from 'moment-timezone'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; - -import { i18n } from '@kbn/i18n'; - -import { - EuiButtonIcon, - EuiCallOut, - EuiCodeBlock, - EuiCopy, - EuiDataGrid, - EuiFlexGroup, - EuiFlexItem, - EuiProgress, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; - -import { KBN_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/common'; - -import { formatHumanReadableDateTimeSeconds } from '../../../../../../common/utils/date_utils'; -import { getNestedProperty } from '../../../../../../common/utils/object_utils'; - -import { - euiDataGridStyle, - euiDataGridToolbarSettings, - EsFieldName, - PivotQuery, - INIT_MAX_COLUMNS, -} from '../../../../common'; -import { SearchItems } from '../../../../hooks/use_search_items'; -import { useToastNotifications } from '../../../../app_dependencies'; - -import { getSourceIndexDevConsoleStatement } from './common'; -import { SOURCE_INDEX_STATUS, useSourceIndexData } from './use_source_index_data'; - -interface SourceIndexPreviewTitle { - indexPatternTitle: string; -} -const SourceIndexPreviewTitle: React.FC = ({ indexPatternTitle }) => ( - - - {i18n.translate('xpack.transform.sourceIndexPreview.sourceIndexPatternTitle', { - defaultMessage: 'Source index {indexPatternTitle}', - values: { indexPatternTitle }, - })} - - -); - -interface Props { - indexPattern: SearchItems['indexPattern']; - query: PivotQuery; -} - -export const SourceIndexPreview: React.FC = React.memo(({ indexPattern, query }) => { - const toastNotifications = useToastNotifications(); - const allFields = indexPattern.fields.map(f => f.name); - const indexPatternFields: string[] = allFields.filter(f => { - if (indexPattern.metaFields.includes(f)) { - return false; - } - - const fieldParts = f.split('.'); - const lastPart = fieldParts.pop(); - if (lastPart === 'keyword' && allFields.includes(fieldParts.join('.'))) { - return false; - } - - return true; - }); - - // Column visibility - const [visibleColumns, setVisibleColumns] = useState([]); - - useEffect(() => { - setVisibleColumns(indexPatternFields.splice(0, INIT_MAX_COLUMNS)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [indexPatternFields.join()]); - - const { - errorMessage, - pagination, - setPagination, - setSortingColumns, - rowCount, - sortingColumns, - status, - tableItems: data, - } = useSourceIndexData(indexPattern, query); - - // EuiDataGrid State - const dataGridColumns = [ - ...indexPatternFields.map(id => { - const field = indexPattern.fields.getByName(id); - - // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] - // To fall back to the default string schema it needs to be undefined. - let schema; - - switch (field?.type) { - case KBN_FIELD_TYPES.BOOLEAN: - schema = 'boolean'; - break; - case KBN_FIELD_TYPES.DATE: - schema = 'datetime'; - break; - case KBN_FIELD_TYPES.GEO_POINT: - case KBN_FIELD_TYPES.GEO_SHAPE: - schema = 'json'; - break; - case KBN_FIELD_TYPES.NUMBER: - schema = 'numeric'; - break; - } - - return { id, schema }; - }), - ]; - - const onSort = useCallback( - (sc: Array<{ id: string; direction: 'asc' | 'desc' }>) => { - // Check if an unsupported column type for sorting was selected. - const invalidSortingColumnns = sc.reduce((arr, current) => { - const columnType = dataGridColumns.find(dgc => dgc.id === current.id); - if (columnType?.schema === 'json') { - arr.push(current.id); - } - return arr; - }, []); - if (invalidSortingColumnns.length === 0) { - setSortingColumns(sc); - } else { - invalidSortingColumnns.forEach(columnId => { - toastNotifications.addDanger( - i18n.translate('xpack.transform.sourceIndexPreview.invalidSortingColumnError', { - defaultMessage: `The column '{columnId}' cannot be used for sorting.`, - values: { columnId }, - }) - ); - }); - } - }, - [dataGridColumns, setSortingColumns, toastNotifications] - ); - - const onChangeItemsPerPage = useCallback( - pageSize => { - setPagination(p => { - const pageIndex = Math.floor((p.pageSize * p.pageIndex) / pageSize); - return { pageIndex, pageSize }; - }); - }, - [setPagination] - ); - - const onChangePage = useCallback(pageIndex => setPagination(p => ({ ...p, pageIndex })), [ - setPagination, - ]); - - const renderCellValue = useMemo(() => { - return ({ - rowIndex, - columnId, - setCellProps, - }: { - rowIndex: number; - columnId: string; - setCellProps: any; - }) => { - const adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; - - const cellValue = data.hasOwnProperty(adjustedRowIndex) - ? getNestedProperty(data[adjustedRowIndex], columnId, null) - : null; - - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - - if (cellValue === undefined || cellValue === null) { - return null; - } - - const field = indexPattern.fields.getByName(columnId); - if (field?.type === KBN_FIELD_TYPES.DATE) { - return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); - } - - if (field?.type === KBN_FIELD_TYPES.BOOLEAN) { - return cellValue ? 'true' : 'false'; - } - - return cellValue; - }; - }, [data, indexPattern.fields, pagination.pageIndex, pagination.pageSize]); - - if (status === SOURCE_INDEX_STATUS.LOADED && data.length === 0) { - return ( -
- - -

- {i18n.translate('xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutBody', { - defaultMessage: - 'The query for the source index returned no results. Please make sure you have sufficient permissions, the index contains documents and your query is not too restrictive.', - })} -

-
-
- ); - } - - const euiCopyText = i18n.translate('xpack.transform.sourceIndexPreview.copyClipboardTooltip', { - defaultMessage: 'Copy Dev Console statement of the source index preview to the clipboard.', - }); - - return ( -
- - - - - - - {(copy: () => void) => ( - - )} - - - -
- {status === SOURCE_INDEX_STATUS.LOADING && } - {status !== SOURCE_INDEX_STATUS.LOADING && ( - - )} -
- {status === SOURCE_INDEX_STATUS.ERROR && ( -
- - - {errorMessage} - - - -
- )} - -
- ); -}); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx deleted file mode 100644 index 5a1d8a8db5b42..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.test.tsx +++ /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 { renderHook } from '@testing-library/react-hooks'; -import '@testing-library/jest-dom/extend-expect'; - -import { SimpleQuery } from '../../../../common'; -import { - SOURCE_INDEX_STATUS, - useSourceIndexData, - UseSourceIndexDataReturnType, -} from './use_source_index_data'; - -jest.mock('../../../../hooks/use_api'); - -const query: SimpleQuery = { - query_string: { - query: '*', - default_operator: 'AND', - }, -}; - -describe('useSourceIndexData', () => { - test('indexPattern set triggers loading', async done => { - const { result, waitForNextUpdate } = renderHook(() => - useSourceIndexData({ id: 'the-id', title: 'the-title', fields: [] }, query) - ); - const sourceIndexObj: UseSourceIndexDataReturnType = result.current; - - await waitForNextUpdate(); - - expect(sourceIndexObj.errorMessage).toBe(''); - expect(sourceIndexObj.status).toBe(SOURCE_INDEX_STATUS.LOADING); - expect(sourceIndexObj.tableItems).toEqual([]); - done(); - }); - - // TODO add more tests to check data retrieved via `api.esSearch()`. - // This needs more investigation in regards to jest's React Hooks support. -}); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts deleted file mode 100644 index 5301a3c168a51..0000000000000 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/use_source_index_data.ts +++ /dev/null @@ -1,143 +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 { useEffect, useState, Dispatch, SetStateAction } from 'react'; - -import { SearchResponse } from 'elasticsearch'; - -import { EuiDataGridPaginationProps, EuiDataGridSorting } from '@elastic/eui'; - -import { IIndexPattern } from 'src/plugins/data/public'; - -import { Dictionary } from '../../../../../../common/types/common'; - -import { isDefaultQuery, matchAllQuery, EsDocSource, PivotQuery } from '../../../../common'; -import { useApi } from '../../../../hooks/use_api'; - -export enum SOURCE_INDEX_STATUS { - UNUSED, - LOADING, - LOADED, - ERROR, -} - -type EsSorting = Dictionary<{ - order: 'asc' | 'desc'; -}>; - -interface ErrorResponse { - request: Dictionary; - response: Dictionary; - body: { - statusCode: number; - error: string; - message: string; - }; - name: string; - req: Dictionary; - res: Dictionary; -} - -const isErrorResponse = (arg: any): arg is ErrorResponse => { - return arg?.body?.error !== undefined && arg?.body?.message !== undefined; -}; - -// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. -interface SearchResponse7 extends SearchResponse { - hits: SearchResponse['hits'] & { - total: { - value: number; - relation: string; - }; - }; -} - -type SourceIndexSearchResponse = SearchResponse7; - -type SourceIndexPagination = Pick; -const defaultPagination: SourceIndexPagination = { pageIndex: 0, pageSize: 5 }; - -export interface UseSourceIndexDataReturnType { - errorMessage: string; - pagination: SourceIndexPagination; - setPagination: Dispatch>; - setSortingColumns: Dispatch>; - rowCount: number; - sortingColumns: EuiDataGridSorting['columns']; - status: SOURCE_INDEX_STATUS; - tableItems: EsDocSource[]; -} - -export const useSourceIndexData = ( - indexPattern: IIndexPattern, - query: PivotQuery -): UseSourceIndexDataReturnType => { - const [errorMessage, setErrorMessage] = useState(''); - const [status, setStatus] = useState(SOURCE_INDEX_STATUS.UNUSED); - const [pagination, setPagination] = useState(defaultPagination); - const [sortingColumns, setSortingColumns] = useState([]); - const [rowCount, setRowCount] = useState(0); - const [tableItems, setTableItems] = useState([]); - const api = useApi(); - - useEffect(() => { - setPagination(defaultPagination); - }, [query]); - - const getSourceIndexData = async function() { - setErrorMessage(''); - setStatus(SOURCE_INDEX_STATUS.LOADING); - - const sort: EsSorting = sortingColumns.reduce((s, column) => { - s[column.id] = { order: column.direction }; - return s; - }, {} as EsSorting); - - const esSearchRequest = { - index: indexPattern.title, - body: { - // Instead of using the default query (`*`), fall back to a more efficient `match_all` query. - query: isDefaultQuery(query) ? matchAllQuery : query, - from: pagination.pageIndex * pagination.pageSize, - size: pagination.pageSize, - ...(Object.keys(sort).length > 0 ? { sort } : {}), - }, - }; - - try { - const resp: SourceIndexSearchResponse = await api.esSearch(esSearchRequest); - - const docs = resp.hits.hits.map(d => d._source); - - setRowCount(resp.hits.total.value); - setTableItems(docs); - setStatus(SOURCE_INDEX_STATUS.LOADED); - } catch (e) { - if (isErrorResponse(e)) { - setErrorMessage(`${e.body.error}: ${e.body.message}`); - } else { - setErrorMessage(JSON.stringify(e, null, 2)); - } - setStatus(SOURCE_INDEX_STATUS.ERROR); - } - }; - - useEffect(() => { - getSourceIndexData(); - // custom comparison - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [indexPattern.title, JSON.stringify([query, pagination, sortingColumns])]); - return { - errorMessage, - pagination, - setPagination, - setSortingColumns, - rowCount, - sortingColumns, - status, - tableItems, - }; -}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index 320e405b5d437..0e6e2c1a38d0e 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -35,19 +35,19 @@ import { import { useXJsonMode } from '../../../../../../../../../src/plugins/es_ui_shared/static/ace_x_json/hooks'; -import { PivotPreview } from '../../../../components/pivot_preview'; +import { DataGrid } from '../../../../../shared_imports'; + +import { + getIndexDevConsoleStatement, + getPivotPreviewDevConsoleStatement, +} from '../../../../common/data_grid'; import { useDocumentationLinks } from '../../../../hooks/use_documentation_links'; import { SavedSearchQuery, SearchItems } from '../../../../hooks/use_search_items'; +import { useIndexData } from '../../../../hooks/use_index_data'; +import { usePivotData } from '../../../../hooks/use_pivot_data'; import { useToastNotifications } from '../../../../app_dependencies'; -import { TransformPivotConfig } from '../../../../common'; import { dictionaryToArray, Dictionary } from '../../../../../../common/types/common'; -import { DropDown } from '../aggregation_dropdown'; -import { AggListForm } from '../aggregation_list'; -import { GroupByListForm } from '../group_by_list'; -import { SourceIndexPreview } from '../source_index_preview'; -import { SwitchModal } from './switch_modal'; - import { getPivotQuery, getPreviewRequestBody, @@ -61,11 +61,17 @@ import { PivotGroupByConfig, PivotGroupByConfigDict, PivotSupportedGroupByAggs, + TransformPivotConfig, PIVOT_SUPPORTED_AGGS, PIVOT_SUPPORTED_GROUP_BY_AGGS, } from '../../../../common'; +import { DropDown } from '../aggregation_dropdown'; +import { AggListForm } from '../aggregation_list'; +import { GroupByListForm } from '../group_by_list'; + import { getPivotDropdownOptions } from './common'; +import { SwitchModal } from './switch_modal'; export interface StepDefineExposedState { aggList: PivotAggsConfigDict; @@ -296,7 +302,6 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange, return; } } catch (e) { - console.log('Invalid syntax', JSON.stringify(e, null, 2)); // eslint-disable-line no-console setErrorMessage({ query: query.query as string, message: e.message }); } }; @@ -593,6 +598,9 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange, /* eslint-enable react-hooks/exhaustive-deps */ ]); + const indexPreviewProps = useIndexData(indexPattern, pivotQuery); + const pivotPreviewProps = usePivotData(indexPattern.title, pivotQuery, aggList, groupByList); + // TODO This should use the actual value of `indices.query.bool.max_clause_count` const maxIndexFields = 1024; const numIndexFields = indexPattern.fields.length; @@ -973,13 +981,37 @@ export const StepDefineForm: FC = React.memo(({ overrides = {}, onChange, - + - diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx index f31514e67003b..b9021f4ee5b11 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx @@ -17,8 +17,19 @@ import { EuiText, } from '@elastic/eui'; -import { getPivotQuery, isDefaultQuery, isMatchAllQuery } from '../../../../common'; -import { PivotPreview } from '../../../../components/pivot_preview'; +import { dictionaryToArray } from '../../../../../../common/types/common'; + +import { DataGrid } from '../../../../../shared_imports'; + +import { useToastNotifications } from '../../../../app_dependencies'; +import { + getPivotQuery, + getPivotPreviewDevConsoleStatement, + getPreviewRequestBody, + isDefaultQuery, + isMatchAllQuery, +} from '../../../../common'; +import { usePivotData } from '../../../../hooks/use_pivot_data'; import { SearchItems } from '../../../../hooks/use_search_items'; import { AggListSummary } from '../aggregation_list'; @@ -35,8 +46,25 @@ export const StepDefineSummary: FC = ({ formState: { searchString, searchQuery, groupByList, aggList }, searchItems, }) => { + const toastNotifications = useToastNotifications(); + const pivotAggsArr = dictionaryToArray(aggList); + const pivotGroupByArr = dictionaryToArray(groupByList); const pivotQuery = getPivotQuery(searchQuery); + const previewRequest = getPreviewRequestBody( + searchItems.indexPattern.title, + pivotQuery, + pivotGroupByArr, + pivotAggsArr + ); + + const pivotPreviewProps = usePivotData( + searchItems.indexPattern.title, + pivotQuery, + aggList, + groupByList + ); + return ( @@ -117,11 +145,20 @@ export const StepDefineSummary: FC = ({ - 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 9a39616fb0989..23f482b5bc76a 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 @@ -240,7 +240,6 @@ export const StepDetailsForm: FC = React.memo( ]} > setTransformId(e.target.value)} aria-label={i18n.translate( @@ -257,15 +256,12 @@ export const StepDetailsForm: FC = React.memo( label={i18n.translate('xpack.transform.stepDetailsForm.transformDescriptionLabel', { defaultMessage: 'Transform description', })} - helpText={i18n.translate( - 'xpack.transform.stepDetailsForm.transformDescriptionHelpText', - { - defaultMessage: 'Optional descriptive text.', - } - )} > setTransformDescription(e.target.value)} aria-label={i18n.translate( @@ -310,7 +306,6 @@ export const StepDetailsForm: FC = React.memo( } > setDestinationIndex(e.target.value)} aria-label={i18n.translate( diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx index eaaedc2eb77ce..e183712b390cf 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx @@ -6,37 +6,39 @@ import React, { FC } from 'react'; -import { SearchItems } from '../../../../hooks/use_search_items'; +import { DataGrid } from '../../../../../shared_imports'; +import { useToastNotifications } from '../../../../app_dependencies'; import { getPivotQuery, TransformPivotConfig } from '../../../../common'; +import { usePivotData } from '../../../../hooks/use_pivot_data'; +import { SearchItems } from '../../../../hooks/use_search_items'; import { applyTransformConfigToDefineState, getDefaultStepDefineState, } from '../../../create_transform/components/step_define/'; -import { PivotPreview } from '../../../../components/pivot_preview'; -interface Props { +interface ExpandedRowPreviewPaneProps { transformConfig: TransformPivotConfig; } -export const ExpandedRowPreviewPane: FC = ({ transformConfig }) => { - const previewConfig = applyTransformConfigToDefineState( +export const ExpandedRowPreviewPane: FC = ({ transformConfig }) => { + const toastNotifications = useToastNotifications(); + const { aggList, groupByList, searchQuery } = applyTransformConfigToDefineState( getDefaultStepDefineState({} as SearchItems), transformConfig ); - + const pivotQuery = getPivotQuery(searchQuery); const indexPatternTitle = Array.isArray(transformConfig.source.index) ? transformConfig.source.index.join(',') : transformConfig.source.index; + const pivotPreviewProps = usePivotData(indexPatternTitle, pivotQuery, aggList, groupByList); return ( - ); }; diff --git a/x-pack/plugins/transform/public/shared_imports.ts b/x-pack/plugins/transform/public/shared_imports.ts index 494b6db6aafe0..bcd8e53e3d191 100644 --- a/x-pack/plugins/transform/public/shared_imports.ts +++ b/x-pack/plugins/transform/public/shared_imports.ts @@ -17,3 +17,18 @@ export { } from '../../../../src/plugins/es_ui_shared/public/request/np_ready_request'; export { getErrorMessage } from '../../ml/common/util/errors'; + +export { + getDataGridSchemaFromKibanaFieldType, + getFieldsFromKibanaIndexPattern, + multiColumnSortFactory, + useDataGrid, + useRenderCellValue, + DataGrid, + EsSorting, + RenderCellValue, + SearchResponse7, + UseDataGridReturnType, + UseIndexDataReturnType, +} from '../../ml/public/application/components/data_grid'; +export { INDEX_STATUS } from '../../ml/public/application/data_frame_analytics/common'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ba79ab6b9aa83..591de8ace1869 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -138,17 +138,6 @@ "charts.controls.rangeErrorMessage": "値は {min} と {max} の間でなければなりません", "charts.controls.vislibBasicOptions.legendPositionLabel": "凡例位置", "charts.controls.vislibBasicOptions.showTooltipLabel": "ツールヒントを表示", - "common.ui.errorAutoCreateIndex.breadcrumbs.errorText": "エラー", - "common.ui.errorAutoCreateIndex.errorDescription": "Elasticsearch クラスターの {autoCreateIndexActionConfig} 設定が原因で、Kibana が保存されたオブジェクトを格納するインデックスを自動的に作成できないようです。Kibana は、保存されたオブジェクトインデックスが適切なマッピング/スキーマを使用し Kibana から Elasticsearch へのポーリングの回数を減らすための最適な手段であるため、この Elasticsearch の機能を使用します。", - "common.ui.errorAutoCreateIndex.errorDisclaimer": "申し訳ございませんが、この問題が解決されるまで Kibana で何も保存することができません。", - "common.ui.errorAutoCreateIndex.errorTitle": "おっと!", - "common.ui.errorAutoCreateIndex.howToFixError.goBackText": "ブラウザの戻るボタンで前の画面に戻ります。", - "common.ui.errorAutoCreateIndex.howToFixError.removeConfigText": "Elasticsearch 構成ファイルから {autoCreateIndexActionConfig} を削除します。", - "common.ui.errorAutoCreateIndex.howToFixError.restartText": "Elasticsearch を再起動します。", - "common.ui.errorAutoCreateIndex.howToFixErrorTitle": "どうすれば良いのでしょう?", - "common.ui.errorAutoCreateIndex.noteImageAriaLabel": "情報", - "common.ui.errorAutoCreateIndex.noteMessage": "{autoCreateIndexActionConfig} は、機能を有効にするパターンのホワイトリストを定義することもできます。Kibana と同じ理由でこの機能を使用する他のプラグイン/操作をすべて把握する必要があるため、この設定のこのような使い方はここでは説明しません。", - "common.ui.errorAutoCreateIndex.noteTitle": "注:", "common.ui.errorUrlOverflow.breadcrumbs.errorText": "エラー", "common.ui.errorUrlOverflow.errorDescription": "とても長い URL ですね。残念なお知らせがあります。ご使用のブラウザは Kibana の超巨大 URL に対応していません。問題を避けるため、Kibana はご使用のブラウザでの URL を {urlCharacterLimit} 文字に制限します。", "common.ui.errorUrlOverflow.errorTitle": "おっと!", @@ -671,6 +660,7 @@ "data.functions.esaggs.inspector.dataRequest.description": "このリクエストは Elasticsearch にクエリし、ビジュアライゼーション用のデータを取得します。", "data.functions.esaggs.inspector.dataRequest.title": "データ", "data.indexPatterns.fetchFieldErrorTitle": "インデックスパターンのフィールド取得中にエラーが発生 {title} (ID: {id})", + "data.indexPatterns.ensureDefaultIndexPattern.bannerLabel": "Kibanaでデータの可視化と閲覧を行うには、Elasticsearchからデータを取得するためのインデックスパターンの作成が必要です。", "data.indexPatterns.unableWriteLabel": "インデックスパターンを書き込めません!このインデックスパターンへの最新の変更を取得するには、ページを更新してください。", "data.indexPatterns.unknownFieldErrorMessage": "インデックスパターン「{title}」のフィールド「{name}」が不明なフィールドタイプを使用しています。", "data.indexPatterns.unknownFieldHeader": "不明なフィールドタイプ {type}", @@ -2186,10 +2176,8 @@ "kbn.management.createIndexPatternHeader": "{indexPatternName} の作成", "kbn.management.createIndexPatternLabel": "Kibana は、可視化などを目的に Elasticsearch インデックスからデータを取得するために、インデックスパターンを使用します。", "kbn.management.editIndexPattern.deleteButton": "削除", - "kbn.management.editIndexPattern.deleteFieldButton": "削除", "kbn.management.editIndexPattern.deleteHeader": "インデックスパターンを削除しますか?", "kbn.management.editIndexPattern.detailsAria": "インデックスパターンの詳細", - "kbn.management.editIndexPattern.editFieldButton": "編集", "kbn.management.editIndexPattern.fields.allLangsDropDown": "すべての言語", "kbn.management.editIndexPattern.fields.allTypesDropDown": "すべてのフィールドタイプ", "kbn.management.editIndexPattern.fields.filterAria": "フィルター", @@ -2215,7 +2203,6 @@ "kbn.management.editIndexPattern.fields.table.typeHeader": "タイプ", "kbn.management.editIndexPattern.mappingConflictHeader": "マッピングの矛盾", "kbn.management.editIndexPattern.mappingConflictLabel": "{conflictFieldsLength, plural, one {フィールドが} other {# フィールドが}}このパターンと一致するインデックスの間で異なるタイプ (文字列、整数など) に定義されています。これらの矛盾したフィールドは Kibana の一部で使用できますが、Kibana がタイプを把握しなければならない機能には使用できません。この問題を修正するにはデータのレンダリングが必要です。", - "kbn.management.editIndexPattern.notDateErrorMessage": "このフィールドは日付ではなく {fieldType} です。", "kbn.management.editIndexPattern.refreshAria": "フィールドリストを再度読み込みます", "kbn.management.editIndexPattern.refreshButton": "更新", "kbn.management.editIndexPattern.refreshHeader": "フィールドリストを更新しますか?", @@ -2431,7 +2418,6 @@ "kibana_legacy.paginate.size.allDropDownOptionLabel": "すべて", "kibana_utils.defaultFeedbackMessage": "フィードバックがありますか?{link} で問題を報告してください。", "kibana_utils.history.savedObjectIsMissingNotificationMessage": "保存されたオブジェクトがありません", - "kibana_utils.indexPattern.bannerLabel": "Kibanaでデータの可視化と閲覧を行うには、Elasticsearchからデータを取得するためのインデックスパターンの作成が必要です。", "kibana_utils.stateManagement.stateHash.unableToRestoreUrlErrorMessage": "URL を完全に復元できません。共有機能を使用していることを確認してください。", "kibana_utils.stateManagement.stateHash.unableToStoreHistoryInSessionErrorMessage": "セッションがいっぱいで安全に削除できるアイテムが見つからないため、Kibana は履歴アイテムを保存できません。\n\nこれは大抵新規タブに移動することで解決されますが、より大きな問題が原因である可能性もあります。このメッセージが定期的に表示される場合は、{gitHubIssuesUrl} で問題を報告してください。", "kibana-react.dualRangeControl.mustSetBothErrorMessage": "下と上の値の両方を設定する必要があります", @@ -5021,8 +5007,6 @@ "xpack.canvas.elements.bubbleChartHelpText": "カスタマイズ可能なバブルチャートです", "xpack.canvas.elements.debugDisplayName": "デバッグ", "xpack.canvas.elements.debugHelpText": "エレメントの構成をダンプします", - "xpack.canvas.elements.donutChartDisplayName": "ドーナッツチャート", - "xpack.canvas.elements.donutChartHelpText": "カスタマイズ可能なドーナッツチャートです", "xpack.canvas.elements.dropdownFilterDisplayName": "ドロップダウンフィルター", "xpack.canvas.elements.dropdownFilterHelpText": "「exactly」フィルターの値を選択できるドロップダウンです", "xpack.canvas.elements.horizontalBarChartDisplayName": "水平棒グラフ", @@ -5057,8 +5041,6 @@ "xpack.canvas.elements.shapeHelpText": "カスタマイズ可能な図形です", "xpack.canvas.elements.tableDisplayName": "データテーブル", "xpack.canvas.elements.tableHelpText": "データをチューブ形式で表示する、スクロール可能なグリッドです", - "xpack.canvas.elements.tiltedPieDisplayName": "傾き円グラフ", - "xpack.canvas.elements.tiltedPieHelpText": "カスタマイズ可能な傾き円グラフです", "xpack.canvas.elements.timeFilterDisplayName": "時間フィルター", "xpack.canvas.elements.timeFilterHelpText": "期間を設定します", "xpack.canvas.elements.verticalBarChartDisplayName": "垂直棒グラフ", @@ -5069,16 +5051,16 @@ "xpack.canvas.elements.verticalProgressPillHelpText": "進捗状況を垂直のピルで表示します", "xpack.canvas.elementSettings.dataTabLabel": "データ", "xpack.canvas.elementSettings.displayTabLabel": "表示", - "xpack.canvas.elementTypes.addNewElementDescription": "ワークパッドのエレメントをグループ化して保存し、新規エレメントを作成します", - "xpack.canvas.elementTypes.addNewElementTitle": "新規エレメントの作成", - "xpack.canvas.elementTypes.cancelButtonLabel": "キャンセル", - "xpack.canvas.elementTypes.deleteButtonLabel": "削除", - "xpack.canvas.elementTypes.deleteElementDescription": "このエレメントを削除してよろしいですか?", - "xpack.canvas.elementTypes.deleteElementTitle": "エレメント「{elementName}」を削除しますか?", - "xpack.canvas.elementTypes.editElementTitle": "エレメントを編集", - "xpack.canvas.elementTypes.elementsTitle": "エレメント", - "xpack.canvas.elementTypes.findElementPlaceholder": "エレメントを検索", - "xpack.canvas.elementTypes.myElementsTitle": "マイエレメント", + "xpack.canvas.savedElementsModal.addNewElementDescription": "ワークパッドのエレメントをグループ化して保存し、新規エレメントを作成します", + "xpack.canvas.savedElementsModal.addNewElementTitle": "新規エレメントの作成", + "xpack.canvas.savedElementsModal.cancelButtonLabel": "キャンセル", + "xpack.canvas.savedElementsModal.deleteButtonLabel": "削除", + "xpack.canvas.savedElementsModal.deleteElementDescription": "このエレメントを削除してよろしいですか?", + "xpack.canvas.savedElementsModal.deleteElementTitle": "エレメント「{elementName}」を削除しますか?", + "xpack.canvas.savedElementsModal.editElementTitle": "エレメントを編集", + "xpack.canvas.savedElementsModal.elementsTitle": "エレメント", + "xpack.canvas.savedElementsModal.findElementPlaceholder": "エレメントを検索", + "xpack.canvas.savedElementsModal.myElementsTitle": "マイエレメント", "xpack.canvas.embedObject.noMatchingObjectsMessage": "一致するオブジェクトが見つかりませんでした。", "xpack.canvas.embedObject.titleText": "オブジェクトの埋め込み", "xpack.canvas.error.actionsElements.invaludArgIndexErrorMessage": "無効な引数インデックス: {index}", @@ -5591,13 +5573,8 @@ "xpack.canvas.sidebarHeader.topAlignMenuItemLabel": "一番上", "xpack.canvas.sidebarHeader.ungroupMenuItemLabel": "グループ解除", "xpack.canvas.sidebarHeader.verticalDistributionMenutItemLabel": "縦", - "xpack.canvas.tags.chartTag": "チャート", - "xpack.canvas.tags.filterTag": "フィルター", - "xpack.canvas.tags.graphicTag": "グラフィック", "xpack.canvas.tags.presentationTag": "プレゼンテーション", - "xpack.canvas.tags.proportionTag": "比率", "xpack.canvas.tags.reportTag": "レポート", - "xpack.canvas.tags.textTag": "テキスト", "xpack.canvas.templates.darkHelp": "ダークカラーテーマのプレゼンテーションデッキです", "xpack.canvas.templates.darkName": "ダーク", "xpack.canvas.templates.lightHelp": "ライトカラーテーマのプレゼンテーションデッキです", @@ -5897,7 +5874,6 @@ "xpack.canvas.workpadHeader.cycleIntervalHoursText": "{hours} {hours, plural, one {時間} other {時間}}ごと", "xpack.canvas.workpadHeader.cycleIntervalMinutesText": "{minutes} {minutes, plural, one {分} other {分}}ごと", "xpack.canvas.workpadHeader.cycleIntervalSecondsText": "{seconds} {seconds, plural, one {秒} other {秒}}ごと", - "xpack.canvas.workpadHeader.embedObjectButtonLabel": "オブジェクトを埋め込む", "xpack.canvas.workpadHeader.fullscreenButtonAriaLabel": "全画面表示", "xpack.canvas.workpadHeader.fullscreenTooltip": "全画面モードを開始します", "xpack.canvas.workpadHeader.hideEditControlTooltip": "編集コントロールを非表示にします", @@ -5907,7 +5883,6 @@ "xpack.canvas.workpadHeaderAutoRefreshControls.intervalFormLabel": "自動更新間隔を変更します", "xpack.canvas.workpadHeaderAutoRefreshControls.refreshListDurationManualText": "手動で", "xpack.canvas.workpadHeaderAutoRefreshControls.refreshListTitle": "エレメントを更新", - "xpack.canvas.workpadHeaderControlSettings.settingsTooltip": "設定をコントロールします", "xpack.canvas.workpadHeaderCustomInterval.confirmButtonLabel": "設定", "xpack.canvas.workpadHeaderCustomInterval.formDescription": "{secondsExample}、{minutesExample}、{hoursExample} のような短い表記を使用します", "xpack.canvas.workpadHeaderCustomInterval.formLabel": "カスタム間隔を設定", @@ -5916,32 +5891,32 @@ "xpack.canvas.workpadHeaderKioskControl.cycleToggleSwitch": "スライドを自動的にサイクル", "xpack.canvas.workpadHeaderRefreshControlSettings.refreshAriaLabel": "エレメントを更新", "xpack.canvas.workpadHeaderRefreshControlSettings.refreshTooltip": "データを更新", - "xpack.canvas.workpadHeaderWorkpadExport.copyPDFMessage": "{PDF} 生成 {URL} がクリップボードにコピーされました。", - "xpack.canvas.workpadHeaderWorkpadExport.copyReportingConfigMessage": "レポート構成がクリップボードにコピーされました", - "xpack.canvas.workpadHeaderWorkpadExport.copyShareConfigMessage": "共有マークアップがクリップボードにコピーされました", - "xpack.canvas.workpadHeaderWorkpadExport.exportPDFErrorMessage": "「{workpadName}」の {PDF} の作成に失敗しました", - "xpack.canvas.workpadHeaderWorkpadExport.exportPDFMessage": "{PDF} をエクスポート中です。管理で進捗を確認できます。", - "xpack.canvas.workpadHeaderWorkpadExport.exportPDFTitle": "ワークパッド「{workpadName}」の {PDF} エクスポート", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyAriaLabel": "この {URL} を使用してスクリプトから、または Watcher で {PDF} を生成することもできます。{URL} をクリップボードにコピーするにはエンターキーを押してください。", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyButtonLabel": "{POST} {URL} をコピー", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyDescription": "{POST} {URL} をコピーして {KIBANA} 外または ウォッチャー から生成することもできます。", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelGenerateButtonLabel": "{PDF} を生成", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelGenerateDescription": "ワークパッドのサイズによって、{PDF} の生成には数分かかる場合があります。", - "xpack.canvas.workpadHeaderWorkpadExport.shareDownloadJSONTitle": "{JSON} をダウンロード", - "xpack.canvas.workpadHeaderWorkpadExport.shareDownloadPDFTitle": "{PDF} レポート", - "xpack.canvas.workpadHeaderWorkpadExport.shareWebsiteErrorTitle": "「{workpadName}」の {ZIP} ファイルの作成に失敗しました。ワークパッドが大きすぎる可能性があります。ファイルを別々にダウンロードする必要があります。", - "xpack.canvas.workpadHeaderWorkpadExport.shareWebsiteTitle": "Web サイトで共有", - "xpack.canvas.workpadHeaderWorkpadExport.shareWorkpadMessage": "このワークパッドを共有", - "xpack.canvas.workpadHeaderWorkpadExport.unknownExportErrorMessage": "未知のエクスポートタイプ: {type}", - "xpack.canvas.workpadHeaderWorkpadExport.unsupportedRendererWarning": "このワークパッドには {CANVAS} シェアラブルワークパッドランタイムがサポートしていないレンダリング関数が含まれています。これらのエレメントはレンダリングされません:", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomControlsAriaLabel": "ズームコントロール", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomControlsTooltip": "ズームコントロール", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomFitToWindowText": "ウィンドウに合わせる", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomInText": "ズームイン", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomOutText": "ズームアウト", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomPanelTitle": "ズーム:", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomPrecentageValue": "リセット", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomResetText": "{scalePercentage}%", + "xpack.canvas.workpadHeaderShareMenu.copyPDFMessage": "{PDF} 生成 {URL} がクリップボードにコピーされました。", + "xpack.canvas.workpadHeaderShareMenu.copyReportingConfigMessage": "レポート構成がクリップボードにコピーされました", + "xpack.canvas.workpadHeaderShareMenu.copyShareConfigMessage": "共有マークアップがクリップボードにコピーされました", + "xpack.canvas.workpadHeaderShareMenu.exportPDFErrorMessage": "「{workpadName}」の {PDF} の作成に失敗しました", + "xpack.canvas.workpadHeaderShareMenu.exportPDFMessage": "{PDF} をエクスポート中です。管理で進捗を確認できます。", + "xpack.canvas.workpadHeaderShareMenu.exportPDFTitle": "ワークパッド「{workpadName}」の {PDF} エクスポート", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyAriaLabel": "この {URL} を使用してスクリプトから、または Watcher で {PDF} を生成することもできます。{URL} をクリップボードにコピーするにはエンターキーを押してください。", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyButtonLabel": "{POST} {URL} をコピー", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyDescription": "{POST} {URL} をコピーして {KIBANA} 外または ウォッチャー から生成することもできます。", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelGenerateButtonLabel": "{PDF} を生成", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelGenerateDescription": "ワークパッドのサイズによって、{PDF} の生成には数分かかる場合があります。", + "xpack.canvas.workpadHeaderShareMenu.shareDownloadJSONTitle": "{JSON} をダウンロード", + "xpack.canvas.workpadHeaderShareMenu.shareDownloadPDFTitle": "{PDF} レポート", + "xpack.canvas.workpadHeaderShareMenu.shareWebsiteErrorTitle": "「{workpadName}」の {ZIP} ファイルの作成に失敗しました。ワークパッドが大きすぎる可能性があります。ファイルを別々にダウンロードする必要があります。", + "xpack.canvas.workpadHeaderShareMenu.shareWebsiteTitle": "Web サイトで共有", + "xpack.canvas.workpadHeaderShareMenu.shareWorkpadMessage": "このワークパッドを共有", + "xpack.canvas.workpadHeaderShareMenu.unknownExportErrorMessage": "未知のエクスポートタイプ: {type}", + "xpack.canvas.workpadHeaderShareMenu.unsupportedRendererWarning": "このワークパッドには {CANVAS} シェアラブルワークパッドランタイムがサポートしていないレンダリング関数が含まれています。これらのエレメントはレンダリングされません:", + "xpack.canvas.workpadHeaderViewMenu.zoomControlsAriaLabel": "ズームコントロール", + "xpack.canvas.workpadHeaderViewMenu.zoomControlsTooltip": "ズームコントロール", + "xpack.canvas.workpadHeaderViewMenu.zoomFitToWindowText": "ウィンドウに合わせる", + "xpack.canvas.workpadHeaderViewMenu.zoomInText": "ズームイン", + "xpack.canvas.workpadHeaderViewMenu.zoomOutText": "ズームアウト", + "xpack.canvas.workpadHeaderViewMenu.zoomPanelTitle": "ズーム:", + "xpack.canvas.workpadHeaderViewMenu.zoomPrecentageValue": "リセット", + "xpack.canvas.workpadHeaderViewMenu.zoomResetText": "{scalePercentage}%", "xpack.canvas.workpadLoader.clonedWorkpadName": "{workpadName} のコピー", "xpack.canvas.workpadLoader.cloneTooltip": "ワークパッドのクローンを作成します", "xpack.canvas.workpadLoader.createWorkpadLoadingDescription": "ワークパッドを作成中...", @@ -8176,7 +8151,6 @@ "xpack.infra.viewSwitcher.mapViewLabel": "マップビュー", "xpack.infra.viewSwitcher.tableViewLabel": "表ビュー", "xpack.infra.waffle.accountAllTitle": "すべて", - "xpack.infra.waffle.accountLabel": "アカウント: {selectedAccount}", "xpack.infra.waffle.aggregationNames.avg": "{field} の平均", "xpack.infra.waffle.aggregationNames.max": "{field} の最大値", "xpack.infra.waffle.aggregationNames.min": "{field} の最小値", @@ -8212,11 +8186,8 @@ "xpack.infra.waffle.customMetrics.modeSwitcher.saveButtonAriaLabel": "カスタムメトリックの変更を保存", "xpack.infra.waffle.customMetrics.submitLabel": "保存", "xpack.infra.waffle.groupByAllTitle": "すべて", - "xpack.infra.waffle.groupByButtonLabel": "グループ分けの条件: ", - "xpack.infra.waffle.inventoryButtonLabel": "ビュー: {selectedText}", "xpack.infra.waffle.loadingDataText": "データを読み込み中", "xpack.infra.waffle.maxGroupByTooltip": "一度に選択できるグループは 2 つのみです", - "xpack.infra.waffle.metricButtonLabel": "メトリック: {selectedMetric}", "xpack.infra.waffle.metricOptions.countText": "カウント", "xpack.infra.waffle.metricOptions.cpuUsageText": "CPU 使用状況", "xpack.infra.waffle.metricOptions.diskIOReadBytes": "ディスク読み取り", @@ -8243,7 +8214,6 @@ "xpack.infra.waffle.noDataDescription": "期間またはフィルターを調整してみてください。", "xpack.infra.waffle.noDataTitle": "表示するデータがありません。", "xpack.infra.waffle.region": "すべて", - "xpack.infra.waffle.regionLabel": "地域: {selectedRegion}", "xpack.infra.waffle.savedView.createHeader": "ビューを保存", "xpack.infra.waffle.savedViews.cancel": "キャンセル", "xpack.infra.waffle.savedViews.cancelButton": "キャンセル", @@ -8849,10 +8819,8 @@ "xpack.logstash.workersTooltip": "パイプラインのフィルターとアウトプットステージを同時に実行するワーカーの数です。イベントが詰まってしまう場合や CPU が飽和状態ではない場合は、マシンの処理能力をより有効に活用するため、この数字を上げてみてください。\n\nデフォルト値:ホストの CPU コア数です", "xpack.maps.addLayerPanel.addLayer": "レイヤーを追加", "xpack.maps.addLayerPanel.changeDataSourceButtonLabel": "データソースを変更", - "xpack.maps.addLayerPanel.chooseDataSourceTitle": "データソースの選択", "xpack.maps.addLayerPanel.footer.cancelButtonLabel": "キャンセル", "xpack.maps.addLayerPanel.importFile": "ファイルのインポート", - "xpack.maps.addLayerPanel.selectSource": "ソースを選択", "xpack.maps.aggs.defaultCountLabel": "カウント", "xpack.maps.appDescription": "マップアプリケーション", "xpack.maps.appTitle": "マップ", @@ -9459,14 +9427,8 @@ "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixLabel": "分類混同行列", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixPredictedLabel": "予測されたラベル", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTooltip": "マルチクラス混同行列には、分析が実際のクラスで正しくデータポイントを分類した発生数と、別のクラスで誤分類した発生数が含まれます。", - "xpack.ml.dataframe.analytics.classificationExploration.documentsShownHelpText": "予測があるドキュメントを示す", "xpack.ml.dataframe.analytics.classificationExploration.evaluateJobIdTitle": "分類ジョブID {jobId}の評価", - "xpack.ml.dataframe.analytics.classificationExploration.firstDocumentsShownHelpText": "予測がある最初の{searchSize}のドキュメントを示す", "xpack.ml.dataframe.analytics.classificationExploration.generalizationDocsCount": "{docsCount, plural, one {# doc} other {# docs}}が評価されました", - "xpack.ml.dataframe.analytics.classificationExploration.jobCapsFetchError": "結果を取得できません。インデックスのフィールドデータの読み込み中にエラーが発生しました。", - "xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationFetchError": "結果を取得できません。ジョブ構成データの読み込み中にエラーが発生しました。", - "xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationNoResultsMessage": "結果が見つかりませんでした。", - "xpack.ml.dataframe.analytics.classificationExploration.regressionDocsLink": "回帰評価ドキュメント ", "xpack.ml.dataframe.analytics.classificationExploration.showActions": "アクションを表示", "xpack.ml.dataframe.analytics.classificationExploration.showAllColumns": "すべての列を表示", "xpack.ml.dataframe.analytics.classificationExploration.tableJobIdTitle": "分類ジョブID {jobId}のデスティネーションインデックス", @@ -9541,31 +9503,17 @@ "xpack.ml.dataframe.analytics.create.startDataFrameAnalyticsSuccessMessage": "データフレーム分析 {jobId} の開始リクエストが受け付けられました。", "xpack.ml.dataframe.analytics.create.trainingPercentLabel": "トレーニングパーセンテージ", "xpack.ml.dataframe.analytics.exploration.colorRangeLegendTitle": "機能影響スコア", - "xpack.ml.dataframe.analytics.exploration.dataGridAriaLabel": "外れ値検出結果表", "xpack.ml.dataframe.analytics.exploration.experimentalBadgeLabel": "実験的", "xpack.ml.dataframe.analytics.exploration.experimentalBadgeTooltipContent": "データフレーム分析は実験段階の機能です。フィードバックをお待ちしています。", "xpack.ml.dataframe.analytics.exploration.indexError": "インデックスデータの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.exploration.jobIdTitle": "外れ値検出ジョブID {jobId}", - "xpack.ml.dataframe.analytics.exploration.noDataCalloutBody": "インデックスのクエリが結果を返しませんでした。インデックスにドキュメントが含まれていて、クエリ要件が妥当であることを確認してください。", "xpack.ml.dataframe.analytics.exploration.title": "分析の探索", - "xpack.ml.dataframe.analytics.regressionExploration.documentsShownHelpText": "予測があるドキュメントを示す", - "xpack.ml.dataframe.analytics.regressionExploration.evaluateError": "データの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.regressionExploration.evaluateJobIdTitle": "回帰ジョブID {jobId}の評価", - "xpack.ml.dataframe.analytics.regressionExploration.fieldSelection": "{docFieldsCount, number} 件中 showing {selectedFieldsLength, number} 件の{docFieldsCount, plural, one {フィールド} other {フィールド}}", - "xpack.ml.dataframe.analytics.regressionExploration.firstDocumentsShownHelpText": "予測がある最初の{searchSize}のドキュメントを示す", - "xpack.ml.dataframe.analytics.regressionExploration.generalError": "データの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.regressionExploration.generalizationDocsCount": "{docsCount, plural, one {# doc} other {# docs}}が評価されました", "xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle": "一般化エラー", "xpack.ml.dataframe.analytics.regressionExploration.indexError": "インデックスデータの読み込み中にエラーが発生しました。", - "xpack.ml.dataframe.analytics.regressionExploration.jobCapsFetchError": "結果を取得できません。インデックスのフィールドデータの読み込み中にエラーが発生しました。", - "xpack.ml.dataframe.analytics.regressionExploration.jobConfigurationFetchError": "結果を取得できません。ジョブ構成データの読み込み中にエラーが発生しました。", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText": "平均二乗エラー", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorTooltipContent": "回帰分析モデルの実行の効果を測定します。真値と予測値の間の差異の二乗平均合計。", - "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutBody": "インデックスのクエリが結果を返しませんでした。ジョブが完了済みで、インデックスにドキュメントがあることを確認してください。", - "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutTitle": "空のインデックスクエリ結果。", - "xpack.ml.dataframe.analytics.regressionExploration.noIndexCalloutBody": "インデックスのクエリが結果を返しませんでした。デスティネーションインデックスが存在し、ドキュメントがあることを確認してください。", - "xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorBody": "クエリ構文が無効であり、結果を返しませんでした。クエリ構文を確認し、再試行してください。", - "xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorMessage": "クエリをパースできません。", "xpack.ml.dataframe.analytics.regressionExploration.rSquaredText": "R の二乗", "xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent": "適合度を表します。モデルによる観察された結果の複製の効果を測定します。", "xpack.ml.dataframe.analytics.regressionExploration.tableJobIdTitle": "回帰ジョブID {jobId}のデスティネーションインデックス", @@ -10910,7 +10858,6 @@ "xpack.monitoring.alerts.licenseExpiration.actionGroups.default": "デフォルト", "xpack.monitoring.alerts.licenseExpiration.newSubject": "NEW X-Pack 監視:ライセンス期限", "xpack.monitoring.alerts.licenseExpiration.resolvedSubject": "RESOLVED X-Pack 監視:ライセンス期限", - "xpack.monitoring.alerts.licenseExpiration.ui.firingMessage": "このクラスターのライセンスは、#relative で #absolute に期限が切れます。", "xpack.monitoring.alerts.licenseExpiration.ui.resolvedMessage": "このクラスターのライセンスはアクティブです。", "xpack.monitoring.alerts.lowSeverityName": "低", "xpack.monitoring.alerts.mediumSeverityName": "中", @@ -12406,7 +12353,6 @@ "xpack.reporting.shareContextMenu.csvReportsButtonLabel": "CSV レポート", "xpack.reporting.shareContextMenu.pdfReportsButtonLabel": "PDF レポート", "xpack.reporting.shareContextMenu.pngReportsButtonLabel": "PNG レポート", - "xpack.rollupJobs.appName": "ロールアップジョブ", "xpack.rollupJobs.appTitle": "ロールアップジョブ", "xpack.rollupJobs.breadcrumbsTitle": "ロールアップジョブ", "xpack.rollupJobs.create.backButton.label": "戻る", @@ -15584,19 +15530,11 @@ "xpack.transform.newTransform.searchSelection.savedObjectType.indexPattern": "インデックスパターン", "xpack.transform.newTransform.searchSelection.savedObjectType.search": "保存検索", "xpack.transform.pivotPreview.copyClipboardTooltip": "ピボットプレビューの開発コンソールステートメントをクリップボードにコピーします。", - "xpack.transform.pivotPreview.PivotPreviewError": "ピボットプレビューの読み込み中にエラーが発生しました。", "xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody": "group-by フィールドと集約を 1 つ以上選んでください。", "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody": "プレビューリクエストはデータを返しませんでした。オプションのクエリがデータを返し、グループ分け基準により使用されるフィールドと集約フィールドに値が存在することを確認してください。", - "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutTitle": "ピボットプレビューを利用できません", "xpack.transform.pivotPreview.PivotPreviewTitle": "ピボットプレビューを変換", "xpack.transform.progress": "進捗", "xpack.transform.sourceIndex": "ソースインデックス", - "xpack.transform.sourceIndexPreview.copyClipboardTooltip": "ソースインデックスプレビューの開発コンソールステートメントをクリップボードにコピーします。", - "xpack.transform.sourceIndexPreview.invalidSortingColumnError": "列「{columnId}」は並べ替えに使用できません。", - "xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutBody": "ソースインデックスのクエリが結果を返しませんでした。インデックスにドキュメントが含まれていて、クエリ要件が妥当であることを確認してください。", - "xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutTitle": "ソースインデックスクエリの結果がありません", - "xpack.transform.sourceIndexPreview.sourceIndexPatternError": "ソースインデックスデータの読み込み中にエラーが発生しました。", - "xpack.transform.sourceIndexPreview.sourceIndexPatternTitle": "ソースインデックス {indexPatternTitle}", "xpack.transform.statsBar.batchTransformsLabel": "一斉", "xpack.transform.statsBar.continuousTransformsLabel": "連続", "xpack.transform.statsBar.failedTransformsLabel": "失敗", @@ -15685,7 +15623,6 @@ "xpack.transform.stepDetailsForm.errorGettingIndexPatternTitles": "既存のインデックスパターンのタイトルの取得中にエラーが発生しました:", "xpack.transform.stepDetailsForm.errorGettingTransformList": "既存の変換 ID の取得中にエラーが発生しました:", "xpack.transform.stepDetailsForm.indexPatternTitleError": "このタイトルのインデックスパターンが既に存在します。", - "xpack.transform.stepDetailsForm.transformDescriptionHelpText": "オプションの説明テキストです。", "xpack.transform.stepDetailsForm.transformDescriptionInputAriaLabel": "オプションの変換の説明を選択してください。", "xpack.transform.stepDetailsForm.transformDescriptionLabel": "変換の説明", "xpack.transform.stepDetailsForm.transformIdExistsError": "この ID の変換が既に存在します。", @@ -15993,7 +15930,6 @@ "xpack.triggersActionsUI.sections.alertForm.loadingActionTypesDescription": "アクションタイプを読み込み中...", "xpack.triggersActionsUI.sections.alertForm.renotifyFieldLabel": "通知間隔", "xpack.triggersActionsUI.sections.alertForm.renotifyWithTooltip": "アラートがアクティブな間にアクションを繰り返す頻度を定義します。", - "xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeEditTitle": "{actionConnectorName}", "xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeTitle": "アクション:アクションタイプを選択してください", "xpack.triggersActionsUI.sections.alertForm.selectAlertTypeTitle": "トリガータイプを選択してください", "xpack.triggersActionsUI.sections.alertForm.selectedAlertTypeTitle": "{alertType}", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a8d785f47144c..c46f395a8c64e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -138,17 +138,6 @@ "charts.controls.rangeErrorMessage": "值必须是在 {min} 到 {max} 的范围内", "charts.controls.vislibBasicOptions.legendPositionLabel": "图例位置", "charts.controls.vislibBasicOptions.showTooltipLabel": "显示工具提示", - "common.ui.errorAutoCreateIndex.breadcrumbs.errorText": "错误", - "common.ui.errorAutoCreateIndex.errorDescription": "似乎 Elasticsearch 集群的 {autoCreateIndexActionConfig} 设置使 Kibana 无法自动创建用于存储已保存对象的索引。Kibana 将使用此 Elasticsearch 功能,因为这是确保已保存对象索引使用正确映射/架构的最好方式,而且其允许 Kibana 较少地轮询 Elasticsearch。", - "common.ui.errorAutoCreateIndex.errorDisclaimer": "但是,只有解决了此问题后,您才能在 Kibana 保存内容。", - "common.ui.errorAutoCreateIndex.errorTitle": "糟糕!", - "common.ui.errorAutoCreateIndex.howToFixError.goBackText": "使用浏览器的后退按钮返回您之前正做的工作。", - "common.ui.errorAutoCreateIndex.howToFixError.removeConfigText": "从 Elasticsearch 配置文件中删除 {autoCreateIndexActionConfig}", - "common.ui.errorAutoCreateIndex.howToFixError.restartText": "重新启动 Elasticsearch。", - "common.ui.errorAutoCreateIndex.howToFixErrorTitle": "那么,我如何解决此问题?", - "common.ui.errorAutoCreateIndex.noteImageAriaLabel": "信息", - "common.ui.errorAutoCreateIndex.noteMessage": "{autoCreateIndexActionConfig} 还可以定义应启用此功能的模式白名单。我们在这里不讨论如何以那种方式使用该设置,因为这和 Kibana 一样需要您了解依赖该功能的所有其他插件/交互。", - "common.ui.errorAutoCreateIndex.noteTitle": "注意:", "common.ui.errorUrlOverflow.breadcrumbs.errorText": "错误", "common.ui.errorUrlOverflow.errorDescription": "您的 URL 真不小。我有一些不幸的消息:您的浏览器与 Kibana 的超长 URL 不太兼容。为了避免您遇到问题,Kibana 在您的浏览器中将 URL 长度限制在 {urlCharacterLimit} 个字符。", "common.ui.errorUrlOverflow.errorTitle": "喔哦!", @@ -671,6 +660,7 @@ "data.functions.esaggs.inspector.dataRequest.description": "此请求将查询 Elasticsearch 以获取用于可视化的数据。", "data.functions.esaggs.inspector.dataRequest.title": "数据", "data.indexPatterns.fetchFieldErrorTitle": "提取索引模式 {title} (ID: {id}) 的字段时出错", + "data.indexPatterns.ensureDefaultIndexPattern.bannerLabel": "若要在 Kibana 中可视化和浏览数据,您需要创建索引模式,以从 Elasticsearch 检索数据。", "data.indexPatterns.unableWriteLabel": "无法写入索引模式!请刷新页面以获取此索引模式的最新更改。", "data.indexPatterns.unknownFieldErrorMessage": "indexPattern “{title}” 中的字段 “{name}” 使用未知字段类型。", "data.indexPatterns.unknownFieldHeader": "未知字段类型 {type}", @@ -2187,10 +2177,8 @@ "kbn.management.createIndexPatternHeader": "创建 {indexPatternName}", "kbn.management.createIndexPatternLabel": "Kibana 使用索引模式从 Elasticsearch 索引中检索数据,以实现诸如可视化等功能。", "kbn.management.editIndexPattern.deleteButton": "删除", - "kbn.management.editIndexPattern.deleteFieldButton": "删除", "kbn.management.editIndexPattern.deleteHeader": "删除索引模式?", "kbn.management.editIndexPattern.detailsAria": "索引模式详细信息", - "kbn.management.editIndexPattern.editFieldButton": "编辑", "kbn.management.editIndexPattern.fields.allLangsDropDown": "所有语言", "kbn.management.editIndexPattern.fields.allTypesDropDown": "所有字段类型", "kbn.management.editIndexPattern.fields.filterAria": "筛选", @@ -2216,7 +2204,6 @@ "kbn.management.editIndexPattern.fields.table.typeHeader": "类型", "kbn.management.editIndexPattern.mappingConflictHeader": "映射冲突", "kbn.management.editIndexPattern.mappingConflictLabel": "匹配此模式的各个索引中{conflictFieldsLength, plural, one {一个字段已} other {# 个字段已}}定义为若干类型(字符串、整数等)。您仍能够在 Kibana 的各个部分中使用这些冲突类型,但它们将无法用于需要 Kibana 知道其类型的函数。要解决此问题,需要重新索引您的数据。", - "kbn.management.editIndexPattern.notDateErrorMessage": "该字段是{fieldType},不是日期。", "kbn.management.editIndexPattern.refreshAria": "重新加载字段列表", "kbn.management.editIndexPattern.refreshButton": "刷新", "kbn.management.editIndexPattern.refreshHeader": "刷新字段列表?", @@ -2432,7 +2419,6 @@ "kibana_legacy.paginate.size.allDropDownOptionLabel": "全部", "kibana_utils.defaultFeedbackMessage": "想反馈?请在 {link} 中创建问题。", "kibana_utils.history.savedObjectIsMissingNotificationMessage": "已保存对象缺失", - "kibana_utils.indexPattern.bannerLabel": "若要在 Kibana 中可视化和浏览数据,您需要创建索引模式,以从 Elasticsearch 检索数据。", "kibana_utils.stateManagement.stateHash.unableToRestoreUrlErrorMessage": "无法完全还原 URL,请确保使用共享功能。", "kibana_utils.stateManagement.stateHash.unableToStoreHistoryInSessionErrorMessage": "Kibana 无法将历史记录项存储在您的会话中,因为其已满,另外,似乎没有任何可安全删除的项目。\n\n通常,这可以通过移到全新的选项卡来解决,但这种情况可能是由更大的问题造成。如果您定期看到这个消息,请在 {gitHubIssuesUrl} 报告问题。", "kibana-react.dualRangeControl.mustSetBothErrorMessage": "下限值和上限值都须设置", @@ -5022,8 +5008,6 @@ "xpack.canvas.elements.bubbleChartHelpText": "可定制的气泡图", "xpack.canvas.elements.debugDisplayName": "“Debug”(故障排查)", "xpack.canvas.elements.debugHelpText": "只需丢弃元素的配置", - "xpack.canvas.elements.donutChartDisplayName": "圆环图", - "xpack.canvas.elements.donutChartHelpText": "可定制的圆环图", "xpack.canvas.elements.dropdownFilterDisplayName": "下拉列表筛选", "xpack.canvas.elements.dropdownFilterHelpText": "可以从其中为“完全”筛选选择值的下拉列表", "xpack.canvas.elements.horizontalBarChartDisplayName": "水平条形图", @@ -5058,8 +5042,6 @@ "xpack.canvas.elements.shapeHelpText": "可定制的形状", "xpack.canvas.elements.tableDisplayName": "数据表", "xpack.canvas.elements.tableHelpText": "用于以表格形式显示数据的可滚动网格", - "xpack.canvas.elements.tiltedPieDisplayName": "斜饼图", - "xpack.canvas.elements.tiltedPieHelpText": "可定制的斜饼图", "xpack.canvas.elements.timeFilterDisplayName": "时间筛选", "xpack.canvas.elements.timeFilterHelpText": "设置时间窗口", "xpack.canvas.elements.verticalBarChartDisplayName": "垂直条形图", @@ -5070,16 +5052,16 @@ "xpack.canvas.elements.verticalProgressPillHelpText": "将进度显示为垂直胶囊的一部分", "xpack.canvas.elementSettings.dataTabLabel": "数据", "xpack.canvas.elementSettings.displayTabLabel": "显示", - "xpack.canvas.elementTypes.addNewElementDescription": "分组并保存 Workpad 元素以创建新元素", - "xpack.canvas.elementTypes.addNewElementTitle": "添加新元素", - "xpack.canvas.elementTypes.cancelButtonLabel": "取消", - "xpack.canvas.elementTypes.deleteButtonLabel": "删除", - "xpack.canvas.elementTypes.deleteElementDescription": "确定要删除此元素?", - "xpack.canvas.elementTypes.deleteElementTitle": "删除元素“{elementName}”?", - "xpack.canvas.elementTypes.editElementTitle": "编辑元素", - "xpack.canvas.elementTypes.elementsTitle": "元素", - "xpack.canvas.elementTypes.findElementPlaceholder": "查找元素", - "xpack.canvas.elementTypes.myElementsTitle": "我的元素", + "xpack.canvas.savedElementsModal.addNewElementDescription": "分组并保存 Workpad 元素以创建新元素", + "xpack.canvas.savedElementsModal.addNewElementTitle": "添加新元素", + "xpack.canvas.savedElementsModal.cancelButtonLabel": "取消", + "xpack.canvas.savedElementsModal.deleteButtonLabel": "删除", + "xpack.canvas.savedElementsModal.deleteElementDescription": "确定要删除此元素?", + "xpack.canvas.savedElementsModal.deleteElementTitle": "删除元素“{elementName}”?", + "xpack.canvas.savedElementsModal.editElementTitle": "编辑元素", + "xpack.canvas.savedElementsModal.elementsTitle": "元素", + "xpack.canvas.savedElementsModal.findElementPlaceholder": "查找元素", + "xpack.canvas.savedElementsModal.myElementsTitle": "我的元素", "xpack.canvas.embedObject.noMatchingObjectsMessage": "未找到任何匹配对象。", "xpack.canvas.embedObject.titleText": "嵌入对象", "xpack.canvas.error.actionsElements.invaludArgIndexErrorMessage": "无效的参数索引:{index}", @@ -5592,13 +5574,8 @@ "xpack.canvas.sidebarHeader.topAlignMenuItemLabel": "上", "xpack.canvas.sidebarHeader.ungroupMenuItemLabel": "取消分组", "xpack.canvas.sidebarHeader.verticalDistributionMenutItemLabel": "垂直", - "xpack.canvas.tags.chartTag": "图表", - "xpack.canvas.tags.filterTag": "筛选", - "xpack.canvas.tags.graphicTag": "图形", "xpack.canvas.tags.presentationTag": "演示", - "xpack.canvas.tags.proportionTag": "比例", "xpack.canvas.tags.reportTag": "报告", - "xpack.canvas.tags.textTag": "文本", "xpack.canvas.templates.darkHelp": "深色主题的演示幻灯片", "xpack.canvas.templates.darkName": "深色", "xpack.canvas.templates.lightHelp": "浅色主题的演示幻灯片", @@ -5899,7 +5876,6 @@ "xpack.canvas.workpadHeader.cycleIntervalHoursText": "每 {hours} {hours, plural, one {小时} other {小时}}", "xpack.canvas.workpadHeader.cycleIntervalMinutesText": "每 {minutes} {minutes, plural, one {分钟} other {分钟}}", "xpack.canvas.workpadHeader.cycleIntervalSecondsText": "每 {seconds} {seconds, plural, one {秒} other {秒}}", - "xpack.canvas.workpadHeader.embedObjectButtonLabel": "嵌入对象", "xpack.canvas.workpadHeader.fullscreenButtonAriaLabel": "全屏查看", "xpack.canvas.workpadHeader.fullscreenTooltip": "进入全屏模式", "xpack.canvas.workpadHeader.hideEditControlTooltip": "隐藏编辑控件", @@ -5909,7 +5885,6 @@ "xpack.canvas.workpadHeaderAutoRefreshControls.intervalFormLabel": "更改自动刷新时间间隔", "xpack.canvas.workpadHeaderAutoRefreshControls.refreshListDurationManualText": "手动", "xpack.canvas.workpadHeaderAutoRefreshControls.refreshListTitle": "刷新元素", - "xpack.canvas.workpadHeaderControlSettings.settingsTooltip": "控制设置", "xpack.canvas.workpadHeaderCustomInterval.confirmButtonLabel": "设置", "xpack.canvas.workpadHeaderCustomInterval.formDescription": "使用速记表示法,如 {secondsExample}、{minutesExample} 或 {hoursExample}", "xpack.canvas.workpadHeaderCustomInterval.formLabel": "设置定制时间间隔", @@ -5918,32 +5893,32 @@ "xpack.canvas.workpadHeaderKioskControl.cycleToggleSwitch": "自动循环播放幻灯片", "xpack.canvas.workpadHeaderRefreshControlSettings.refreshAriaLabel": "刷新元素", "xpack.canvas.workpadHeaderRefreshControlSettings.refreshTooltip": "刷新数据", - "xpack.canvas.workpadHeaderWorkpadExport.copyPDFMessage": "{PDF} 生成 {URL} 已复制到剪贴板", - "xpack.canvas.workpadHeaderWorkpadExport.copyReportingConfigMessage": "已将报告配置复制到剪贴板", - "xpack.canvas.workpadHeaderWorkpadExport.copyShareConfigMessage": "已将共享标记复制到剪贴板", - "xpack.canvas.workpadHeaderWorkpadExport.exportPDFErrorMessage": "无法为“{workpadName}”创建 {PDF}", - "xpack.canvas.workpadHeaderWorkpadExport.exportPDFMessage": "正在导出 {PDF}。可以在“管理”中跟踪进度。", - "xpack.canvas.workpadHeaderWorkpadExport.exportPDFTitle": "Workpad“{workpadName}”的 {PDF} 导出", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyAriaLabel": "或者,也可以从脚本或使用 {URL} 通过 Watcher 生成 {PDF}。按 Enter 键可将 {URL} 复制到剪贴板。", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyButtonLabel": "复制 {POST} {URL}", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelCopyDescription": "或者,复制此 {POST} {URL} 以从 {KIBANA} 外部或从 Watcher 调用生成。", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelGenerateButtonLabel": "生成 {PDF}", - "xpack.canvas.workpadHeaderWorkpadExport.pdfPanelGenerateDescription": "{PDF} 可能会花费 1 或 2 分钟生成,取决于 Workpad 的大小。", - "xpack.canvas.workpadHeaderWorkpadExport.shareDownloadJSONTitle": "下载为 {JSON}", - "xpack.canvas.workpadHeaderWorkpadExport.shareDownloadPDFTitle": "{PDF} 报告", - "xpack.canvas.workpadHeaderWorkpadExport.shareWebsiteErrorTitle": "无法为“{workpadName}”创建 {ZIP} 文件。Workpad 可能过大。您将需要分别下载文件。", - "xpack.canvas.workpadHeaderWorkpadExport.shareWebsiteTitle": "在网站上共享", - "xpack.canvas.workpadHeaderWorkpadExport.shareWorkpadMessage": "共享此 Workpad", - "xpack.canvas.workpadHeaderWorkpadExport.unknownExportErrorMessage": "未知导出类型:{type}", - "xpack.canvas.workpadHeaderWorkpadExport.unsupportedRendererWarning": "此 Workpad 包含 {CANVAS} Shareable Workpad Runtime 不支持的呈现函数。将不会呈现以下元素:", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomControlsAriaLabel": "缩放控制", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomControlsTooltip": "缩放控制", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomFitToWindowText": "适应窗口大小", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomInText": "放大", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomOutText": "缩小", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomPanelTitle": "缩放", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomPrecentageValue": "重置", - "xpack.canvas.workpadHeaderWorkpadZoom.zoomResetText": "{scalePercentage}%", + "xpack.canvas.workpadHeaderShareMenu.copyPDFMessage": "{PDF} 生成 {URL} 已复制到剪贴板", + "xpack.canvas.workpadHeaderShareMenu.copyReportingConfigMessage": "已将报告配置复制到剪贴板", + "xpack.canvas.workpadHeaderShareMenu.copyShareConfigMessage": "已将共享标记复制到剪贴板", + "xpack.canvas.workpadHeaderShareMenu.exportPDFErrorMessage": "无法为“{workpadName}”创建 {PDF}", + "xpack.canvas.workpadHeaderShareMenu.exportPDFMessage": "正在导出 {PDF}。可以在“管理”中跟踪进度。", + "xpack.canvas.workpadHeaderShareMenu.exportPDFTitle": "Workpad“{workpadName}”的 {PDF} 导出", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyAriaLabel": "或者,也可以从脚本或使用 {URL} 通过 Watcher 生成 {PDF}。按 Enter 键可将 {URL} 复制到剪贴板。", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyButtonLabel": "复制 {POST} {URL}", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelCopyDescription": "或者,复制此 {POST} {URL} 以从 {KIBANA} 外部或从 Watcher 调用生成。", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelGenerateButtonLabel": "生成 {PDF}", + "xpack.canvas.workpadHeaderShareMenu.pdfPanelGenerateDescription": "{PDF} 可能会花费 1 或 2 分钟生成,取决于 Workpad 的大小。", + "xpack.canvas.workpadHeaderShareMenu.shareDownloadJSONTitle": "下载为 {JSON}", + "xpack.canvas.workpadHeaderShareMenu.shareDownloadPDFTitle": "{PDF} 报告", + "xpack.canvas.workpadHeaderShareMenu.shareWebsiteErrorTitle": "无法为“{workpadName}”创建 {ZIP} 文件。Workpad 可能过大。您将需要分别下载文件。", + "xpack.canvas.workpadHeaderShareMenu.shareWebsiteTitle": "在网站上共享", + "xpack.canvas.workpadHeaderShareMenu.shareWorkpadMessage": "共享此 Workpad", + "xpack.canvas.workpadHeaderShareMenu.unknownExportErrorMessage": "未知导出类型:{type}", + "xpack.canvas.workpadHeaderShareMenu.unsupportedRendererWarning": "此 Workpad 包含 {CANVAS} Shareable Workpad Runtime 不支持的呈现函数。将不会呈现以下元素:", + "xpack.canvas.workpadHeaderViewMenu.zoomControlsAriaLabel": "缩放控制", + "xpack.canvas.workpadHeaderViewMenu.zoomControlsTooltip": "缩放控制", + "xpack.canvas.workpadHeaderViewMenu.zoomFitToWindowText": "适应窗口大小", + "xpack.canvas.workpadHeaderViewMenu.zoomInText": "放大", + "xpack.canvas.workpadHeaderViewMenu.zoomOutText": "缩小", + "xpack.canvas.workpadHeaderViewMenu.zoomPanelTitle": "缩放", + "xpack.canvas.workpadHeaderViewMenu.zoomPrecentageValue": "重置", + "xpack.canvas.workpadHeaderViewMenu.zoomResetText": "{scalePercentage}%", "xpack.canvas.workpadLoader.clonedWorkpadName": "{workpadName} 的副本", "xpack.canvas.workpadLoader.cloneTooltip": "克隆 Workpad", "xpack.canvas.workpadLoader.createWorkpadLoadingDescription": "正在创建 Workpad......", @@ -8179,7 +8154,6 @@ "xpack.infra.viewSwitcher.mapViewLabel": "地图视图", "xpack.infra.viewSwitcher.tableViewLabel": "表视图", "xpack.infra.waffle.accountAllTitle": "全部", - "xpack.infra.waffle.accountLabel": "帐户:{selectedAccount}", "xpack.infra.waffle.aggregationNames.avg": "“{field}”的平均值", "xpack.infra.waffle.aggregationNames.max": "“{field}”的最大值", "xpack.infra.waffle.aggregationNames.min": "“{field}”的最小值", @@ -8215,11 +8189,8 @@ "xpack.infra.waffle.customMetrics.modeSwitcher.saveButtonAriaLabel": "保存定制指标的更改", "xpack.infra.waffle.customMetrics.submitLabel": "保存", "xpack.infra.waffle.groupByAllTitle": "全部", - "xpack.infra.waffle.groupByButtonLabel": "分组依据: ", - "xpack.infra.waffle.inventoryButtonLabel": "视图:{selectedText}", "xpack.infra.waffle.loadingDataText": "正在加载数据", "xpack.infra.waffle.maxGroupByTooltip": "一次只能选择两个分组", - "xpack.infra.waffle.metricButtonLabel": "指标:{selectedMetric}", "xpack.infra.waffle.metricOptions.countText": "计数", "xpack.infra.waffle.metricOptions.cpuUsageText": "CPU 使用", "xpack.infra.waffle.metricOptions.diskIOReadBytes": "磁盘读取", @@ -8246,7 +8217,6 @@ "xpack.infra.waffle.noDataDescription": "尝试调整您的时间或筛选。", "xpack.infra.waffle.noDataTitle": "没有可显示的数据。", "xpack.infra.waffle.region": "全部", - "xpack.infra.waffle.regionLabel": "地区:{selectedRegion}", "xpack.infra.waffle.savedView.createHeader": "保存视图", "xpack.infra.waffle.savedViews.cancel": "取消", "xpack.infra.waffle.savedViews.cancelButton": "取消", @@ -8852,10 +8822,8 @@ "xpack.logstash.workersTooltip": "并行执行管道的筛选和输出阶段的工作线程数目。如果您发现事件出现积压或 CPU 未饱和,请考虑增大此数值,以更好地利用机器处理能力。\n\n默认值:主机的 CPU 核心数", "xpack.maps.addLayerPanel.addLayer": "添加图层", "xpack.maps.addLayerPanel.changeDataSourceButtonLabel": "更改数据源", - "xpack.maps.addLayerPanel.chooseDataSourceTitle": "选择数据源", "xpack.maps.addLayerPanel.footer.cancelButtonLabel": "鍙栨秷", "xpack.maps.addLayerPanel.importFile": "导入文件", - "xpack.maps.addLayerPanel.selectSource": "选择源", "xpack.maps.aggs.defaultCountLabel": "计数", "xpack.maps.appDescription": "地图应用程序", "xpack.maps.appTitle": "Maps", @@ -9462,14 +9430,8 @@ "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixLabel": "分类混淆矩阵", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixPredictedLabel": "预测标签", "xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTooltip": "多类混淆矩阵包含分析使用数据点的实际类正确分类数据点的次数以及分析使用其他类错误分类这些数据点的次数", - "xpack.ml.dataframe.analytics.classificationExploration.documentsShownHelpText": "正在显示有相关预测存在的文档", "xpack.ml.dataframe.analytics.classificationExploration.evaluateJobIdTitle": "分类作业 ID {jobId} 的评估", - "xpack.ml.dataframe.analytics.classificationExploration.firstDocumentsShownHelpText": "正在显示有相关预测存在的前 {searchSize} 个文档", "xpack.ml.dataframe.analytics.classificationExploration.generalizationDocsCount": "{docsCount, plural, one {# 个文档} other {# 个文档}}已评估", - "xpack.ml.dataframe.analytics.classificationExploration.jobCapsFetchError": "无法提取结果。加载索引的字段数据时发生错误。", - "xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationFetchError": "无法提取结果。加载作业配置数据时发生错误。", - "xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationNoResultsMessage": "未找到结果。", - "xpack.ml.dataframe.analytics.classificationExploration.regressionDocsLink": "回归评估文档 ", "xpack.ml.dataframe.analytics.classificationExploration.showActions": "显示操作", "xpack.ml.dataframe.analytics.classificationExploration.showAllColumns": "显示所有列", "xpack.ml.dataframe.analytics.classificationExploration.tableJobIdTitle": "分类作业 ID {jobId} 的目标索引", @@ -9544,31 +9506,17 @@ "xpack.ml.dataframe.analytics.create.startDataFrameAnalyticsSuccessMessage": "数据帧分析 {jobId} 启动请求已确认。", "xpack.ml.dataframe.analytics.create.trainingPercentLabel": "训练百分比", "xpack.ml.dataframe.analytics.exploration.colorRangeLegendTitle": "功能影响分数", - "xpack.ml.dataframe.analytics.exploration.dataGridAriaLabel": "离群值检测结果表", "xpack.ml.dataframe.analytics.exploration.experimentalBadgeLabel": "实验性", "xpack.ml.dataframe.analytics.exploration.experimentalBadgeTooltipContent": "数据帧分析为实验功能。我们很乐意听取您的反馈意见。", "xpack.ml.dataframe.analytics.exploration.indexError": "加载索引数据时出错。", "xpack.ml.dataframe.analytics.exploration.jobIdTitle": "离群值检测作业 ID {jobId}", - "xpack.ml.dataframe.analytics.exploration.noDataCalloutBody": "该索引的查询未返回结果。请确保索引包含文档且您的查询限制不过于严格。", "xpack.ml.dataframe.analytics.exploration.title": "分析浏览", - "xpack.ml.dataframe.analytics.regressionExploration.documentsShownHelpText": "正在显示有相关预测存在的文档", - "xpack.ml.dataframe.analytics.regressionExploration.evaluateError": "加载数据时出错。", "xpack.ml.dataframe.analytics.regressionExploration.evaluateJobIdTitle": "回归作业 ID {jobId} 的评估", - "xpack.ml.dataframe.analytics.regressionExploration.fieldSelection": "已选择 {docFieldsCount, number} 个{docFieldsCount, plural, one {字段} other {字段}}中的 {selectedFieldsLength, number} 个", - "xpack.ml.dataframe.analytics.regressionExploration.firstDocumentsShownHelpText": "正在显示有相关预测存在的前 {searchSize} 个文档", - "xpack.ml.dataframe.analytics.regressionExploration.generalError": "加载数据时出错。", "xpack.ml.dataframe.analytics.regressionExploration.generalizationDocsCount": "{docsCount, plural, one {# 个文档} other {# 个文档}}已评估", "xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle": "泛化误差", "xpack.ml.dataframe.analytics.regressionExploration.indexError": "加载索引数据时出错。", - "xpack.ml.dataframe.analytics.regressionExploration.jobCapsFetchError": "无法提取结果。加载索引的字段数据时发生错误。", - "xpack.ml.dataframe.analytics.regressionExploration.jobConfigurationFetchError": "无法提取结果。加载作业配置数据时发生错误。", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText": "均方误差", "xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorTooltipContent": "度量回归分析模型的表现。真实值与预测值之差的平均平方和。", - "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutBody": "该索引的查询未返回结果。请确保作业已完成且索引包含文档。", - "xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutTitle": "空的索引查询结果。", - "xpack.ml.dataframe.analytics.regressionExploration.noIndexCalloutBody": "该索引的查询未返回结果。请确保目标索引存在且包含文档。", - "xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorBody": "查询语法无效,未返回任何结果。请检查查询语法并重试。", - "xpack.ml.dataframe.analytics.regressionExploration.queryParsingErrorMessage": "无法解析查询。", "xpack.ml.dataframe.analytics.regressionExploration.rSquaredText": "R 平方", "xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent": "表示拟合优度。度量模型复制被观察结果的优良性。", "xpack.ml.dataframe.analytics.regressionExploration.tableJobIdTitle": "回归作业 ID {jobId} 的目标索引", @@ -10914,7 +10862,6 @@ "xpack.monitoring.alerts.licenseExpiration.actionGroups.default": "默认值", "xpack.monitoring.alerts.licenseExpiration.newSubject": "新 X-Pack Monitoring:许可证到期", "xpack.monitoring.alerts.licenseExpiration.resolvedSubject": "已解决 X-Pack Monitoring:许可证到期", - "xpack.monitoring.alerts.licenseExpiration.ui.firingMessage": "此集群的许可证将在 #relative 后,即 #absolute到期", "xpack.monitoring.alerts.licenseExpiration.ui.resolvedMessage": "此集群的许可证处于活动状态。", "xpack.monitoring.alerts.lowSeverityName": "低", "xpack.monitoring.alerts.mediumSeverityName": "中", @@ -12410,7 +12357,6 @@ "xpack.reporting.shareContextMenu.csvReportsButtonLabel": "CSV 报告", "xpack.reporting.shareContextMenu.pdfReportsButtonLabel": "PDF 报告", "xpack.reporting.shareContextMenu.pngReportsButtonLabel": "PNG 报告", - "xpack.rollupJobs.appName": "汇总/打包作业", "xpack.rollupJobs.appTitle": "汇总/打包作业", "xpack.rollupJobs.breadcrumbsTitle": "汇总/打包作业", "xpack.rollupJobs.create.backButton.label": "上一步", @@ -15588,19 +15534,11 @@ "xpack.transform.newTransform.searchSelection.savedObjectType.indexPattern": "索引模式", "xpack.transform.newTransform.searchSelection.savedObjectType.search": "已保存搜索", "xpack.transform.pivotPreview.copyClipboardTooltip": "将透视预览的开发控制台语句复制到剪贴板。", - "xpack.transform.pivotPreview.PivotPreviewError": "加载数据透视表预览时出错。", "xpack.transform.pivotPreview.PivotPreviewIncompleteConfigCalloutBody": "请至少选择一个分组依据字段和聚合。", "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody": "预览请求未返回任何数据。请确保可选查询返回数据且存在分组依据和聚合字段使用的字段的值。", - "xpack.transform.pivotPreview.PivotPreviewNoDataCalloutTitle": "数据透视表预览不可用", "xpack.transform.pivotPreview.PivotPreviewTitle": "转换数据透视表预览", "xpack.transform.progress": "进度", "xpack.transform.sourceIndex": "源索引", - "xpack.transform.sourceIndexPreview.copyClipboardTooltip": "将源索引预览的开发控制台语句复制到剪贴板。", - "xpack.transform.sourceIndexPreview.invalidSortingColumnError": "列“{columnId}”无法用于排序。", - "xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutBody": "源索引的查询未返回结果。请确保索引包含文档且您的查询限制不过于严格。", - "xpack.transform.sourceIndexPreview.SourceIndexNoDataCalloutTitle": "源索引查询结果为空。", - "xpack.transform.sourceIndexPreview.sourceIndexPatternError": "加载源索引数据时出错。", - "xpack.transform.sourceIndexPreview.sourceIndexPatternTitle": "源索引 {indexPatternTitle}", "xpack.transform.statsBar.batchTransformsLabel": "批量", "xpack.transform.statsBar.continuousTransformsLabel": "连续", "xpack.transform.statsBar.failedTransformsLabel": "失败", @@ -15689,7 +15627,6 @@ "xpack.transform.stepDetailsForm.errorGettingIndexPatternTitles": "获取现有索引模式标题时发生错误:", "xpack.transform.stepDetailsForm.errorGettingTransformList": "获取现有转换 ID 时发生错误:", "xpack.transform.stepDetailsForm.indexPatternTitleError": "具有此名称的索引模式已存在。", - "xpack.transform.stepDetailsForm.transformDescriptionHelpText": "(可选)描述性文本。", "xpack.transform.stepDetailsForm.transformDescriptionInputAriaLabel": "选择可选的转换描述。", "xpack.transform.stepDetailsForm.transformDescriptionLabel": "转换描述", "xpack.transform.stepDetailsForm.transformIdExistsError": "已存在具有此 ID 的转换。", @@ -15998,7 +15935,6 @@ "xpack.triggersActionsUI.sections.alertForm.loadingActionTypesDescription": "正在加载操作类型……", "xpack.triggersActionsUI.sections.alertForm.renotifyFieldLabel": "通知频率", "xpack.triggersActionsUI.sections.alertForm.renotifyWithTooltip": "定义告警处于活动状态时重复操作的频率。", - "xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeEditTitle": "{actionConnectorName}", "xpack.triggersActionsUI.sections.alertForm.selectAlertActionTypeTitle": "操作:选择操作类型", "xpack.triggersActionsUI.sections.alertForm.selectAlertTypeTitle": "选择触发器类型", "xpack.triggersActionsUI.sections.alertForm.selectedAlertTypeTitle": "{alertType}", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.test.ts index 9ce50cf47560a..0a2ec3f203a9a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.test.ts @@ -33,11 +33,20 @@ test('should sort enabled action types first', async () => { enabledInConfig: true, enabledInLicense: true, }, + { + id: '4', + minimumLicenseRequired: 'basic', + name: 'x-fourth', + enabled: true, + enabledInConfig: false, + enabledInLicense: true, + }, ]; const result = [...actionTypes].sort(actionTypeCompare); expect(result[0]).toEqual(actionTypes[0]); expect(result[1]).toEqual(actionTypes[2]); - expect(result[2]).toEqual(actionTypes[1]); + expect(result[2]).toEqual(actionTypes[3]); + expect(result[3]).toEqual(actionTypes[1]); }); test('should sort by name when all enabled', async () => { @@ -66,9 +75,18 @@ test('should sort by name when all enabled', async () => { enabledInConfig: true, enabledInLicense: true, }, + { + id: '4', + minimumLicenseRequired: 'basic', + name: 'x-fourth', + enabled: true, + enabledInConfig: false, + enabledInLicense: true, + }, ]; const result = [...actionTypes].sort(actionTypeCompare); expect(result[0]).toEqual(actionTypes[1]); expect(result[1]).toEqual(actionTypes[2]); expect(result[2]).toEqual(actionTypes[0]); + expect(result[3]).toEqual(actionTypes[3]); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.ts index d18cb21b3a0fe..8078ef4938e50 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_type_compare.ts @@ -4,14 +4,35 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ActionType } from '../../types'; +import { ActionType, ActionConnector } from '../../types'; -export function actionTypeCompare(a: ActionType, b: ActionType) { - if (a.enabled === true && b.enabled === false) { +export function actionTypeCompare( + a: ActionType, + b: ActionType, + preconfiguredConnectors?: ActionConnector[] +) { + const aEnabled = getIsEnabledValue(a, preconfiguredConnectors); + const bEnabled = getIsEnabledValue(b, preconfiguredConnectors); + + if (aEnabled === true && bEnabled === false) { return -1; } - if (a.enabled === false && b.enabled === true) { + if (aEnabled === false && bEnabled === true) { return 1; } return a.name.localeCompare(b.name); } + +const getIsEnabledValue = (actionType: ActionType, preconfiguredConnectors?: ActionConnector[]) => { + let isEnabled = actionType.enabled; + if ( + !actionType.enabledInConfig && + preconfiguredConnectors && + preconfiguredConnectors.length > 0 + ) { + isEnabled = + preconfiguredConnectors.find(connector => connector.actionTypeId === actionType.id) !== + undefined; + } + return isEnabled; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx index 566ed7935e013..9c017aa6fd31f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx @@ -4,43 +4,47 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ActionType } from '../../types'; -import { checkActionTypeEnabled } from './check_action_type_enabled'; +import { ActionType, ActionConnector } from '../../types'; +import { + checkActionTypeEnabled, + checkActionFormActionTypeEnabled, +} from './check_action_type_enabled'; -test(`returns isEnabled:true when action type isn't provided`, async () => { - expect(checkActionTypeEnabled()).toMatchInlineSnapshot(` +describe('checkActionTypeEnabled', () => { + test(`returns isEnabled:true when action type isn't provided`, async () => { + expect(checkActionTypeEnabled()).toMatchInlineSnapshot(` Object { "isEnabled": true, } `); -}); + }); -test('returns isEnabled:true when action type is enabled', async () => { - const actionType: ActionType = { - id: '1', - minimumLicenseRequired: 'basic', - name: 'my action', - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - }; - expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(` + test('returns isEnabled:true when action type is enabled', async () => { + const actionType: ActionType = { + id: '1', + minimumLicenseRequired: 'basic', + name: 'my action', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + }; + expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(` Object { "isEnabled": true, } `); -}); + }); -test('returns isEnabled:false when action type is disabled by license', async () => { - const actionType: ActionType = { - id: '1', - minimumLicenseRequired: 'basic', - name: 'my action', - enabled: false, - enabledInConfig: true, - enabledInLicense: false, - }; - expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(` + test('returns isEnabled:false when action type is disabled by license', async () => { + const actionType: ActionType = { + id: '1', + minimumLicenseRequired: 'basic', + name: 'my action', + enabled: false, + enabledInConfig: true, + enabledInLicense: false, + }; + expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(` Object { "isEnabled": false, "message": "This connector requires a Basic license.", @@ -63,18 +67,82 @@ test('returns isEnabled:false when action type is disabled by license', async () , } `); + }); + + test('returns isEnabled:false when action type is disabled by config', async () => { + const actionType: ActionType = { + id: '1', + minimumLicenseRequired: 'basic', + name: 'my action', + enabled: false, + enabledInConfig: false, + enabledInLicense: true, + }; + expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(` + Object { + "isEnabled": false, + "message": "This connector is disabled by the Kibana configuration.", + "messageCard": , + } + `); + }); }); -test('returns isEnabled:false when action type is disabled by config', async () => { - const actionType: ActionType = { - id: '1', - minimumLicenseRequired: 'basic', - name: 'my action', - enabled: false, - enabledInConfig: false, - enabledInLicense: true, - }; - expect(checkActionTypeEnabled(actionType)).toMatchInlineSnapshot(` +describe('checkActionFormActionTypeEnabled', () => { + const preconfiguredConnectors: ActionConnector[] = [ + { + actionTypeId: '1', + config: {}, + id: 'test1', + isPreconfigured: true, + name: 'test', + secrets: {}, + referencedByCount: 0, + }, + { + actionTypeId: '2', + config: {}, + id: 'test2', + isPreconfigured: true, + name: 'test', + secrets: {}, + referencedByCount: 0, + }, + ]; + + test('returns isEnabled:true when action type is preconfigured', async () => { + const actionType: ActionType = { + id: '1', + minimumLicenseRequired: 'basic', + name: 'my action', + enabled: true, + enabledInConfig: false, + enabledInLicense: true, + }; + + expect(checkActionFormActionTypeEnabled(actionType, preconfiguredConnectors)) + .toMatchInlineSnapshot(` + Object { + "isEnabled": true, + } + `); + }); + + test('returns isEnabled:false when action type is disabled by config and not preconfigured', async () => { + const actionType: ActionType = { + id: 'disabled-by-config', + minimumLicenseRequired: 'basic', + name: 'my action', + enabled: true, + enabledInConfig: false, + enabledInLicense: true, + }; + expect(checkActionFormActionTypeEnabled(actionType, preconfiguredConnectors)) + .toMatchInlineSnapshot(` Object { "isEnabled": false, "message": "This connector is disabled by the Kibana configuration.", @@ -85,4 +153,5 @@ test('returns isEnabled:false when action type is disabled by config', async () />, } `); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx index 263502a82ec79..971d6dbbb57bf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx @@ -9,7 +9,7 @@ import { capitalize } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCard, EuiLink } from '@elastic/eui'; -import { ActionType } from '../../types'; +import { ActionType, ActionConnector } from '../../types'; import { VIEW_LICENSE_OPTIONS_LINK } from '../../common/constants'; import './check_action_type_enabled.scss'; @@ -22,71 +22,98 @@ export interface IsDisabledResult { messageCard: JSX.Element; } +const getLicenseCheckResult = (actionType: ActionType) => { + return { + isEnabled: false, + message: i18n.translate( + 'xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByLicenseMessage', + { + defaultMessage: 'This connector requires a {minimumLicenseRequired} license.', + values: { + minimumLicenseRequired: capitalize(actionType.minimumLicenseRequired), + }, + } + ), + messageCard: ( + + + + } + /> + ), + }; +}; + +const configurationCheckResult = { + isEnabled: false, + message: i18n.translate( + 'xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByConfigMessage', + { defaultMessage: 'This connector is disabled by the Kibana configuration.' } + ), + messageCard: ( + + ), +}; + export function checkActionTypeEnabled( actionType?: ActionType ): IsEnabledResult | IsDisabledResult { if (actionType?.enabledInLicense === false) { - return { - isEnabled: false, - message: i18n.translate( - 'xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByLicenseMessage', - { - defaultMessage: 'This connector requires a {minimumLicenseRequired} license.', - values: { - minimumLicenseRequired: capitalize(actionType.minimumLicenseRequired), - }, - } - ), - messageCard: ( - - - - } - /> - ), - }; + return getLicenseCheckResult(actionType); } if (actionType?.enabledInConfig === false) { - return { - isEnabled: false, - message: i18n.translate( - 'xpack.triggersActionsUI.checkActionTypeEnabled.actionTypeDisabledByConfigMessage', - { defaultMessage: 'This connector is disabled by the Kibana configuration.' } - ), - messageCard: ( - - ), - }; + return configurationCheckResult; + } + + return { isEnabled: true }; +} + +export function checkActionFormActionTypeEnabled( + actionType: ActionType, + preconfiguredConnectors: ActionConnector[] +): IsEnabledResult | IsDisabledResult { + if (actionType?.enabledInLicense === false) { + return getLicenseCheckResult(actionType); + } + + if ( + actionType?.enabledInConfig === false && + // do not disable action type if it contains preconfigured connectors (is preconfigured) + !preconfiguredConnectors.find( + preconfiguredConnector => preconfiguredConnector.actionTypeId === actionType.id + ) + ) { + return configurationCheckResult; } return { isEnabled: true }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index d4def86b07b1f..aed7d18bd9f3d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -73,6 +73,21 @@ describe('action_form', () => { actionParamsFields: null, }; + const preconfiguredOnly = { + id: 'preconfigured', + iconClass: 'test', + selectMessage: 'test', + validateConnector: (): ValidationResult => { + return { errors: {} }; + }, + validateParams: (): ValidationResult => { + const validationResult = { errors: {} }; + return validationResult; + }, + actionConnectorFields: null, + actionParamsFields: null, + }; + describe('action_form in alert', () => { let wrapper: ReactWrapper; @@ -95,6 +110,22 @@ describe('action_form', () => { config: {}, isPreconfigured: true, }, + { + secrets: {}, + id: 'test3', + actionTypeId: preconfiguredOnly.id, + name: 'Preconfigured Only', + config: {}, + isPreconfigured: true, + }, + { + secrets: {}, + id: 'test4', + actionTypeId: preconfiguredOnly.id, + name: 'Regular connector', + config: {}, + isPreconfigured: false, + }, ]); const mockes = coreMock.createSetup(); deps = { @@ -106,6 +137,7 @@ describe('action_form', () => { actionType, disabledByConfigActionType, disabledByLicenseActionType, + preconfiguredOnly, ]); actionTypeRegistry.has.mockReturnValue(true); actionTypeRegistry.get.mockReturnValue(actionType); @@ -166,6 +198,14 @@ describe('action_form', () => { enabledInLicense: true, minimumLicenseRequired: 'basic', }, + { + id: 'preconfigured', + name: 'Preconfigured only', + enabled: true, + enabledInConfig: false, + enabledInLicense: true, + minimumLicenseRequired: 'basic', + }, { id: 'disabled-by-config', name: 'Disabled by config', @@ -207,21 +247,27 @@ describe('action_form', () => { ).toBeFalsy(); }); - it(`doesn't render action types disabled by config`, async () => { + it('does not render action types disabled by config', async () => { await setup(); const actionOption = wrapper.find( - `[data-test-subj="disabled-by-config-ActionTypeSelectOption"]` + '[data-test-subj="disabled-by-config-ActionTypeSelectOption"]' ); expect(actionOption.exists()).toBeFalsy(); }); - it(`renders available connectors for the selected action type`, async () => { + it('render action types which is preconfigured only (disabled by config and with preconfigured connectors)', async () => { + await setup(); + const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]'); + expect(actionOption.exists()).toBeTruthy(); + }); + + it('renders available connectors for the selected action type', async () => { await setup(); const actionOption = wrapper.find( `[data-test-subj="${actionType.id}-ActionTypeSelectOption"]` ); actionOption.first().simulate('click'); - const combobox = wrapper.find(`[data-test-subj="selectActionConnector"]`); + const combobox = wrapper.find(`[data-test-subj="selectActionConnector-${actionType.id}"]`); expect((combobox.first().props() as any).options).toMatchInlineSnapshot(` Array [ Object { @@ -238,10 +284,37 @@ describe('action_form', () => { `); }); + it('renders only preconfigured connectors for the selected preconfigured action type', async () => { + await setup(); + const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]'); + actionOption.first().simulate('click'); + const combobox = wrapper.find('[data-test-subj="selectActionConnector-preconfigured"]'); + expect((combobox.first().props() as any).options).toMatchInlineSnapshot(` + Array [ + Object { + "id": "test3", + "key": "test3", + "label": "Preconfigured Only (preconfigured)", + }, + ] + `); + }); + + it('does not render "Add new" button for preconfigured only action type', async () => { + await setup(); + const actionOption = wrapper.find('[data-test-subj="preconfigured-ActionTypeSelectOption"]'); + actionOption.first().simulate('click'); + const preconfigPannel = wrapper.find('[data-test-subj="alertActionAccordion-default"]'); + const addNewConnectorButton = preconfigPannel.find( + '[data-test-subj="addNewActionConnectorButton-preconfigured"]' + ); + expect(addNewConnectorButton.exists()).toBeFalsy(); + }); + it('renders action types disabled by license', async () => { await setup(); const actionOption = wrapper.find( - `[data-test-subj="disabled-by-license-ActionTypeSelectOption"]` + '[data-test-subj="disabled-by-license-ActionTypeSelectOption"]' ); expect(actionOption.exists()).toBeTruthy(); expect( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 4199cfb7b4b7f..0027837c913d1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -29,7 +29,7 @@ import { EuiText, } from '@elastic/eui'; import { HttpSetup, ToastsApi } from 'kibana/public'; -import { loadActionTypes, loadAllActions } from '../../lib/action_connector_api'; +import { loadActionTypes, loadAllActions as loadConnectors } from '../../lib/action_connector_api'; import { IErrorObject, ActionTypeModel, @@ -42,7 +42,7 @@ import { SectionLoading } from '../../components/section_loading'; import { ConnectorAddModal } from './connector_add_modal'; import { TypeRegistry } from '../../type_registry'; import { actionTypeCompare } from '../../lib/action_type_compare'; -import { checkActionTypeEnabled } from '../../lib/check_action_type_enabled'; +import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled'; import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; interface ActionAccordionFormProps { @@ -111,14 +111,12 @@ export const ActionForm = ({ setHasActionsDisabled(hasActionsDisabled); } } catch (e) { - if (toastNotifications) { - toastNotifications.addDanger({ - title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionTypesMessage', - { defaultMessage: 'Unable to load action types' } - ), - }); - } + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionTypesMessage', + { defaultMessage: 'Unable to load action types' } + ), + }); } finally { setIsLoadingActionTypes(false); } @@ -126,41 +124,50 @@ export const ActionForm = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // load connectors useEffect(() => { - loadConnectors(); + (async () => { + try { + setIsLoadingConnectors(true); + setConnectors(await loadConnectors({ http })); + } catch (e) { + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionsMessage', + { + defaultMessage: 'Unable to load connectors', + } + ), + }); + } finally { + setIsLoadingConnectors(false); + } + })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - async function loadConnectors() { - try { - setIsLoadingConnectors(true); - const actionsResponse = await loadAllActions({ http }); - setConnectors(actionsResponse); - } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionsMessage', - { - defaultMessage: 'Unable to load connectors', - } - ), - }); - } finally { - setIsLoadingConnectors(false); - } - } const preconfiguredMessage = i18n.translate( 'xpack.triggersActionsUI.sections.actionForm.preconfiguredTitleMessage', { defaultMessage: '(preconfigured)', } ); + const getSelectedOptions = (actionItemId: string) => { - const val = connectors.find(connector => connector.id === actionItemId); - if (!val) { + const selectedConnector = connectors.find(connector => connector.id === actionItemId); + if ( + !selectedConnector || + // if selected connector is not preconfigured and action type is for preconfiguration only, + // do not show regular connectors of this type + (actionTypesIndex && + !actionTypesIndex[selectedConnector.actionTypeId].enabledInConfig && + !selectedConnector.isPreconfigured) + ) { return []; } - const optionTitle = `${val.name} ${val.isPreconfigured ? preconfiguredMessage : ''}`; + const optionTitle = `${selectedConnector.name} ${ + selectedConnector.isPreconfigured ? preconfiguredMessage : '' + }`; return [ { label: optionTitle, @@ -179,8 +186,15 @@ export const ActionForm = ({ }, index: number ) => { + const actionType = actionTypesIndex![actionItem.actionTypeId]; + const optionsList = connectors - .filter(connectorItem => connectorItem.actionTypeId === actionItem.actionTypeId) + .filter( + connectorItem => + connectorItem.actionTypeId === actionItem.actionTypeId && + // include only enabled by config connectors or preconfigured + (actionType.enabledInConfig || connectorItem.isPreconfigured) + ) .map(({ name, id, isPreconfigured }) => ({ label: `${name} ${isPreconfigured ? preconfiguredMessage : ''}`, key: id, @@ -189,8 +203,9 @@ export const ActionForm = ({ const actionTypeRegistered = actionTypeRegistry.get(actionConnector.actionTypeId); if (!actionTypeRegistered || actionItem.group !== defaultActionGroupId) return null; const ParamsFieldsComponent = actionTypeRegistered.actionParamsFields; - const checkEnabledResult = checkActionTypeEnabled( - actionTypesIndex && actionTypesIndex[actionConnector.actionTypeId] + const checkEnabledResult = checkActionFormActionTypeEnabled( + actionTypesIndex![actionConnector.actionTypeId], + connectors.filter(connector => connector.isPreconfigured) ); const accordionContent = checkEnabledResult.isEnabled ? ( @@ -211,19 +226,21 @@ export const ActionForm = ({ /> } labelAppend={ - { - setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index }); - setAddModalVisibility(true); - }} - > - - + actionTypesIndex![actionConnector.actionTypeId].enabledInConfig ? ( + { + setActiveActionItem({ actionTypeId: actionItem.actionTypeId, index }); + setAddModalVisibility(true); + }} + > + + + ) : null } > { setActionIdByIndex(selectedOptions[0].id ?? '', index); @@ -258,10 +275,9 @@ export const ActionForm = ({ ); return ( - + -

+

-

+
@@ -349,10 +365,9 @@ export const ActionForm = ({ const actionTypeRegistered = actionTypeRegistry.get(actionItem.actionTypeId); if (!actionTypeRegistered || actionItem.group !== defaultActionGroupId) return null; return ( - + -

+

-

+
@@ -486,18 +501,26 @@ export const ActionForm = ({ } } - let actionTypeNodes: JSX.Element[] | null = null; + let actionTypeNodes: Array | null = null; let hasDisabledByLicenseActionTypes = false; if (actionTypesIndex) { + const preconfiguredConnectors = connectors.filter(connector => connector.isPreconfigured); actionTypeNodes = actionTypeRegistry .list() - .filter( - item => actionTypesIndex[item.id] && actionTypesIndex[item.id].enabledInConfig === true + .filter(item => actionTypesIndex[item.id]) + .sort((a, b) => + actionTypeCompare(actionTypesIndex[a.id], actionTypesIndex[b.id], preconfiguredConnectors) ) - .sort((a, b) => actionTypeCompare(actionTypesIndex[a.id], actionTypesIndex[b.id])) .map(function(item, index) { const actionType = actionTypesIndex[item.id]; - const checkEnabledResult = checkActionTypeEnabled(actionTypesIndex[item.id]); + const checkEnabledResult = checkActionFormActionTypeEnabled( + actionTypesIndex[item.id], + preconfiguredConnectors + ); + // if action type is not enabled in config and not preconfigured, it shouldn't be displayed + if (!actionType.enabledInConfig && !checkEnabledResult.isEnabled) { + return null; + } if (!actionType.enabledInLicense) { hasDisabledByLicenseActionTypes = true; } diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts index 4228426d62159..5301f6364529d 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/polling_service.test.ts @@ -6,7 +6,7 @@ import { ReindexStatus, ReindexStep } from '../../../../../../../common/types'; import { ReindexPollingService } from './polling_service'; -import { httpServiceMock } from 'src/core/public/http/http_service.mock'; +import { httpServiceMock } from 'src/core/public/mocks'; const mockClient = httpServiceMock.createSetupContract(); diff --git a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts index b2b8ccf1ca57a..78b03275e0ef9 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.test.ts @@ -5,7 +5,7 @@ */ import { kibanaResponseFactory } from 'src/core/server'; -import { savedObjectsServiceMock } from 'src/core/server/saved_objects/saved_objects_service.mock'; +import { savedObjectsServiceMock } from 'src/core/server/mocks'; import { createMockRouter, MockRouter, routeHandlerContextMock } from './__mocks__/routes.mock'; import { createRequestMock } from './__mocks__/request.mock'; diff --git a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts index 2cc6f23ebaae5..4f4c6e3011ad1 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts @@ -16,7 +16,7 @@ import { AlertType } from '../../../../../alerting/server'; import { IRouter } from 'kibana/server'; import { UMServerLibs } from '../../lib'; import { UptimeCoreSetup } from '../../adapters'; -import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; import { alertsMock, AlertServicesMock } from '../../../../../alerting/server/mocks'; /** @@ -52,7 +52,7 @@ const mockOptions = ( id: '', type: '', references: [], - attributes: defaultDynamicSettings, + attributes: DYNAMIC_SETTINGS_DEFAULTS, }); return { params, @@ -88,9 +88,9 @@ describe('status check alert', () => { Object { "callES": [MockFunction], "dynamicSettings": Object { - "certificatesThresholds": Object { - "errorState": 7, - "warningState": 30, + "certThresholds": Object { + "age": 365, + "expiration": 30, }, "heartbeatIndices": "heartbeat-8*", }, @@ -135,9 +135,9 @@ describe('status check alert', () => { Object { "callES": [MockFunction], "dynamicSettings": Object { - "certificatesThresholds": Object { - "errorState": 7, - "warningState": 30, + "certThresholds": Object { + "age": 365, + "expiration": 30, }, "heartbeatIndices": "heartbeat-8*", }, diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts index 539344dfca791..b49a6b22ff976 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_certs.test.ts @@ -5,6 +5,7 @@ */ import { getCerts } from '../get_certs'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; describe('getCerts', () => { let mockHits: any; @@ -86,7 +87,10 @@ describe('getCerts', () => { it('parses query result and returns expected values', async () => { const result = await getCerts({ callES: mockCallES, - dynamicSettings: { heartbeatIndices: 'heartbeat*' }, + dynamicSettings: { + heartbeatIndices: 'heartbeat*', + certThresholds: DYNAMIC_SETTINGS_DEFAULTS.certThresholds, + }, index: 1, from: 'now-2d', to: 'now+1h', diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts index cf8414a3b0a68..03e2bc7a44bd0 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_latest_monitor.test.ts @@ -5,14 +5,14 @@ */ import { getLatestMonitor } from '../get_latest_monitor'; -import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; describe('getLatestMonitor', () => { let expectedGetLatestSearchParams: any; let mockEsSearchResult: any; beforeEach(() => { expectedGetLatestSearchParams = { - index: defaultDynamicSettings.heartbeatIndices, + index: DYNAMIC_SETTINGS_DEFAULTS.heartbeatIndices, body: { query: { bool: { @@ -64,7 +64,7 @@ describe('getLatestMonitor', () => { const mockEsClient = jest.fn(async (_request: any, _params: any) => mockEsSearchResult); const result = await getLatestMonitor({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, dateStart: 'now-1h', dateEnd: 'now', monitorId: 'testMonitor', diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts index c740581734fdd..5d3f9ce8d4ad9 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts @@ -7,7 +7,7 @@ import { set } from 'lodash'; import mockChartsData from './monitor_charts_mock.json'; import { getMonitorDurationChart } from '../get_monitor_duration'; -import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; describe('ElasticsearchMonitorsAdapter', () => { it('getMonitorChartsData will provide expected filters', async () => { @@ -16,7 +16,7 @@ describe('ElasticsearchMonitorsAdapter', () => { const search = searchMock.bind({}); await getMonitorDurationChart({ callES: search, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, monitorId: 'fooID', dateStart: 'now-15m', dateEnd: 'now', @@ -39,7 +39,7 @@ describe('ElasticsearchMonitorsAdapter', () => { expect( await getMonitorDurationChart({ callES: search, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, monitorId: 'id', dateStart: 'now-15m', dateEnd: 'now', diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts index e429de9ae0d68..e47be617d7c99 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_status.test.ts @@ -6,8 +6,8 @@ import { elasticsearchServiceMock } from '../../../../../../../src/core/server/mocks'; import { getMonitorStatus } from '../get_monitor_status'; -import { ScopedClusterClient } from 'src/core/server/elasticsearch'; -import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; +import { ScopedClusterClient } from 'src/core/server'; interface BucketItemCriteria { monitor_id: string; @@ -103,7 +103,7 @@ describe('getMonitorStatus', () => { }`; await getMonitorStatus({ callES, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, filters: exampleFilter, locations: [], numTimes: 5, @@ -206,7 +206,7 @@ describe('getMonitorStatus', () => { const [callES, esMock] = setupMock([]); await getMonitorStatus({ callES, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, locations: ['fairbanks', 'harrisburg'], numTimes: 1, timerange: { @@ -329,7 +329,7 @@ describe('getMonitorStatus', () => { }; const result = await getMonitorStatus({ callES, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, ...clientParameters, }); expect(esMock.callAsCurrentUser).toHaveBeenCalledTimes(1); @@ -494,7 +494,7 @@ describe('getMonitorStatus', () => { const [callES] = setupMock(criteria); const result = await getMonitorStatus({ callES, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, locations: [], numTimes: 5, timerange: { diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts index faeb291bb533b..4de7d3ffd2a7d 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_ping_histogram.test.ts @@ -5,7 +5,7 @@ */ import { getPingHistogram } from '../get_ping_histogram'; -import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; describe('getPingHistogram', () => { const standardMockResponse: any = { @@ -59,7 +59,7 @@ describe('getPingHistogram', () => { const result = await getPingHistogram({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, from: 'now-15m', to: 'now', filters: null, @@ -78,7 +78,7 @@ describe('getPingHistogram', () => { const result = await getPingHistogram({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, from: 'now-15m', to: 'now', filters: null, @@ -140,7 +140,7 @@ describe('getPingHistogram', () => { const result = await getPingHistogram({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, from: '1234', to: '5678', filters: JSON.stringify(searchFilter), @@ -196,7 +196,7 @@ describe('getPingHistogram', () => { const filters = `{"bool":{"must":[{"simple_query_string":{"query":"http"}}]}}`; const result = await getPingHistogram({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, from: 'now-15m', to: 'now', filters, @@ -213,7 +213,7 @@ describe('getPingHistogram', () => { mockEsClient.mockReturnValue(standardMockResponse); const result = await getPingHistogram({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, from: '1234', to: '5678', filters: '', @@ -234,7 +234,7 @@ describe('getPingHistogram', () => { const result = await getPingHistogram({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, from: '1234', to: '5678', filters: '', diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts index fcf773db23de6..abd3655cc6402 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts @@ -6,7 +6,7 @@ import { getPings } from '../get_pings'; import { set } from 'lodash'; -import { defaultDynamicSettings } from '../../../../../../legacy/plugins/uptime/common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../../legacy/plugins/uptime/common/constants'; describe('getAll', () => { let mockEsSearchResult: any; @@ -62,7 +62,7 @@ describe('getAll', () => { }, }; expectedGetAllParams = { - index: defaultDynamicSettings.heartbeatIndices, + index: DYNAMIC_SETTINGS_DEFAULTS.heartbeatIndices, body: { query: { bool: { @@ -88,7 +88,7 @@ describe('getAll', () => { mockEsClient.mockReturnValue(mockEsSearchResult); const result = await getPings({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, dateRange: { from: 'now-1h', to: 'now' }, sort: 'asc', size: 12, @@ -110,7 +110,7 @@ describe('getAll', () => { mockEsClient.mockReturnValue(mockEsSearchResult); await getPings({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, dateRange: { from: 'now-1h', to: 'now' }, sort: 'asc', size: 12, @@ -166,7 +166,7 @@ describe('getAll', () => { mockEsClient.mockReturnValue(mockEsSearchResult); await getPings({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, dateRange: { from: 'now-1h', to: 'now' }, size: 12, }); @@ -220,7 +220,7 @@ describe('getAll', () => { mockEsClient.mockReturnValue(mockEsSearchResult); await getPings({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, dateRange: { from: 'now-1h', to: 'now' }, sort: 'desc', }); @@ -274,7 +274,7 @@ describe('getAll', () => { mockEsClient.mockReturnValue(mockEsSearchResult); await getPings({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, dateRange: { from: 'now-1h', to: 'now' }, monitorId: 'testmonitorid', }); @@ -333,7 +333,7 @@ describe('getAll', () => { mockEsClient.mockReturnValue(mockEsSearchResult); await getPings({ callES: mockEsClient, - dynamicSettings: defaultDynamicSettings, + dynamicSettings: DYNAMIC_SETTINGS_DEFAULTS, dateRange: { from: 'now-1h', to: 'now' }, status: 'down', }); diff --git a/x-pack/plugins/uptime/server/lib/saved_objects.ts b/x-pack/plugins/uptime/server/lib/saved_objects.ts index 3ccfd498c44bf..d849fbd8ce0a8 100644 --- a/x-pack/plugins/uptime/server/lib/saved_objects.ts +++ b/x-pack/plugins/uptime/server/lib/saved_objects.ts @@ -4,10 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - DynamicSettings, - defaultDynamicSettings, -} from '../../../../legacy/plugins/uptime/common/runtime_types'; +import { DynamicSettings } from '../../../../legacy/plugins/uptime/common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../legacy/plugins/uptime/common/constants'; import { SavedObjectsType, SavedObjectsErrorHelpers } from '../../../../../src/core/server'; import { UMSavedObjectsQueryFn } from './adapters'; @@ -28,12 +26,12 @@ export const umDynamicSettings: SavedObjectsType = { heartbeatIndices: { type: 'keyword', }, - certificatesThresholds: { + certThresholds: { properties: { - errorState: { + expiration: { type: 'long', }, - warningState: { + age: { type: 'long', }, }, @@ -49,7 +47,7 @@ export const savedObjectsAdapter: UMSavedObjectsAdapter = { return obj.attributes; } catch (getErr) { if (SavedObjectsErrorHelpers.isNotFoundError(getErr)) { - return defaultDynamicSettings; + return DYNAMIC_SETTINGS_DEFAULTS; } throw getErr; } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts index c84b089d48c85..e43f9dba4b2dc 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get.ts @@ -141,9 +141,6 @@ export default function getActionTests({ getService }: FtrProviderContext) { actionTypeId: '.slack', name: 'Slack#xyz', isPreconfigured: true, - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, }); break; default: diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts index 0b637326d4667..95b564e63d715 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/get_all.ts @@ -73,11 +73,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.index', name: 'preconfigured_es_index_action', - config: { - index: 'functional-test-actions-index-preconfigured', - refresh: true, - executionTimeField: 'timestamp', - }, referencedByCount: 0, }, { @@ -85,9 +80,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.slack', name: 'Slack#xyz', - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, referencedByCount: 0, }, { @@ -95,11 +87,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'system-abc-action-type', name: 'SystemABC', - config: { - xyzConfig1: 'value1', - xyzConfig2: 'value2', - listOfThings: ['a', 'b', 'c', 'd'], - }, referencedByCount: 0, }, { @@ -107,9 +94,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', - config: { - unencrypted: 'ignored-but-required', - }, referencedByCount: 0, }, ]); @@ -194,11 +178,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.index', name: 'preconfigured_es_index_action', - config: { - index: 'functional-test-actions-index-preconfigured', - refresh: true, - executionTimeField: 'timestamp', - }, referencedByCount: 0, }, { @@ -206,9 +185,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.slack', name: 'Slack#xyz', - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, referencedByCount: 1, }, { @@ -216,11 +192,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'system-abc-action-type', name: 'SystemABC', - config: { - xyzConfig1: 'value1', - xyzConfig2: 'value2', - listOfThings: ['a', 'b', 'c', 'd'], - }, referencedByCount: 0, }, { @@ -228,9 +199,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', - config: { - unencrypted: 'ignored-but-required', - }, referencedByCount: 0, }, ]); @@ -281,11 +249,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.index', name: 'preconfigured_es_index_action', - config: { - index: 'functional-test-actions-index-preconfigured', - refresh: true, - executionTimeField: 'timestamp', - }, referencedByCount: 0, }, { @@ -293,9 +256,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.slack', name: 'Slack#xyz', - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, referencedByCount: 0, }, { @@ -303,11 +263,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'system-abc-action-type', name: 'SystemABC', - config: { - xyzConfig1: 'value1', - xyzConfig2: 'value2', - listOfThings: ['a', 'b', 'c', 'd'], - }, referencedByCount: 0, }, { @@ -315,9 +270,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', - config: { - unencrypted: 'ignored-but-required', - }, referencedByCount: 0, }, ]); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts index a4a13441fb766..4eb8c16f4fb3a 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get.ts @@ -79,9 +79,6 @@ export default function getActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.slack', name: 'Slack#xyz', - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, }); }); }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts index ec59e56b08308..62abdddc6a1bf 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/actions/get_all.ts @@ -50,11 +50,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.index', name: 'preconfigured_es_index_action', - config: { - index: 'functional-test-actions-index-preconfigured', - refresh: true, - executionTimeField: 'timestamp', - }, referencedByCount: 0, }, { @@ -62,9 +57,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.slack', name: 'Slack#xyz', - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, referencedByCount: 0, }, { @@ -72,11 +64,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'system-abc-action-type', name: 'SystemABC', - config: { - xyzConfig1: 'value1', - xyzConfig2: 'value2', - listOfThings: ['a', 'b', 'c', 'd'], - }, referencedByCount: 0, }, { @@ -84,9 +71,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', - config: { - unencrypted: 'ignored-but-required', - }, referencedByCount: 0, }, ]); @@ -115,11 +99,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.index', name: 'preconfigured_es_index_action', - config: { - index: 'functional-test-actions-index-preconfigured', - refresh: true, - executionTimeField: 'timestamp', - }, referencedByCount: 0, }, { @@ -127,9 +106,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: '.slack', name: 'Slack#xyz', - config: { - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz', - }, referencedByCount: 0, }, { @@ -137,11 +113,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'system-abc-action-type', name: 'SystemABC', - config: { - xyzConfig1: 'value1', - xyzConfig2: 'value2', - listOfThings: ['a', 'b', 'c', 'd'], - }, referencedByCount: 0, }, { @@ -149,9 +120,6 @@ export default function getAllActionTests({ getService }: FtrProviderContext) { isPreconfigured: true, actionTypeId: 'test.index-record', name: 'Test:_Preconfigured_Index_Record', - config: { - unencrypted: 'ignored-but-required', - }, referencedByCount: 0, }, ]); diff --git a/x-pack/test/api_integration/apis/apm/agent_configuration.ts b/x-pack/test/api_integration/apis/apm/agent_configuration.ts index 41d78995711f2..8af648e062cf4 100644 --- a/x-pack/test/api_integration/apis/apm/agent_configuration.ts +++ b/x-pack/test/api_integration/apis/apm/agent_configuration.ts @@ -182,15 +182,21 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte service: { name: 'myservice', environment: 'development' }, settings: { transaction_sample_rate: '0.9' }, }; + const configProduction = { + service: { name: 'myservice', environment: 'production' }, + settings: { transaction_sample_rate: '0.9' }, + }; let etag: string; before(async () => { log.debug('creating agent configuration'); await createConfiguration(config); + await createConfiguration(configProduction); }); after(async () => { await deleteConfiguration(config); + await deleteConfiguration(configProduction); }); it(`should have 'applied_by_agent=false' before supplying etag`, async () => { @@ -210,17 +216,45 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte }); it(`should have 'applied_by_agent=true' after supplying etag`, async () => { - async function getAppliedByAgent() { + await searchConfigurations({ + service: { name: 'myservice', environment: 'development' }, + etag, + }); + + async function hasBeenAppliedByAgent() { const { body } = await searchConfigurations({ service: { name: 'myservice', environment: 'development' }, - etag, }); return body._source.applied_by_agent; } // wait until `applied_by_agent` has been updated in elasticsearch - expect(await waitFor(getAppliedByAgent)).to.be(true); + expect(await waitFor(hasBeenAppliedByAgent)).to.be(true); + }); + it(`should have 'applied_by_agent=false' before marking as applied`, async () => { + const res1 = await searchConfigurations({ + service: { name: 'myservice', environment: 'production' }, + }); + + expect(res1.body._source.applied_by_agent).to.be(false); + }); + it(`should have 'applied_by_agent=true' when 'mark_as_applied_by_agent' attribute is true`, async () => { + await searchConfigurations({ + service: { name: 'myservice', environment: 'production' }, + mark_as_applied_by_agent: true, + }); + + async function hasBeenAppliedByAgent() { + const { body } = await searchConfigurations({ + service: { name: 'myservice', environment: 'production' }, + }); + + return body._source.applied_by_agent; + } + + // wait until `applied_by_agent` has been updated in elasticsearch + expect(await waitFor(hasBeenAppliedByAgent)).to.be(true); }); }); }); diff --git a/x-pack/test/api_integration/apis/fleet/agents/acks.ts b/x-pack/test/api_integration/apis/fleet/agents/acks.ts index f08ce33d8b60f..adde6dd184b81 100644 --- a/x-pack/test/api_integration/apis/fleet/agents/acks.ts +++ b/x-pack/test/api_integration/apis/fleet/agents/acks.ts @@ -32,12 +32,12 @@ export default function(providerContext: FtrProviderContext) { body: { _source: agentDoc }, } = await esClient.get({ index: '.kibana', - id: 'agents:agent1', + id: 'fleet-agents:agent1', }); - agentDoc.agents.access_api_key_id = apiKey.id; + agentDoc['fleet-agents'].access_api_key_id = apiKey.id; await esClient.update({ index: '.kibana', - id: 'agents:agent1', + id: 'fleet-agents:agent1', refresh: 'true', body: { doc: agentDoc, diff --git a/x-pack/test/api_integration/apis/fleet/agents/actions.ts b/x-pack/test/api_integration/apis/fleet/agents/actions.ts index cf0641acf9e1c..577299e652610 100644 --- a/x-pack/test/api_integration/apis/fleet/agents/actions.ts +++ b/x-pack/test/api_integration/apis/fleet/agents/actions.ts @@ -67,7 +67,7 @@ export default function(providerContext: FtrProviderContext) { }, }) .expect(404); - expect(apiResponse.message).to.eql('Saved object [agents/agent100] not found'); + expect(apiResponse.message).to.eql('Saved object [fleet-agents/agent100] not found'); }); }); } diff --git a/x-pack/test/api_integration/apis/fleet/agents/checkin.ts b/x-pack/test/api_integration/apis/fleet/agents/checkin.ts index ca51676126e73..b405b5065bc0e 100644 --- a/x-pack/test/api_integration/apis/fleet/agents/checkin.ts +++ b/x-pack/test/api_integration/apis/fleet/agents/checkin.ts @@ -32,12 +32,12 @@ export default function(providerContext: FtrProviderContext) { body: { _source: agentDoc }, } = await esClient.get({ index: '.kibana', - id: 'agents:agent1', + id: 'fleet-agents:agent1', }); - agentDoc.agents.access_api_key_id = apiKey.id; + agentDoc['fleet-agents'].access_api_key_id = apiKey.id; await esClient.update({ index: '.kibana', - id: 'agents:agent1', + id: 'fleet-agents:agent1', refresh: 'true', body: { doc: agentDoc, diff --git a/x-pack/test/api_integration/apis/fleet/agents/enroll.ts b/x-pack/test/api_integration/apis/fleet/agents/enroll.ts index d8e9749744ea4..c934ddf8a406b 100644 --- a/x-pack/test/api_integration/apis/fleet/agents/enroll.ts +++ b/x-pack/test/api_integration/apis/fleet/agents/enroll.ts @@ -33,13 +33,13 @@ export default function(providerContext: FtrProviderContext) { body: { _source: enrollmentApiKeyDoc }, } = await esClient.get({ index: '.kibana', - id: 'enrollment_api_keys:ed22ca17-e178-4cfe-8b02-54ea29fbd6d0', + id: 'fleet-enrollment-api-keys:ed22ca17-e178-4cfe-8b02-54ea29fbd6d0', }); // @ts-ignore - enrollmentApiKeyDoc.enrollment_api_keys.api_key_id = apiKey.id; + enrollmentApiKeyDoc['fleet-enrollment-api-keys'].api_key_id = apiKey.id; await esClient.update({ index: '.kibana', - id: 'enrollment_api_keys:ed22ca17-e178-4cfe-8b02-54ea29fbd6d0', + id: 'fleet-enrollment-api-keys:ed22ca17-e178-4cfe-8b02-54ea29fbd6d0', refresh: 'true', body: { doc: enrollmentApiKeyDoc, diff --git a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts index 2acfca63995f1..5b8e03269ceef 100644 --- a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts +++ b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts @@ -40,17 +40,18 @@ export default function(providerContext: FtrProviderContext) { body: { _source: agentDoc }, } = await esClient.get({ index: '.kibana', - id: 'agents:agent1', + id: 'fleet-agents:agent1', }); // @ts-ignore - agentDoc.agents.access_api_key_id = accessAPIKeyId; - agentDoc.agents.default_api_key = Buffer.from( + agentDoc['fleet-agents'].access_api_key_id = accessAPIKeyId; + agentDoc['fleet-agents'].default_api_key_id = outputAPIKeyBody.id; + agentDoc['fleet-agents'].default_api_key = Buffer.from( `${outputAPIKeyBody.id}:${outputAPIKeyBody.api_key}` ).toString('base64'); await esClient.update({ index: '.kibana', - id: 'agents:agent1', + id: 'fleet-agents:agent1', refresh: 'true', body: { doc: agentDoc, diff --git a/x-pack/test/api_integration/apis/infra/log_entries.ts b/x-pack/test/api_integration/apis/infra/log_entries.ts index 3c12f5e4dc789..991dc4a7f96cf 100644 --- a/x-pack/test/api_integration/apis/infra/log_entries.ts +++ b/x-pack/test/api_integration/apis/infra/log_entries.ts @@ -126,7 +126,7 @@ export default function({ getService }: FtrProviderContext) { expect(messageColumn.message.length).to.be.greaterThan(0); }); - it('Returns the context fields', async () => { + it('Does not build context if entry does not have all fields', async () => { const { body } = await supertest .post(LOG_ENTRIES_PATH) .set(COMMON_HEADERS) @@ -147,9 +147,7 @@ export default function({ getService }: FtrProviderContext) { const entries = logEntriesResponse.data.entries; const entry = entries[0]; - - expect(entry.context).to.have.property('host.name'); - expect(entry.context['host.name']).to.be('demo-stack-nginx-01'); + expect(entry.context).to.eql({}); }); it('Paginates correctly with `after`', async () => { diff --git a/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts b/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts new file mode 100644 index 0000000000000..bbc766df34dcf --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/anomaly_detectors/create.ts @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const jobId = `fq_single_${Date.now()}`; + + const testDataList = [ + { + testTitle: 'ML Poweruser creates a single metric job', + user: USER.ML_POWERUSER, + jobId: `${jobId}_1`, + requestBody: { + job_id: `${jobId}_1`, + description: + 'Single metric job based on the farequote dataset with 30m bucketspan and mean(responsetime)', + groups: ['automated', 'farequote', 'single-metric'], + analysis_config: { + bucket_span: '30m', + detectors: [{ function: 'mean', field_name: 'responsetime' }], + influencers: [], + summary_count_field_name: 'doc_count', + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '11MB' }, + model_plot_config: { enabled: true }, + }, + expected: { + responseCode: 200, + responseBody: { + job_id: `${jobId}_1`, + job_type: 'anomaly_detector', + groups: ['automated', 'farequote', 'single-metric'], + description: + 'Single metric job based on the farequote dataset with 30m bucketspan and mean(responsetime)', + analysis_config: { + bucket_span: '30m', + summary_count_field_name: 'doc_count', + detectors: [ + { + detector_description: 'mean(responsetime)', + function: 'mean', + field_name: 'responsetime', + detector_index: 0, + }, + ], + influencers: [], + }, + analysis_limits: { model_memory_limit: '11mb', categorization_examples_limit: 4 }, + data_description: { time_field: '@timestamp', time_format: 'epoch_ms' }, + model_plot_config: { enabled: true }, + model_snapshot_retention_days: 1, + results_index_name: 'shared', + allow_lazy_open: false, + }, + }, + }, + { + testTitle: 'ML viewer cannot create a job', + user: USER.ML_VIEWER, + jobId: `${jobId}_2`, + requestBody: { + job_id: `${jobId}_2`, + description: + 'Single metric job based on the farequote dataset with 30m bucketspan and mean(responsetime)', + groups: ['automated', 'farequote', 'single-metric'], + analysis_config: { + bucket_span: '30m', + detectors: [{ function: 'mean', field_name: 'responsetime' }], + influencers: [], + summary_count_field_name: 'doc_count', + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '11MB' }, + model_plot_config: { enabled: true }, + }, + expected: { + responseCode: 403, + responseBody: { + statusCode: 403, + error: 'Forbidden', + message: + '[security_exception] action [cluster:admin/xpack/ml/job/put] is unauthorized for user [ml_viewer]', + }, + }, + }, + ]; + + describe('create', function() { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + for (const testData of testDataList) { + it(`${testData.testTitle}`, async () => { + const { body } = await supertest + .put(`/api/ml/anomaly_detectors/${testData.jobId}`) + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) + .set(COMMON_HEADERS) + .send(testData.requestBody) + .expect(testData.expected.responseCode); + + if (body.error === undefined) { + // Validate the important parts of the response. + const expectedResponse = testData.expected.responseBody; + expect(body.job_id).to.eql(expectedResponse.job_id); + expect(body.groups).to.eql(expectedResponse.groups); + expect(body.analysis_config!.bucket_span).to.eql( + expectedResponse.analysis_config!.bucket_span + ); + expect(body.analysis_config.detectors).to.have.length( + expectedResponse.analysis_config!.detectors.length + ); + expect(body.analysis_config.detectors[0]).to.eql( + expectedResponse.analysis_config!.detectors[0] + ); + } else { + expect(body.error).to.eql(testData.expected.responseBody.error); + expect(body.message).to.eql(testData.expected.responseBody.message); + } + }); + } + }); +}; diff --git a/x-pack/legacy/plugins/rollup/server/index.ts b/x-pack/test/api_integration/apis/ml/anomaly_detectors/index.ts similarity index 51% rename from x-pack/legacy/plugins/rollup/server/index.ts rename to x-pack/test/api_integration/apis/ml/anomaly_detectors/index.ts index 6bbd00ac6576e..fb8acaf5c3ae9 100644 --- a/x-pack/legacy/plugins/rollup/server/index.ts +++ b/x-pack/test/api_integration/apis/ml/anomaly_detectors/index.ts @@ -3,7 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'src/core/server'; -import { RollupsServerPlugin } from './plugin'; +import { FtrProviderContext } from '../../../ftr_provider_context'; -export const plugin = (ctx: PluginInitializerContext) => new RollupsServerPlugin(ctx); +export default function({ loadTestFile }: FtrProviderContext) { + describe('anomaly detectors', function() { + loadTestFile(require.resolve('./create')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts new file mode 100644 index 0000000000000..dfa81b5d78c65 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts @@ -0,0 +1,248 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const metricFieldsTestData = { + testTitle: 'returns stats for metric fields over all time', + index: 'ft_farequote', + user: USER.ML_POWERUSER, + requestBody: { + query: { + bool: { + must: { + term: { airline: 'JZA' }, // Only use one airline to ensure no sampling. + }, + }, + }, + fields: [ + { type: 'number', cardinality: 0 }, + { fieldName: 'responsetime', type: 'number', cardinality: 4249 }, + ], + samplerShardSize: -1, // No sampling, as otherwise counts could vary on each run. + timeFieldName: '@timestamp', + interval: '1d', + maxExamples: 10, + }, + expected: { + responseCode: 200, + responseBody: [ + { + documentCounts: { + interval: '1d', + buckets: { + '1454803200000': 846, + '1454889600000': 846, + '1454976000000': 859, + '1455062400000': 851, + '1455148800000': 858, + }, + }, + }, + { + // Cannot verify median and percentiles responses as the ES percentiles agg is non-deterministic. + fieldName: 'responsetime', + count: 4260, + min: 963.4293212890625, + max: 1042.13525390625, + avg: 1000.0378077547315, + isTopValuesSampled: false, + topValues: [ + { key: 980.0411987304688, doc_count: 2 }, + { key: 989.278076171875, doc_count: 2 }, + { key: 989.763916015625, doc_count: 2 }, + { key: 991.290771484375, doc_count: 2 }, + { key: 992.0765991210938, doc_count: 2 }, + { key: 993.8115844726562, doc_count: 2 }, + { key: 993.8973999023438, doc_count: 2 }, + { key: 994.0230102539062, doc_count: 2 }, + { key: 994.364990234375, doc_count: 2 }, + { key: 994.916015625, doc_count: 2 }, + ], + topValuesSampleSize: 4260, + topValuesSamplerShardSize: -1, + }, + ], + }, + }; + + const nonMetricFieldsTestData = { + testTitle: 'returns stats for non-metric fields specifying query and time range', + index: 'ft_farequote', + user: USER.ML_POWERUSER, + requestBody: { + query: { + bool: { + must: { + term: { airline: 'AAL' }, + }, + }, + }, + fields: [ + { fieldName: '@timestamp', type: 'date', cardinality: 4751 }, + { fieldName: '@version.keyword', type: 'keyword', cardinality: 1 }, + { fieldName: 'airline', type: 'keyword', cardinality: 19 }, + { fieldName: 'type', type: 'text', cardinality: 0 }, + { fieldName: 'type.keyword', type: 'keyword', cardinality: 1 }, + ], + samplerShardSize: -1, // No sampling, as otherwise counts would vary on each run. + timeFieldName: '@timestamp', + earliest: 1454889600000, // February 8, 2016 12:00:00 AM GMT + latest: 1454976000000, // February 9, 2016 12:00:00 AM GMT + maxExamples: 10, + }, + expected: { + responseCode: 200, + responseBody: [ + { fieldName: '@timestamp', count: 1733, earliest: 1454889602000, latest: 1454975948000 }, + { + fieldName: '@version.keyword', + isTopValuesSampled: false, + topValues: [{ key: '1', doc_count: 1733 }], + topValuesSampleSize: 1733, + topValuesSamplerShardSize: -1, + }, + { + fieldName: 'airline', + isTopValuesSampled: false, + topValues: [{ key: 'AAL', doc_count: 1733 }], + topValuesSampleSize: 1733, + topValuesSamplerShardSize: -1, + }, + { + fieldName: 'type.keyword', + isTopValuesSampled: false, + topValues: [{ key: 'farequote', doc_count: 1733 }], + topValuesSampleSize: 1733, + topValuesSamplerShardSize: -1, + }, + { fieldName: 'type', examples: ['farequote'] }, + ], + }, + }; + + const errorTestData = { + testTitle: 'returns error for index which does not exist', + index: 'ft_farequote_not_exists', + user: USER.ML_POWERUSER, + requestBody: { + query: { bool: { must: [{ match_all: {} }] } }, + fields: [ + { type: 'number', cardinality: 0 }, + { fieldName: 'responsetime', type: 'number', cardinality: 4249 }, + ], + samplerShardSize: -1, // No sampling, as otherwise counts could vary on each run. + timeFieldName: '@timestamp', + maxExamples: 10, + }, + expected: { + responseCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: + '[index_not_found_exception] no such index [ft_farequote_not_exists], with { resource.type="index_or_alias" & resource.id="ft_farequote_not_exists" & index_uuid="_na_" & index="ft_farequote_not_exists" }', + }, + }, + }; + + async function runGetFieldStatsRequest( + index: string, + user: USER, + requestBody: object, + expectedResponsecode: number + ): Promise { + const { body } = await supertest + .post(`/api/ml/data_visualizer/get_field_stats/${index}`) + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(COMMON_HEADERS) + .send(requestBody) + .expect(expectedResponsecode); + + return body; + } + + function compareByFieldName(a: { fieldName: string }, b: { fieldName: string }) { + if (a.fieldName < b.fieldName) { + return -1; + } + if (a.fieldName > b.fieldName) { + return 1; + } + return 0; + } + + describe('get_field_stats', function() { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + it(`${metricFieldsTestData.testTitle}`, async () => { + const body = await runGetFieldStatsRequest( + metricFieldsTestData.index, + metricFieldsTestData.user, + metricFieldsTestData.requestBody, + metricFieldsTestData.expected.responseCode + ); + + // Cannot verify median and percentiles responses as the ES percentiles agg is non-deterministic. + const expected = metricFieldsTestData.expected; + expect(body).to.have.length(expected.responseBody.length); + + const actualDocCounts = body[0]; + const expectedDocCounts = expected.responseBody[0]; + expect(actualDocCounts).to.eql(expectedDocCounts); + + const actualFieldData = { ...body[1] }; + delete actualFieldData.median; + delete actualFieldData.distribution; + + expect(actualFieldData).to.eql(expected.responseBody[1]); + }); + + it(`${nonMetricFieldsTestData.testTitle}`, async () => { + const body = await runGetFieldStatsRequest( + nonMetricFieldsTestData.index, + nonMetricFieldsTestData.user, + nonMetricFieldsTestData.requestBody, + nonMetricFieldsTestData.expected.responseCode + ); + + // Sort the fields in the response before validating. + const expectedRspFields = nonMetricFieldsTestData.expected.responseBody.sort( + compareByFieldName + ); + const actualRspFields = body.sort(compareByFieldName); + expect(actualRspFields).to.eql(expectedRspFields); + }); + + it(`${errorTestData.testTitle}`, async () => { + const body = await runGetFieldStatsRequest( + errorTestData.index, + errorTestData.user, + errorTestData.requestBody, + errorTestData.expected.responseCode + ); + + expect(body.error).to.eql(errorTestData.expected.responseBody.error); + expect(body.message).to.eql(errorTestData.expected.responseBody.message); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts new file mode 100644 index 0000000000000..6490c19c64483 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/data_visualizer/get_overall_stats.ts @@ -0,0 +1,154 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testDataList = [ + { + testTitle: 'returns stats over all time', + index: 'ft_farequote', + user: USER.ML_POWERUSER, + requestBody: { + query: { bool: { must: [{ match_all: {} }] } }, + aggregatableFields: ['@timestamp', 'airline', 'responsetime', 'sourcetype'], + nonAggregatableFields: ['type'], + samplerShardSize: -1, // No sampling, as otherwise counts would vary on each run. + timeFieldName: '@timestamp', + }, + expected: { + responseCode: 200, + responseBody: { + totalCount: 86274, + aggregatableExistsFields: [ + { + fieldName: '@timestamp', + existsInDocs: true, + stats: { sampleCount: 86274, count: 86274, cardinality: 78580 }, + }, + { + fieldName: 'airline', + existsInDocs: true, + stats: { sampleCount: 86274, count: 86274, cardinality: 19 }, + }, + { + fieldName: 'responsetime', + existsInDocs: true, + stats: { sampleCount: 86274, count: 86274, cardinality: 83346 }, + }, + ], + aggregatableNotExistsFields: [{ fieldName: 'sourcetype', existsInDocs: false }], + nonAggregatableExistsFields: [{ fieldName: 'type', existsInDocs: true, stats: {} }], + nonAggregatableNotExistsFields: [], + }, + }, + }, + { + testTitle: 'returns stats when specifying query and time range', + index: 'ft_farequote', + user: USER.ML_POWERUSER, + requestBody: { + query: { + bool: { + must: { + term: { airline: 'AAL' }, + }, + }, + }, + aggregatableFields: ['@timestamp', 'airline', 'responsetime', 'sourcetype'], + nonAggregatableFields: ['type'], + samplerShardSize: -1, // No sampling, as otherwise counts would vary on each run. + timeFieldName: '@timestamp', + earliest: 1454889600000, // February 8, 2016 12:00:00 AM GMT + latest: 1454976000000, // February 9, 2016 12:00:00 AM GMT + }, + expected: { + responseCode: 200, + responseBody: { + totalCount: 1733, + aggregatableExistsFields: [ + { + fieldName: '@timestamp', + existsInDocs: true, + stats: { sampleCount: 1733, count: 1733, cardinality: 1713 }, + }, + { + fieldName: 'airline', + existsInDocs: true, + stats: { sampleCount: 1733, count: 1733, cardinality: 1 }, + }, + { + fieldName: 'responsetime', + existsInDocs: true, + stats: { sampleCount: 1733, count: 1733, cardinality: 1730 }, + }, + ], + aggregatableNotExistsFields: [{ fieldName: 'sourcetype', existsInDocs: false }], + nonAggregatableExistsFields: [{ fieldName: 'type', existsInDocs: true, stats: {} }], + nonAggregatableNotExistsFields: [], + }, + }, + }, + { + testTitle: 'returns error for index which does not exist', + index: 'ft_farequote_not_exist', + user: USER.ML_POWERUSER, + requestBody: { + query: { bool: { must: [{ match_all: {} }] } }, + aggregatableFields: ['@timestamp', 'airline', 'responsetime', 'sourcetype'], + nonAggregatableFields: ['@version', 'type'], + samplerShardSize: 1000, + timeFieldName: '@timestamp', + }, + expected: { + responseCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: + '[index_not_found_exception] no such index [ft_farequote_not_exist], with { resource.type="index_or_alias" & resource.id="ft_farequote_not_exist" & index_uuid="_na_" & index="ft_farequote_not_exist" }', + }, + }, + }, + ]; + + describe('get_overall_stats', function() { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + for (const testData of testDataList) { + it(`${testData.testTitle}`, async () => { + const { body } = await supertest + .post(`/api/ml/data_visualizer/get_overall_stats/${testData.index}`) + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) + .set(COMMON_HEADERS) + .send(testData.requestBody) + .expect(testData.expected.responseCode); + + if (body.error === undefined) { + expect(body).to.eql(testData.expected.responseBody); + } else { + expect(body.error).to.eql(testData.expected.responseBody.error); + expect(body.message).to.eql(testData.expected.responseBody.message); + } + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/index.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/index.ts new file mode 100644 index 0000000000000..ce9e44618f1af --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/data_visualizer/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('data visualizer', function() { + loadTestFile(require.resolve('./get_field_stats')); + loadTestFile(require.resolve('./get_overall_stats')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/fields_service/field_cardinality.ts b/x-pack/test/api_integration/apis/ml/fields_service/field_cardinality.ts new file mode 100644 index 0000000000000..245375562b5c1 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/fields_service/field_cardinality.ts @@ -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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testDataList = [ + { + testTitle: 'returns cardinality of customer name fields over full time range', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce', + fieldNames: ['customer_first_name.keyword', 'customer_last_name.keyword'], + query: { bool: { must: [{ match_all: {} }] } }, + timeFieldName: 'order_date', + }, + expected: { + responseBody: { + 'customer_first_name.keyword': 46, + 'customer_last_name.keyword': 183, + }, + }, + }, + { + testTitle: 'returns cardinality of geoip fields over specified range', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce', + fieldNames: ['geoip.city_name', 'geoip.continent_name', 'geoip.country_iso_code'], + query: { bool: { must: [{ match_all: {} }] } }, + timeFieldName: 'order_date', + earliestMs: 1560556800000, // June 15, 2019 12:00:00 AM GMT + latestMs: 1560643199000, // June 15, 2019 11:59:59 PM GMT + }, + expected: { + responseBody: { + 'geoip.city_name': 10, + 'geoip.continent_name': 5, + 'geoip.country_iso_code': 9, + }, + }, + }, + { + testTitle: 'returns empty response for non aggregatable field', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce', + fieldNames: ['manufacturer'], + query: { bool: { must: [{ match_all: {} }] } }, + timeFieldName: 'order_date', + earliestMs: 1560556800000, // June 15, 2019 12:00:00 AM GMT + latestMs: 1560643199000, // June 15, 2019 11:59:59 PM GMT + }, + expected: { + responseBody: {}, + }, + }, + { + testTitle: 'returns error for index which does not exist', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce_not_exist', + fieldNames: ['customer_first_name.keyword', 'customer_last_name.keyword'], + timeFieldName: 'order_date', + }, + expected: { + responseBody: { + statusCode: 404, + error: 'Not Found', + message: + '[index_not_found_exception] no such index [ft_ecommerce_not_exist], with { resource.type="index_or_alias" & resource.id="ft_ecommerce_not_exist" & index_uuid="_na_" & index="ft_ecommerce_not_exist" }', + }, + }, + }, + ]; + + describe('field_cardinality', function() { + before(async () => { + await esArchiver.loadIfNeeded('ml/ecommerce'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + for (const testData of testDataList) { + it(`${testData.testTitle}`, async () => { + const { body } = await supertest + .post('/api/ml/fields_service/field_cardinality') + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) + .set(COMMON_HEADERS) + .send(testData.requestBody); + + if (body.error === undefined) { + expect(body).to.eql(testData.expected.responseBody); + } else { + expect(body.error).to.eql(testData.expected.responseBody.error); + expect(body.message).to.eql(testData.expected.responseBody.message); + } + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/fields_service/index.ts b/x-pack/test/api_integration/apis/ml/fields_service/index.ts new file mode 100644 index 0000000000000..312602e589119 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/fields_service/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('fields service', function() { + loadTestFile(require.resolve('./field_cardinality')); + loadTestFile(require.resolve('./time_field_range')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/fields_service/time_field_range.ts b/x-pack/test/api_integration/apis/ml/fields_service/time_field_range.ts new file mode 100644 index 0000000000000..2f0fd4fc6c5e3 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/fields_service/time_field_range.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testDataList = [ + { + testTitle: 'returns expected time range with index and match_all query', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce', + query: { bool: { must: [{ match_all: {} }] } }, + timeFieldName: 'order_date', + }, + expected: { + responseCode: 200, + responseBody: { + start: { + epoch: 1560297859000, + string: '2019-06-12T00:04:19.000Z', + }, + end: { + epoch: 1562975136000, + string: '2019-07-12T23:45:36.000Z', + }, + success: true, + }, + }, + }, + { + testTitle: 'returns expected time range with index and query', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce', + query: { + term: { + 'customer_first_name.keyword': { + value: 'Brigitte', + }, + }, + }, + timeFieldName: 'order_date', + }, + expected: { + responseCode: 200, + responseBody: { + start: { + epoch: 1560298982000, + string: '2019-06-12T00:23:02.000Z', + }, + end: { + epoch: 1562973754000, + string: '2019-07-12T23:22:34.000Z', + }, + success: true, + }, + }, + }, + { + testTitle: 'returns error for index which does not exist', + user: USER.ML_POWERUSER, + requestBody: { + index: 'ft_ecommerce_not_exist', + query: { bool: { must: [{ match_all: {} }] } }, + timeFieldName: 'order_date', + }, + expected: { + responseCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: + '[index_not_found_exception] no such index [ft_ecommerce_not_exist], with { resource.type="index_or_alias" & resource.id="ft_ecommerce_not_exist" & index_uuid="_na_" & index="ft_ecommerce_not_exist" }', + }, + }, + }, + ]; + + describe('time_field_range', function() { + before(async () => { + await esArchiver.loadIfNeeded('ml/ecommerce'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + for (const testData of testDataList) { + it(`${testData.testTitle}`, async () => { + const { body } = await supertest + .post('/api/ml/fields_service/time_field_range') + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) + .set(COMMON_HEADERS) + .send(testData.requestBody) + .expect(testData.expected.responseCode); + + if (body.error === undefined) { + expect(body).to.eql(testData.expected.responseBody); + } else { + expect(body.error).to.eql(testData.expected.responseBody.error); + expect(body.message).to.eql(testData.expected.responseBody.message); + } + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index f012883c46ca3..58356637c63ac 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -31,11 +31,11 @@ export default function({ getService, loadTestFile }: FtrProviderContext) { await ml.testResources.resetKibanaTimeZone(); }); - loadTestFile(require.resolve('./bucket_span_estimator')); - loadTestFile(require.resolve('./calculate_model_memory_limit')); - loadTestFile(require.resolve('./categorization_field_examples')); - loadTestFile(require.resolve('./get_module')); - loadTestFile(require.resolve('./recognize_module')); - loadTestFile(require.resolve('./setup_module')); + loadTestFile(require.resolve('./modules')); + loadTestFile(require.resolve('./anomaly_detectors')); + loadTestFile(require.resolve('./data_visualizer')); + loadTestFile(require.resolve('./fields_service')); + loadTestFile(require.resolve('./job_validation')); + loadTestFile(require.resolve('./jobs')); }); } diff --git a/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts b/x-pack/test/api_integration/apis/ml/job_validation/bucket_span_estimator.ts similarity index 97% rename from x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts rename to x-pack/test/api_integration/apis/ml/job_validation/bucket_span_estimator.ts index bc0dc3019d7c9..0b4aca9660be4 100644 --- a/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts +++ b/x-pack/test/api_integration/apis/ml/job_validation/bucket_span_estimator.ts @@ -6,8 +6,8 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { USER } from '../../../functional/services/machine_learning/security_common'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; const COMMON_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', diff --git a/x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts b/x-pack/test/api_integration/apis/ml/job_validation/calculate_model_memory_limit.ts similarity index 97% rename from x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts rename to x-pack/test/api_integration/apis/ml/job_validation/calculate_model_memory_limit.ts index 59e3dfcca00f9..f17814633ce8f 100644 --- a/x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts +++ b/x-pack/test/api_integration/apis/ml/job_validation/calculate_model_memory_limit.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FtrProviderContext } from '../../ftr_provider_context'; -import { USER } from '../../../functional/services/machine_learning/security_common'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; const COMMON_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', diff --git a/x-pack/test/api_integration/apis/ml/job_validation/index.ts b/x-pack/test/api_integration/apis/ml/job_validation/index.ts new file mode 100644 index 0000000000000..6ca9dcbbe9e5b --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/job_validation/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('job validation', function() { + loadTestFile(require.resolve('./bucket_span_estimator')); + loadTestFile(require.resolve('./calculate_model_memory_limit')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts b/x-pack/test/api_integration/apis/ml/jobs/categorization_field_examples.ts similarity index 98% rename from x-pack/test/api_integration/apis/ml/categorization_field_examples.ts rename to x-pack/test/api_integration/apis/ml/jobs/categorization_field_examples.ts index df0153f965942..bcc6c4907100c 100644 --- a/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts +++ b/x-pack/test/api_integration/apis/ml/jobs/categorization_field_examples.ts @@ -6,8 +6,8 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { USER } from '../../../functional/services/machine_learning/security_common'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; const COMMON_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', diff --git a/x-pack/test/api_integration/apis/ml/jobs/index.ts b/x-pack/test/api_integration/apis/ml/jobs/index.ts new file mode 100644 index 0000000000000..70a64f198d6f4 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/jobs/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('jobs', function() { + loadTestFile(require.resolve('./categorization_field_examples')); + loadTestFile(require.resolve('./jobs_summary')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts b/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts new file mode 100644 index 0000000000000..6a57db1687868 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/jobs/jobs_summary.ts @@ -0,0 +1,374 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; +import { Job } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +const SINGLE_METRIC_JOB_CONFIG: Job = { + job_id: `jobs_summary_fq_single_${Date.now()}`, + description: 'mean(responsetime) on farequote dataset with 15m bucket span', + groups: ['farequote', 'automated', 'single-metric'], + analysis_config: { + bucket_span: '15m', + influencers: [], + detectors: [ + { + function: 'mean', + field_name: 'responsetime', + }, + ], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '10mb' }, + model_plot_config: { enabled: true }, +}; + +const MULTI_METRIC_JOB_CONFIG: Job = { + job_id: `jobs_summary_fq_multi_${Date.now()}`, + description: 'mean(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: true }, +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testSetupJobConfigs = [SINGLE_METRIC_JOB_CONFIG, MULTI_METRIC_JOB_CONFIG]; + + const testDataListNoJobId = [ + { + testTitle: 'as ML Poweruser', + user: USER.ML_POWERUSER, + requestBody: {}, + expected: { + responseCode: 200, + responseBody: [ + { + id: SINGLE_METRIC_JOB_CONFIG.job_id, + description: SINGLE_METRIC_JOB_CONFIG.description, + groups: SINGLE_METRIC_JOB_CONFIG.groups, + processed_record_count: 0, + memory_status: 'ok', + jobState: 'closed', + hasDatafeed: false, + datafeedId: '', + datafeedIndices: [], + datafeedState: '', + isSingleMetricViewerJob: true, + }, + { + id: MULTI_METRIC_JOB_CONFIG.job_id, + description: MULTI_METRIC_JOB_CONFIG.description, + groups: MULTI_METRIC_JOB_CONFIG.groups, + processed_record_count: 0, + memory_status: 'ok', + jobState: 'closed', + hasDatafeed: false, + datafeedId: '', + datafeedIndices: [], + datafeedState: '', + isSingleMetricViewerJob: true, + }, + ], + }, + }, + { + testTitle: 'as ML Viewer', + user: USER.ML_VIEWER, + requestBody: {}, + expected: { + responseCode: 200, + responseBody: [ + { + id: SINGLE_METRIC_JOB_CONFIG.job_id, + description: SINGLE_METRIC_JOB_CONFIG.description, + groups: SINGLE_METRIC_JOB_CONFIG.groups, + processed_record_count: 0, + memory_status: 'ok', + jobState: 'closed', + hasDatafeed: false, + datafeedId: '', + datafeedIndices: [], + datafeedState: '', + isSingleMetricViewerJob: true, + }, + { + id: MULTI_METRIC_JOB_CONFIG.job_id, + description: MULTI_METRIC_JOB_CONFIG.description, + groups: MULTI_METRIC_JOB_CONFIG.groups, + processed_record_count: 0, + memory_status: 'ok', + jobState: 'closed', + hasDatafeed: false, + datafeedId: '', + datafeedIndices: [], + datafeedState: '', + isSingleMetricViewerJob: true, + }, + ], + }, + }, + ]; + + const testDataListWithJobId = [ + { + testTitle: 'as ML Poweruser', + user: USER.ML_POWERUSER, + requestBody: { + jobIds: [SINGLE_METRIC_JOB_CONFIG.job_id], + }, + expected: { + responseCode: 200, + responseBody: [ + { + id: SINGLE_METRIC_JOB_CONFIG.job_id, + description: SINGLE_METRIC_JOB_CONFIG.description, + groups: SINGLE_METRIC_JOB_CONFIG.groups, + processed_record_count: 0, + memory_status: 'ok', + jobState: 'closed', + hasDatafeed: false, + datafeedId: '', + datafeedIndices: [], + datafeedState: '', + isSingleMetricViewerJob: true, + fullJob: { + // Only tests against some of the fields in the fullJob property. + job_id: SINGLE_METRIC_JOB_CONFIG.job_id, + job_type: 'anomaly_detector', + description: SINGLE_METRIC_JOB_CONFIG.description, + groups: SINGLE_METRIC_JOB_CONFIG.groups, + analysis_config: { + bucket_span: '15m', + detectors: [ + { + detector_description: 'mean(responsetime)', + function: 'mean', + field_name: 'responsetime', + detector_index: 0, + }, + ], + influencers: [], + }, + }, + }, + { + id: MULTI_METRIC_JOB_CONFIG.job_id, + description: MULTI_METRIC_JOB_CONFIG.description, + groups: MULTI_METRIC_JOB_CONFIG.groups, + processed_record_count: 0, + memory_status: 'ok', + jobState: 'closed', + hasDatafeed: false, + datafeedId: '', + datafeedIndices: [], + datafeedState: '', + isSingleMetricViewerJob: true, + }, + ], + }, + }, + ]; + + const testDataListNegative = [ + { + testTitle: 'as ML Unauthorized user', + user: USER.ML_UNAUTHORIZED, + requestBody: {}, + // Note that the jobs and datafeeds are loaded async so the actual error message is not deterministic. + expected: { + responseCode: 403, + error: 'Forbidden', + }, + }, + ]; + + async function runJobsSummaryRequest( + user: USER, + requestBody: object, + expectedResponsecode: number + ): Promise { + const { body } = await supertest + .post('/api/ml/jobs/jobs_summary') + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(COMMON_HEADERS) + .send(requestBody) + .expect(expectedResponsecode); + + return body; + } + + function compareById(a: { id: string }, b: { id: string }) { + if (a.id < b.id) { + return -1; + } + if (a.id > b.id) { + return 1; + } + return 0; + } + + function getGroups(jobs: Array<{ groups: string[] }>) { + const groupIds: string[] = []; + jobs.forEach(job => { + const groups = job.groups; + groups.forEach(group => { + if (groupIds.indexOf(group) === -1) { + groupIds.push(group); + } + }); + }); + return groupIds.sort(); + } + + describe('jobs_summary', function() { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('sets up jobs', async () => { + for (const job of testSetupJobConfigs) { + await ml.api.createAnomalyDetectionJob(job); + } + }); + + for (const testData of testDataListNoJobId) { + describe('gets job summary with no job IDs supplied', function() { + it(`${testData.testTitle}`, async () => { + const body = await runJobsSummaryRequest( + testData.user, + testData.requestBody, + testData.expected.responseCode + ); + + // Validate the important parts of the response. + const expectedResponse = testData.expected.responseBody; + + // Validate job count. + expect(body).to.have.length(expectedResponse.length); + + // Validate job IDs. + const expectedRspJobIds = expectedResponse + .map((job: { id: string }) => { + return { id: job.id }; + }) + .sort(compareById); + const actualRspJobIds = body + .map((job: { id: string }) => { + return { id: job.id }; + }) + .sort(compareById); + + expect(actualRspJobIds).to.eql(expectedRspJobIds); + + // Validate created group IDs. + const expectedRspGroupIds = getGroups(expectedResponse); + const actualRspGroupsIds = getGroups(body); + expect(actualRspGroupsIds).to.eql(expectedRspGroupIds); + }); + }); + } + + for (const testData of testDataListWithJobId) { + describe('gets job summary with job ID supplied', function() { + it(`${testData.testTitle}`, async () => { + const body = await runJobsSummaryRequest( + testData.user, + testData.requestBody, + testData.expected.responseCode + ); + + // Validate the important parts of the response. + const expectedResponse = testData.expected.responseBody; + + // Validate job count. + expect(body).to.have.length(expectedResponse.length); + + // Validate job IDs. + const expectedRspJobIds = expectedResponse + .map((job: { id: string }) => { + return { id: job.id }; + }) + .sort(compareById); + const actualRspJobIds = body + .map((job: { id: string }) => { + return { id: job.id }; + }) + .sort(compareById); + + expect(actualRspJobIds).to.eql(expectedRspJobIds); + + // Validate created group IDs. + const expectedRspGroupIds = getGroups(expectedResponse); + const actualRspGroupsIds = getGroups(body); + expect(actualRspGroupsIds).to.eql(expectedRspGroupIds); + + // Validate the response for the specified job IDs contains a fullJob property. + const requestedJobIds = testData.requestBody.jobIds; + for (const job of body) { + if (requestedJobIds.includes(job.id)) { + expect(job).to.have.property('fullJob'); + } else { + expect(job).not.to.have.property('fullJob'); + } + } + + for (const expectedJob of expectedResponse) { + const expectedJobId = expectedJob.id; + const actualJob = body.find((job: { id: string }) => job.id === expectedJobId); + if (expectedJob.fullJob) { + expect(actualJob).to.have.property('fullJob'); + expect(actualJob.fullJob).to.have.property('analysis_config'); + expect(actualJob.fullJob.analysis_config).to.eql(expectedJob.fullJob.analysis_config); + } else { + expect(actualJob).not.to.have.property('fullJob'); + } + } + }); + }); + } + + for (const testData of testDataListNegative) { + describe('rejects request', function() { + it(testData.testTitle, async () => { + const body = await runJobsSummaryRequest( + testData.user, + testData.requestBody, + testData.expected.responseCode + ); + + expect(body) + .to.have.property('error') + .eql(testData.expected.error); + + expect(body).to.have.property('message'); + }); + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/get_module.ts b/x-pack/test/api_integration/apis/ml/modules/get_module.ts similarity index 92% rename from x-pack/test/api_integration/apis/ml/get_module.ts rename to x-pack/test/api_integration/apis/ml/modules/get_module.ts index a50d3c0abe430..e19d45999c88e 100644 --- a/x-pack/test/api_integration/apis/ml/get_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/get_module.ts @@ -6,8 +6,8 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { USER } from '../../../functional/services/machine_learning/security_common'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; const COMMON_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', diff --git a/x-pack/test/api_integration/apis/ml/modules/index.ts b/x-pack/test/api_integration/apis/ml/modules/index.ts new file mode 100644 index 0000000000000..4fdc404c607aa --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/modules/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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('modules', function() { + loadTestFile(require.resolve('./get_module')); + loadTestFile(require.resolve('./recognize_module')); + loadTestFile(require.resolve('./setup_module')); + }); +} diff --git a/x-pack/test/api_integration/apis/ml/recognize_module.ts b/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts similarity index 93% rename from x-pack/test/api_integration/apis/ml/recognize_module.ts rename to x-pack/test/api_integration/apis/ml/modules/recognize_module.ts index 8e360579c1459..948728189b8bd 100644 --- a/x-pack/test/api_integration/apis/ml/recognize_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts @@ -6,8 +6,8 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { USER } from '../../../functional/services/machine_learning/security_common'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; const COMMON_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', diff --git a/x-pack/test/api_integration/apis/ml/setup_module.ts b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts similarity index 96% rename from x-pack/test/api_integration/apis/ml/setup_module.ts rename to x-pack/test/api_integration/apis/ml/modules/setup_module.ts index e603782b25717..23ddd3b63a2ef 100644 --- a/x-pack/test/api_integration/apis/ml/setup_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts @@ -6,10 +6,10 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; -import { JOB_STATE, DATAFEED_STATE } from '../../../../plugins/ml/common/constants/states'; -import { USER } from '../../../functional/services/machine_learning/security_common'; +import { JOB_STATE, DATAFEED_STATE } from '../../../../../plugins/ml/common/constants/states'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; const COMMON_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', diff --git a/x-pack/test/api_integration/apis/security/api_keys.ts b/x-pack/test/api_integration/apis/security/api_keys.ts new file mode 100644 index 0000000000000..276a5367a419e --- /dev/null +++ b/x-pack/test/api_integration/apis/security/api_keys.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect/expect.js'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('API Keys', () => { + describe('GET /internal/security/api_key/_enabled', () => { + it('should indicate that API Keys are enabled', async () => { + await supertest + .get('/internal/security/api_key/_enabled') + .set('kbn-xsrf', 'xxx') + .send() + .expect(200) + .then((response: Record) => { + const payload = response.body; + expect(payload).to.eql({ apiKeysEnabled: true }); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/security/index.js b/x-pack/test/api_integration/apis/security/index.js index ad1876cb717f1..7bb79a589d522 100644 --- a/x-pack/test/api_integration/apis/security/index.js +++ b/x-pack/test/api_integration/apis/security/index.js @@ -11,6 +11,7 @@ export default function({ loadTestFile }) { // Updates here should be mirrored in `./security_basic.ts` if tests // should also run under a basic license. + loadTestFile(require.resolve('./api_keys')); loadTestFile(require.resolve('./basic_login')); loadTestFile(require.resolve('./builtin_es_privileges')); loadTestFile(require.resolve('./change_password')); diff --git a/x-pack/test/api_integration/apis/security/security_basic.ts b/x-pack/test/api_integration/apis/security/security_basic.ts index dcbdb17724249..3e426f210afa8 100644 --- a/x-pack/test/api_integration/apis/security/security_basic.ts +++ b/x-pack/test/api_integration/apis/security/security_basic.ts @@ -13,6 +13,7 @@ export default function({ loadTestFile }: FtrProviderContext) { // Updates here should be mirrored in `./index.js` if tests // should also run under a trial/platinum license. + loadTestFile(require.resolve('./api_keys')); loadTestFile(require.resolve('./basic_login')); loadTestFile(require.resolve('./builtin_es_privileges')); loadTestFile(require.resolve('./change_password')); diff --git a/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts b/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts index a1b731169f0a0..ea980721b831b 100644 --- a/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts +++ b/x-pack/test/api_integration/apis/uptime/rest/dynamic_settings.ts @@ -5,8 +5,10 @@ */ import expect from '@kbn/expect'; +import { isRight } from 'fp-ts/lib/Either'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { defaultDynamicSettings } from '../../../../../legacy/plugins/uptime/common/runtime_types/dynamic_settings'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../legacy/plugins/uptime/common/constants'; +import { DynamicSettingsType } from '../../../../../legacy/plugins/uptime/common/runtime_types'; export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -14,15 +16,16 @@ export default function({ getService }: FtrProviderContext) { describe('dynamic settings', () => { it('returns the defaults when no user settings have been saved', async () => { const apiResponse = await supertest.get(`/api/uptime/dynamic_settings`); - expect(apiResponse.body).to.eql(defaultDynamicSettings as any); + expect(apiResponse.body).to.eql(DYNAMIC_SETTINGS_DEFAULTS); + expect(isRight(DynamicSettingsType.decode(apiResponse.body))).to.be.ok(); }); it('can change the settings', async () => { const newSettings = { heartbeatIndices: 'myIndex1*', - certificatesThresholds: { - errorState: 5, - warningState: 15, + certThresholds: { + expiration: 5, + age: 15, }, }; const postResponse = await supertest @@ -35,6 +38,7 @@ export default function({ getService }: FtrProviderContext) { const getResponse = await supertest.get(`/api/uptime/dynamic_settings`); expect(getResponse.body).to.eql(newSettings); + expect(isRight(DynamicSettingsType.decode(getResponse.body))).to.be.ok(); }); }); } diff --git a/x-pack/test/api_integration/config_security_basic.js b/x-pack/test/api_integration/config_security_basic.js index d21bfa4d7031a..4c4b77ee5b080 100644 --- a/x-pack/test/api_integration/config_security_basic.js +++ b/x-pack/test/api_integration/config_security_basic.js @@ -13,6 +13,7 @@ export default async function({ readConfigFile }) { config.esTestCluster.serverArgs = [ 'xpack.license.self_generated.type=basic', 'xpack.security.enabled=true', + 'xpack.security.authc.api_key.enabled=true', ]; config.testFiles = [require.resolve('./apis/security/security_basic')]; return config; diff --git a/x-pack/test/case_api_integration/basic/config.ts b/x-pack/test/case_api_integration/basic/config.ts new file mode 100644 index 0000000000000..f9c248ec3d56f --- /dev/null +++ b/x-pack/test/case_api_integration/basic/config.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('basic', { + disabledPlugins: [], + license: 'basic', + ssl: true, +}); diff --git a/x-pack/test/case_api_integration/basic/tests/configure/get_configure.ts b/x-pack/test/case_api_integration/basic/tests/configure/get_configure.ts new file mode 100644 index 0000000000000..a9fc2706a6ba2 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/configure/get_configure.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +import { CASE_CONFIGURE_URL } from '../../../../../plugins/case/common/constants'; +import { + getConfiguration, + removeServerGeneratedPropertiesFromConfigure, + getConfigurationOutput, + deleteConfiguration, +} from '../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('get_configure', () => { + afterEach(async () => { + await deleteConfiguration(es); + }); + + it('should return an empty find body correctly if no configuration is loaded', async () => { + const { body } = await supertest + .get(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).to.eql({}); + }); + + it('should return a configuration', async () => { + await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration()) + .expect(200); + + const { body } = await supertest + .get(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const data = removeServerGeneratedPropertiesFromConfigure(body); + expect(data).to.eql(getConfigurationOutput()); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts b/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts new file mode 100644 index 0000000000000..836c76d500034 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/configure/get_connectors.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../plugins/case/common/constants'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + + describe('get_connectors', () => { + it('should return an empty find body correctly if no connectors are loaded', async () => { + const { body } = await supertest + .get(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).to.eql([]); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/configure/patch_configure.ts b/x-pack/test/case_api_integration/basic/tests/configure/patch_configure.ts new file mode 100644 index 0000000000000..d66baa2a2eee2 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/configure/patch_configure.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 '../../../common/ftr_provider_context'; + +import { CASE_CONFIGURE_URL } from '../../../../../plugins/case/common/constants'; +import { + getConfiguration, + removeServerGeneratedPropertiesFromConfigure, + getConfigurationOutput, + deleteConfiguration, +} from '../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('post_configure', () => { + afterEach(async () => { + await deleteConfiguration(es); + }); + + it('should patch a configuration', async () => { + const res = await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration()) + .expect(200); + + const { body } = await supertest + .patch(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send({ closure_type: 'close-by-pushing', version: res.body.version }) + .expect(200); + + const data = removeServerGeneratedPropertiesFromConfigure(body); + expect(data).to.eql({ ...getConfigurationOutput(true), closure_type: 'close-by-pushing' }); + }); + + it('should handle patch request when there is no configuration', async () => { + const { body } = await supertest + .patch(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send({ closure_type: 'close-by-pushing', version: 'no-version' }) + .expect(409); + + expect(body).to.eql({ + error: 'Conflict', + message: + 'You can not patch this configuration since you did not created first with a post.', + statusCode: 409, + }); + }); + + it('should handle patch request when versions are different', async () => { + await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration()) + .expect(200); + + const { body } = await supertest + .patch(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send({ closure_type: 'close-by-pushing', version: 'no-version' }) + .expect(409); + + expect(body).to.eql({ + error: 'Conflict', + message: + 'This configuration has been updated. Please refresh before saving additional updates.', + statusCode: 409, + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/configure/post_configure.ts b/x-pack/test/case_api_integration/basic/tests/configure/post_configure.ts new file mode 100644 index 0000000000000..c2284492e5b77 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/configure/post_configure.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +import { CASE_CONFIGURE_URL } from '../../../../../plugins/case/common/constants'; +import { + getConfiguration, + removeServerGeneratedPropertiesFromConfigure, + getConfigurationOutput, + deleteConfiguration, +} from '../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('legacyEs'); + + describe('post_configure', () => { + afterEach(async () => { + await deleteConfiguration(es); + }); + + it('should create a configuration', async () => { + const { body } = await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration()) + .expect(200); + + const data = removeServerGeneratedPropertiesFromConfigure(body); + expect(data).to.eql(getConfigurationOutput()); + }); + + it('should keep only the latest configuration', async () => { + await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration('connector-2')) + .expect(200); + + await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration()) + .expect(200); + + const { body } = await supertest + .get(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const data = removeServerGeneratedPropertiesFromConfigure(body); + expect(data).to.eql(getConfigurationOutput()); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/index.ts b/x-pack/test/case_api_integration/basic/tests/index.ts new file mode 100644 index 0000000000000..efd5369c019d8 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/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 '../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('case api basic', function() { + // Fastest ciGroup for the moment. + this.tags('ciGroup2'); + + loadTestFile(require.resolve('./configure/get_configure')); + loadTestFile(require.resolve('./configure/post_configure')); + loadTestFile(require.resolve('./configure/patch_configure')); + loadTestFile(require.resolve('./configure/get_connectors')); + }); +}; diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts new file mode 100644 index 0000000000000..862705ab9610b --- /dev/null +++ b/x-pack/test/case_api_integration/common/config.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 path from 'path'; + +import { CA_CERT_PATH } from '@kbn/dev-utils'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; + +import { services } from './services'; + +interface CreateTestConfigOptions { + license: string; + disabledPlugins?: string[]; + ssl?: boolean; +} + +// test.not-enabled is specifically not enabled +const enabledActionTypes = [ + '.email', + '.index', + '.pagerduty', + '.server-log', + '.servicenow', + '.slack', + '.webhook', + 'test.authorization', + 'test.failing', + 'test.index-record', + 'test.noop', + 'test.rate-limit', +]; + +export function createTestConfig(name: string, options: CreateTestConfigOptions) { + const { license = 'trial', disabledPlugins = [], ssl = false } = options; + + return async ({ readConfigFile }: FtrConfigProviderContext) => { + const xPackApiIntegrationTestsConfig = await readConfigFile( + require.resolve('../../api_integration/config.js') + ); + + const servers = { + ...xPackApiIntegrationTestsConfig.get('servers'), + elasticsearch: { + ...xPackApiIntegrationTestsConfig.get('servers.elasticsearch'), + protocol: ssl ? 'https' : 'http', + }, + }; + + return { + testFiles: [require.resolve(`../${name}/tests/`)], + servers, + services, + junit: { + reportName: 'X-Pack Case API Integration Tests', + }, + esArchiver: xPackApiIntegrationTestsConfig.get('esArchiver'), + esTestCluster: { + ...xPackApiIntegrationTestsConfig.get('esTestCluster'), + license, + ssl, + serverArgs: [ + `xpack.license.self_generated.type=${license}`, + `xpack.security.enabled=${!disabledPlugins.includes('security') && + ['trial', 'basic'].includes(license)}`, + ], + }, + kbnTestServer: { + ...xPackApiIntegrationTestsConfig.get('kbnTestServer'), + serverArgs: [ + ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), + `--xpack.actions.whitelistedHosts=${JSON.stringify([ + 'localhost', + 'some.non.existent.com', + ])}`, + `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, + '--xpack.alerting.enabled=true', + '--xpack.eventLog.logEntries=true', + ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), + `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'alerts')}`, + `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'actions')}`, + ...(ssl + ? [ + `--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, + `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, + ] + : []), + ], + }, + }; + }; +} diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.js b/x-pack/test/case_api_integration/common/ftr_provider_context.d.ts similarity index 56% rename from x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.js rename to x-pack/test/case_api_integration/common/ftr_provider_context.d.ts index 6a59a6795d45a..e3add3748f56d 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/tags/proportion.js +++ b/x-pack/test/case_api_integration/common/ftr_provider_context.d.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { euiPaletteColorBlind } from '@elastic/eui'; -const euiVisPalette = euiPaletteColorBlind(); +import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; -export const proportion = () => ({ name: 'proportion', color: euiVisPalette[3] }); +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts new file mode 100644 index 0000000000000..6d0db69309b90 --- /dev/null +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CasesConfigureRequest, CasesConfigureResponse } from '../../../../plugins/case/common/api'; + +export const getConfiguration = (connector_id: string = 'connector-1'): CasesConfigureRequest => { + return { + connector_id, + connector_name: 'Connector 1', + closure_type: 'close-by-user', + }; +}; + +export const getConfigurationOutput = (update = false): Partial => { + return { + ...getConfiguration(), + created_by: { email: null, full_name: null, username: 'elastic' }, + updated_by: update ? { email: null, full_name: null, username: 'elastic' } : null, + }; +}; + +export const removeServerGeneratedPropertiesFromConfigure = ( + config: Partial +): Partial => { + const { created_at, updated_at, version, ...rest } = config; + return rest; +}; + +export const deleteConfiguration = async (es: any): Promise => { + await es.deleteByQuery({ + index: '.kibana', + q: 'type:cases-configure', + waitForCompletion: true, + refresh: 'wait_for', + body: {}, + }); +}; + +export const getConnector = () => ({ + name: 'ServiceNow Connector', + actionTypeId: '.servicenow', + secrets: { + username: 'admin', + password: 'admin', + }, + config: { + apiUrl: 'localhost', + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + }, +}); diff --git a/x-pack/test/case_api_integration/common/services.ts b/x-pack/test/case_api_integration/common/services.ts new file mode 100644 index 0000000000000..a927a31469bab --- /dev/null +++ b/x-pack/test/case_api_integration/common/services.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { services } from '../../api_integration/services'; diff --git a/x-pack/test/functional/apps/api_keys/home_page.ts b/x-pack/test/functional/apps/api_keys/home_page.ts index 1c83a17e78ca7..abfd90bd6ad8e 100644 --- a/x-pack/test/functional/apps/api_keys/home_page.ts +++ b/x-pack/test/functional/apps/api_keys/home_page.ts @@ -13,7 +13,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const security = getService('security'); describe('Home page', function() { - this.tags('smoke'); before(async () => { await security.testUser.setRoles(['kibana_admin']); await pageObjects.common.navigateToApp('apiKeys'); diff --git a/x-pack/test/functional/apps/canvas/custom_elements.ts b/x-pack/test/functional/apps/canvas/custom_elements.ts index de3976509be1f..d4e1702368879 100644 --- a/x-pack/test/functional/apps/canvas/custom_elements.ts +++ b/x-pack/test/functional/apps/canvas/custom_elements.ts @@ -19,8 +19,7 @@ export default function canvasCustomElementTest({ const PageObjects = getPageObjects(['canvas', 'common']); const find = getService('find'); - // FLAKY: https://github.com/elastic/kibana/issues/62927 - describe.skip('custom elements', function() { + describe('custom elements', function() { this.tags('skipFirefox'); before(async () => { @@ -42,7 +41,7 @@ export default function canvasCustomElementTest({ await testSubjects.click('canvasWorkpadPage > canvasWorkpadPageElementContent', 20000); // click the "Save as new element" button - await find.clickByCssSelector('[aria-label="Save as new element"]', 20000); + await testSubjects.click('canvasSidebarHeader__saveElementButton', 20000); // fill out the custom element form and submit it await PageObjects.canvas.fillOutCustomElementForm( @@ -57,15 +56,11 @@ export default function canvasCustomElementTest({ }); it('adds the custom element to the workpad when prompted', async () => { - await PageObjects.canvas.openAddElementModal(); - - // open the custom elements tab - await find.clickByCssSelector('#customElements', 20000); + // open the saved elements modal + await PageObjects.canvas.openSavedElementsModal(); // ensure the custom element is the one expected and click it to add to the workpad - const customElement = await find.byCssSelector( - '[aria-labelledby="customElements"] .canvasElementCard__wrapper' - ); + const customElement = await find.byCssSelector('.canvasElementCard__wrapper'); const elementName = await customElement.findByCssSelector('.euiCard__title'); expect(await elementName.getVisibleText()).to.contain('My New Element'); customElement.click(); @@ -95,14 +90,11 @@ export default function canvasCustomElementTest({ }); it('saves custom element modifications', async () => { - await PageObjects.canvas.openAddElementModal(); - - // open the custom elements tab - await find.clickByCssSelector('#customElements', 20000); + // open the saved elements modal + await PageObjects.canvas.openSavedElementsModal(); // ensure the correct amount of custom elements exist - const container = await find.byCssSelector('[aria-labelledby="customElements"]'); - const customElements = await container.findAllByCssSelector('.canvasElementCard__wrapper'); + const customElements = await find.allByCssSelector('.canvasElementCard__wrapper'); expect(customElements).to.have.length(1); // hover over the custom element to bring up the edit and delete icons @@ -110,8 +102,7 @@ export default function canvasCustomElementTest({ await customElement.moveMouseTo(); // click the edit element button - const editBtn = await customElement.findByCssSelector('[aria-label="Edit element"]'); - await editBtn.click(); + await testSubjects.click('canvasElementCard__editButton', 20000); // fill out the custom element form and submit it await PageObjects.canvas.fillOutCustomElementForm( @@ -121,22 +112,21 @@ export default function canvasCustomElementTest({ // ensure the custom element in the modal shows the updated text await retry.try(async () => { - const elementName = await find.byCssSelector( - '[aria-labelledby="customElements"] .canvasElementCard__wrapper .euiCard__title' - ); + const elementName = await find.byCssSelector('.canvasElementCard__wrapper .euiCard__title'); expect(await elementName.getVisibleText()).to.contain('My Edited New Element'); }); + + // Close the modal + await PageObjects.canvas.closeSavedElementsModal(); }); it('deletes custom element when prompted', async () => { - // open the custom elements tab - await find.clickByCssSelector('#customElements', 20000); + // open the saved elements modal + await PageObjects.canvas.openSavedElementsModal(); // ensure the correct amount of custom elements exist - const customElements = await find.allByCssSelector( - '[aria-labelledby="customElements"] .canvasElementCard__wrapper' - ); + const customElements = await find.allByCssSelector('.canvasElementCard__wrapper'); expect(customElements).to.have.length(1); // hover over the custom element to bring up the edit and delete icons @@ -144,22 +134,18 @@ export default function canvasCustomElementTest({ await customElement.moveMouseTo(); // click the delete element button - const editBtn = await customElement.findByCssSelector('[aria-label="Delete element"]'); - await editBtn.click(); + await testSubjects.click('canvasElementCard__deleteButton', 20000); - await testSubjects.click('confirmModalConfirmButton'); + await testSubjects.click('confirmModalConfirmButton', 20000); // ensure the custom element was deleted await retry.try(async () => { - const containerAgain = await find.byCssSelector('[aria-labelledby="customElements"]'); - const customElementsAgain = await containerAgain.findAllByCssSelector( - '.canvasElementCard__wrapper' - ); + const customElementsAgain = await find.allByCssSelector('.canvasElementCard__wrapper'); expect(customElementsAgain).to.have.length(0); }); // Close the modal - await browser.pressKeys(browser.keys.ESCAPE); + await PageObjects.canvas.closeSavedElementsModal(); }); }); } diff --git a/x-pack/test/functional/apps/canvas/smoke_test.js b/x-pack/test/functional/apps/canvas/smoke_test.js index a240a55b9765c..df41b725f6daf 100644 --- a/x-pack/test/functional/apps/canvas/smoke_test.js +++ b/x-pack/test/functional/apps/canvas/smoke_test.js @@ -15,7 +15,7 @@ export default function canvasSmokeTest({ getService, getPageObjects }) { const PageObjects = getPageObjects(['common']); describe('smoke test', function() { - this.tags('smoke'); + this.tags('includeFirefox'); const workpadListSelector = 'canvasWorkpadLoaderTable > canvasWorkpadLoaderWorkpad'; const testWorkpadId = 'workpad-1705f884-6224-47de-ba49-ca224fe6ec31'; diff --git a/x-pack/test/functional/apps/cross_cluster_replication/home_page.ts b/x-pack/test/functional/apps/cross_cluster_replication/home_page.ts index eef47ae0341c8..8d3b98004245e 100644 --- a/x-pack/test/functional/apps/cross_cluster_replication/home_page.ts +++ b/x-pack/test/functional/apps/cross_cluster_replication/home_page.ts @@ -12,7 +12,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const log = getService('log'); describe('Home page', function() { - this.tags('smoke'); before(async () => { await pageObjects.common.navigateToApp('crossClusterReplication'); }); diff --git a/x-pack/test/functional/apps/grok_debugger/grok_debugger.js b/x-pack/test/functional/apps/grok_debugger/grok_debugger.js index 994f65a79011e..09d246a3a6e1b 100644 --- a/x-pack/test/functional/apps/grok_debugger/grok_debugger.js +++ b/x-pack/test/functional/apps/grok_debugger/grok_debugger.js @@ -12,7 +12,7 @@ export default function({ getService, getPageObjects }) { const PageObjects = getPageObjects(['grokDebugger']); describe('grok debugger app', function() { - this.tags('smoke'); + this.tags('includeFirefox'); before(async () => { await esArchiver.load('empty_kibana'); // Increase window height to ensure "Simulate" button is shown above the diff --git a/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts b/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts index c7ad3f933ea64..b5d43be21f0c5 100644 --- a/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts +++ b/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts @@ -12,7 +12,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const log = getService('log'); describe('Home page', function() { - this.tags('smoke'); before(async () => { await pageObjects.common.navigateToApp('indexLifecycleManagement'); }); diff --git a/x-pack/test/functional/apps/index_management/home_page.ts b/x-pack/test/functional/apps/index_management/home_page.ts index c6b7517fc1858..046b8ec44b9fa 100644 --- a/x-pack/test/functional/apps/index_management/home_page.ts +++ b/x-pack/test/functional/apps/index_management/home_page.ts @@ -14,7 +14,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const browser = getService('browser'); describe('Home page', function() { - this.tags('smoke'); before(async () => { await pageObjects.common.navigateToApp('indexManagement'); }); diff --git a/x-pack/test/functional/apps/infra/home_page.ts b/x-pack/test/functional/apps/infra/home_page.ts index 0c1d57202b8eb..ed8bec570ab60 100644 --- a/x-pack/test/functional/apps/infra/home_page.ts +++ b/x-pack/test/functional/apps/infra/home_page.ts @@ -15,7 +15,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'infraHome']); describe('Home page', function() { - this.tags('smoke'); + this.tags('includeFirefox'); before(async () => { await esArchiver.load('empty_kibana'); }); diff --git a/x-pack/test/functional/apps/infra/link_to.ts b/x-pack/test/functional/apps/infra/link_to.ts index a287d53d5df0b..4e5ebab90880e 100644 --- a/x-pack/test/functional/apps/infra/link_to.ts +++ b/x-pack/test/functional/apps/infra/link_to.ts @@ -21,7 +21,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const traceId = '433b4651687e18be2c6c8e3b11f53d09'; describe('Infra link-to', function() { - this.tags('smoke'); it('redirects to the logs app and parses URL search params correctly', async () => { const location = { hash: '', diff --git a/x-pack/test/functional/apps/infra/log_entry_categories_tab.ts b/x-pack/test/functional/apps/infra/log_entry_categories_tab.ts index c703738e37228..3dd1592286a3a 100644 --- a/x-pack/test/functional/apps/infra/log_entry_categories_tab.ts +++ b/x-pack/test/functional/apps/infra/log_entry_categories_tab.ts @@ -13,7 +13,7 @@ export default ({ getService }: FtrProviderContext) => { const retry = getService('retry'); describe('Log Entry Categories Tab', function() { - this.tags('smoke'); + this.tags('includeFirefox'); describe('with a trial license', () => { it('is visible', async () => { diff --git a/x-pack/test/functional/apps/infra/log_entry_rate_tab.ts b/x-pack/test/functional/apps/infra/log_entry_rate_tab.ts index 95228a520aaa2..ee4b59d04899f 100644 --- a/x-pack/test/functional/apps/infra/log_entry_rate_tab.ts +++ b/x-pack/test/functional/apps/infra/log_entry_rate_tab.ts @@ -13,7 +13,7 @@ export default ({ getService }: FtrProviderContext) => { const retry = getService('retry'); describe('Log Entry Rate Tab', function() { - this.tags('smoke'); + this.tags('includeFirefox'); describe('with a trial license', () => { it('is visible', async () => { diff --git a/x-pack/test/functional/apps/infra/logs_source_configuration.ts b/x-pack/test/functional/apps/infra/logs_source_configuration.ts index f40c908f23c80..7293405ce80ff 100644 --- a/x-pack/test/functional/apps/infra/logs_source_configuration.ts +++ b/x-pack/test/functional/apps/infra/logs_source_configuration.ts @@ -17,8 +17,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const retry = getService('retry'); describe('Logs Source Configuration', function() { - this.tags('smoke'); - before(async () => { await esArchiver.load('empty_kibana'); }); diff --git a/x-pack/test/functional/apps/infra/metrics_source_configuration.ts b/x-pack/test/functional/apps/infra/metrics_source_configuration.ts index d334fa7956be4..1c03b3ebf6681 100644 --- a/x-pack/test/functional/apps/infra/metrics_source_configuration.ts +++ b/x-pack/test/functional/apps/infra/metrics_source_configuration.ts @@ -15,7 +15,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'infraHome']); describe('Infrastructure Source Configuration', function() { - this.tags('smoke'); before(async () => { await esArchiver.load('empty_kibana'); }); diff --git a/x-pack/test/functional/apps/logstash/index.js b/x-pack/test/functional/apps/logstash/index.js index ee710b00b0be8..0ca09aa317bc0 100644 --- a/x-pack/test/functional/apps/logstash/index.js +++ b/x-pack/test/functional/apps/logstash/index.js @@ -6,7 +6,7 @@ export default function({ loadTestFile }) { describe('logstash', function() { - this.tags(['ciGroup2', 'smoke']); + this.tags(['ciGroup2']); loadTestFile(require.resolve('./pipeline_list')); loadTestFile(require.resolve('./pipeline_create')); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts index a5faf325aa6cb..a9133bb380179 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts @@ -195,7 +195,6 @@ export default function({ getService }: FtrProviderContext) { modelSizeStats: { result_type: 'model_size_stats', model_bytes_exceeded: '0.0 B', - model_bytes_memory_limit: '10.0 MB', total_by_field_count: '37', total_over_field_count: '92', total_partition_field_count: '8', @@ -262,7 +261,6 @@ export default function({ getService }: FtrProviderContext) { modelSizeStats: { result_type: 'model_size_stats', model_bytes_exceeded: '0.0 B', - model_bytes_memory_limit: '100.0 MB', total_by_field_count: '994', total_over_field_count: '0', total_partition_field_count: '2', @@ -277,7 +275,7 @@ export default function({ getService }: FtrProviderContext) { const calendarId = `wizard-test-calendar_${Date.now()}`; describe('advanced job', function() { - this.tags(['smoke', 'mlqa']); + this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('ml/ecommerce'); await ml.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); @@ -549,7 +547,6 @@ export default function({ getService }: FtrProviderContext) { job_id: testData.jobId, result_type: testData.expected.modelSizeStats.result_type, model_bytes_exceeded: testData.expected.modelSizeStats.model_bytes_exceeded, - model_bytes_memory_limit: testData.expected.modelSizeStats.model_bytes_memory_limit, total_by_field_count: testData.expected.modelSizeStats.total_by_field_count, total_over_field_count: testData.expected.modelSizeStats.total_over_field_count, total_partition_field_count: @@ -813,7 +810,6 @@ export default function({ getService }: FtrProviderContext) { job_id: testData.jobIdClone, result_type: testData.expected.modelSizeStats.result_type, model_bytes_exceeded: testData.expected.modelSizeStats.model_bytes_exceeded, - model_bytes_memory_limit: testData.expected.modelSizeStats.model_bytes_memory_limit, total_by_field_count: testData.expected.modelSizeStats.total_by_field_count, total_over_field_count: testData.expected.modelSizeStats.total_over_field_count, total_partition_field_count: diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/anomaly_explorer.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/anomaly_explorer.ts index 8827559a5f470..ae88208fa9a11 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/anomaly_explorer.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/anomaly_explorer.ts @@ -57,7 +57,7 @@ export default function({ getService }: FtrProviderContext) { const ml = getService('ml'); describe('anomaly explorer', function() { - this.tags(['smoke', 'mlqa']); + this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('ml/farequote'); await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/categorization_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/categorization_job.ts index 9b5ae171d4115..b8ac646c35875 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/categorization_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/categorization_job.ts @@ -64,7 +64,6 @@ export default function({ getService }: FtrProviderContext) { job_id: expectedJobId, result_type: 'model_size_stats', model_bytes_exceeded: '0.0 B', - model_bytes_memory_limit: '15.0 MB', total_by_field_count: '30', total_over_field_count: '0', total_partition_field_count: '2', @@ -77,7 +76,7 @@ export default function({ getService }: FtrProviderContext) { const calendarId = `wizard-test-calendar_${Date.now()}`; describe('categorization', function() { - this.tags(['smoke', 'mlqa']); + this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('ml/categorization'); await ml.testResources.createIndexPatternIfNeeded('ft_categorization', '@timestamp'); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/date_nanos_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/date_nanos_job.ts index 570deee01c684..d3934d674124e 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/date_nanos_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/date_nanos_job.ts @@ -153,7 +153,6 @@ export default function({ getService }: FtrProviderContext) { modelSizeStats: { result_type: 'model_size_stats', model_bytes_exceeded: '0.0 B', - model_bytes_memory_limit: '10.0 MB', total_by_field_count: '3', total_over_field_count: '0', total_partition_field_count: '2', @@ -166,7 +165,7 @@ export default function({ getService }: FtrProviderContext) { ]; describe('job on data set with date_nanos time field', function() { - this.tags(['smoke', 'mlqa']); + this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('ml/event_rate_nanos'); await ml.testResources.createIndexPatternIfNeeded( @@ -422,7 +421,6 @@ export default function({ getService }: FtrProviderContext) { job_id: testData.jobId, result_type: testData.expected.modelSizeStats.result_type, model_bytes_exceeded: testData.expected.modelSizeStats.model_bytes_exceeded, - model_bytes_memory_limit: testData.expected.modelSizeStats.model_bytes_memory_limit, total_by_field_count: testData.expected.modelSizeStats.total_by_field_count, total_over_field_count: testData.expected.modelSizeStats.total_over_field_count, total_partition_field_count: diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts index 4739f987541d6..f6a9c96492f39 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts @@ -61,7 +61,6 @@ export default function({ getService }: FtrProviderContext) { job_id: expectedJobId, result_type: 'model_size_stats', model_bytes_exceeded: '0.0 B', - model_bytes_memory_limit: '20.0 MB', total_by_field_count: '59', total_over_field_count: '0', total_partition_field_count: '58', @@ -74,7 +73,7 @@ export default function({ getService }: FtrProviderContext) { const calendarId = `wizard-test-calendar_${Date.now()}`; describe('multi metric', function() { - this.tags(['smoke', 'mlqa']); + this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('ml/farequote'); await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts index 0279c70bb73a9..547c489411b5f 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts @@ -75,7 +75,6 @@ export default function({ getService }: FtrProviderContext) { job_id: expectedJobId, result_type: 'model_size_stats', model_bytes_exceeded: '0.0 B', - model_bytes_memory_limit: '8.0 MB', total_by_field_count: '25', total_over_field_count: '92', total_partition_field_count: '3', @@ -88,7 +87,7 @@ export default function({ getService }: FtrProviderContext) { const calendarId = `wizard-test-calendar_${Date.now()}`; describe('population', function() { - this.tags(['smoke', 'mlqa']); + this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('ml/ecommerce'); await ml.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts index a5652d76358eb..b1fee1633641a 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts @@ -54,7 +54,6 @@ export default function({ getService }: FtrProviderContext) { modelSizeStats: { result_type: 'model_size_stats', model_bytes_exceeded: '0.0 B', - model_bytes_memory_limit: '20.0 MB', total_by_field_count: '3', total_over_field_count: '0', total_partition_field_count: '2', @@ -105,7 +104,6 @@ export default function({ getService }: FtrProviderContext) { modelSizeStats: { result_type: 'model_size_stats', model_bytes_exceeded: '0.0 B', - model_bytes_memory_limit: '20.0 MB', total_by_field_count: '7', total_over_field_count: '0', total_partition_field_count: '6', @@ -156,7 +154,6 @@ export default function({ getService }: FtrProviderContext) { modelSizeStats: { result_type: 'model_size_stats', model_bytes_exceeded: '0.0 B', - model_bytes_memory_limit: '20.0 MB', total_by_field_count: '7', total_over_field_count: '0', total_partition_field_count: '6', @@ -208,7 +205,6 @@ export default function({ getService }: FtrProviderContext) { modelSizeStats: { result_type: 'model_size_stats', model_bytes_exceeded: '0.0 B', - model_bytes_memory_limit: '20.0 MB', total_by_field_count: '3', total_over_field_count: '0', total_partition_field_count: '2', @@ -259,7 +255,6 @@ export default function({ getService }: FtrProviderContext) { modelSizeStats: { result_type: 'model_size_stats', model_bytes_exceeded: '0.0 B', - model_bytes_memory_limit: '20.0 MB', total_by_field_count: '3', total_over_field_count: '0', total_partition_field_count: '2', @@ -272,7 +267,7 @@ export default function({ getService }: FtrProviderContext) { ]; describe('saved search', function() { - this.tags(['smoke', 'mlqa']); + this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('ml/farequote'); await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); @@ -464,7 +459,6 @@ export default function({ getService }: FtrProviderContext) { job_id: testData.jobId, result_type: testData.expected.modelSizeStats.result_type, model_bytes_exceeded: testData.expected.modelSizeStats.model_bytes_exceeded, - model_bytes_memory_limit: testData.expected.modelSizeStats.model_bytes_memory_limit, total_by_field_count: testData.expected.modelSizeStats.total_by_field_count, total_over_field_count: testData.expected.modelSizeStats.total_over_field_count, total_partition_field_count: diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts index 43053decb3924..0f8655e3c6bbc 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts @@ -60,7 +60,6 @@ export default function({ getService }: FtrProviderContext) { job_id: expectedJobId, result_type: 'model_size_stats', model_bytes_exceeded: '0.0 B', - model_bytes_memory_limit: '15.0 MB', total_by_field_count: '3', total_over_field_count: '0', total_partition_field_count: '2', @@ -73,7 +72,7 @@ export default function({ getService }: FtrProviderContext) { const calendarId = `wizard-test-calendar_${Date.now()}`; describe('single metric', function() { - this.tags(['smoke', 'mlqa']); + this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('ml/farequote'); await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_viewer.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_viewer.ts index cc7c9828ce87d..9e1829998bec4 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_viewer.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_viewer.ts @@ -40,7 +40,7 @@ export default function({ getService }: FtrProviderContext) { const ml = getService('ml'); describe('single metric viewer', function() { - this.tags(['smoke', 'mlqa']); + this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('ml/farequote'); await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); diff --git a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/classification_creation.ts b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/classification_creation.ts index 8a6741bd88daa..eb57a743f14cb 100644 --- a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/classification_creation.ts +++ b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/classification_creation.ts @@ -12,7 +12,6 @@ export default function({ getService }: FtrProviderContext) { const ml = getService('ml'); describe('classification creation', function() { - this.tags(['smoke']); before(async () => { await esArchiver.loadIfNeeded('ml/bm_classification'); await ml.testResources.createIndexPatternIfNeeded('ft_bank_marketing', '@timestamp'); diff --git a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts index d98d8feaaf4fe..93f225989592e 100644 --- a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts +++ b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/cloning.ts @@ -14,8 +14,6 @@ export default function({ getService }: FtrProviderContext) { const ml = getService('ml'); describe('jobs cloning supported by UI form', function() { - this.tags(['smoke']); - const testDataList: Array<{ suiteTitle: string; archive: string; diff --git a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/outlier_detection_creation.ts b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/outlier_detection_creation.ts index 8dfe058cf6885..4e8a9000598b3 100644 --- a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/outlier_detection_creation.ts +++ b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/outlier_detection_creation.ts @@ -12,7 +12,6 @@ export default function({ getService }: FtrProviderContext) { const ml = getService('ml'); describe('outlier detection creation', function() { - this.tags(['smoke']); before(async () => { await esArchiver.loadIfNeeded('ml/ihp_outlier'); await ml.testResources.createIndexPatternIfNeeded('ft_ihp_outlier', '@timestamp'); diff --git a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/regression_creation.ts index 271f3e2018dad..c3718e421d451 100644 --- a/x-pack/test/functional/apps/machine_learning/data_frame_analytics/regression_creation.ts +++ b/x-pack/test/functional/apps/machine_learning/data_frame_analytics/regression_creation.ts @@ -12,7 +12,6 @@ export default function({ getService }: FtrProviderContext) { const ml = getService('ml'); describe('regression creation', function() { - this.tags(['smoke']); before(async () => { await esArchiver.loadIfNeeded('ml/egs_regression'); await ml.testResources.createIndexPatternIfNeeded('ft_egs_regression', '@timestamp'); diff --git a/x-pack/test/functional/apps/machine_learning/data_visualizer/file_data_visualizer.ts b/x-pack/test/functional/apps/machine_learning/data_visualizer/file_data_visualizer.ts index ae958ad7f570f..868e75228f1cc 100644 --- a/x-pack/test/functional/apps/machine_learning/data_visualizer/file_data_visualizer.ts +++ b/x-pack/test/functional/apps/machine_learning/data_visualizer/file_data_visualizer.ts @@ -34,7 +34,7 @@ export default function({ getService }: FtrProviderContext) { ]; describe('file based', function() { - this.tags(['smoke', 'mlqa']); + this.tags(['mlqa']); before(async () => { await ml.testResources.setKibanaTimeZoneToUTC(); diff --git a/x-pack/test/functional/apps/machine_learning/data_visualizer/index_data_visualizer.ts b/x-pack/test/functional/apps/machine_learning/data_visualizer/index_data_visualizer.ts index e71b57a4562e7..e4e0c1c92d73a 100644 --- a/x-pack/test/functional/apps/machine_learning/data_visualizer/index_data_visualizer.ts +++ b/x-pack/test/functional/apps/machine_learning/data_visualizer/index_data_visualizer.ts @@ -376,7 +376,7 @@ export default function({ getService }: FtrProviderContext) { } describe('index based', function() { - this.tags(['smoke', 'mlqa']); + this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('ml/farequote'); await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); diff --git a/x-pack/test/functional/apps/machine_learning/pages.ts b/x-pack/test/functional/apps/machine_learning/pages.ts index 95930f18061fa..35536b4eeda12 100644 --- a/x-pack/test/functional/apps/machine_learning/pages.ts +++ b/x-pack/test/functional/apps/machine_learning/pages.ts @@ -10,7 +10,7 @@ export default function({ getService }: FtrProviderContext) { const ml = getService('ml'); describe('page navigation', function() { - this.tags(['smoke', 'mlqa']); + this.tags(['skipFirefox', 'mlqa']); before(async () => { await ml.api.cleanMlIndices(); await ml.securityUI.loginAsMlPowerUser(); diff --git a/x-pack/test/functional/apps/remote_clusters/home_page.ts b/x-pack/test/functional/apps/remote_clusters/home_page.ts index 879a0a11ede78..394e48404b927 100644 --- a/x-pack/test/functional/apps/remote_clusters/home_page.ts +++ b/x-pack/test/functional/apps/remote_clusters/home_page.ts @@ -11,7 +11,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'remoteClusters']); describe('Home page', function() { - this.tags('smoke'); before(async () => { await pageObjects.common.navigateToApp('remoteClusters'); }); diff --git a/x-pack/test/functional/apps/reporting_management/index.ts b/x-pack/test/functional/apps/reporting_management/index.ts new file mode 100644 index 0000000000000..07306d8fb2956 --- /dev/null +++ b/x-pack/test/functional/apps/reporting_management/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * 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 ({ loadTestFile }: FtrProviderContext) => { + describe('reporting management app', function() { + this.tags('ciGroup7'); + loadTestFile(require.resolve('./report_delete_pagination')); + }); +}; diff --git a/x-pack/test/functional/apps/reporting_management/report_delete_pagination.ts b/x-pack/test/functional/apps/reporting_management/report_delete_pagination.ts new file mode 100644 index 0000000000000..5b2edad9e6d95 --- /dev/null +++ b/x-pack/test/functional/apps/reporting_management/report_delete_pagination.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const pageObjects = getPageObjects(['common', 'reporting']); + const log = getService('log'); + const retry = getService('retry'); + const security = getService('security'); + + const testSubjects = getService('testSubjects'); + const esArchiver = getService('esArchiver'); + + describe('Delete reports', function() { + before(async () => { + await security.testUser.setRoles(['global_discover_read', 'reporting_user']); + await esArchiver.load('empty_kibana'); + await esArchiver.load('reporting/archived_reports'); + await pageObjects.common.navigateToActualUrl('kibana', '/management/kibana/reporting'); + await testSubjects.existOrFail('reportJobListing', { timeout: 200000 }); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + await esArchiver.unload('reporting/archived_reports'); + await security.testUser.restoreDefaults(); + }); + + it('Confirm single report deletion works', async () => { + log.debug('Checking for reports.'); + await retry.try(async () => { + await testSubjects.click('checkboxSelectRow-k9a9xlwl0gpe1457b10rraq3'); + }); + const deleteButton = await testSubjects.find('deleteReportButton'); + await retry.waitFor('delete button to become enabled', async () => { + return await deleteButton.isEnabled(); + }); + await deleteButton.click(); + await testSubjects.exists('confirmModalBodyText'); + await testSubjects.click('confirmModalConfirmButton'); + await retry.try(async () => { + await testSubjects.waitForDeleted('checkboxSelectRow-k9a9xlwl0gpe1457b10rraq3'); + }); + }); + + // functional test for report pagination: https://github.com/elastic/kibana/pull/62881 + it('Report pagination', async () => { + const previousButton = await testSubjects.find('pagination-button-previous'); + expect(await previousButton.getAttribute('disabled')).to.be('true'); + await testSubjects.click('pagination-button-1'); + expect(await previousButton.getAttribute('disabled')).to.be(null); + }); + }); +}; diff --git a/x-pack/test/functional/apps/security/management.js b/x-pack/test/functional/apps/security/management.js index 8ab84126b2b30..3bcf504c17a6f 100644 --- a/x-pack/test/functional/apps/security/management.js +++ b/x-pack/test/functional/apps/security/management.js @@ -19,7 +19,8 @@ export default function({ getService, getPageObjects }) { const browser = getService('browser'); const PageObjects = getPageObjects(['security', 'settings', 'common', 'header']); - describe('Management', function() { + // FLAKY: https://github.com/elastic/kibana/issues/61173 + describe.skip('Management', function() { this.tags(['skipFirefox']); before(async () => { diff --git a/x-pack/test/functional/apps/security/security.ts b/x-pack/test/functional/apps/security/security.ts index 2096a7755e01d..37516acec7c4b 100644 --- a/x-pack/test/functional/apps/security/security.ts +++ b/x-pack/test/functional/apps/security/security.ts @@ -16,7 +16,7 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const spaces = getService('spaces'); describe('Security', function() { - this.tags('smoke'); + this.tags('includeFirefox'); describe('Login Page', () => { before(async () => { await esArchiver.load('empty_kibana'); diff --git a/x-pack/test/functional/apps/security/user_email.js b/x-pack/test/functional/apps/security/user_email.js index a007c40a06b62..b4adfe1ce2b66 100644 --- a/x-pack/test/functional/apps/security/user_email.js +++ b/x-pack/test/functional/apps/security/user_email.js @@ -12,7 +12,6 @@ export default function({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); describe('useremail', function() { - this.tags('smoke'); before(async () => { await esArchiver.load('security/discover'); await PageObjects.settings.navigateTo(); diff --git a/x-pack/test/functional/apps/security/users.js b/x-pack/test/functional/apps/security/users.js index f49a74a661a63..04d59334a01c4 100644 --- a/x-pack/test/functional/apps/security/users.js +++ b/x-pack/test/functional/apps/security/users.js @@ -12,7 +12,6 @@ export default function({ getService, getPageObjects }) { const log = getService('log'); describe('users', function() { - this.tags('smoke'); before(async () => { log.debug('users'); await PageObjects.settings.navigateTo(); diff --git a/x-pack/test/functional/apps/snapshot_restore/home_page.ts b/x-pack/test/functional/apps/snapshot_restore/home_page.ts index 608c7f321a08f..56ebee79d06ff 100644 --- a/x-pack/test/functional/apps/snapshot_restore/home_page.ts +++ b/x-pack/test/functional/apps/snapshot_restore/home_page.ts @@ -13,7 +13,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const es = getService('legacyEs'); describe('Home page', function() { - this.tags('smoke'); before(async () => { await pageObjects.common.navigateToApp('snapshotRestore'); }); diff --git a/x-pack/test/functional/apps/spaces/enter_space.ts b/x-pack/test/functional/apps/spaces/enter_space.ts index 38220c15cb266..d45b8a1ea4cdb 100644 --- a/x-pack/test/functional/apps/spaces/enter_space.ts +++ b/x-pack/test/functional/apps/spaces/enter_space.ts @@ -13,7 +13,7 @@ export default function enterSpaceFunctonalTests({ const PageObjects = getPageObjects(['security', 'spaceSelector']); describe('Enter Space', function() { - this.tags('smoke'); + this.tags('includeFirefox'); before(async () => { await esArchiver.load('spaces/enter_space'); await PageObjects.security.forceLogout(); diff --git a/x-pack/test/functional/apps/spaces/spaces_selection.ts b/x-pack/test/functional/apps/spaces/spaces_selection.ts index 7b4a1e6e2b8a0..b88311597d765 100644 --- a/x-pack/test/functional/apps/spaces/spaces_selection.ts +++ b/x-pack/test/functional/apps/spaces/spaces_selection.ts @@ -21,7 +21,7 @@ export default function spaceSelectorFunctonalTests({ ]); describe('Spaces', function() { - this.tags('smoke'); + this.tags('includeFirefox'); describe('Space Selector', () => { before(async () => { await esArchiver.load('spaces/selector'); diff --git a/x-pack/test/functional/apps/status_page/status_page.ts b/x-pack/test/functional/apps/status_page/status_page.ts index 58551aaaf4112..fa5a0c0aa0402 100644 --- a/x-pack/test/functional/apps/status_page/status_page.ts +++ b/x-pack/test/functional/apps/status_page/status_page.ts @@ -13,7 +13,7 @@ export default function statusPageFunctonalTests({ const PageObjects = getPageObjects(['security', 'statusPage', 'home']); describe('Status Page', function() { - this.tags('smoke'); + this.tags('includeFirefox'); before(async () => await esArchiver.load('empty_kibana')); after(async () => await esArchiver.unload('empty_kibana')); diff --git a/x-pack/test/functional/apps/transform/cloning.ts b/x-pack/test/functional/apps/transform/cloning.ts index e6e12f60f0bcc..4f71d81eacdf8 100644 --- a/x-pack/test/functional/apps/transform/cloning.ts +++ b/x-pack/test/functional/apps/transform/cloning.ts @@ -27,7 +27,6 @@ export default function({ getService }: FtrProviderContext) { const transform = getService('transform'); describe('cloning', function() { - this.tags(['smoke']); const transformConfig = getTransformConfig(); before(async () => { diff --git a/x-pack/test/functional/apps/transform/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation_index_pattern.ts index bea6b814ee8a3..0e61635fb70e4 100644 --- a/x-pack/test/functional/apps/transform/creation_index_pattern.ts +++ b/x-pack/test/functional/apps/transform/creation_index_pattern.ts @@ -18,7 +18,6 @@ export default function({ getService }: FtrProviderContext) { const transform = getService('transform'); describe('creation_index_pattern', function() { - this.tags(['smoke']); before(async () => { await esArchiver.loadIfNeeded('ml/ecommerce'); await transform.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); @@ -90,7 +89,7 @@ export default function({ getService }: FtrProviderContext) { mode: 'batch', progress: '100', }, - sourcePreview: { + indexPreview: { columns: 20, rows: 5, }, @@ -144,7 +143,7 @@ export default function({ getService }: FtrProviderContext) { mode: 'batch', progress: '100', }, - sourcePreview: { + indexPreview: { columns: 20, rows: 5, }, @@ -180,14 +179,14 @@ export default function({ getService }: FtrProviderContext) { await transform.wizard.assertDefineStepActive(); }); - it('loads the source index preview', async () => { - await transform.wizard.assertSourceIndexPreviewLoaded(); + it('loads the index preview', async () => { + await transform.wizard.assertIndexPreviewLoaded(); }); - it('shows the source index preview', async () => { - await transform.wizard.assertSourceIndexPreview( - testData.expected.sourcePreview.columns, - testData.expected.sourcePreview.rows + it('shows the index preview', async () => { + await transform.wizard.assertIndexPreview( + testData.expected.indexPreview.columns, + testData.expected.indexPreview.rows ); }); diff --git a/x-pack/test/functional/apps/transform/creation_saved_search.ts b/x-pack/test/functional/apps/transform/creation_saved_search.ts index 993bd3a79abbc..26aec913e4756 100644 --- a/x-pack/test/functional/apps/transform/creation_saved_search.ts +++ b/x-pack/test/functional/apps/transform/creation_saved_search.ts @@ -18,7 +18,6 @@ export default function({ getService }: FtrProviderContext) { const transform = getService('transform'); describe('creation_saved_search', function() { - this.tags(['smoke']); before(async () => { await esArchiver.loadIfNeeded('ml/farequote'); await transform.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); @@ -65,7 +64,7 @@ export default function({ getService }: FtrProviderContext) { progress: '100', }, sourceIndex: 'ft_farequote', - sourcePreview: { + indexPreview: { column: 2, values: ['ASA'], }, @@ -101,14 +100,14 @@ export default function({ getService }: FtrProviderContext) { await transform.wizard.assertDefineStepActive(); }); - it('loads the source index preview', async () => { - await transform.wizard.assertSourceIndexPreviewLoaded(); + it('loads the index preview', async () => { + await transform.wizard.assertIndexPreviewLoaded(); }); - it('shows the filtered source index preview', async () => { - await transform.wizard.assertSourceIndexPreviewColumnValues( - testData.expected.sourcePreview.column, - testData.expected.sourcePreview.values + it('shows the filtered index preview', async () => { + await transform.wizard.assertIndexPreviewColumnValues( + testData.expected.indexPreview.column, + testData.expected.indexPreview.values ); }); diff --git a/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts b/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts index 42118f4c63b1f..d2210bee0063d 100644 --- a/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts +++ b/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts @@ -14,7 +14,7 @@ export default function upgradeAssistantFunctionalTests({ const PageObjects = getPageObjects(['upgradeAssistant']); describe('Upgrade Checkup', function() { - this.tags('smoke'); + this.tags('includeFirefox'); before(async () => await esArchiver.load('empty_kibana')); after(async () => { await PageObjects.upgradeAssistant.expectTelemetryHasFinish(); diff --git a/x-pack/test/functional/apps/uptime/settings.ts b/x-pack/test/functional/apps/uptime/settings.ts index e81bbc5ae42f9..64b6300e0df63 100644 --- a/x-pack/test/functional/apps/uptime/settings.ts +++ b/x-pack/test/functional/apps/uptime/settings.ts @@ -6,10 +6,8 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { - defaultDynamicSettings, - DynamicSettings, -} from '../../../../legacy/plugins/uptime/common/runtime_types'; +import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../legacy/plugins/uptime/common/constants'; +import { DynamicSettings } from '../../../../legacy/plugins/uptime/common/runtime_types'; import { makeChecks } from '../../../api_integration/apis/uptime/rest/helper/make_checks'; export default ({ getPageObjects, getService }: FtrProviderContext) => { @@ -32,7 +30,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await settings.go(); const fields = await settings.loadFields(); - expect(fields).to.eql(defaultDynamicSettings); + expect(fields).to.eql(DYNAMIC_SETTINGS_DEFAULTS); }); it('should disable the apply button when invalid or unchanged', async () => { @@ -62,7 +60,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await settings.go(); - const newFieldValues: DynamicSettings = { heartbeatIndices: 'new*' }; + const newFieldValues: DynamicSettings = { + heartbeatIndices: 'new*', + certThresholds: { + age: 365, + expiration: 30, + }, + }; await settings.changeHeartbeatIndicesInput(newFieldValues.heartbeatIndices); await settings.apply(); @@ -91,7 +95,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Verify that the settings page shows the value we previously saved await settings.go(); const fields = await settings.loadFields(); - expect(fields.certificatesThresholds.errorState).to.eql(newErrorThreshold); + expect(fields.certThresholds?.expiration).to.eql(newErrorThreshold); }); it('changing certificate expiration warning threshold is reflected in settings page', async () => { @@ -108,7 +112,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { // Verify that the settings page shows the value we previously saved await settings.go(); const fields = await settings.loadFields(); - expect(fields.certificatesThresholds.warningState).to.eql(newWarningThreshold); + expect(fields.certThresholds?.age).to.eql(newWarningThreshold); }); }); }; diff --git a/x-pack/test/functional/apps/watcher/index.js b/x-pack/test/functional/apps/watcher/index.js index 9f0d5a5de405a..e894a890fdb50 100644 --- a/x-pack/test/functional/apps/watcher/index.js +++ b/x-pack/test/functional/apps/watcher/index.js @@ -6,7 +6,7 @@ export default function({ loadTestFile }) { describe('watcher app', function() { - this.tags(['ciGroup1', 'smoke']); + this.tags(['ciGroup1', 'includeFirefox']); //loadTestFile(require.resolve('./management')); loadTestFile(require.resolve('./watcher_test')); diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index f26110513a9b3..2c6238704bea0 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -57,6 +57,8 @@ export default async function({ readConfigFile }) { resolve(__dirname, './apps/cross_cluster_replication'), resolve(__dirname, './apps/remote_clusters'), resolve(__dirname, './apps/transform'), + resolve(__dirname, './apps/reporting_management'), + // This license_management file must be last because it is destructive. resolve(__dirname, './apps/license_management'), ], @@ -196,6 +198,10 @@ export default async function({ readConfigFile }) { pathname: '/app/kibana/', hash: '/management/elasticsearch/transform', }, + reporting: { + pathname: '/app/kibana/', + hash: '/management/kibana/reporting', + }, }, // choose where esArchiver should load archives from @@ -228,6 +234,17 @@ export default async function({ readConfigFile }) { kibana: [], }, + global_discover_read: { + kibana: [ + { + feature: { + discover: ['read'], + }, + spaces: ['*'], + }, + ], + }, + //Kibana feature privilege isn't specific to advancedSetting. It can be anything. https://github.com/elastic/kibana/issues/35965 test_api_keys: { elasticsearch: { diff --git a/x-pack/test/functional/es_archives/empty_kibana/data.json.gz b/x-pack/test/functional/es_archives/empty_kibana/data.json.gz deleted file mode 100644 index 8334749a696d7..0000000000000 Binary files a/x-pack/test/functional/es_archives/empty_kibana/data.json.gz and /dev/null differ 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 1ffb119ca1023..3fe4f828ba128 100644 --- a/x-pack/test/functional/es_archives/fleet/agents/data.json +++ b/x-pack/test/functional/es_archives/fleet/agents/data.json @@ -1,11 +1,11 @@ { "type": "doc", "value": { - "id": "agents:agent1", + "id": "fleet-agents:agent1", "index": ".kibana", "source": { - "type": "agents", - "agents": { + "type": "fleet-agents", + "fleet-agents": { "access_api_key_id": "api-key-2", "active": true, "shared_id": "agent1_filebeat", @@ -21,11 +21,11 @@ { "type": "doc", "value": { - "id": "agents:agent2", + "id": "fleet-agents:agent2", "index": ".kibana", "source": { - "type": "agents", - "agents": { + "type": "fleet-agents", + "fleet-agents": { "access_api_key_id": "api-key-2", "active": true, "shared_id": "agent2_filebeat", @@ -40,11 +40,11 @@ { "type": "doc", "value": { - "id": "agents:agent3", + "id": "fleet-agents:agent3", "index": ".kibana", "source": { - "type": "agents", - "agents": { + "type": "fleet-agents", + "fleet-agents": { "access_api_key_id": "api-key-3", "active": true, "shared_id": "agent3_metricbeat", @@ -59,11 +59,11 @@ { "type": "doc", "value": { - "id": "agents:agent4", + "id": "fleet-agents:agent4", "index": ".kibana", "source": { - "type": "agents", - "agents": { + "type": "fleet-agents", + "fleet-agents": { "access_api_key_id": "api-key-4", "active": true, "shared_id": "agent4_metricbeat", @@ -78,17 +78,17 @@ { "type": "doc", "value": { - "id": "enrollment_api_keys:ed22ca17-e178-4cfe-8b02-54ea29fbd6d0", + "id": "fleet-enrollment-api-keys:ed22ca17-e178-4cfe-8b02-54ea29fbd6d0", "index": ".kibana", "source": { - "enrollment_api_keys" : { + "fleet-enrollment-api-keys" : { "created_at" : "2019-10-10T16:31:12.518Z", "name": "FleetEnrollmentKey:1", "api_key_id" : "key", "config_id" : "policy:1", "active" : true }, - "type" : "enrollment_api_keys", + "type" : "fleet-enrollment-api-keys", "references": [] } } @@ -97,11 +97,11 @@ { "type": "doc", "value": { - "id": "events:event1", + "id": "fleet-agent-events:event1", "index": ".kibana", "source": { - "type": "agent_events", - "agent_events": { + "type": "fleet-agent-events", + "fleet-agent-events": { "agent_id": "agent1", "type": "STATE", "subtype": "STARTED", @@ -116,11 +116,11 @@ { "type": "doc", "value": { - "id": "events:event2", + "id": "fleet-agent-events:event2", "index": ".kibana", "source": { - "type": "agent_events", - "agent_events": { + "type": "fleet-agent-events", + "fleet-agent-events": { "agent_id": "agent1", "type": "STATE", "subtype": "STOPPED", @@ -135,11 +135,11 @@ { "type": "doc", "value": { - "id": "agent_actions:37ed51ff-e80f-4f2a-a62d-f4fa975e7d85", + "id": "fleet-agent-actions:37ed51ff-e80f-4f2a-a62d-f4fa975e7d85", "index": ".kibana", "source": { - "type": "agent_actions", - "agent_actions": { + "type": "fleet-agent-actions", + "fleet-agent-actions": { "agent_id": "agent1", "created_at": "2019-09-04T15:04:07+0000", "type": "RESUME", @@ -152,11 +152,11 @@ { "type": "doc", "value": { - "id": "agent_actions:b400439c-bbbf-43d5-83cb-cf8b7e32506f", + "id": "fleet-agent-actions:b400439c-bbbf-43d5-83cb-cf8b7e32506f", "index": ".kibana", "source": { - "type": "agent_actions", - "agent_actions": { + "type": "fleet-agent-actions", + "fleet-agent-actions": { "agent_id": "agent1", "type": "PAUSE", "created_at": "2019-09-04T15:01:07+0000", @@ -169,11 +169,11 @@ { "type": "doc", "value": { - "id": "agent_actions:48cebde1-c906-4893-b89f-595d943b72a1", + "id": "fleet-agent-actions:48cebde1-c906-4893-b89f-595d943b72a1", "index": ".kibana", "source": { - "type": "agent_actions", - "agent_actions": { + "type": "fleet-agent-actions", + "fleet-agent-actions": { "agent_id": "agent1", "type": "CONFIG_CHANGE", "created_at": "2020-03-15T03:47:15.129Z", @@ -186,11 +186,11 @@ { "type": "doc", "value": { - "id": "agent_actions:48cebde1-c906-4893-b89f-595d943b72a2", + "id": "fleet-agent-actions:48cebde1-c906-4893-b89f-595d943b72a2", "index": ".kibana", "source": { - "type": "agent_actions", - "agent_actions": { + "type": "fleet-agent-actions", + "fleet-agent-actions": { "agent_id": "agent1", "type": "CONFIG_CHANGE", "created_at": "2020-03-15T03:47:15.129Z", diff --git a/x-pack/test/functional/es_archives/fleet/agents/mappings.json b/x-pack/test/functional/es_archives/fleet/agents/mappings.json index 31ae161049303..5d5d373797d4c 100644 --- a/x-pack/test/functional/es_archives/fleet/agents/mappings.json +++ b/x-pack/test/functional/es_archives/fleet/agents/mappings.json @@ -9,7 +9,7 @@ "dynamic": "strict", "_meta": { "migrationMappingPropertyHashes": { - "outputs": "aee9782e0d500b867859650a36280165", + "ingest-outputs": "aee9782e0d500b867859650a36280165", "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", "visualization": "52d7a13ad68a150c4525b292d23e12cc", "references": "7997cf5a56cc02bdc9c93361bde732b0", @@ -23,14 +23,14 @@ "dashboard": "d00f614b29a80360e1190193fd333bab", "metrics-explorer-view": "53c5365793677328df0ccb6138bf3cdd", "siem-detection-engine-rule-actions": "90eee2e4635260f4be0a1da8f5bc0aa0", - "agent_events": "3231653fafe4ef3196fe3b32ab774bf2", + "fleet-agent-events": "3231653fafe4ef3196fe3b32ab774bf2", "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", "inventory-view": "9ecce5b58867403613d82fe496470b34", - "enrollment_api_keys": "28b91e20b105b6f928e2012600085d8f", + "fleet-enrollment-api-keys": "28b91e20b105b6f928e2012600085d8f", "upgrade-assistant-reindex-operation": "a53a20fe086b72c9a86da3cc12dad8a6", "cases-comments": "c2061fb929f585df57425102fa928b4b", "canvas-element": "7390014e1091044523666d97247392fc", @@ -50,20 +50,20 @@ "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", "map": "23d7aa4a720d4938ccde3983f87bd58d", "uptime-dynamic-settings": "b6289473c8985c79b6c47eebc19a0ca5", - "epm-package": "75d12cd13c867fd713d7dfb27366bc20", + "epm-packages": "75d12cd13c867fd713d7dfb27366bc20", "apm-telemetry": "3525d7c22c42bc80f5e6e9cb3f2b26a2", "cases": "08b8b110dbca273d37e8aef131ecab61", "siem-ui-timeline": "ac8020190f5950dd3250b6499144e7fb", "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", "url": "c7f66a0df8b1b52f17c28c4adb111105", - "agents": "c3eeb7b9d97176f15f6d126370ab23c7", + "fleet-agents": "c3eeb7b9d97176f15f6d126370ab23c7", "migrationVersion": "4a1746014a75ade3a714e1db5763276f", "index-pattern": "66eccb05066c5a89924f48a9e9736499", "maps-telemetry": "268da3a48066123fc5baf35abaa55014", "namespace": "2f4316de49999235636386fe51dc06c1", "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", - "agent_actions": "ed270b46812f0fa1439366c428a2cf17", + "fleet-agent-actions": "ed270b46812f0fa1439366c428a2cf17", "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", "config": "ae24d22d5986d04124cc6568f771066f", @@ -107,7 +107,7 @@ } } }, - "agent_actions": { + "fleet-agent-actions": { "properties": { "agent_id": { "type": "keyword" @@ -160,7 +160,7 @@ } } }, - "agent_events": { + "fleet-agent-events": { "properties": { "action_id": { "type": "keyword" @@ -194,7 +194,7 @@ } } }, - "agents": { + "fleet-agents": { "properties": { "access_api_key_id": { "type": "keyword" @@ -1705,7 +1705,7 @@ } } }, - "enrollment_api_keys": { + "fleet-enrollment-api-keys": { "properties": { "active": { "type": "boolean" @@ -1736,7 +1736,7 @@ } } }, - "epm-package": { + "epm-packages": { "properties": { "installed": { "type": "nested", @@ -2211,7 +2211,7 @@ "namespace": { "type": "keyword" }, - "outputs": { + "ingest-outputs": { "properties": { "api_key": { "type": "keyword" diff --git a/x-pack/test/functional/es_archives/reporting/archived_reports/data.json.gz b/x-pack/test/functional/es_archives/reporting/archived_reports/data.json.gz new file mode 100644 index 0000000000000..34a30bd84a592 Binary files /dev/null and b/x-pack/test/functional/es_archives/reporting/archived_reports/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/reporting/archived_reports/mappings.json b/x-pack/test/functional/es_archives/reporting/archived_reports/mappings.json new file mode 100644 index 0000000000000..20f8f840ee863 --- /dev/null +++ b/x-pack/test/functional/es_archives/reporting/archived_reports/mappings.json @@ -0,0 +1,108 @@ +{ + "type": "index", + "value": { + "aliases": { + }, + "index": ".reporting-2020.04.19", + "mappings": { + "properties": { + "attempts": { + "type": "short" + }, + "browser_type": { + "type": "keyword" + }, + "completed_at": { + "type": "date" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "jobtype": { + "type": "keyword" + }, + "kibana_id": { + "type": "keyword" + }, + "kibana_name": { + "type": "keyword" + }, + "max_attempts": { + "type": "short" + }, + "meta": { + "properties": { + "layout": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "objectType": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "output": { + "properties": { + "content": { + "enabled": false, + "type": "object" + }, + "content_type": { + "type": "keyword" + }, + "csv_contains_formulas": { + "type": "boolean" + }, + "max_size_reached": { + "type": "boolean" + }, + "size": { + "type": "long" + } + } + }, + "payload": { + "enabled": false, + "type": "object" + }, + "priority": { + "type": "byte" + }, + "process_expiration": { + "type": "date" + }, + "started_at": { + "type": "date" + }, + "status": { + "type": "keyword" + }, + "timeout": { + "type": "long" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1", + "prefer_v2_templates": "false" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/page_objects/canvas_page.ts b/x-pack/test/functional/page_objects/canvas_page.ts index de826097a5be6..94ad393ead3a3 100644 --- a/x-pack/test/functional/page_objects/canvas_page.ts +++ b/x-pack/test/functional/page_objects/canvas_page.ts @@ -51,8 +51,12 @@ export function CanvasPageProvider({ getService }: FtrProviderContext) { expect(disabledAttr).to.be('true'); }, - async openAddElementModal() { + async openSavedElementsModal() { await testSubjects.click('add-element-button'); + await testSubjects.click('saved-elements-menu-option'); + }, + async closeSavedElementsModal() { + await testSubjects.click('saved-elements-modal-close-button'); }, async expectAddElementButton() { diff --git a/x-pack/test/functional/page_objects/reporting_page.js b/x-pack/test/functional/page_objects/reporting_page.js index cdfafeec1bf46..35a76c74d4811 100644 --- a/x-pack/test/functional/page_objects/reporting_page.js +++ b/x-pack/test/functional/page_objects/reporting_page.js @@ -170,6 +170,5 @@ export function ReportingPageProvider({ getService, getPageObjects }) { await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); } } - return new ReportingPage(); } diff --git a/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts b/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts index bada1d42b564a..bd7d76e34b447 100644 --- a/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts +++ b/x-pack/test/functional/services/machine_learning/data_frame_analytics.ts @@ -41,7 +41,7 @@ export function MachineLearningDataFrameAnalyticsProvider( }, async assertRegressionTablePanelExists() { - await testSubjects.existOrFail('mlDFAnalyticsRegressionExplorationTablePanel'); + await testSubjects.existOrFail('mlDFAnalyticsExplorationTablePanel'); }, async assertClassificationEvaluatePanelElementsExists() { @@ -50,7 +50,7 @@ export function MachineLearningDataFrameAnalyticsProvider( }, async assertClassificationTablePanelExists() { - await testSubjects.existOrFail('mlDFAnalyticsClassificationExplorationTablePanel'); + await testSubjects.existOrFail('mlDFAnalyticsExplorationTablePanel'); }, async assertOutlierTablePanelExists() { diff --git a/x-pack/test/functional/services/machine_learning/job_table.ts b/x-pack/test/functional/services/machine_learning/job_table.ts index 0e638963f2367..e2451328ea941 100644 --- a/x-pack/test/functional/services/machine_learning/job_table.ts +++ b/x-pack/test/functional/services/machine_learning/job_table.ts @@ -187,44 +187,24 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte expectedCounts: object, expectedModelSizeStats: object ) { - const countDetails = await this.parseJobCounts(jobId); - const counts = countDetails.counts; - - // fields that have changing values are only validated - // to be present and then removed so they don't make - // the object validation fail - expect(counts).to.have.property('last_data_time'); - delete counts.last_data_time; - - expect(counts).to.eql(expectedCounts); - - const modelSizeStats = countDetails.modelSizeStats; - - // fields that have changing values are only validated - // to be present and then removed so they don't make - // the object validation fail - expect(modelSizeStats).to.have.property('log_time'); - delete modelSizeStats.log_time; - expect(modelSizeStats).to.have.property('model_bytes'); - delete modelSizeStats.model_bytes; - - // remove categorization fields from validation until - // the ES version is updated - delete modelSizeStats.categorization_status; - delete modelSizeStats.categorized_doc_count; - delete modelSizeStats.dead_category_count; - delete modelSizeStats.frequent_category_count; - delete modelSizeStats.rare_category_count; - delete modelSizeStats.total_category_count; - - // MML during clone has changed in #61589 - // TODO: adjust test code to reflect the new behavior - expect(modelSizeStats).to.have.property('model_bytes_memory_limit'); - delete modelSizeStats.model_bytes_memory_limit; - // @ts-ignore - delete expectedModelSizeStats.model_bytes_memory_limit; - - expect(modelSizeStats).to.eql(expectedModelSizeStats); + const { counts, modelSizeStats } = await this.parseJobCounts(jobId); + + // Only check for expected keys / values, ignore additional properties + // This way the tests stay stable when new properties are added on the ES side + for (const [key, value] of Object.entries(expectedCounts)) { + expect(counts) + .to.have.property(key) + .eql(value, `Expected counts property '${key}' to exist with value '${value}'`); + } + + for (const [key, value] of Object.entries(expectedModelSizeStats)) { + expect(modelSizeStats) + .to.have.property(key) + .eql( + value, + `Expected model size stats property '${key}' to exist with value '${value}')` + ); + } } public async clickActionsMenu(jobId: string) { diff --git a/x-pack/test/functional/services/transform_ui/wizard.ts b/x-pack/test/functional/services/transform_ui/wizard.ts index ba9096a372d9a..e63af493438d6 100644 --- a/x-pack/test/functional/services/transform_ui/wizard.ts +++ b/x-pack/test/functional/services/transform_ui/wizard.ts @@ -52,8 +52,8 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { await this.assertDetailsSummaryExists(); }, - async assertSourceIndexPreviewExists(subSelector?: string) { - let selector = 'transformSourceIndexPreview'; + async assertIndexPreviewExists(subSelector?: string) { + let selector = 'transformIndexPreview'; if (subSelector !== undefined) { selector = `${selector} ${subSelector}`; } else { @@ -62,8 +62,8 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { await testSubjects.existOrFail(selector); }, - async assertSourceIndexPreviewLoaded() { - await this.assertSourceIndexPreviewExists('loaded'); + async assertIndexPreviewLoaded() { + await this.assertIndexPreviewExists('loaded'); }, async assertPivotPreviewExists(subSelector?: string) { @@ -124,10 +124,10 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { }); }, - async assertSourceIndexPreview(columns: number, rows: number) { + async assertIndexPreview(columns: number, rows: number) { await retry.tryForTime(2000, async () => { // get a 2D array of rows and cell values - const rowsData = await this.parseEuiDataGrid('transformSourceIndexPreview'); + const rowsData = await this.parseEuiDataGrid('transformIndexPreview'); expect(rowsData).to.length( rows, @@ -143,8 +143,8 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { }); }, - async assertSourceIndexPreviewColumnValues(column: number, values: string[]) { - await this.assertEuiDataGridColumnValues('transformSourceIndexPreview', column, values); + async assertIndexPreviewColumnValues(column: number, values: string[]) { + await this.assertEuiDataGridColumnValues('transformIndexPreview', column, values); }, async assertPivotPreviewColumnValues(column: number, values: string[]) { diff --git a/x-pack/test/functional/services/uptime/settings.ts b/x-pack/test/functional/services/uptime/settings.ts index 14cab368b766a..9719152b62d35 100644 --- a/x-pack/test/functional/services/uptime/settings.ts +++ b/x-pack/test/functional/services/uptime/settings.ts @@ -5,6 +5,7 @@ */ import { FtrProviderContext } from '../../ftr_provider_context'; +import { DynamicSettings } from '../../../../legacy/plugins/uptime/common/runtime_types'; export function UptimeSettingsProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); @@ -25,24 +26,24 @@ export function UptimeSettingsProvider({ getService }: FtrProviderContext) { await changeInputField(text, 'heartbeat-indices-input-loaded'); }, changeErrorThresholdInput: async (text: string) => { - await changeInputField(text, 'error-state-threshold-input-loaded'); + await changeInputField(text, 'expiration-threshold-input-loaded'); }, changeWarningThresholdInput: async (text: string) => { - await changeInputField(text, 'warning-state-threshold-input-loaded'); + await changeInputField(text, 'age-threshold-input-loaded'); }, - loadFields: async () => { + loadFields: async (): Promise => { const indInput = await testSubjects.find('heartbeat-indices-input-loaded', 5000); - const errorInput = await testSubjects.find('error-state-threshold-input-loaded', 5000); - const warningInput = await testSubjects.find('warning-state-threshold-input-loaded', 5000); + const expirationInput = await testSubjects.find('expiration-threshold-input-loaded', 5000); + const ageInput = await testSubjects.find('age-threshold-input-loaded', 5000); const heartbeatIndices = await indInput.getAttribute('value'); - const errorThreshold = await errorInput.getAttribute('value'); - const warningThreshold = await warningInput.getAttribute('value'); + const expiration = await expirationInput.getAttribute('value'); + const age = await ageInput.getAttribute('value'); return { heartbeatIndices, - certificatesThresholds: { - errorState: errorThreshold, - warningState: warningThreshold, + certThresholds: { + age: parseInt(age, 10), + expiration: parseInt(expiration, 10), }, }; }, diff --git a/x-pack/test/functional_endpoint/apps/endpoint/feature_controls/endpoint_spaces.ts b/x-pack/test/functional_endpoint/apps/endpoint/feature_controls/endpoint_spaces.ts index c543046031e9f..fdebdae9e5d0e 100644 --- a/x-pack/test/functional_endpoint/apps/endpoint/feature_controls/endpoint_spaces.ts +++ b/x-pack/test/functional_endpoint/apps/endpoint/feature_controls/endpoint_spaces.ts @@ -41,13 +41,13 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await testSubjects.existOrFail('welcomeTitle'); }); - it(`endpoint management shows 'Hosts'`, async () => { + it(`endpoint hosts shows hosts lists page`, async () => { await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/hosts', undefined, { basePath: '/s/custom_space', ensureCurrentUrl: false, shouldLoginIfPrompted: false, }); - await testSubjects.existOrFail('hostListTitle'); + await testSubjects.existOrFail('hostPage'); }); }); diff --git a/x-pack/test/functional_endpoint/apps/endpoint/header_nav.ts b/x-pack/test/functional_endpoint/apps/endpoint/header_nav.ts index c2c4068212484..b944056e00911 100644 --- a/x-pack/test/functional_endpoint/apps/endpoint/header_nav.ts +++ b/x-pack/test/functional_endpoint/apps/endpoint/header_nav.ts @@ -31,7 +31,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('renders the hosts page when the Hosts tab is selected', async () => { await (await testSubjects.find('hostsEndpointTab')).click(); - await testSubjects.existOrFail('hostListTitle'); + await testSubjects.existOrFail('hostPage'); }); it('renders the alerts page when the Alerts tab is selected', async () => { @@ -46,7 +46,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('renders the home page when Home tab is selected after selecting another tab', async () => { await (await testSubjects.find('hostsEndpointTab')).click(); - await testSubjects.existOrFail('hostListTitle'); + await testSubjects.existOrFail('hostPage'); await (await testSubjects.find('homeEndpointTab')).click(); await testSubjects.existOrFail('welcomeTitle'); 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 bbf8881f0c62a..597f1ad9119b0 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 @@ -68,7 +68,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await nameInput.click(); await testSubjects.click('.slack-ActionTypeSelectOption'); - await testSubjects.click('createActionConnectorButton'); + await testSubjects.click('addNewActionConnectorButton-.slack'); const slackConnectorName = generateUniqueKey(); await testSubjects.setValue('nameInput', slackConnectorName); await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); diff --git a/x-pack/test/saml_api_integration/apis/security/saml_login.ts b/x-pack/test/saml_api_integration/apis/security/saml_login.ts index 3b11ef61a1ab2..0b127288e7958 100644 --- a/x-pack/test/saml_api_integration/apis/security/saml_login.ts +++ b/x-pack/test/saml_api_integration/apis/security/saml_login.ts @@ -35,7 +35,7 @@ export default function({ getService }: FtrProviderContext) { }); } - async function checkSessionCookie(sessionCookie: Cookie) { + async function checkSessionCookie(sessionCookie: Cookie, username = 'a@b.c') { expect(sessionCookie.key).to.be('sid'); expect(sessionCookie.value).to.not.be.empty(); expect(sessionCookie.path).to.be('/'); @@ -59,7 +59,7 @@ export default function({ getService }: FtrProviderContext) { 'authentication_provider', ]); - expect(apiResponse.body.username).to.be('a@b.c'); + expect(apiResponse.body.username).to.be(username); } describe('SAML authentication', () => { @@ -668,6 +668,29 @@ export default function({ getService }: FtrProviderContext) { const existingUsername = 'a@b.c'; let existingSessionCookie: Cookie; + const testScenarios: Array<[string, () => Promise]> = [ + // Default scenario when active cookie has an active access token. + ['when access token is valid', async () => {}], + // Scenario when active cookie has an expired access token. Access token expiration is set + // to 15s for API integration tests so we need to wait for 20s to make sure token expires. + ['when access token is expired', async () => await delay(20000)], + // Scenario when active cookie references to access/refresh token pair that were already + // removed from Elasticsearch (to simulate 24h when expired tokens are removed). + [ + 'when access token document is missing', + async () => { + const esResponse = await getService('legacyEs').deleteByQuery({ + index: '.security-tokens', + q: 'doc_type:token', + refresh: true, + }); + expect(esResponse) + .to.have.property('deleted') + .greaterThan(0); + }, + ], + ]; + beforeEach(async () => { const captureURLResponse = await supertest .get('/abc/xyz/handshake?one=two three') @@ -701,76 +724,76 @@ export default function({ getService }: FtrProviderContext) { )!; }); - it('should renew session and redirect to the home page if login is for the same user', async () => { - const samlAuthenticationResponse = await supertest - .post('/api/security/saml/callback') - .set('kbn-xsrf', 'xxx') - .set('Cookie', existingSessionCookie.cookieString()) - .send({ SAMLResponse: await createSAMLResponse({ username: existingUsername }) }) - .expect('location', '/') - .expect(302); - - const newSessionCookie = request.cookie( - samlAuthenticationResponse.headers['set-cookie'][0] - )!; - expect(newSessionCookie.value).to.not.be.empty(); - expect(newSessionCookie.value).to.not.equal(existingSessionCookie.value); - - // Tokens from old cookie are invalidated. - const rejectedResponse = await supertest - .get('/internal/security/me') - .set('kbn-xsrf', 'xxx') - .set('Cookie', existingSessionCookie.cookieString()) - .expect(400); - expect(rejectedResponse.body).to.have.property( - 'message', - 'Both access and refresh tokens are expired.' - ); - - // Only tokens from new session are valid. - const acceptedResponse = await supertest - .get('/internal/security/me') - .set('kbn-xsrf', 'xxx') - .set('Cookie', newSessionCookie.cookieString()) - .expect(200); - expect(acceptedResponse.body).to.have.property('username', existingUsername); - }); - - it('should create a new session and redirect to the `overwritten_session` if login is for another user', async () => { - const newUsername = 'c@d.e'; - const samlAuthenticationResponse = await supertest - .post('/api/security/saml/callback') - .set('kbn-xsrf', 'xxx') - .set('Cookie', existingSessionCookie.cookieString()) - .send({ SAMLResponse: await createSAMLResponse({ username: newUsername }) }) - .expect('location', '/security/overwritten_session') - .expect(302); - - const newSessionCookie = request.cookie( - samlAuthenticationResponse.headers['set-cookie'][0] - )!; - expect(newSessionCookie.value).to.not.be.empty(); - expect(newSessionCookie.value).to.not.equal(existingSessionCookie.value); - - // Tokens from old cookie are invalidated. - const rejectedResponse = await supertest - .get('/internal/security/me') - .set('kbn-xsrf', 'xxx') - .set('Cookie', existingSessionCookie.cookieString()) - .expect(400); - expect(rejectedResponse.body).to.have.property( - 'message', - 'Both access and refresh tokens are expired.' - ); + for (const [description, setup] of testScenarios) { + it(`should renew session and redirect to the home page if login is for the same user ${description}`, async () => { + await setup(); + + const samlAuthenticationResponse = await supertest + .post('/api/security/saml/callback') + .set('kbn-xsrf', 'xxx') + .set('Cookie', existingSessionCookie.cookieString()) + .send({ SAMLResponse: await createSAMLResponse({ username: existingUsername }) }) + .expect(302); + + expect(samlAuthenticationResponse.headers.location).to.be('/'); + + const newSessionCookie = request.cookie( + samlAuthenticationResponse.headers['set-cookie'][0] + )!; + expect(newSessionCookie.value).to.not.be.empty(); + expect(newSessionCookie.value).to.not.equal(existingSessionCookie.value); + + // Tokens from old cookie are invalidated. + const rejectedResponse = await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'xxx') + .set('Cookie', existingSessionCookie.cookieString()) + .expect(400); + expect(rejectedResponse.body).to.have.property( + 'message', + 'Both access and refresh tokens are expired.' + ); + + // Only tokens from new session are valid. + await checkSessionCookie(newSessionCookie); + }); - // Only tokens from new session are valid. - const acceptedResponse = await supertest - .get('/internal/security/me') - .set('kbn-xsrf', 'xxx') - .set('Cookie', newSessionCookie.cookieString()) - .expect(200); - expect(acceptedResponse.body).to.have.property('username', newUsername); - }); + it(`should create a new session and redirect to the \`overwritten_session\` if login is for another user ${description}`, async () => { + await setup(); + + const newUsername = 'c@d.e'; + const samlAuthenticationResponse = await supertest + .post('/api/security/saml/callback') + .set('kbn-xsrf', 'xxx') + .set('Cookie', existingSessionCookie.cookieString()) + .send({ SAMLResponse: await createSAMLResponse({ username: newUsername }) }) + .expect(302); + + expect(samlAuthenticationResponse.headers.location).to.be( + '/security/overwritten_session' + ); + + const newSessionCookie = request.cookie( + samlAuthenticationResponse.headers['set-cookie'][0] + )!; + expect(newSessionCookie.value).to.not.be.empty(); + expect(newSessionCookie.value).to.not.equal(existingSessionCookie.value); + + // Tokens from old cookie are invalidated. + const rejectedResponse = await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'xxx') + .set('Cookie', existingSessionCookie.cookieString()) + .expect(400); + expect(rejectedResponse.body).to.have.property( + 'message', + 'Both access and refresh tokens are expired.' + ); + + // Only tokens from new session are valid. + await checkSessionCookie(newSessionCookie, newUsername); + }); + } }); describe('handshake with very long URL path or fragment', () => { diff --git a/yarn.lock b/yarn.lock index 30747ee555fe2..f348aa99e6f20 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1165,7 +1165,7 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" -"@babel/types@^7.9.5": +"@babel/types@^7.4", "@babel/types@^7.9.5": version "7.9.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.5.tgz#89231f82915a8a566a703b3b20133f73da6b9444" integrity sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg== @@ -1192,6 +1192,32 @@ date-fns "^1.27.2" figures "^1.7.0" +"@cypress/request@2.88.5": + version "2.88.5" + resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.5.tgz#8d7ecd17b53a849cfd5ab06d5abe7d84976375d7" + integrity sha512-TzEC1XMi1hJkywWpRfD2clreTa/Z+lOrXDCxxBTBPEcY5azdPi56A6Xw+O4tWJnaJH3iIE7G5aDXZC6JgRZLcA== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + "@cypress/webpack-preprocessor@^4.1.0": version "4.1.0" resolved "https://registry.yarnpkg.com/@cypress/webpack-preprocessor/-/webpack-preprocessor-4.1.0.tgz#8c4debc0b1abf045b62524d1996dd9aeaf7e86a8" @@ -1236,10 +1262,10 @@ dependencies: "@elastic/apm-rum-core" "^5.2.0" -"@elastic/charts@18.3.0": - version "18.3.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-18.3.0.tgz#cbdeec1860af274edc7a5f5b9dd26ec48c64bb64" - integrity sha512-4kSlSwdDRsVKVX8vRUkwxOu1IT6WIepgLnP0OZT7cFjgrC1SV/16c3YLw2NZDaVe0M/H4rpeNWW30VyrzZVhyw== +"@elastic/charts@18.4.2": + version "18.4.2" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-18.4.2.tgz#7d3c40dca8a7a701fb7227382191b84d36d8b32a" + integrity sha512-fmEDRUeFEtVWGurafhp/5bHBypOjdXiRXY074tCqnez43hA2iA4v1KrZL8tPFlMePgc/kpZf9wb8aEwxlfWumw== dependencies: classnames "^2.2.6" d3-array "^1.2.4" @@ -1377,10 +1403,10 @@ through2 "^2.0.0" update-notifier "^0.5.0" -"@elastic/maki@6.2.0": - version "6.2.0" - resolved "https://registry.yarnpkg.com/@elastic/maki/-/maki-6.2.0.tgz#d0a85aa248bdc14dca44e1f9430c0b670f65e489" - integrity sha512-QkmRNpEY4Dy6eqwDimR5X9leMgdPFjdANmpEIwEW1XVUG2U4YtB2BXhDxsnMmNTUrJUjtnjnwgwBUyg0pU0FTg== +"@elastic/maki@6.3.0": + version "6.3.0" + resolved "https://registry.yarnpkg.com/@elastic/maki/-/maki-6.3.0.tgz#09780650f1510554bef9121b9db86ce297f021f1" + integrity sha512-a2U2DaemIJaW+3nL/sN/+JScdrkoggoGHLDtRPurk2Axnpa9O9QHekmMXLO7eLK1brDpYcplqGE6hwFaMvRRUg== "@elastic/node-crypto@1.1.1": version "1.1.1" @@ -1538,11 +1564,11 @@ through2 "^2.0.3" "@hapi/boom@7.x.x": - version "7.4.2" - resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-7.4.2.tgz#c16957cd09796f6c1bfb4031bdc39d66d6d750c3" - integrity sha512-T2CYcTI0AqSvC6YC7keu/fh9LVSMzfoMLharBnPbOwmc+Cexj9joIc5yNDKunaxYq9LPuOwMS0f2B3S1tFQUNw== + version "7.4.11" + resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-7.4.11.tgz#37af8417eb9416aef3367aa60fa04a1a9f1fc262" + integrity sha512-VSU/Cnj1DXouukYxxkes4nNJonCnlogHvIff1v1RVoN4xzkKhMXX+GRmb3NyH1iar10I9WFPDv2JPwfH3GaV0A== dependencies: - "@hapi/hoek" "6.x.x" + "@hapi/hoek" "8.x.x" "@hapi/bourne@1.x.x": version "1.3.2" @@ -1557,11 +1583,6 @@ "@hapi/hoek" "8.x.x" fast-safe-stringify "2.x.x" -"@hapi/hoek@6.x.x": - version "6.2.1" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-6.2.1.tgz#d3a66329159af879bfdf0b0cff2229c43c5a3451" - integrity sha512-+ryw4GU9pjr1uT6lBuErHJg3NYqzwJTvZ75nKuJijEzpd00Uqi6oiawTGDDf5Hl0zWmI7qHfOtaqB0kpQZJQzA== - "@hapi/hoek@8.x.x": version "8.5.1" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-8.5.1.tgz#fde96064ca446dec8c55a8c2f130957b070c6e06" @@ -3704,15 +3725,20 @@ resolved "https://registry.yarnpkg.com/@types/base64-js/-/base64-js-1.2.5.tgz#582b2476169a6cba460a214d476c744441d873d5" integrity sha1-WCskdhaabLpGCiFNR2x0REHYc9U= -"@types/bluebird@*": - version "3.5.28" - resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.28.tgz#04c1a520ff076649236bc8ca21198542ce2bdb09" - integrity sha512-0Vk/kqkukxPKSzP9c8WJgisgGDx5oZDbsLLWIP5t70yThO/YleE+GEm2S1GlRALTaack3O7U5OS5qEm7q2kciA== +"@types/blob-util@1.3.3": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@types/blob-util/-/blob-util-1.3.3.tgz#adba644ae34f88e1dd9a5864c66ad651caaf628a" + integrity sha512-4ahcL/QDnpjWA2Qs16ZMQif7HjGP2cw3AGjHabybjw7Vm1EKu+cfQN1D78BaZbS1WJNa1opSMF5HNMztx7lR0w== -"@types/bluebird@^3.1.1": - version "3.5.20" - resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.20.tgz#f6363172add6f4eabb8cada53ca9af2781e8d6a1" - integrity sha512-Wk41MVdF+cHBfVXj/ufUHJeO3BlIQr1McbHZANErMykaCWeDSZbH5erGjNBw2/3UlRdSxZbLfSuQTzFmPOYFsA== +"@types/bluebird@*", "@types/bluebird@^3.1.1": + version "3.5.30" + resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.30.tgz#ee034a0eeea8b84ed868b1aa60d690b08a6cfbc5" + integrity sha512-8LhzvcjIoqoi1TghEkRMkbbmM+jhHnBokPGkJWjclMK+Ks0MxEBow3/p2/iFTZ+OIbJHQDSfpgdZEb+af3gfVw== + +"@types/bluebird@3.5.29": + version "3.5.29" + resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.29.tgz#7cd933c902c4fc83046517a1bef973886d00bdb6" + integrity sha512-kmVtnxTuUuhCET669irqQmPAez4KFnFVKvpleVRyfC3g+SHD1hIkFZcWLim9BVcwUBLO59o8VZE4yGCmTif8Yw== "@types/boom@*", "@types/boom@^7.2.0": version "7.2.0" @@ -3744,6 +3770,19 @@ resolved "https://registry.yarnpkg.com/@types/catbox/-/catbox-10.0.1.tgz#266679017749041fe9873fee1131dd2aaa04a07e" integrity sha512-ECuJ+f5gGHiLeiE4RlE/xdqv/0JVDToegPV1aTb10tQStYa0Ycq2OJfQukDv3IFaw3B+CMV46jHc5bXe6QXEQg== +"@types/chai-jquery@1.1.40": + version "1.1.40" + resolved "https://registry.yarnpkg.com/@types/chai-jquery/-/chai-jquery-1.1.40.tgz#445bedcbbb2ae4e3027f46fa2c1733c43481ffa1" + integrity sha512-mCNEZ3GKP7T7kftKeIs7QmfZZQM7hslGSpYzKbOlR2a2HCFf9ph4nlMRA9UnuOETeOQYJVhJQK7MwGqNZVyUtQ== + dependencies: + "@types/chai" "*" + "@types/jquery" "*" + +"@types/chai@*", "@types/chai@4.2.7", "@types/chai@^4.2.11": + version "4.2.11" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.11.tgz#d3614d6c5f500142358e6ed24e1bf16657536c50" + integrity sha512-t7uW6eFafjO+qJ3BIV2gGUyZs27egcNRkUdalkud+Qa3+kg//f129iuOFivHDXQ+vnU3fDXuwgv0cqMCbcE8sw== + "@types/chance@^1.0.0": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/chance/-/chance-1.0.1.tgz#c10703020369602c40dd9428cc6e1437027116df" @@ -4181,7 +4220,7 @@ resolved "https://registry.yarnpkg.com/@types/joi/-/joi-13.6.1.tgz#325486a397504f8e22c8c551dc8b0e1d41d5d5ae" integrity sha512-JxZ0NP8NuB0BJOXi1KvAA6rySLTPmhOy4n2gzSFq/IFM3LNFm0h+2Vn/bPPgEYlWqzS2NPeLgKqfm75baX+Hog== -"@types/jquery@*", "@types/jquery@^3.3.31": +"@types/jquery@*", "@types/jquery@3.3.31", "@types/jquery@^3.3.31": version "3.3.31" resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.31.tgz#27c706e4bf488474e1cb54a71d8303f37c93451b" integrity sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg== @@ -4287,9 +4326,14 @@ "@types/lodash" "*" "@types/lodash@*", "@types/lodash@^4.14.110", "@types/lodash@^4.14.116": - version "4.14.137" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.137.tgz#8a4804937dc6462274ffcc088df8f14fc1b368e2" - integrity sha512-g4rNK5SRKloO+sUGbuO7aPtwbwzMgjK+bm9BBhLD7jGUiGR7zhwYEhSln/ihgYQBeIJ5j7xjyaYzrWTcu3UotQ== + version "4.14.150" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.150.tgz#649fe44684c3f1fcb6164d943c5a61977e8cf0bd" + integrity sha512-kMNLM5JBcasgYscD9x/Gvr6lTAv2NVgsKtet/hm93qMyf/D1pt+7jeEZklKJKxMVmXjxbRVQQGfqDSfipYCO6w== + +"@types/lodash@4.14.149": + version "4.14.149" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.149.tgz#1342d63d948c6062838fbf961012f74d4e638440" + integrity sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ== "@types/lodash@^3.10.1": version "3.10.2" @@ -4347,7 +4391,7 @@ dependencies: "@types/mime-db" "*" -"@types/minimatch@*", "@types/minimatch@^3.0.3": +"@types/minimatch@*", "@types/minimatch@3.0.3", "@types/minimatch@^3.0.3": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== @@ -4364,6 +4408,11 @@ dependencies: "@types/node" "*" +"@types/mocha@5.2.7": + version "5.2.7" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea" + integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ== + "@types/mocha@^7.0.2": version "7.0.2" resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-7.0.2.tgz#b17f16cf933597e10d6d78eae3251e692ce8b0ce" @@ -4756,11 +4805,36 @@ dependencies: "@types/node" "*" +"@types/sinon-chai@3.2.3": + version "3.2.3" + resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.3.tgz#afe392303dda95cc8069685d1e537ff434fa506e" + integrity sha512-TOUFS6vqS0PVL1I8NGVSNcFaNJtFoyZPXZ5zur+qlhDfOmQECZZM4H4kKgca6O8L+QceX/ymODZASfUfn+y4yQ== + dependencies: + "@types/chai" "*" + "@types/sinon" "*" + +"@types/sinon@*": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.0.tgz#5b70a360f55645dd64f205defd2a31b749a59799" + integrity sha512-v2TkYHkts4VXshMkcmot/H+ERZ2SevKa10saGaJPGCJ8vh3lKrC4u663zYEeRZxep+VbG6YRDtQ6gVqw9dYzPA== + dependencies: + "@types/sinonjs__fake-timers" "*" + +"@types/sinon@7.5.1": + version "7.5.1" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.5.1.tgz#d27b81af0d1cfe1f9b24eebe7a24f74ae40f5b7c" + integrity sha512-EZQUP3hSZQyTQRfiLqelC9NMWd1kqLcmQE0dMiklxBkgi84T+cHOhnKpgk4NnOWpGX863yE6+IaGnOXUNFqDnQ== + "@types/sinon@^7.0.13": version "7.0.13" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.0.13.tgz#ca039c23a9e27ebea53e0901ef928ea2a1a6d313" integrity sha512-d7c/C/+H/knZ3L8/cxhicHUiTDxdgap0b/aNJfsmLwFu/iOP17mdgbQsbHA3SJmrzsjD0l3UEE5SN4xxuz5ung== +"@types/sinonjs__fake-timers@*": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e" + integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA== + "@types/sizzle@*", "@types/sizzle@2.3.2": version "2.3.2" resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" @@ -5506,11 +5580,6 @@ adm-zip@0.4.11: resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.11.tgz#2aa54c84c4b01a9d0fb89bb11982a51f13e3d62a" integrity sha512-L8vcjDTCOIJk7wFvmlEUN7AsSb8T+2JrdP7KINBjzr24TJ5Mwj590sLu3BC7zNZowvJWa/JtPmD8eJCzdtDWjA== -adm-zip@^0.4.13: - version "0.4.13" - resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.13.tgz#597e2f8cc3672151e1307d3e95cddbc75672314a" - integrity sha512-fERNJX8sOXfel6qCBCMPvZLzENBEhZTzKqg6vrOW5pvoEaQuJhRU4ndTAh6lHOxn1I6jnz2NHra56ZODM751uw== - affine-hull@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/affine-hull/-/affine-hull-1.0.0.tgz#763ff1d38d063ceb7e272f17ee4d7bbcaf905c5d" @@ -6417,11 +6486,6 @@ array-reduce@~0.0.0: resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b" integrity sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys= -array-slice@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5" - integrity sha1-3Tz7gO15c6dRF82sabC5nshhhvU= - array-slice@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-1.1.0.tgz#e368ea15f89bc7069f7ffb89aec3a6c7d4ac22d4" @@ -6453,11 +6517,6 @@ array-uniq@^1.0.0, array-uniq@^1.0.1: resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= -array-unique@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" - integrity sha1-odl8yvy8JiXMcPrc6zalDFiwGlM= - array-unique@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" @@ -6684,7 +6743,7 @@ async@^2.6.0, async@^2.6.1: dependencies: lodash "^4.17.10" -async@^2.6.3: +async@^2.6.2, async@^2.6.3: version "2.6.3" resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== @@ -7125,6 +7184,14 @@ babel-plugin-transform-define@^1.3.1: lodash "^4.17.11" traverse "0.6.6" +babel-plugin-transform-imports@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-imports/-/babel-plugin-transform-imports-2.0.0.tgz#9e5f49f751a9d34ba8f4bb988c7e48ed2419c6b6" + integrity sha512-65ewumYJ85QiXdcB/jmiU0y0jg6eL6CdnDqQAqQ8JMOKh1E52VPG3NJzbVKWcgovUR5GBH8IWpCXQ7I8Q3wjgw== + dependencies: + "@babel/types" "^7.4" + is-valid-path "^0.1.1" + babel-plugin-transform-inline-consecutive-adds@^0.4.3: version "0.4.3" resolved "https://registry.yarnpkg.com/babel-plugin-transform-inline-consecutive-adds/-/babel-plugin-transform-inline-consecutive-adds-0.4.3.tgz#323d47a3ea63a83a7ac3c811ae8e6941faf2b0d1" @@ -7570,7 +7637,7 @@ bluebird@3.7.2: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -bluebird@^3.3.0, bluebird@^3.3.1: +bluebird@^3.3.1: version "3.5.1" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" integrity sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA== @@ -7765,13 +7832,6 @@ brace@0.11.1, brace@^0.11.0, brace@^0.11.1: resolved "https://registry.yarnpkg.com/brace/-/brace-0.11.1.tgz#4896fcc9d544eef45f4bb7660db320d3b379fe58" integrity sha1-SJb8ydVE7vRfS7dmDbMg07N5/lg= -braces@^0.1.2: - version "0.1.5" - resolved "https://registry.yarnpkg.com/braces/-/braces-0.1.5.tgz#c085711085291d8b75fdd74eab0f8597280711e6" - integrity sha1-wIVxEIUpHYt1/ddOqw+FlygHEeY= - dependencies: - expand-range "^0.1.0" - braces@^2.3.0, braces@^2.3.1, braces@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" @@ -7788,7 +7848,7 @@ braces@^2.3.0, braces@^2.3.1, braces@^2.3.2: split-string "^3.0.2" to-regex "^3.0.1" -braces@^3.0.1, braces@~3.0.2: +braces@^3.0.1, braces@^3.0.2, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -8693,7 +8753,7 @@ cheerio@^1.0.0-rc.3: lodash "^4.15.0" parse5 "^3.0.1" -chokidar@2.1.2, chokidar@^2.0.2, chokidar@^2.0.3, chokidar@^2.0.4: +chokidar@2.1.2, chokidar@^2.0.2, chokidar@^2.0.4: version "2.1.2" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.2.tgz#9c23ea40b01638439e0513864d362aeacc5ad058" integrity sha512-IwXUx0FXc5ibYmPC2XeEj5mpXoV66sR+t3jqu2NS2GYwCktt3KF1/Qqjws/NkegajBA4RbZ5+DDwlOiJsxDHEg== @@ -8761,7 +8821,7 @@ chokidar@^2.0.0, chokidar@^2.1.2, chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" -chokidar@^3.2.2: +chokidar@^3.0.0, chokidar@^3.2.2: version "3.3.1" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450" integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg== @@ -8840,11 +8900,6 @@ circular-json@^0.3.1: resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A== -circular-json@^0.5.5: - version "0.5.9" - resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.5.9.tgz#932763ae88f4f7dead7a0d09c8a51a4743a53b1d" - integrity sha512-4ivwqHpIFJZBuhN3g/pEcdbnGUywkBblloGbkglyloVjjR3uT6tieI89MVOfbP2tHX5sgb01FuLgAOzebNlJNQ== - class-extend@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/class-extend/-/class-extend-0.1.2.tgz#8057a82b00f53f82a5d62c50ef8cffdec6fabc34" @@ -9309,13 +9364,6 @@ colorspace@1.1.x: color "3.0.x" text-hex "1.0.x" -combine-lists@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/combine-lists/-/combine-lists-1.0.1.tgz#458c07e09e0d900fc28b70a3fec2dacd1d2cb7f6" - integrity sha1-RYwH4J4NkA/Ci3Cj/sLazR0st/Y= - dependencies: - lodash "^4.5.0" - combined-stream@^1.0.5: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -9471,15 +9519,6 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -concat-stream@1.6.0, concat-stream@^1.4.7, concat-stream@~1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" - integrity sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc= - dependencies: - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" - concat-stream@1.6.2, concat-stream@^1.4.6, concat-stream@^1.5.0, concat-stream@^1.6.0, concat-stream@^1.6.1, concat-stream@^1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" @@ -9490,6 +9529,15 @@ concat-stream@1.6.2, concat-stream@^1.4.6, concat-stream@^1.5.0, concat-stream@^ readable-stream "^2.2.2" typedarray "^0.0.6" +concat-stream@^1.4.7, concat-stream@~1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" + integrity sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc= + dependencies: + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + concat-stream@~1.5.0: version "1.5.2" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266" @@ -9865,7 +9913,7 @@ core-js@^1.0.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY= -core-js@^2.2.0, core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.1, core-js@^2.5.3, core-js@^2.6.5, core-js@^2.6.9: +core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.1, core-js@^2.5.3, core-js@^2.6.5, core-js@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== @@ -10401,13 +10449,24 @@ cypress-multi-reporters@^1.2.3: debug "^4.1.1" lodash "^4.17.11" -cypress@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.2.0.tgz#45673fb648b1a77b9a78d73e58b89ed05212d243" - integrity sha512-8LdreL91S/QiTCLYLNbIjLL8Ht4fJmu/4HGLxUI20Tc7JSfqEfCmXELrRfuPT0kjosJwJJZacdSji9XSRkPKUw== +cypress@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.4.1.tgz#f5aa1aa5f328f1299bff328103f7cbad89e80f29" + integrity sha512-LcskZ/PXRG9XTlEeeenKqz/KddT1x+7O7dqXsdKWPII01LxLNmNHIvHnlUqApchVbinJ5vir6J255CkELSeL0A== dependencies: "@cypress/listr-verbose-renderer" "0.4.1" + "@cypress/request" "2.88.5" "@cypress/xvfb" "1.2.4" + "@types/blob-util" "1.3.3" + "@types/bluebird" "3.5.29" + "@types/chai" "4.2.7" + "@types/chai-jquery" "1.1.40" + "@types/jquery" "3.3.31" + "@types/lodash" "4.14.149" + "@types/minimatch" "3.0.3" + "@types/mocha" "5.2.7" + "@types/sinon" "7.5.1" + "@types/sinon-chai" "3.2.3" "@types/sizzle" "2.3.2" arch "2.1.1" bluebird "3.7.2" @@ -10421,7 +10480,7 @@ cypress@^4.2.0: eventemitter2 "4.1.2" execa "1.0.0" executable "4.1.1" - extract-zip "1.6.7" + extract-zip "1.7.0" fs-extra "8.1.0" getos "3.1.4" is-ci "2.0.0" @@ -10430,12 +10489,11 @@ cypress@^4.2.0: listr "0.14.3" lodash "4.17.15" log-symbols "3.0.0" - minimist "1.2.2" + minimist "1.2.5" moment "2.24.0" ospath "1.2.2" pretty-bytes "5.3.0" ramda "0.26.1" - request cypress-io/request#b5af0d1fa47eec97ba980cde90a13e69a2afcd16 request-progress "3.0.0" supports-color "7.1.0" tmp "0.1.0" @@ -10744,10 +10802,10 @@ date-fns@^1.27.2: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6" integrity sha512-lbTXWZ6M20cWH8N9S6afb0SBm6tMk+uUg6z3MqHPKE9atmsY3kJkTm8vKe93izJ2B2+q5MV990sM2CHgtAZaOw== -date-format@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/date-format/-/date-format-1.2.0.tgz#615e828e233dd1ab9bb9ae0950e0ceccfa6ecad8" - integrity sha1-YV6CjiM90aubua4JUODOzPpuytg= +date-format@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-2.1.0.tgz#31d5b5ea211cf5fd764cd38baf9d033df7e125cf" + integrity sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA== date-now@^0.1.4: version "0.1.4" @@ -12992,15 +13050,6 @@ exit@^0.1.2, exit@~0.1.1: resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= -expand-braces@^0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/expand-braces/-/expand-braces-0.1.2.tgz#488b1d1d2451cb3d3a6b192cfc030f44c5855fea" - integrity sha1-SIsdHSRRyz06axks/AMPRMWFX+o= - dependencies: - array-slice "^0.2.3" - array-unique "^0.2.1" - braces "^0.1.2" - expand-brackets@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" @@ -13014,14 +13063,6 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" -expand-range@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-0.1.1.tgz#4cb8eda0993ca56fa4f41fc42f3cbb4ccadff044" - integrity sha1-TLjtoJk8pW+k9B/ELzy7TMrf8EQ= - dependencies: - is-number "^0.1.1" - repeat-string "^0.2.2" - expand-tilde@^2.0.0, expand-tilde@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" @@ -13235,27 +13276,7 @@ extract-stack@^1.0.0: resolved "https://registry.yarnpkg.com/extract-stack/-/extract-stack-1.0.0.tgz#b97acaf9441eea2332529624b732fc5a1c8165fa" integrity sha1-uXrK+UQe6iMyUpYktzL8WhyBZfo= -extract-zip@1.6.6: - version "1.6.6" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.6.tgz#1290ede8d20d0872b429fd3f351ca128ec5ef85c" - integrity sha1-EpDt6NINCHK0Kf0/NRyhKOxe+Fw= - dependencies: - concat-stream "1.6.0" - debug "2.6.9" - mkdirp "0.5.0" - yauzl "2.4.1" - -extract-zip@1.6.7: - version "1.6.7" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9" - integrity sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k= - dependencies: - concat-stream "1.6.2" - debug "2.6.9" - mkdirp "0.5.1" - yauzl "2.4.1" - -extract-zip@^1.6.6, extract-zip@^1.7.0: +extract-zip@1.7.0, extract-zip@^1.6.6, extract-zip@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== @@ -14939,11 +14960,11 @@ gonzales-pe-sl@^4.2.3: minimist "1.1.x" gonzales-pe@^4.2.3: - version "4.2.4" - resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.2.4.tgz#356ae36a312c46fe0f1026dd6cb539039f8500d2" - integrity sha512-v0Ts/8IsSbh9n1OJRnSfa7Nlxi4AkXIsWB6vPept8FDbL4bXn3FNuxjYtO/nmBGu7GDkL9MFeGebeSu6l55EPQ== + version "4.3.0" + resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.3.0.tgz#fe9dec5f3c557eead09ff868c65826be54d067b3" + integrity sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ== dependencies: - minimist "1.1.x" + minimist "^1.2.5" good-listener@^1.2.2: version "1.2.2" @@ -15603,14 +15624,15 @@ handle-thing@^2.0.0: resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== -handlebars@4.5.3, handlebars@^4.0.1, handlebars@^4.1.2: - version "4.5.3" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.5.3.tgz#5cf75bd8714f7605713511a56be7c349becb0482" - integrity sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA== +handlebars@4.7.6, handlebars@^4.0.1, handlebars@^4.1.2: + version "4.7.6" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e" + integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA== dependencies: + minimist "^1.2.5" neo-async "^2.6.0" - optimist "^0.6.1" source-map "^0.6.1" + wordwrap "^1.0.0" optionalDependencies: uglify-js "^3.1.4" @@ -16368,16 +16390,14 @@ idx@^2.5.6: resolved "https://registry.yarnpkg.com/idx/-/idx-2.5.6.tgz#1f824595070100ae9ad585c86db08dc74f83a59d" integrity sha512-WFXLF7JgPytbMgelpRY46nHz5tyDcedJ76pLV+RJWdb8h33bxFq4bdZau38DhNSzk5eVniBf1K3jwfK+Lb5nYA== -iedriver@^3.14.1: - version "3.14.1" - resolved "https://registry.yarnpkg.com/iedriver/-/iedriver-3.14.1.tgz#447c49be83c62d3f2f158283d58ccf7b35002be8" - integrity sha512-YyCi703BGK7R37A8QlSe2B87xgwDGGoPqBrlXe4Q68o/MNLJrR53/IpTs6J1+KKk51MLiTbWa57N7P3KZ11tow== +iedriver@^3.14.2: + version "3.14.2" + resolved "https://registry.yarnpkg.com/iedriver/-/iedriver-3.14.2.tgz#a19391ff123e21823ce0afe300e38b58a7dc79c4" + integrity sha512-vvFwfpOOZXmpXT/3Oa9SOFrr4uZNNUtBKPLRz7z8oZigvvIOokDiBlbImrd80q+rgjkmqUGi6a2NnpyCOAXnOw== dependencies: - adm-zip "^0.4.13" - extract-zip "1.6.6" + extract-zip "1.7.0" kew "~0.1.7" - md5-file "^1.1.4" - mkdirp "0.3.5" + mkdirp "0.5.4" npmconf "^2.1.3" request "^2.88.0" rimraf "~2.0.2" @@ -17310,11 +17330,6 @@ is-number-object@^1.0.4: resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== -is-number@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-0.1.1.tgz#69a7af116963d47206ec9bd9b48a14216f1e3806" - integrity sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY= - is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" @@ -17579,7 +17594,7 @@ is-valid-glob@^1.0.0: resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa" integrity sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao= -is-valid-path@0.1.1: +is-valid-path@0.1.1, is-valid-path@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-valid-path/-/is-valid-path-0.1.1.tgz#110f9ff74c37f663e1ec7915eb451f2db93ac9df" integrity sha1-EQ+f90w39mPh7HkV60UfLbk6yd8= @@ -17640,10 +17655,10 @@ isbinaryfile@4.0.2: resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.2.tgz#bfc45642da645681c610cca831022e30af426488" integrity sha512-C3FSxJdNrEr2F4z6uFtNzECDM5hXk+46fxaa+cwBe5/XrWSmzdG8DDgyjfX6/NRdBB21q2JXuRAzPCUs+fclnQ== -isbinaryfile@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.2.tgz#4a3e974ec0cba9004d3fc6cde7209ea69368a621" - integrity sha1-Sj6XTsDLqQBNP8bN5yCeppNopiE= +isbinaryfile@^4.0.2: + version "4.0.6" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.6.tgz#edcb62b224e2b4710830b67498c8e4e5a4d2610b" + integrity sha512-ORrEy+SNVqUhrCaal4hA4fBzhggQQ+BaLntyPOdoEiwlKZW9BZiJXjg3RMiruE4tPEI3pyVPpySHQF/dKWperg== isemail@3.x.x: version "3.1.4" @@ -18841,51 +18856,47 @@ karma-junit-reporter@1.2.0: path-is-absolute "^1.0.0" xmlbuilder "8.2.2" -karma-mocha@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-1.3.0.tgz#eeaac7ffc0e201eb63c467440d2b69c7cf3778bf" - integrity sha1-7qrH/8DiAetjxGdEDStpx883eL8= +karma-mocha@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-2.0.0.tgz#ad6b56b6a72e9b191e4c432dd30f4a44fc2435bc" + integrity sha512-qiZkZDJnn2kb9t2m4LoM4cYJHJVPoxvAYYe0B+go5s+A/3vc/3psUT05zW4yFz4vT0xHf+XzTTery8zdr8GWgA== dependencies: - minimist "1.2.0" + minimist "^1.2.3" karma-safari-launcher@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/karma-safari-launcher/-/karma-safari-launcher-1.0.0.tgz#96982a2cc47d066aae71c553babb28319115a2ce" integrity sha1-lpgqLMR9BmquccVTursoMZEVos4= -karma@3.1.4: - version "3.1.4" - resolved "https://registry.yarnpkg.com/karma/-/karma-3.1.4.tgz#3890ca9722b10d1d14b726e1335931455788499e" - integrity sha512-31Vo8Qr5glN+dZEVIpnPCxEGleqE0EY6CtC2X9TagRV3rRQ3SNrvfhddICkJgUK3AgqpeKSZau03QumTGhGoSw== +karma@5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/karma/-/karma-5.0.2.tgz#e404373dac6e3fa08409ae4d9eda7d83adb43ee5" + integrity sha512-RpUuCuGJfN3WnjYPGIH+VBF8023Lfm3TQH6D1kcNL+FxtEPc2UUz/nVjbVAGXH4Pm+Q7FVOAQjdAeFUpXpQ3IA== dependencies: - bluebird "^3.3.0" body-parser "^1.16.1" - chokidar "^2.0.3" + braces "^3.0.2" + chokidar "^3.0.0" colors "^1.1.0" - combine-lists "^1.0.0" connect "^3.6.0" - core-js "^2.2.0" di "^0.0.1" dom-serialize "^2.2.0" - expand-braces "^0.1.1" flatted "^2.0.0" glob "^7.1.1" graceful-fs "^4.1.2" http-proxy "^1.13.0" - isbinaryfile "^3.0.0" - lodash "^4.17.5" - log4js "^3.0.0" + isbinaryfile "^4.0.2" + lodash "^4.17.14" + log4js "^4.0.0" mime "^2.3.1" minimatch "^3.0.2" - optimist "^0.6.1" qjobs "^1.1.4" range-parser "^1.2.0" rimraf "^2.6.0" - safe-buffer "^5.0.1" socket.io "2.1.1" source-map "^0.6.1" tmp "0.0.33" - useragent "2.3.0" + ua-parser-js "0.7.21" + yargs "^15.3.1" kdbush@^3.0.0: version "3.0.0" @@ -19750,7 +19761,7 @@ lodash.uniqby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" integrity sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI= -lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.6.1, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: +lodash@4.17.11, lodash@4.17.15, lodash@>4.17.4, lodash@^4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.10.0, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.6.1, lodash@~4.17.10, lodash@~4.17.15, lodash@~4.17.5: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -19811,16 +19822,16 @@ log-update@^1.0.2: ansi-escapes "^1.0.0" cli-cursor "^1.0.2" -log4js@^3.0.0: - version "3.0.6" - resolved "https://registry.yarnpkg.com/log4js/-/log4js-3.0.6.tgz#e6caced94967eeeb9ce399f9f8682a4b2b28c8ff" - integrity sha512-ezXZk6oPJCWL483zj64pNkMuY/NcRX5MPiB0zE6tjZM137aeusrOnW1ecxgF9cmwMWkBMhjteQxBPoZBh9FDxQ== +log4js@^4.0.0: + version "4.5.1" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-4.5.1.tgz#e543625e97d9e6f3e6e7c9fc196dd6ab2cae30b5" + integrity sha512-EEEgFcE9bLgaYUKuozyFfytQM2wDHtXn4tAN41pkaxpNjAykv11GVdeI4tHtmPWW4Xrgh9R/2d7XYghDVjbKKw== dependencies: - circular-json "^0.5.5" - date-format "^1.2.0" - debug "^3.1.0" - rfdc "^1.1.2" - streamroller "0.7.0" + date-format "^2.0.0" + debug "^4.1.1" + flatted "^2.0.0" + rfdc "^1.1.4" + streamroller "^1.0.6" logform@^2.1.1: version "2.1.2" @@ -20177,11 +20188,6 @@ material-colors@^1.2.1: resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.5.tgz#5292593e6754cb1bcc2b98030e4e0d6a3afc9ea1" integrity sha1-UpJZPmdUyxvMK5gDDk4Najr8nqE= -md5-file@^1.1.4: - version "1.1.10" - resolved "https://registry.yarnpkg.com/md5-file/-/md5-file-1.1.10.tgz#d8f4fce76c92cb20b7d143a59f58ca49b4cf3174" - integrity sha1-2PT852ySyyC30UOln1jKSbTPMXQ= - md5.js@^1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d" @@ -20652,21 +20658,11 @@ minimist@1.2.0, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2 resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= -minimist@1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.2.tgz#b00a00230a1108c48c169e69a291aafda3aacd63" - integrity sha512-rIqbOrKb8GJmx/5bc2M0QchhUouMXSpd1RTclXsB41JdL+VtnojfaJR+h7F9k18/4kHUsBFgk80Uk+q569vjPA== - -minimist@^1.2.5: +minimist@1.2.5, minimist@^1.2.3, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -minimist@~0.0.1: - version "0.0.10" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" - integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= - minimost@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/minimost/-/minimost-1.0.0.tgz#1d07954aa0268873408b95552fbffc5977dfc78b" @@ -20766,18 +20762,6 @@ mixin-object@^2.0.1: for-in "^0.1.3" is-extendable "^0.1.1" -mkdirp@0.3.5, mkdirp@^0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.5.tgz#de3e5f8961c88c787ee1368df849ac4413eca8d7" - integrity sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc= - -mkdirp@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.0.tgz#1d73076a6df986cd9344e15e71fcc05a4c9abf12" - integrity sha1-HXMHam35hs2TROFecfzAWkyavxI= - dependencies: - minimist "0.0.8" - mkdirp@0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" @@ -20792,13 +20776,18 @@ mkdirp@0.5.3: dependencies: minimist "^1.2.5" -mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.4, mkdirp@~0.5.0, mkdirp@~0.5.1: +mkdirp@0.5.4, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.4, mkdirp@~0.5.0, mkdirp@~0.5.1: version "0.5.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.4.tgz#fd01504a6797ec5c9be81ff43d204961ed64a512" integrity sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw== dependencies: minimist "^1.2.5" +mkdirp@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.5.tgz#de3e5f8961c88c787ee1368df849ac4413eca8d7" + integrity sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc= + mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" @@ -22131,14 +22120,6 @@ optimism@^0.9.0: dependencies: "@wry/context" "^0.4.0" -optimist@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" - integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= - dependencies: - minimist "~0.0.1" - wordwrap "~0.0.2" - optional-js@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/optional-js/-/optional-js-2.1.1.tgz#c2dc519ad119648510b4d241dbb60b1167c36a46" @@ -25130,7 +25111,7 @@ readable-stream@1.0, "readable-stream@>=1.0.33-1 <1.1.0-0": string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.3, readable-stream@~2.3.6: +readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.3, readable-stream@~2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== @@ -25684,11 +25665,6 @@ repeat-element@^1.1.2: resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" integrity sha1-7wiaF40Ug7quTZPrmLT55OEdmQo= -repeat-string@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-0.2.2.tgz#c7a8d3236068362059a7e4651fc6884e8b1fb4ae" - integrity sha1-x6jTI2BoNiBZp+RlH8aITosftK4= - repeat-string@^1.5.0, repeat-string@^1.5.2, repeat-string@^1.5.4, repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" @@ -25891,31 +25867,6 @@ request@^2.87.0: tunnel-agent "^0.6.0" uuid "^3.1.0" -request@cypress-io/request#b5af0d1fa47eec97ba980cde90a13e69a2afcd16: - version "2.88.1" - resolved "https://codeload.github.com/cypress-io/request/tar.gz/b5af0d1fa47eec97ba980cde90a13e69a2afcd16" - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.5.0" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - require-ancestors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/require-ancestors/-/require-ancestors-1.0.0.tgz#807831f8f8081fb12863da81ddb15c8f2a73a004" @@ -26249,10 +26200,10 @@ rework@1.0.1: convert-source-map "^0.3.3" css "^2.0.0" -rfdc@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.2.tgz#e6e72d74f5dc39de8f538f65e00c36c18018e349" - integrity sha512-92ktAgvZhBzYTIK0Mja9uen5q5J3NRVMoDkJL2VMwq6SXjVCgqvQeVP2XAaUY6HT+XpQYeLSjb3UoitBryKmdA== +rfdc@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2" + integrity sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug== right-align@^0.1.1: version "0.1.3" @@ -27919,15 +27870,16 @@ stream-spigot@~2.1.2: dependencies: readable-stream "~1.1.0" -streamroller@0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-0.7.0.tgz#a1d1b7cf83d39afb0d63049a5acbf93493bdf64b" - integrity sha512-WREzfy0r0zUqp3lGO096wRuUp7ho1X6uo/7DJfTlEi0Iv/4gT7YHqXDjKC2ioVGBZtE8QzsQD9nx1nIuoZ57jQ== +streamroller@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-1.0.6.tgz#8167d8496ed9f19f05ee4b158d9611321b8cacd9" + integrity sha512-3QC47Mhv3/aZNFpDDVO44qQb9gwB9QggMEE0sQmkTAwBVYdBRWISdsywlkfm5II1Q5y/pmrHflti/IgmIzdDBg== dependencies: - date-format "^1.2.0" - debug "^3.1.0" - mkdirp "^0.5.1" - readable-stream "^2.3.0" + async "^2.6.2" + date-format "^2.0.0" + debug "^3.2.6" + fs-extra "^7.0.1" + lodash "^4.17.14" strict-uri-encode@^1.0.0: version "1.1.0" @@ -30084,10 +30036,10 @@ typings-tester@^0.3.2: dependencies: commander "^2.12.2" -ua-parser-js@^0.7.18, ua-parser-js@^0.7.9: - version "0.7.19" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b" - integrity sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ== +ua-parser-js@0.7.21, ua-parser-js@^0.7.18, ua-parser-js@^0.7.9: + version "0.7.21" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777" + integrity sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.5" @@ -30656,7 +30608,7 @@ user-home@^2.0.0: dependencies: os-homedir "^1.0.0" -useragent@2.3.0, useragent@^2.3.0: +useragent@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.3.0.tgz#217f943ad540cb2128658ab23fc960f6a88c9972" integrity sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw== @@ -31934,11 +31886,6 @@ wordwrap@^1.0.0, wordwrap@~1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= -wordwrap@~0.0.2: - version "0.0.3" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" - integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= - worker-farm@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8" @@ -32558,13 +32505,6 @@ yauzl@2.10.0, yauzl@^2.10.0: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" -yauzl@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" - integrity sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU= - dependencies: - fd-slicer "~1.0.1" - yauzl@^2.4.2: version "2.9.1" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.9.1.tgz#a81981ea70a57946133883f029c5821a89359a7f"