diff --git a/.backportrc.json b/.backportrc.json index e44d3ce1142993..384e221329a4f6 100644 --- a/.backportrc.json +++ b/.backportrc.json @@ -3,6 +3,7 @@ "targetBranchChoices": [ { "name": "master", "checked": true }, { "name": "7.x", "checked": true }, + "7.12", "7.11", "7.10", "7.9", @@ -29,7 +30,9 @@ "targetPRLabels": ["backport"], "branchLabelMapping": { "^v8.0.0$": "master", - "^v7.12.0$": "7.x", + "^v7.13.0$": "7.x", "^v(\\d+).(\\d+).\\d+$": "$1.$2" - } + }, + "autoMerge": true, + "autoMergeMethod": "squash" } diff --git a/.ci/.storybook/main.js b/.ci/.storybook/main.js new file mode 100644 index 00000000000000..e399ec087e1687 --- /dev/null +++ b/.ci/.storybook/main.js @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const config = require('@kbn/storybook').defaultConfig; +const aliases = require('../../src/dev/storybook/aliases.ts').storybookAliases; + +config.refs = {}; + +for (const alias of Object.keys(aliases).filter((a) => a !== 'ci_composite')) { + // snake_case -> Title Case + const title = alias + .replace(/_/g, ' ') + .split(' ') + .map((n) => n[0].toUpperCase() + n.slice(1)) + .join(' '); + + config.refs[alias] = { + title: title, + url: `${process.env.STORYBOOK_BASE_URL}/${alias}`, + }; +} + +module.exports = config; diff --git a/.ci/Jenkinsfile_coverage b/.ci/Jenkinsfile_coverage index 1c1d21024ce917..0aa962a58f53cc 100644 --- a/.ci/Jenkinsfile_coverage +++ b/.ci/Jenkinsfile_coverage @@ -3,14 +3,14 @@ library 'kibana-pipeline-library' kibanaLibrary.load() // load from the Jenkins instance -kibanaPipeline(timeoutMinutes: 240) { +kibanaPipeline(timeoutMinutes: 300) { catchErrors { def timestamp = new Date(currentBuild.startTimeInMillis).format("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone("UTC")) withEnv([ "TIME_STAMP=${timestamp}", 'CODE_COVERAGE=1', // Enables coverage. Needed for multiple ci scripts, such as remote.ts, test/scripts/*.sh, schema.js, etc. ]) { - workers.base(name: 'coverage-worker', size: 'l', ramDisk: false, bootstrapped: false) { + workers.base(name: 'coverage-worker', size: 'xl', ramDisk: false, bootstrapped: false) { catchError { kibanaPipeline.bash(""" diff --git a/.ci/Jenkinsfile_flaky b/.ci/Jenkinsfile_flaky index b9880c410fc686..7eafc66465bc72 100644 --- a/.ci/Jenkinsfile_flaky +++ b/.ci/Jenkinsfile_flaky @@ -3,47 +3,39 @@ library 'kibana-pipeline-library' kibanaLibrary.load() -def CI_GROUP_PARAM = params.CI_GROUP +def TASK_PARAM = params.TASK ?: params.CI_GROUP // Looks like 'oss:ciGroup:1', 'oss:firefoxSmoke' -def JOB_PARTS = CI_GROUP_PARAM.split(':') +def JOB_PARTS = TASK_PARAM.split(':') def IS_XPACK = JOB_PARTS[0] == 'xpack' -def JOB = JOB_PARTS[1] +def JOB = JOB_PARTS.size() > 1 ? JOB_PARTS[1] : JOB_PARTS[0] def CI_GROUP = JOB_PARTS.size() > 2 ? JOB_PARTS[2] : '' def EXECUTIONS = params.NUMBER_EXECUTIONS.toInteger() def AGENT_COUNT = getAgentCount(EXECUTIONS) - -def worker = getWorkerFromParams(IS_XPACK, JOB, CI_GROUP) - -def workerFailures = [] +def NEED_BUILD = JOB != 'jestIntegration' && JOB != 'apiIntegration' currentBuild.displayName += trunc(" ${params.GITHUB_OWNER}:${params.branch_specifier}", 24) currentBuild.description = "${params.CI_GROUP}
Agents: ${AGENT_COUNT}
Executions: ${params.NUMBER_EXECUTIONS}" kibanaPipeline(timeoutMinutes: 180) { def agents = [:] + def workerFailures = [] + + def worker = getWorkerFromParams(IS_XPACK, JOB, CI_GROUP) + for(def agentNumber = 1; agentNumber <= AGENT_COUNT; agentNumber++) { - def agentNumberInside = agentNumber def agentExecutions = floor(EXECUTIONS/AGENT_COUNT) + (agentNumber <= EXECUTIONS%AGENT_COUNT ? 1 : 0) + agents["agent-${agentNumber}"] = { - catchErrors { - print "Agent ${agentNumberInside} - ${agentExecutions} executions" - - withEnv([ - 'IGNORE_SHIP_CI_STATS_ERROR=true', - ]) { - workers.functional('flaky-test-runner', { - if (!IS_XPACK) { - kibanaPipeline.buildOss() - if (CI_GROUP == '1') { - runbld("./test/scripts/jenkins_build_kbn_sample_panel_action.sh", "Build kbn tp sample panel action for ciGroup1") - } - } else { - kibanaPipeline.buildXpack() - } - }, getWorkerMap(agentNumberInside, agentExecutions, worker, workerFailures))() - } - } + agentProcess( + agentNumber: agentNumber, + agentExecutions: agentExecutions, + worker: worker, + workerFailures: workerFailures, + needBuild: NEED_BUILD, + isXpack: IS_XPACK, + ciGroup: CI_GROUP + ) } } @@ -59,14 +51,70 @@ kibanaPipeline(timeoutMinutes: 180) { } } +def agentProcess(Map params = [:]) { + def config = [ + agentNumber: 1, + agentExecutions: 0, + worker: {}, + workerFailures: [], + needBuild: false, + isXpack: false, + ciGroup: null, + ] + params + + catchErrors { + print "Agent ${config.agentNumber} - ${config.agentExecutions} executions" + + withEnv([ + 'IGNORE_SHIP_CI_STATS_ERROR=true', + ]) { + kibanaPipeline.withTasks([ + parallel: 20, + ]) { + task { + if (config.needBuild) { + if (!config.isXpack) { + kibanaPipeline.buildOss() + } else { + kibanaPipeline.buildXpack() + } + } + + for(def i = 0; i < config.agentExecutions; i++) { + def taskNumber = i + task({ + withEnv([ + "REMOVE_KIBANA_INSTALL_DIR=1", + ]) { + catchErrors { + try { + config.worker() + } catch (ex) { + config.workerFailures << "agent-${config.agentNumber}-${taskNumber}" + throw ex + } + } + } + }) + } + } + } + } + } +} + def getWorkerFromParams(isXpack, job, ciGroup) { if (!isXpack) { if (job == 'accessibility') { return kibanaPipeline.functionalTestProcess('kibana-accessibility', './test/scripts/jenkins_accessibility.sh') } else if (job == 'firefoxSmoke') { return kibanaPipeline.functionalTestProcess('firefoxSmoke', './test/scripts/jenkins_firefox_smoke.sh') - } else if(job == 'visualRegression') { + } else if (job == 'visualRegression') { return kibanaPipeline.functionalTestProcess('visualRegression', './test/scripts/jenkins_visual_regression.sh') + } else if (job == 'jestIntegration') { + return kibanaPipeline.scriptTaskDocker('Jest Integration Tests', 'test/scripts/test/jest_integration.sh') + } else if (job == 'apiIntegration') { + return kibanaPipeline.scriptTask('API Integration Tests', 'test/scripts/test/api_integration.sh') } else { return kibanaPipeline.ossCiGroupProcess(ciGroup) } @@ -76,45 +124,16 @@ def getWorkerFromParams(isXpack, job, ciGroup) { return kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh') } else if (job == 'firefoxSmoke') { return kibanaPipeline.functionalTestProcess('xpack-firefoxSmoke', './test/scripts/jenkins_xpack_firefox_smoke.sh') - } else if(job == 'visualRegression') { + } else if (job == 'visualRegression') { return kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh') } else { return kibanaPipeline.xpackCiGroupProcess(ciGroup) } } -def getWorkerMap(agentNumber, numberOfExecutions, worker, workerFailures, maxWorkerProcesses = 12) { - def workerMap = [:] - def numberOfWorkers = Math.min(numberOfExecutions, maxWorkerProcesses) - - for(def i = 1; i <= numberOfWorkers; i++) { - def workerExecutions = floor(numberOfExecutions/numberOfWorkers + (i <= numberOfExecutions%numberOfWorkers ? 1 : 0)) - - workerMap["agent-${agentNumber}-worker-${i}"] = { workerNumber -> - for(def j = 0; j < workerExecutions; j++) { - print "Execute agent-${agentNumber} worker-${workerNumber}: ${j}" - withEnv([ - "REMOVE_KIBANA_INSTALL_DIR=1", - ]) { - catchErrors { - try { - worker(workerNumber) - } catch (ex) { - workerFailures << "agent-${agentNumber} worker-${workerNumber}-${j}" - throw ex - } - } - } - } - } - } - - return workerMap -} - def getAgentCount(executions) { - // Increase agent count every 24 worker processess, up to 3 agents maximum - return Math.min(3, 1 + floor(executions/24)) + // Increase agent count every 20 worker processess, up to 3 agents maximum + return Math.min(3, 1 + floor(executions/20)) } def trunc(str, length) { diff --git a/.ci/es-snapshots/Jenkinsfile_verify_es b/.ci/es-snapshots/Jenkinsfile_verify_es index 736a71b73d14d4..f3f07d5f355be1 100644 --- a/.ci/es-snapshots/Jenkinsfile_verify_es +++ b/.ci/es-snapshots/Jenkinsfile_verify_es @@ -19,7 +19,7 @@ currentBuild.description = "ES: ${SNAPSHOT_VERSION}
Kibana: ${params.branch def SNAPSHOT_MANIFEST = "https://storage.googleapis.com/kibana-ci-es-snapshots-daily/${SNAPSHOT_VERSION}/archives/${SNAPSHOT_ID}/manifest.json" -kibanaPipeline(timeoutMinutes: 150) { +kibanaPipeline(timeoutMinutes: 210) { catchErrors { slackNotifications.onFailure( title: "*<${env.BUILD_URL}|[${SNAPSHOT_VERSION}] ES Snapshot Verification Failure>*", diff --git a/.eslintignore b/.eslintignore index ea8ab55ad77269..4559711bb9dd31 100644 --- a/.eslintignore +++ b/.eslintignore @@ -15,6 +15,7 @@ node_modules target snapshots.js +!/.ci !/.eslintrc.js !.storybook diff --git a/.gitignore b/.gitignore index c7d7e37732ca04..fbe28b8f1e77cc 100644 --- a/.gitignore +++ b/.gitignore @@ -61,9 +61,6 @@ npm-debug.log* .ci/bash_standard_lib.sh .gradle -# apm plugin -/x-pack/plugins/apm/tsconfig.json -apm.tsconfig.json ## @cypress/snapshot from apm plugin snapshots.js diff --git a/.i18nrc.json b/.i18nrc.json index 0cdcae08e54e0a..efbb5ecc0194e9 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -29,6 +29,7 @@ "maps_legacy": "src/plugins/maps_legacy", "monaco": "packages/kbn-monaco/src", "presentationUtil": "src/plugins/presentation_util", + "indexPatternFieldEditor": "src/plugins/index_pattern_field_editor", "indexPatternManagement": "src/plugins/index_pattern_management", "advancedSettings": "src/plugins/advanced_settings", "kibana_legacy": "src/plugins/kibana_legacy", diff --git a/Jenkinsfile b/Jenkinsfile index 3b68cde206573a..8ab3fecb07a1ba 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -3,7 +3,7 @@ library 'kibana-pipeline-library' kibanaLibrary.load() -kibanaPipeline(timeoutMinutes: 155, checkPrChanges: true, setCommitStatus: true) { +kibanaPipeline(timeoutMinutes: 210, checkPrChanges: true, setCommitStatus: true) { slackNotifications.onFailure(disabled: !params.NOTIFY_ON_FAILURE) { githubPr.withDefaultPrComments { ciStats.trackBuild { diff --git a/docs/api/actions-and-connectors.asciidoc b/docs/api/actions-and-connectors.asciidoc new file mode 100644 index 00000000000000..17e7ea1b7672a3 --- /dev/null +++ b/docs/api/actions-and-connectors.asciidoc @@ -0,0 +1,30 @@ +[[actions-and-connectors-api]] +== Action and connector APIs + +Manage Actions and Connectors. + +The following action APIs are available: + +* <> to retrieve a single action by ID + +* <> to retrieve all actions + +* <> to retrieve a list of all action types + +* <> to create actions + +* <> to update the attributes for an existing action + +* <> to execute an action by ID + +* <> to delete an action by ID + +For information about the actions and connectors that {kib} supports, refer to <>. + +include::actions-and-connectors/get.asciidoc[] +include::actions-and-connectors/get_all.asciidoc[] +include::actions-and-connectors/list.asciidoc[] +include::actions-and-connectors/create.asciidoc[] +include::actions-and-connectors/update.asciidoc[] +include::actions-and-connectors/execute.asciidoc[] +include::actions-and-connectors/delete.asciidoc[] diff --git a/docs/api/actions-and-connectors/create.asciidoc b/docs/api/actions-and-connectors/create.asciidoc new file mode 100644 index 00000000000000..af5ddd050e40ec --- /dev/null +++ b/docs/api/actions-and-connectors/create.asciidoc @@ -0,0 +1,68 @@ +[[actions-and-connectors-api-create]] +=== Create action API +++++ +Create action API +++++ + +Creates an action. + +[[actions-and-connectors-api-create-request]] +==== Request + +`POST :/api/actions/action` + +[[actions-and-connectors-api-create-request-body]] +==== Request body + +`name`:: + (Required, string) The display name for the action. + +`actionTypeId`:: + (Required, string) The action type ID for the action. + +`config`:: + (Required, object) The configuration for the action. Configuration properties vary depending on + the action type. For information about the configuration properties, refer to <>. + +`secrets`:: + (Required, object) The secrets configuration for the action. Secrets configuration properties vary + depending on the action type. For information about the secrets configuration properties, refer to <>. + +[[actions-and-connectors-api-create-request-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +[[actions-and-connectors-api-create-example]] +==== Example + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/actions/action -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d ' +{ + "name": "my-action", + "actionTypeId": ".index", + "config": { + "index": "test-index" + } +}' +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "id": "c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad", + "actionTypeId": ".index", + "name": "my-action", + "config": { + "index": "test-index", + "refresh": false, + "executionTimeField": null + }, + "isPreconfigured": false +} +-------------------------------------------------- \ No newline at end of file diff --git a/docs/api/actions-and-connectors/delete.asciidoc b/docs/api/actions-and-connectors/delete.asciidoc new file mode 100644 index 00000000000000..e90b9ae44c5bd3 --- /dev/null +++ b/docs/api/actions-and-connectors/delete.asciidoc @@ -0,0 +1,35 @@ +[[actions-and-connectors-api-delete]] +=== Delete action API +++++ +Delete action API +++++ + +Deletes an action by ID. + +WARNING: When you delete an action, _it cannot be recovered_. + +[[actions-and-connectors-api-delete-request]] +==== Request + +`DELETE :/api/actions/action/` + +[[actions-and-connectors-api-delete-path-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the action. + +[[actions-and-connectors-api-delete-response-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +==== Example + +[source,sh] +-------------------------------------------------- +$ curl -X DELETE api/actions/action/c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad +-------------------------------------------------- +// KIBANA + diff --git a/docs/api/actions-and-connectors/execute.asciidoc b/docs/api/actions-and-connectors/execute.asciidoc new file mode 100644 index 00000000000000..12f1405eb44560 --- /dev/null +++ b/docs/api/actions-and-connectors/execute.asciidoc @@ -0,0 +1,83 @@ +[[actions-and-connectors-api-execute]] +=== Execute action API +++++ +Execute action API +++++ + +Executes an action by ID. + +[[actions-and-connectors-api-execute-request]] +==== Request + +`POST :/api/actions/action//_execute` + +[[actions-and-connectors-api-execute-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the action. + +[[actions-and-connectors-api-execute-request-body]] +==== Request body + +`params`:: + (Required, object) The parameters of the action. Parameter properties vary depending on + the action type. For information about the parameter properties, refer to <>. + +[[actions-and-connectors-api-execute-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +[[actions-and-connectors-api-execute-example]] +==== Example + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/actions/action/c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad/_execute -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d ' +{ + "params": { + "documents": [ + { + "id": "test_doc_id", + "name": "test_doc_name", + "message": "hello, world" + } + ] + } +}' +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "status": "ok", + "data": { + "took": 197, + "errors": false, + "items": [ + { + "index": { + "_index": "updated-index", + "_id": "iKyijHcBKCsmXNFrQe3T", + "_version": 1, + "result": "created", + "_shards": { + "total": 2, + "successful": 1, + "failed": 0 + }, + "_seq_no": 0, + "_primary_term": 1, + "status": 201 + } + } + ] + }, + "actionId": "c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad" +} +-------------------------------------------------- \ No newline at end of file diff --git a/docs/api/actions-and-connectors/get.asciidoc b/docs/api/actions-and-connectors/get.asciidoc new file mode 100644 index 00000000000000..6be554e65db049 --- /dev/null +++ b/docs/api/actions-and-connectors/get.asciidoc @@ -0,0 +1,50 @@ +[[actions-and-connectors-api-get]] +=== Get action API +++++ +Get action API +++++ + +Retrieves an action by ID. + +[[actions-and-connectors-api-get-request]] +==== Request + +`GET :/api/actions/action/` + +[[actions-and-connectors-api-get-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the action. + +[[actions-and-connectors-api-get-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +[[actions-and-connectors-api-get-example]] +==== Example + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/actions/action/c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "id": "c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad", + "actionTypeId": ".index", + "name": "my-action", + "config": { + "index": "test-index", + "refresh": false, + "executionTimeField": null + }, + "isPreconfigured": false +} +-------------------------------------------------- diff --git a/docs/api/actions-and-connectors/get_all.asciidoc b/docs/api/actions-and-connectors/get_all.asciidoc new file mode 100644 index 00000000000000..9863963c8395e6 --- /dev/null +++ b/docs/api/actions-and-connectors/get_all.asciidoc @@ -0,0 +1,52 @@ +[[actions-and-connectors-api-get-all]] +=== Get all actions API +++++ +Get all actions API +++++ + +Retrieves all actions. + +[[actions-and-connectors-api-get-all-request]] +==== Request + +`GET :/api/actions` + +[[actions-and-connectors-api-get-all-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +[[actions-and-connectors-api-get-all-example]] +==== Example + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/actions +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +[ + { + "id": "preconfigured-mail-action", + "actionTypeId": ".email", + "name": "email: preconfigured-mail-action", + "isPreconfigured": true + }, + { + "id": "c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad", + "actionTypeId": ".index", + "name": "my-action", + "config": { + "index": "test-index", + "refresh": false, + "executionTimeField": null + }, + "isPreconfigured": false + } +] +-------------------------------------------------- diff --git a/docs/api/actions-and-connectors/list.asciidoc b/docs/api/actions-and-connectors/list.asciidoc new file mode 100644 index 00000000000000..b800b7ff3b4f2f --- /dev/null +++ b/docs/api/actions-and-connectors/list.asciidoc @@ -0,0 +1,59 @@ +[[actions-and-connectors-api-list]] +=== List action types API +++++ +List all action types API +++++ + +Retrieves a list of all action types. + +[[actions-and-connectors-api-list-request]] +==== Request + +`GET :/api/actions/list_action_types` + +[[actions-and-connectors-api-list-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +[[actions-and-connectors-api-list-example]] +==== Example + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/actions/list_action_types +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +[ + { + "id": ".email", <1> + "name": "Email", <2> + "minimumLicenseRequired": "gold", <3> + "enabled": false, <4> + "enabledInConfig": true, <5> + "enabledInLicense": false <6> + }, + { + "id": ".index", + "name": "Index", + "minimumLicenseRequired": "basic", + "enabled": true, + "enabledInConfig": true, + "enabledInLicense": true + } +] +-------------------------------------------------- + + +<1> `id` - The unique ID of the action type. +<2> `name` - The name of the action type. +<3> `minimumLicenseRequired` - The license required to use the action type. +<4> `enabled` - Specifies if the action type is enabled or disabled in {kib}. +<5> `enabledInConfig` - Specifies if the action type is enabled or enabled in the {kib} .yml file. +<6> `enabledInLicense` - Specifies if the action type is enabled or disabled in the license. diff --git a/docs/api/actions-and-connectors/update.asciidoc b/docs/api/actions-and-connectors/update.asciidoc new file mode 100644 index 00000000000000..e08ec2f8da1b67 --- /dev/null +++ b/docs/api/actions-and-connectors/update.asciidoc @@ -0,0 +1,68 @@ +[[actions-and-connectors-api-update]] +=== Update action API +++++ +Update action API +++++ + +Updates the attributes for an existing action. + +[[actions-and-connectors-api-update-request]] +==== Request + +`PUT :/api/actions/action/` + +[[actions-and-connectors-api-update-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the action. + +[[actions-and-connectors-api-update-request-body]] +==== Request body + +`name`:: + (Required, string) The new name of the action. + +`config`:: + (Required, object) The new action configuration. Configuration properties vary depending on the action type. For information about the configuration properties, refer to <>. + +`secrets`:: + (Required, object) The updated secrets configuration for the action. Secrets properties vary depending on the action type. For information about the secrets configuration properties, refer to <>. + +[[actions-and-connectors-api-update-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +[[actions-and-connectors-api-update-example]] +==== Example + +[source,sh] +-------------------------------------------------- +$ curl -X PUT api/actions/action/c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d ' +{ + "name": "updated-action", + "config": { + "index": "updated-index" + } +}' +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "id": "c55b6eb0-6bad-11eb-9f3b-611eebc6c3ad", + "actionTypeId": ".index", + "name": "updated-action", + "config": { + "index": "updated-index", + "refresh": false, + "executionTimeField": null + }, + "isPreconfigured": false +} +-------------------------------------------------- diff --git a/docs/apm/agent-configuration.asciidoc b/docs/apm/agent-configuration.asciidoc index d911c2154ea4c5..aaaca867a5a01b 100644 --- a/docs/apm/agent-configuration.asciidoc +++ b/docs/apm/agent-configuration.asciidoc @@ -46,6 +46,7 @@ Go Agent:: {apm-go-ref}/configuration.html[Configuration reference] Java Agent:: {apm-java-ref}/configuration.html[Configuration reference] .NET Agent:: {apm-dotnet-ref}/configuration.html[Configuration reference] Node.js Agent:: {apm-node-ref}/configuration.html[Configuration reference] +PHP Agent:: _Not yet supported_ Python Agent:: {apm-py-ref}/configuration.html[Configuration reference] Ruby Agent:: {apm-ruby-ref}/configuration.html[Configuration reference] Real User Monitoring (RUM) Agent:: {apm-rum-ref}/configuration.html[Configuration reference] diff --git a/docs/apm/filters.asciidoc b/docs/apm/filters.asciidoc index c405ea10ade3d2..3fe9146658eefc 100644 --- a/docs/apm/filters.asciidoc +++ b/docs/apm/filters.asciidoc @@ -52,8 +52,9 @@ See the documentation for each agent you're using to learn how to configure serv * *Go:* {apm-go-ref}/configuration.html#config-environment[`ELASTIC_APM_ENVIRONMENT`] * *Java:* {apm-java-ref}/config-core.html#config-environment[`environment`] -* *.NET* {apm-dotnet-ref}/config-core.html#config-environment[`Environment`] +* *.NET:* {apm-dotnet-ref}/config-core.html#config-environment[`Environment`] * *Node.js:* {apm-node-ref}/configuration.html#environment[`environment`] +* *PHP:* {apm-php-ref}/configuration-reference.html#config-environment[`environment`] * *Python:* {apm-py-ref}/configuration.html#config-environment[`environment`] * *Ruby:* {apm-ruby-ref}/configuration.html#config-environment[`environment`] * *Real User Monitoring:* {apm-rum-ref}/configuration.html#environment[`environment`] diff --git a/docs/apm/service-maps.asciidoc b/docs/apm/service-maps.asciidoc index 7cc4da8a1fc1de..a3ac62a4c83436 100644 --- a/docs/apm/service-maps.asciidoc +++ b/docs/apm/service-maps.asciidoc @@ -87,10 +87,11 @@ Type and subtype are based on `span.type`, and `span.subtype`. Service maps are supported for the following Agent versions: [horizontal] -Go Agent:: ≥ v1.7.0 -Java Agent:: ≥ v1.13.0 -.NET Agent:: ≥ v1.3.0 -Node.js Agent:: ≥ v3.6.0 -Python Agent:: ≥ v5.5.0 -Ruby Agent:: ≥ v3.6.0 -Real User Monitoring (RUM) Agent:: ≥ v4.7.0 +Go agent:: ≥ v1.7.0 +Java agent:: ≥ v1.13.0 +.NET agent:: ≥ v1.3.0 +Node.js agent:: ≥ v3.6.0 +PHP agent:: _Not yet supported_ +Python agent:: ≥ v5.5.0 +Ruby agent:: ≥ v3.6.0 +Real User Monitoring (RUM) agent:: ≥ v4.7.0 diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc index 465a3d652046db..5049321363f88e 100644 --- a/docs/apm/troubleshooting.asciidoc +++ b/docs/apm/troubleshooting.asciidoc @@ -17,6 +17,7 @@ don't forget to check our other troubleshooting guides or discussion forum: * {apm-go-ref}/troubleshooting.html[Go agent troubleshooting] * {apm-java-ref}/trouble-shooting.html[Java agent troubleshooting] * {apm-node-ref}/troubleshooting.html[Node.js agent troubleshooting] +* {apm-php-ref}/troubleshooting.html[PHP agent troubleshooting] * {apm-py-ref}/troubleshooting.html[Python agent troubleshooting] * {apm-ruby-ref}/debugging.html[Ruby agent troubleshooting] * {apm-rum-ref/troubleshooting.html[RUM troubleshooting] diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 5564b4cdcf79da..38b053ea127521 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -93,6 +93,10 @@ for use in their own application. |Moves the legacy ui/registry/feature_catalogue module for registering "features" that should be shown in the home page's feature catalogue to a service within a "home" plugin. The feature catalogue refered to here should not be confused with the "feature" plugin for registering features used to derive UI capabilities for feature controls. +|{kib-repo}blob/{branch}/src/plugins/index_pattern_field_editor/README.md[indexPatternFieldEditor] +|The reusable field editor across Kibana! + + |{kib-repo}blob/{branch}/src/plugins/index_pattern_management[indexPatternManagement] |WARNING: Missing README. diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index 017e3ec57d3404..54c065480b113c 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -53,6 +53,8 @@ readonly links: { readonly base: string; }; readonly aggs: { + readonly composite: string; + readonly composite_missing_bucket: string; readonly date_histogram: string; readonly date_range: string; readonly date_format_pattern: string; diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index dc6804b0630bd4..0bca16a0bb7107 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,4 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: string;
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessSyntax: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
};
readonly addData: string;
readonly kibana: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
putComponentTemplateMetadata: string;
putWatch: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: string;
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
};
readonly addData: string;
readonly kibana: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putWatch: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
} | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md b/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md index d7eafdce017e43..551cbe3c937504 100644 --- a/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md +++ b/docs/development/core/server/kibana-plugin-core-server.kibanaresponsefactory.md @@ -19,7 +19,6 @@ kibanaResponseFactory: { forbidden: (options?: ErrorHttpResponseOptions) => KibanaResponse; notFound: (options?: ErrorHttpResponseOptions) => KibanaResponse; conflict: (options?: ErrorHttpResponseOptions) => KibanaResponse; - internalError: (options?: ErrorHttpResponseOptions) => KibanaResponse; customError: (options: CustomHttpResponseOptions) => KibanaResponse; redirected: (options: RedirectResponseOptions) => KibanaResponse | Buffer | Stream>; ok: (options?: HttpResponseOptions) => KibanaResponse | Buffer | Stream>; diff --git a/docs/development/core/server/kibana-plugin-core-server.loggingservicesetup.configure.md b/docs/development/core/server/kibana-plugin-core-server.loggingservicesetup.configure.md index 04a3cf9aff6448..52ab5f1098457c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.loggingservicesetup.configure.md +++ b/docs/development/core/server/kibana-plugin-core-server.loggingservicesetup.configure.md @@ -34,7 +34,7 @@ Customize the configuration for the plugins.data.search context. core.logging.configure( of({ appenders: new Map(), - loggers: [{ context: 'search', appenders: ['default'] }] + loggers: [{ name: 'search', appenders: ['default'] }] }) ) diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 3ec63840a67cba..d14e41cfb56ecd 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -283,7 +283,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginOpaqueId](./kibana-plugin-core-server.pluginopaqueid.md) | | | [PublicUiSettingsParams](./kibana-plugin-core-server.publicuisettingsparams.md) | A sub-set of [UiSettingsParams](./kibana-plugin-core-server.uisettingsparams.md) exposed to the client-side. | | [RedirectResponseOptions](./kibana-plugin-core-server.redirectresponseoptions.md) | HTTP response parameters for redirection response | -| [RequestHandler](./kibana-plugin-core-server.requesthandler.md) | A function executed when route path matched requested resource path. Request handler is expected to return a result of one of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) functions. | +| [RequestHandler](./kibana-plugin-core-server.requesthandler.md) | A function executed when route path matched requested resource path. Request handler is expected to return a result of one of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) functions. If anything else is returned, or an error is thrown, the HTTP service will automatically log the error and respond 500 - Internal Server Error. | | [RequestHandlerContextContainer](./kibana-plugin-core-server.requesthandlercontextcontainer.md) | An object that handles registration of http request context providers. | | [RequestHandlerContextProvider](./kibana-plugin-core-server.requesthandlercontextprovider.md) | Context provider for request handler. Extends request context object with provided functionality or data. | | [RequestHandlerWrapper](./kibana-plugin-core-server.requesthandlerwrapper.md) | Type-safe wrapper for [RequestHandler](./kibana-plugin-core-server.requesthandler.md) function. | diff --git a/docs/development/core/server/kibana-plugin-core-server.requesthandler.md b/docs/development/core/server/kibana-plugin-core-server.requesthandler.md index 0032e52a0e9060..d32ac4d80c337b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.requesthandler.md +++ b/docs/development/core/server/kibana-plugin-core-server.requesthandler.md @@ -4,7 +4,7 @@ ## RequestHandler type -A function executed when route path matched requested resource path. Request handler is expected to return a result of one of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) functions. +A function executed when route path matched requested resource path. Request handler is expected to return a result of one of [KibanaResponseFactory](./kibana-plugin-core-server.kibanaresponsefactory.md) functions. If anything else is returned, or an error is thrown, the HTTP service will automatically log the error and respond `500 - Internal Server Error`. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.sort.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.sort.md index 3cc02c404c8d71..17f52687243321 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.sort.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsfindresult.sort.md @@ -25,7 +25,7 @@ const page1 = await savedObjectsClient.find({ type: 'visualization', sortField: 'updated_at', sortOrder: 'asc', - pit, + pit: { id }, }); const lastHit = page1.saved_objects[page1.saved_objects.length - 1]; const page2 = await savedObjectsClient.find({ diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md index 63956ebee68f7b..6b668824845202 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsrepository.openpointintimefortype.md @@ -29,19 +29,16 @@ openPointInTimeForType(type: string | string[], { keepAlive, preference }?: Save ```ts -const repository = coreStart.savedObjects.createInternalRepository(); - -const { id } = await repository.openPointInTimeForType( - type: 'index-pattern', - { keepAlive: '2m' }, +const { id } = await savedObjectsClient.openPointInTimeForType( + type: 'visualization', + { keepAlive: '5m' }, ); const page1 = await savedObjectsClient.find({ type: 'visualization', sortField: 'updated_at', sortOrder: 'asc', - pit, + pit: { id, keepAlive: '2m' }, }); - const lastHit = page1.saved_objects[page1.saved_objects.length - 1]; const page2 = await savedObjectsClient.find({ type: 'visualization', @@ -50,7 +47,6 @@ const page2 = await savedObjectsClient.find({ pit: { id: page1.pit_id }, searchAfter: lastHit.sort, }); - await savedObjectsClient.closePointInTime(page2.pit_id); ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.idatapluginservices.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.idatapluginservices.md index 5f940bf70a12bd..44cfb0c65e3875 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.idatapluginservices.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.idatapluginservices.md @@ -21,4 +21,5 @@ export interface IDataPluginServices extends Partial | [savedObjects](./kibana-plugin-plugins-data-public.idatapluginservices.savedobjects.md) | CoreStart['savedObjects'] | | | [storage](./kibana-plugin-plugins-data-public.idatapluginservices.storage.md) | IStorageWrapper | | | [uiSettings](./kibana-plugin-plugins-data-public.idatapluginservices.uisettings.md) | CoreStart['uiSettings'] | | +| [usageCollection](./kibana-plugin-plugins-data-public.idatapluginservices.usagecollection.md) | UsageCollectionStart | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.idatapluginservices.usagecollection.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.idatapluginservices.usagecollection.md new file mode 100644 index 00000000000000..b803dca76203f4 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.idatapluginservices.usagecollection.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IDataPluginServices](./kibana-plugin-plugins-data-public.idatapluginservices.md) > [usageCollection](./kibana-plugin-plugins-data-public.idatapluginservices.usagecollection.md) + +## IDataPluginServices.usageCollection property + +Signature: + +```typescript +usageCollection?: UsageCollectionStart; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md index 83fbc00860ca5e..786ac4f9d61a90 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md @@ -7,7 +7,7 @@ Signature: ```typescript -SearchBar: React.ComponentClass, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "trackUiMetric" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +SearchBar: React.ComponentClass, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; } ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfields.md index b0ccedb819c95d..856e43588ffb78 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfields.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfields.md @@ -9,15 +9,8 @@ returns all search source fields Signature: ```typescript -getFields(recurse?: boolean): SearchSourceFields; +getFields(): SearchSourceFields; ``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| recurse | boolean | | - Returns: `SearchSourceFields` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md index 3250561c8b82e9..b2382d35f7d768 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md @@ -35,7 +35,7 @@ export declare class SearchSource | [fetch(options)](./kibana-plugin-plugins-data-public.searchsource.fetch.md) | | Fetch this source and reject the returned Promise on error | | [fetch$(options)](./kibana-plugin-plugins-data-public.searchsource.fetch_.md) | | Fetch this source from Elasticsearch, returning an observable over the response(s) | | [getField(field, recurse)](./kibana-plugin-plugins-data-public.searchsource.getfield.md) | | Gets a single field from the fields | -| [getFields(recurse)](./kibana-plugin-plugins-data-public.searchsource.getfields.md) | | returns all search source fields | +| [getFields()](./kibana-plugin-plugins-data-public.searchsource.getfields.md) | | returns all search source fields | | [getId()](./kibana-plugin-plugins-data-public.searchsource.getid.md) | | returns search source id | | [getOwnField(field)](./kibana-plugin-plugins-data-public.searchsource.getownfield.md) | | Get the field from our own fields, don't traverse up the chain | | [getParent()](./kibana-plugin-plugins-data-public.searchsource.getparent.md) | | Get the parent of this SearchSource {undefined\|searchSource} | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md index 683a35fabf5710..1d4547bb21d103 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md @@ -24,6 +24,7 @@ export interface SearchSourceFields | [highlight](./kibana-plugin-plugins-data-public.searchsourcefields.highlight.md) | any | | | [highlightAll](./kibana-plugin-plugins-data-public.searchsourcefields.highlightall.md) | boolean | | | [index](./kibana-plugin-plugins-data-public.searchsourcefields.index.md) | IndexPattern | | +| [parent](./kibana-plugin-plugins-data-public.searchsourcefields.parent.md) | SearchSourceFields | | | [query](./kibana-plugin-plugins-data-public.searchsourcefields.query.md) | Query | [Query](./kibana-plugin-plugins-data-public.query.md) | | [searchAfter](./kibana-plugin-plugins-data-public.searchsourcefields.searchafter.md) | EsQuerySearchAfter | | | [size](./kibana-plugin-plugins-data-public.searchsourcefields.size.md) | number | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.parent.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.parent.md new file mode 100644 index 00000000000000..3adb34a50ff9eb --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.parent.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) > [parent](./kibana-plugin-plugins-data-public.searchsourcefields.parent.md) + +## SearchSourceFields.parent property + +Signature: + +```typescript +parent?: SearchSourceFields; +``` diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index bf4f7d9d827047..c7d5242da69de4 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -258,8 +258,12 @@ Highlights results in *Discover* and saved searches on dashboards. Highlighting slows requests when working on big documents. [[doctable-legacy]]`doc_table:legacy`:: -Control the way the Discover's table looks and works. Set this property to `true` to revert to the legacy implementation. +Controls the way the document table looks and works. Set this property to `true` to revert to the legacy implementation. +[[discover-searchFieldsFromSource]]`discover:searchFieldsFromSource`:: +Load fields from the original JSON {ref}/mapping-source-field.html[`_source`]. +When disabled, *Discover* loads fields using the {es} search API's +{ref}/search-fields.html#search-fields-param[`fields`] parameter. [float] [[kibana-ml-settings]] @@ -453,7 +457,7 @@ of buckets to try to represent. [horizontal] [[visualization-visualize-chartslibrary]]`visualization:visualize:legacyChartsLibrary`:: -Enables legacy charts library for area, line and bar charts in visualize. +Enables the legacy charts library for aggregation-based area, line, and bar charts in *Visualize*. [[visualization-colormapping]]`visualization:colorMapping`:: **This setting is deprecated and will not be supported as of 8.0.** @@ -461,24 +465,25 @@ Maps values to specific colors in *Visualize* charts and *TSVB*. This setting do [[visualization-dimmingopacity]]`visualization:dimmingOpacity`:: The opacity of the chart items that are dimmed when highlighting another element -of the chart. The lower this number, the more the highlighted element stands out. -This must be a number between 0 and 1. +of the chart. Use numbers between 0 and 1. The lower the number, the more the highlighted element stands out. + +[[visualization-heatmap-maxbuckets]]`visualization:heatmap:maxBuckets`:: +The maximum number of buckets a datasource can return. High numbers can have a negative impact on your browser rendering performance. [[visualization-regionmap-showwarnings]]`visualization:regionmap:showWarnings`:: Shows a warning in a region map when terms cannot be joined to a shape. [[visualization-tilemap-wmsdefaults]]`visualization:tileMap:WMSdefaults`:: -The default properties for the WMS map server support in the coordinate map. +The default properties for the WMS map server supported in the coordinate map. [[visualization-tilemap-maxprecision]]`visualization:tileMap:maxPrecision`:: -The maximum geoHash precision displayed on tile maps: 7 is high, 10 is very high, -and 12 is the maximum. See this -{ref}/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator[explanation of cell dimensions]. +The maximum geoHash precision displayed in tile maps. 7 is high, 10 is very high, +and 12 is the maximum. For more information, refer to +{ref}/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator[Cell dimensions at the equator]. [[visualize-enablelabs]]`visualize:enableLabs`:: -Enables users to create, view, and edit experimental visualizations. If disabled, -only visualizations that are considered production-ready are available to the -user. +Enables users to create, view, and edit experimental visualizations. When disabled, +only production-ready visualizations are available to users. [float] diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index afcb7bc21b66b6..7ffb6b66f5a2b4 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -356,26 +356,26 @@ To enable the <>, specify wh [source,yaml] ---------------------------------------- xpack.security.audit.appender: - kind: rolling-file - path: ./audit.log + type: rolling-file + fileName: ./audit.log policy: - kind: time-interval + type: time-interval interval: 24h <1> strategy: - kind: numeric + type: numeric max: 10 <2> layout: - kind: json + type: json ---------------------------------------- <1> Rotates log files every 24 hours. <2> Keeps maximum of 10 log files before deleting older ones. -| `xpack.security.audit.appender.kind` +| `xpack.security.audit.appender.type` | Required. Specifies where audit logs should be written to. Allowed values are `console`, `file`, or `rolling-file`. Refer to <> and <> for appender specific settings. -| `xpack.security.audit.appender.layout.kind` +| `xpack.security.audit.appender.layout.type` | Required. Specifies how audit logs should be formatted. Allowed values are `json` or `pattern`. Refer to <> for layout specific settings. @@ -396,7 +396,7 @@ The `file` appender writes to a file and can be configured using the following s [cols="2*<"] |====== -| `xpack.security.audit.appender.path` +| `xpack.security.audit.appender.fileName` | Required. Full file path the log file should be written to. |====== @@ -408,14 +408,14 @@ The `rolling-file` appender writes to a file and rotates it using a rolling stra [cols="2*<"] |====== -| `xpack.security.audit.appender.path` +| `xpack.security.audit.appender.fileName` | Required. Full file path the log file should be written to. -| `xpack.security.audit.appender.policy.kind` +| `xpack.security.audit.appender.policy.type` | Specifies when a rollover should occur. Allowed values are `size-limit` and `time-interval`. *Default:* `time-interval`. Refer to <> and <> for policy specific settings. -| `xpack.security.audit.appender.strategy.kind` +| `xpack.security.audit.appender.strategy.type` | Specifies how the rollover should occur. Only allowed value is currently `numeric`. *Default:* `numeric` Refer to <> for strategy specific settings. diff --git a/docs/user/alerting/action-types/email.asciidoc b/docs/user/alerting/action-types/email.asciidoc index d7a9373a6e2a99..3562be1e405f6f 100644 --- a/docs/user/alerting/action-types/email.asciidoc +++ b/docs/user/alerting/action-types/email.asciidoc @@ -37,6 +37,7 @@ Password:: password for 'login' type authentication. password: passwordkeystorevalue -- +[[email-connector-config-properties]] `config` defines the action type specific to the configuration and contains the following properties: [cols="2*<"] diff --git a/docs/user/alerting/action-types/index.asciidoc b/docs/user/alerting/action-types/index.asciidoc index 2c6da7c7c3026b..2f459edea28f14 100644 --- a/docs/user/alerting/action-types/index.asciidoc +++ b/docs/user/alerting/action-types/index.asciidoc @@ -30,6 +30,7 @@ Execution time field:: This field will be automatically set to the time the ale executionTimeField: somedate -- +[[index-connector-config-properties]] `config` defines the action type specific to the configuration and contains the following properties: [cols="2*<"] diff --git a/docs/user/alerting/action-types/jira.asciidoc b/docs/user/alerting/action-types/jira.asciidoc index 65e5ee4fc4a013..6e47d5618d5982 100644 --- a/docs/user/alerting/action-types/jira.asciidoc +++ b/docs/user/alerting/action-types/jira.asciidoc @@ -33,6 +33,7 @@ API token (or password):: Jira API authentication token (or password) for HTTP apiToken: tokenkeystorevalue -- +[[jira-connector-config-properties]] `config` defines the action type specific to the configuration and contains the following properties: [cols="2*<"] diff --git a/docs/user/alerting/action-types/pagerduty.asciidoc b/docs/user/alerting/action-types/pagerduty.asciidoc index aad192dbddb30f..e1078a55ddd0d5 100644 --- a/docs/user/alerting/action-types/pagerduty.asciidoc +++ b/docs/user/alerting/action-types/pagerduty.asciidoc @@ -150,6 +150,7 @@ Integration Key:: A 32 character PagerDuty Integration Key for an integration routingKey: testroutingkey -- +[[pagerduty-connector-config-properties]] `config` defines the action type specific to the configuration. `config` contains `apiURL`, a string that corresponds to *API URL*. diff --git a/docs/user/alerting/action-types/resilient.asciidoc b/docs/user/alerting/action-types/resilient.asciidoc index b5ddb76d49b0cd..112246ab91162e 100644 --- a/docs/user/alerting/action-types/resilient.asciidoc +++ b/docs/user/alerting/action-types/resilient.asciidoc @@ -33,6 +33,7 @@ API key secret:: The authentication key secret for HTTP Basic authentication. apiKeySecret: tokenkeystorevalue -- +[[resilient-connector-config-properties]] `config` defines the action type specific to the configuration and contains the following properties: [cols="2*<"] diff --git a/docs/user/alerting/action-types/servicenow.asciidoc b/docs/user/alerting/action-types/servicenow.asciidoc index 0acb92bcdb5ee5..5d8782c14e5815 100644 --- a/docs/user/alerting/action-types/servicenow.asciidoc +++ b/docs/user/alerting/action-types/servicenow.asciidoc @@ -31,6 +31,7 @@ Password:: Password for HTTP Basic authentication. password: passwordkeystorevalue -- +[[servicenow-connector-config-properties]] `config` defines the action type specific to the configuration and contains the following properties: [cols="2*<"] diff --git a/docs/user/alerting/action-types/slack.asciidoc b/docs/user/alerting/action-types/slack.asciidoc index a1fe7a2521b22a..6a38e5c827ab28 100644 --- a/docs/user/alerting/action-types/slack.asciidoc +++ b/docs/user/alerting/action-types/slack.asciidoc @@ -26,6 +26,7 @@ Webhook URL:: The URL of the incoming webhook. See https://api.slack.com/messa webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz' -- +[[slack-connector-config-properties]] `config` defines the action type specific to the configuration. `config` contains `webhookUrl`, a string that corresponds to *Webhook URL*. diff --git a/docs/user/alerting/action-types/teams.asciidoc b/docs/user/alerting/action-types/teams.asciidoc index 6706dd2e5643fd..e1ce91fc0c1231 100644 --- a/docs/user/alerting/action-types/teams.asciidoc +++ b/docs/user/alerting/action-types/teams.asciidoc @@ -26,6 +26,7 @@ Webhook URL:: The URL of the incoming webhook. See https://docs.microsoft.com/ webhookUrl: 'https://outlook.office.com/webhook/abcd@0123456/IncomingWebhook/abcdefgh/ijklmnopqrstuvwxyz' -- +[[teams-connector-config-properties]] `config` defines the action type specific to the configuration. `config` contains `webhookUrl`, a string that corresponds to *Webhook URL*. diff --git a/docs/user/alerting/action-types/webhook.asciidoc b/docs/user/alerting/action-types/webhook.asciidoc index fff6814325ea45..2d626d53d1c77e 100644 --- a/docs/user/alerting/action-types/webhook.asciidoc +++ b/docs/user/alerting/action-types/webhook.asciidoc @@ -36,6 +36,7 @@ Password:: An optional password. If set, HTTP basic authentication is used. Cur password: passwordkeystorevalue -- +[[webhook-connector-config-properties]] `config` defines the action type specific to the configuration and contains the following properties: [cols="2*<"] diff --git a/docs/user/alerting/alert-types.asciidoc b/docs/user/alerting/alert-types.asciidoc index 016ecc3167298d..0877f067eee217 100644 --- a/docs/user/alerting/alert-types.asciidoc +++ b/docs/user/alerting/alert-types.asciidoc @@ -2,7 +2,7 @@ [[alert-types]] == Standard stack alert types -{kib} supplies alert types in two ways: some are built into {kib} (these are known as stack alerts), while domain-specific alert types are registered by {kib} apps such as <>, <>, and <>. +{kib} supplies alert types in two ways: some are built into {kib} (these are known as stack alerts), while domain-specific alert types are registered by {kib} apps such as <>, <>, <>, and <>. This section covers stack alerts. For domain-specific alert types, refer to the documentation for that app. Users will need `all` access to the *Stack Alerts* feature to be able to create and edit any of the alerts listed below. diff --git a/docs/user/alerting/alerting-getting-started.asciidoc b/docs/user/alerting/alerting-getting-started.asciidoc index 06370c64aedf82..6186fce8a51c49 100644 --- a/docs/user/alerting/alerting-getting-started.asciidoc +++ b/docs/user/alerting/alerting-getting-started.asciidoc @@ -172,6 +172,7 @@ To access alerting in a space, a user must have access to one of the following f * Alerting * <> * <> +* <> * <> * <> * <> @@ -202,4 +203,4 @@ If an alert requires certain privileges to run such as index privileges, keep in For security reasons you may wish to limit the extent to which {kib} can connect to external services. <> allows you to disable certain <> and whitelist the hostnames that {kib} can connect with. --- \ No newline at end of file +-- diff --git a/docs/user/api.asciidoc b/docs/user/api.asciidoc index 20f1fc89367f29..2ae83bee1e06c7 100644 --- a/docs/user/api.asciidoc +++ b/docs/user/api.asciidoc @@ -36,6 +36,7 @@ include::{kib-repo-dir}/api/features.asciidoc[] include::{kib-repo-dir}/api/spaces-management.asciidoc[] include::{kib-repo-dir}/api/role-management.asciidoc[] include::{kib-repo-dir}/api/saved-objects.asciidoc[] +include::{kib-repo-dir}/api/actions-and-connectors.asciidoc[] include::{kib-repo-dir}/api/dashboard-api.asciidoc[] include::{kib-repo-dir}/api/logstash-configuration-management.asciidoc[] include::{kib-repo-dir}/api/url-shortening.asciidoc[] diff --git a/docs/user/dashboard/url-drilldown.asciidoc b/docs/user/dashboard/url-drilldown.asciidoc index ad3dd17fcfa111..7a092b4686e2d2 100644 --- a/docs/user/dashboard/url-drilldown.asciidoc +++ b/docs/user/dashboard/url-drilldown.asciidoc @@ -256,7 +256,7 @@ Note: | *Range selection* | event.from + event.to -| `from` and `to` values of selected range. Depending on your data, could be either a date or number. + +| `from` and `to` values of the selected range as numbers. + Tip: Consider using <> helper for date formatting. | diff --git a/docs/user/introduction.asciidoc b/docs/user/introduction.asciidoc index fb91f6a6a1c9a2..50eb81279fc16e 100644 --- a/docs/user/introduction.asciidoc +++ b/docs/user/introduction.asciidoc @@ -7,20 +7,19 @@ {kib} enables you to give shape to your data and navigate the Elastic Stack. With {kib}, you can: -* *Visualize and analyze your data.* -Search for hidden insights, visualize what you've found in charts, gauges, -maps and more, and combine them in a dashboard. - * *Search, observe, and protect.* From discovering documents to analyzing logs to finding security vulnerabilities, {kib} is your portal for accessing these capabilities and more. +* *Visualize and analyze your data.* +Search for hidden insights, visualize what you've found in charts, gauges, +maps and more, and combine them in a dashboard. + * *Manage, monitor, and secure the Elastic Stack.* Manage your indices and ingest pipelines, monitor the health of your Elastic Stack cluster, and control which users have access to which features. - *{kib} is for administrators, analysts, and business users.* As an admin, your role is to manage the Elastic Stack, from creating your deployment to getting {es} data into {kib}, and then @@ -37,103 +36,84 @@ No matter your data, {kib} can help you uncover patterns and relationships and v [[kibana-home-page]] === Where to start -Start with the home page, where you’re guided toward the most common use cases. -For a quick reference of {kib} use cases, refer to <> +Start with the home page, where you’re presented options for adding your data. +You can collect data from an app or service or upload a file that contains your data. +If you’re not ready to use your own data, you can add a sample data set. + +The home page provides access to the *Enterprise Search*, *Observability*, and *Security* solutions, +and everything you need to visualize and analyze your data. [role="screenshot"] image::images/home-page.png[Kibana home page] -The main menu gets you to where you need to go. Like the home page, -the menu is organized by use case. Want to work with your logs, metrics, APM, or -Uptime data? The apps you need are under *Observability*. The main menu also includes -*Recently viewed*, so you can easily access your previously opened apps. - -Hidden by default, you open the main menu by clicking the -hamburger icon. To keep the main menu visible at all times, click the *Dock navigation* item. +To access all of {kib} features, use the main menu. +Open this menu by clicking the +menu icon. To keep the main menu visible at all times, click *Dock navigation*. +For a quick reference of all {kib} features, refer to <> [role="screenshot"] image::images/kibana-main-menu.png[Kibana main menu] [float] -[[kibana-navigation-search]] -=== Search {kib} - -Using the Search field in the global header, you can -search for applications and objects, such as -dashboards and visualizations. - -Search suggestions include deep links into applications, -allowing you to directly navigate to the views you need most. - -[role="screenshot"] -image::images/app-navigation-search.png[Example of searching for apps] - -When searching for objects, you can search by type, name, and tag. -Tags are keywords or labels that you assign to {kib} objects, -so you can classify the objects in a way that is meaningful to you. -You can then quickly search for related objects based on shared tags. +[[extend-your-use-case]] +=== Search, observe, and protect -[role="screenshot"] -image::images/tags-search.png[Example of searching for tags] +Being able to search, observe, and protect your data is a requirement for any analyst. +{kib} provides solutions for each of these use cases. -To get the most from the search feature, follow these tips: +* https://www.elastic.co/guide/en/enterprise-search/current/index.html[*Enterprise Search*] enables you to create a search experience for your app, workplace, and website. -* Use the keyboard shortcut—Ctrl+/ on Windows and Linux, Command+/ on MacOS—to focus on the input at any time. +* {observability-guide}/observability-introduction.html[*Elastic Observability*] enables you to monitor and apply analytics in real time +to events happening across all your environments. You can analyze log events, monitor the performance metrics for the host or container +that it ran in, trace the transaction, and check the overall service availability. -* Use the provided syntax keywords. +* Designed for security analysts, {security-guide}/es-overview.html[*Elastic Security*] provides an overview of +the events and alerts from your environment. Elastic Security helps you defend +your organization from threats before damage and loss occur. + -[cols=2*] -|=== -|Search by type -|`type:dashboard` - -Available types: `application`, `canvas-workpad`, `dashboard`, `index-pattern`, `lens`, `maps`, `query`, `search`, `visualization` - -|Search by tag -|`tag:mytagname` + -`tag:"tag name with spaces"` - -|Search by type and name -|`type:dashboard my_dashboard_title` - -|Advanced searches -|`tag:(tagname1 or tagname2) my_dashboard_title` + -`type:lens tag:(tagname1 or tagname2)` + -`type:(dashboard or canvas-workpad) logs` + -|=== +[role="screenshot"] +image::siem/images/detections-ui.png[Detections view in Elastic Security] [float] [[visualize-and-analyze]] -=== Analyze your data +=== Visualize and analyze -Data analysis is the core functionality of {kib}. +Data analysis is a core functionality of {kib}. You can quickly search through large amounts of data, explore fields and values, and then use {kib}’s drag-and-drop interface to rapidly build charts, tables, metrics, and more. [role="screenshot"] -image::images/visualization-journey.png[User visualization journey] +image::images/visualization-journey.png[User data analysis journey] [[get-data-into-kibana]] -. *Add data.* The best way to add {es} data to {kib} is to use one of our guided processes, -available from the <>. You can collect data from an app or service, upload a -file, or add a sample data set. +[cols=2*] +|=== -. *Explore.* With <>, you can search your data for hidden +| *1* +| *Add data.* The best way to add {es} data to {kib} is to use one of our guided processes, +available from the <>. + +| *2* +| *Explore.* With <>, you can search your data for hidden insights and relationships. Ask your questions, and then filter the results to just the data you want. -You can also limit your results to the most recent documents added to {es}. +You can limit your results to the most recent documents added to {es}. -. *Visualize.* {kib} provides many options to create visualizations of your data, from +| *3* +| *Visualize.* {kib} provides many options to create visualizations of your data, from aggregation-based data to time series data. <> is your starting point to create visualizations, and then pulling them together to show your data from multiple perspectives. -. *Present.* With <>, you can display your data on a visually +| *4* +| *Present.* With <>, you can display your data on a visually compelling, pixel-perfect workpad. **Canvas** can give your data the “wow” factor needed to impress your CEO and captivate coworkers with a big-screen display. -. *Share.* Ready to <> your findings with a larger audience? {kib} offers many options—embed +| *5* +| *Share.* Ready to <> your findings with a larger audience? {kib} offers many options—embed a dashboard, share a link, export to PDF, and more. +|=== [float] ==== Plot location data on a map @@ -147,7 +127,7 @@ You can also visualize and track movement over space and through time. [float] ==== Model data behavior -To model the behavior of your data, you'll want to use +To model the behavior of your data, you'll use <>. This app can help you extract insights from your data that you might otherwise miss. You can forecast unusual behavior in your time series data. @@ -164,37 +144,17 @@ can help you uncover website vulnerabilities that hackers are targeting, so you can harden your website. Or, you might provide graph-based personalized recommendations to your e-commerce customers. -[float] -[[extend-your-use-case]] -=== Search, observe, and protect - -Being able to search, observe, and protect your data is a requirement for any analyst. -{kib} provides solutions for each of these use cases. - -* https://www.elastic.co/guide/en/enterprise-search/current/index.html[*Enterprise Search*] enables you to create a search experience for your app, workplace, and website. - -* {observability-guide}/observability-introduction.html[*Elastic Observability*] enables you to monitor and apply analytics in real time -to events happening across all your environments. You can analyze log events, monitor the performance metrics for the host or container -that it ran in, trace the transaction, and check the overall service availability. - -* Designed for security analysts, {security-guide}/es-overview.html[*Elastic Security*] provides an overview of -the events and alerts from your environment. Elastic Security helps you defend -your organization from threats before damage and loss occur. -+ -[role="screenshot"] -image::siem/images/detections-ui.png[] - [float] [[manage-all-things-stack]] === Manage all things Elastic Stack -{kib}'s <> takes you under the hood, -so you can twist the levers and turn the knobs. *Stack Management* provides +{kib}'s <> UIs takes you under the hood, +so you can twist the levers and turn the knobs. You'll find guided processes for administering all things Elastic Stack, including data, indices, clusters, alerts, and security. [role="screenshot"] -image::images/intro-management.png[] +image::images/intro-management.png[Index Management view in Stack Management] [float] ==== Manage your data, indices, and clusters @@ -216,8 +176,8 @@ that exists in almost every use case. For example, you might set an alert to not * System resources, such as memory, CPU and disk space, take a dip. * An unusually high number of service requests, suspicious processes, and login attempts occurs. -An alert is triggered when a specified condition is met. For example, -an alert might trigger when the average or max of one of +An alert triggers when a specified condition is met. For example, +you can trigger an alert when the average or max of one of your metrics exceeds a threshold within a specified time frame. When the alert triggers, you can send a notification to a system that is part of @@ -240,7 +200,7 @@ Think of a space as its own mini {kib} installation—it’s isolated from a so you can tailor it to your specific needs without impacting others. [role="screenshot"] -image::images/select-your-space.png[Space selector screen] +image::images/select-your-space.png[Space selector view] Most of {kib}’s entities are space-aware, including dashboards, visualizations, index patterns, Canvas workpads, Timelion visualizations, graphs, tags, and machine learning jobs. @@ -271,7 +231,7 @@ to specific features on a per-user basis, you must configure <>. [role="screenshot"] -image::images/features-control.png[Features Controls screen] +image::images/features-control.png[Features Controls view] [float] [[intro-kibana-Security]] @@ -289,7 +249,7 @@ Kibana supports several <>, allowing you to login using {es}’s built-in realms, or by your own single sign-on provider. [role="screenshot"] -image::images/login-screen.png[Login screen] +image::images/login-screen.png[Login page] [float] ==== Secure access @@ -320,6 +280,52 @@ record of who did what, when. The {kib} audit log will record this information f which can then be correlated with {es} audit logs to gain more insights into your users’ behavior. For more information, see <>. +[float] +[[kibana-navigation-search]] +=== Quickly find apps and objects + +Using the search field in the global header, you can +search for applications and objects, such as +dashboards and visualizations. Search suggestions include deep links into applications, +allowing you to directly navigate to the views you need most. + +[role="screenshot"] +image::images/app-navigation-search.png[Example of searching for apps] + +When searching for objects, you can search by type, name, and tag. +Tags are keywords or labels that you assign to {kib} objects, +so you can classify the objects in a way that is meaningful to you. +You can then quickly search for related objects based on shared tags. + +[role="screenshot"] +image::images/tags-search.png[Example of searching for tags] + +To get the most from the search feature, follow these tips: + +* Use the keyboard shortcut—Ctrl+/ on Windows and Linux, Command+/ on MacOS—to focus on the input at any time. + +* Use the provided syntax keywords. ++ +[cols=2*] +|=== +|Search by type +|`type:dashboard` + +Available types: `application`, `canvas-workpad`, `dashboard`, `index-pattern`, `lens`, `maps`, `query`, `search`, `visualization` + +|Search by tag +|`tag:mytagname` + +`tag:"tag name with spaces"` + +|Search by type and name +|`type:dashboard my_dashboard_title` + +|Advanced searches +|`tag:(tagname1 or tagname2) my_dashboard_title` + +`type:lens tag:(tagname1 or tagname2)` + +`type:(dashboard or canvas-workpad) logs` + +|=== + [float] [[whats-the-right-app]] === What’s the right app for you? @@ -345,32 +351,6 @@ the <>. |See the full list of {kib} features |The https://www.elastic.co/kibana/features[{kib} features page on elastic.co] -2+| *Analyze and visualize your data* - -|Know what’s in your data -|<> - -|Create charts and other visualizations -|<> - -|Show your data from different perspectives -|<> - -|Work with location data -|<> - -|Create a presentation of your data -|<> - -|Generate models for your data’s behavior -|<> - -|Explore connections in your data -|<> - -|Share your data -|<>, <> - 2+|*Build a search experience* |Create a search experience for your workplace @@ -414,6 +394,32 @@ the <>. |View and manage hosts that are running Endpoint Security |{security-guide}/admin-page-ov.html[Administration] +2+| *Analyze and visualize your data* + +|Know what’s in your data +|<> + +|Create charts and other visualizations +|<> + +|Show your data from different perspectives +|<> + +|Work with location data +|<> + +|Create a presentation of your data +|<> + +|Generate models for your data’s behavior +|<> + +|Explore connections in your data +|<> + +|Share your data +|<>, <> + 2+|*Administer your Kibana instance* |Manage your Elasticsearch data @@ -435,7 +441,7 @@ the <>. [float] [[try-kibana]] -=== Getting help +=== How to get help Using our in-product guidance can help you get up and running, faster. Click the help icon image:images/intro-help-icon.png[Help icon in navigation bar] diff --git a/package.json b/package.json index 67f1f019e73a3e..a65c12fce46990 100644 --- a/package.json +++ b/package.json @@ -96,9 +96,9 @@ "@babel/core": "^7.12.10", "@babel/runtime": "^7.12.5", "@elastic/datemath": "link:packages/elastic-datemath", - "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary", + "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.2", "@elastic/ems-client": "7.12.0", - "@elastic/eui": "31.4.0", + "@elastic/eui": "31.7.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "^9.0.1-kibana3", "@elastic/node-crypto": "1.2.1", @@ -327,7 +327,6 @@ "wellknown": "^0.5.0", "whatwg-fetch": "^3.0.0", "xml2js": "^0.4.22", - "xregexp": "4.2.4", "yauzl": "^2.10.0" }, "devDependencies": { @@ -352,7 +351,7 @@ "@cypress/webpack-preprocessor": "^5.5.0", "@elastic/apm-rum": "^5.6.1", "@elastic/apm-rum-react": "^1.2.5", - "@elastic/charts": "24.6.0", + "@elastic/charts": "25.0.1", "@elastic/eslint-config-kibana": "link:packages/elastic-eslint-config-kibana", "@elastic/eslint-plugin-eui": "0.0.2", "@elastic/github-checks-reporter": "0.0.20b3", @@ -593,7 +592,7 @@ "babel-plugin-require-context-hook": "^1.0.0", "babel-plugin-styled-components": "^1.10.7", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", - "backport": "^5.6.4", + "backport": "^5.6.6", "base64-js": "^1.3.1", "base64url": "^3.0.1", "broadcast-channel": "^3.0.3", diff --git a/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap b/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap index 6351a227ff90b8..2801e0a0688cc6 100644 --- a/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap +++ b/packages/kbn-config/src/legacy/__snapshots__/legacy_object_to_config_adapter.test.ts.snap @@ -68,10 +68,10 @@ exports[`#get correctly handles silent logging config. 1`] = ` Object { "appenders": Object { "default": Object { - "kind": "legacy-appender", "legacyLoggingConfig": Object { "silent": true, }, + "type": "legacy-appender", }, }, "loggers": undefined, @@ -85,12 +85,12 @@ exports[`#get correctly handles verbose file logging config with json format. 1` Object { "appenders": Object { "default": Object { - "kind": "legacy-appender", "legacyLoggingConfig": Object { "dest": "/some/path.log", "json": true, "verbose": true, }, + "type": "legacy-appender", }, }, "loggers": undefined, diff --git a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts index 4d877a26b76418..8ec26ff1f8e71c 100644 --- a/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts +++ b/packages/kbn-config/src/legacy/legacy_object_to_config_adapter.ts @@ -44,7 +44,7 @@ export class LegacyObjectToConfigAdapter extends ObjectToConfigAdapter { const loggingConfig = { appenders: { ...appenders, - default: { kind: 'legacy-appender', legacyLoggingConfig }, + default: { type: 'legacy-appender', legacyLoggingConfig }, }, root: { level: 'info', ...root }, loggers, diff --git a/packages/kbn-monaco/src/painless/diagnostics_adapter.ts b/packages/kbn-monaco/src/painless/diagnostics_adapter.ts index fd08cc9c6b57a2..dc5f1ed95205cf 100644 --- a/packages/kbn-monaco/src/painless/diagnostics_adapter.ts +++ b/packages/kbn-monaco/src/painless/diagnostics_adapter.ts @@ -18,7 +18,12 @@ const toDiagnostics = (error: PainlessError): monaco.editor.IMarkerData => { }; }; +export interface SyntaxErrors { + [modelId: string]: PainlessError[]; +} export class DiagnosticsAdapter { + private errors: SyntaxErrors = {}; + constructor(private worker: WorkerAccessor) { const onModelAdd = (model: monaco.editor.IModel): void => { let handle: any; @@ -55,8 +60,16 @@ export class DiagnosticsAdapter { if (errorMarkers) { const model = monaco.editor.getModel(resource); + this.errors = { + ...this.errors, + [model!.id]: errorMarkers, + }; // Set the error markers and underline them with "Error" severity monaco.editor.setModelMarkers(model!, ID, errorMarkers.map(toDiagnostics)); } } + + public getSyntaxErrors() { + return this.errors; + } } diff --git a/packages/kbn-monaco/src/painless/index.ts b/packages/kbn-monaco/src/painless/index.ts index 5845186776b486..68582097564308 100644 --- a/packages/kbn-monaco/src/painless/index.ts +++ b/packages/kbn-monaco/src/painless/index.ts @@ -8,8 +8,14 @@ import { ID } from './constants'; import { lexerRules, languageConfiguration } from './lexer_rules'; -import { getSuggestionProvider } from './language'; +import { getSuggestionProvider, getSyntaxErrors } from './language'; -export const PainlessLang = { ID, getSuggestionProvider, lexerRules, languageConfiguration }; +export const PainlessLang = { + ID, + getSuggestionProvider, + lexerRules, + languageConfiguration, + getSyntaxErrors, +}; export { PainlessContext, PainlessAutocompleteField } from './types'; diff --git a/packages/kbn-monaco/src/painless/language.ts b/packages/kbn-monaco/src/painless/language.ts index 74199561bc3948..3cb26d970fc7d0 100644 --- a/packages/kbn-monaco/src/painless/language.ts +++ b/packages/kbn-monaco/src/painless/language.ts @@ -13,7 +13,7 @@ import { ID } from './constants'; import { PainlessContext, PainlessAutocompleteField } from './types'; import { PainlessWorker } from './worker'; import { PainlessCompletionAdapter } from './completion_adapter'; -import { DiagnosticsAdapter } from './diagnostics_adapter'; +import { DiagnosticsAdapter, SyntaxErrors } from './diagnostics_adapter'; const workerProxyService = new WorkerProxyService(); const editorStateService = new EditorStateService(); @@ -33,8 +33,15 @@ export const getSuggestionProvider = ( return new PainlessCompletionAdapter(worker, editorStateService); }; +let diagnosticsAdapter: DiagnosticsAdapter; + +// Returns syntax errors for all models by model id +export const getSyntaxErrors = (): SyntaxErrors => { + return diagnosticsAdapter.getSyntaxErrors(); +}; + monaco.languages.onLanguage(ID, async () => { workerProxyService.setup(); - new DiagnosticsAdapter(worker); + diagnosticsAdapter = new DiagnosticsAdapter(worker); }); diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 6d81b39df71135..1a157624d7a8ab 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -33,7 +33,7 @@ pageLoadAssetSize: home: 41661 indexLifecycleManagement: 107090 indexManagement: 140608 - indexPatternManagement: 154222 + indexPatternManagement: 28222 infra: 204800 fleet: 415829 ingestPipelines: 58003 @@ -103,6 +103,7 @@ pageLoadAssetSize: stackAlerts: 29684 presentationUtil: 28545 spacesOss: 18817 + indexPatternFieldEditor: 90489 osquery: 107090 fileUpload: 25664 banners: 17946 diff --git a/packages/kbn-storybook/lib/default_config.ts b/packages/kbn-storybook/lib/default_config.ts index 53c51e9cf29fe6..1b049761a3a985 100644 --- a/packages/kbn-storybook/lib/default_config.ts +++ b/packages/kbn-storybook/lib/default_config.ts @@ -14,4 +14,11 @@ export const defaultConfig: StorybookConfig = { typescript: { reactDocgen: false, }, + webpackFinal: (config, options) => { + if (process.env.CI) { + config.parallelism = 4; + config.cache = true; + } + return config; + }, }; diff --git a/packages/kbn-storybook/lib/templates/index.ejs b/packages/kbn-storybook/lib/templates/index.ejs index a4f8204c95d7a2..b193c87824d40f 100644 --- a/packages/kbn-storybook/lib/templates/index.ejs +++ b/packages/kbn-storybook/lib/templates/index.ejs @@ -16,12 +16,12 @@ - - - - + + + + <% if (typeof headHtmlSnippet !== 'undefined') { %> <%= headHtmlSnippet %> <% } %> <% diff --git a/renovate.json5 b/renovate.json5 index f1e773427a1034..f8fa3b916946f8 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -11,24 +11,10 @@ ], baseBranches: [ 'master', + '7.x', ], - labels: [ - 'release_note:skip', - 'Team:Operations', - 'renovate', - 'v8.0.0', - 'v7.11.0', - ], - major: { - labels: [ - 'release_note:skip', - 'Team:Operations', - 'renovate', - 'v8.0.0', - 'v7.11.0', - 'renovate:major', - ], - }, + prConcurrentLimit: 0, + prHourlyLimit: 0, separateMajorMinor: false, masterIssue: true, rangeStrategy: 'bump', @@ -51,12 +37,31 @@ groupName: '@elastic/charts', packageNames: ['@elastic/charts'], reviewers: ['markov00'], + matchBaseBranches: ['master'], + labels: ['release_note:skip', 'v8.0.0', 'v7.13.0'], + enabled: true, + }, + { + groupName: '@elastic/elasticsearch', + packageNames: ['@elastic/elasticsearch'], + reviewers: ['team:kibana-operations'], + matchBaseBranches: ['master'], + labels: ['release_note:skip', 'v8.0.0', 'Team:Operations', 'backport:skip'], + enabled: true, + }, + { + groupName: '@elastic/elasticsearch', + packageNames: ['@elastic/elasticsearch'], + reviewers: ['team:kibana-operations'], + matchBaseBranches: ['7.x'], + labels: ['release_note:skip', 'v7.13.0', 'Team:Operations', 'backport:skip'], enabled: true, }, { groupName: 'vega related modules', packageNames: ['vega', 'vega-lite', 'vega-schema-url-parser', 'vega-tooltip'], reviewers: ['team:kibana-app'], + matchBaseBranches: ['master'], labels: ['Feature:Vega', 'Team:KibanaApp'], enabled: true, }, diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 80e23a32ca5570..575a247ffeccb5 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -715,8 +715,11 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
Flyout content
"`; +exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"
Flyout content
"`; exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 1`] = ` Array [ @@ -59,4 +59,4 @@ Array [ ] `; -exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
Flyout content 2
"`; +exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
Flyout content 2
"`; diff --git a/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap b/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap index 7e79725c20307b..d52cc090d5d195 100644 --- a/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap +++ b/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap @@ -11,21 +11,19 @@ Array [ exports[`ModalService openConfirm() renders a mountpoint confirm message 1`] = ` Array [ Array [ - - - - - - - , + + + + + ,
, ], ] @@ -36,18 +34,16 @@ exports[`ModalService openConfirm() renders a mountpoint confirm message 2`] = ` exports[`ModalService openConfirm() renders a string confirm message 1`] = ` Array [ Array [ - - - - Some message - - - , + + + Some message + + ,
, ], ] @@ -58,33 +54,29 @@ exports[`ModalService openConfirm() renders a string confirm message 2`] = `" - - - confirm 1 - - - , + + + confirm 1 + + ,
, ], Array [ - - - - some confirm - - - , + + + some confirm + + ,
, ], ] @@ -93,33 +85,29 @@ Array [ exports[`ModalService openConfirm() with a currently active modal replaces the current modal with the new confirm 1`] = ` Array [ Array [ - - - - - - - , + + + + + ,
, ], Array [ - - - - some confirm - - - , + + + some confirm + + ,
, ], ] @@ -128,18 +116,16 @@ Array [ exports[`ModalService openModal() renders a modal to the DOM 1`] = ` Array [ Array [ - - - - - - - , + + + + + ,
, ], ] @@ -150,33 +136,29 @@ exports[`ModalService openModal() renders a modal to the DOM 2`] = `"
- - - confirm 1 - - - , + + + confirm 1 + + ,
, ], Array [ - - - - some confirm - - - , + + + some confirm + + ,
, ], ] @@ -185,33 +167,29 @@ Array [ exports[`ModalService openModal() with a currently active modal replaces the current modal with a new one 1`] = ` Array [ Array [ - - - - - - - , + + + + + ,
, ], Array [ - - - - - - - , + + + + + ,
, ], ] diff --git a/src/core/public/overlays/modal/modal_service.tsx b/src/core/public/overlays/modal/modal_service.tsx index 1f96e00fef0f89..7e4aee94c958ec 100644 --- a/src/core/public/overlays/modal/modal_service.tsx +++ b/src/core/public/overlays/modal/modal_service.tsx @@ -9,7 +9,7 @@ /* eslint-disable max-classes-per-file */ import { i18n as t } from '@kbn/i18n'; -import { EuiModal, EuiConfirmModal, EuiOverlayMask, EuiConfirmModalProps } from '@elastic/eui'; +import { EuiModal, EuiConfirmModal, EuiConfirmModalProps } from '@elastic/eui'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { Subject } from 'rxjs'; @@ -137,13 +137,11 @@ export class ModalService { this.activeModal = modal; render( - - - modal.close()}> - - - - , + + modal.close()}> + + + , targetDomElement ); @@ -199,11 +197,9 @@ export class ModalService { }; render( - - - - - , + + + , targetDomElement ); }); diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 2e23b26f636c8f..e29173d1495af7 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -523,6 +523,8 @@ export interface DocLinksStart { readonly base: string; }; readonly aggs: { + readonly composite: string; + readonly composite_missing_bucket: string; readonly date_histogram: string; readonly date_range: string; readonly date_format_pattern: string; diff --git a/src/core/server/core_usage_data/core_usage_data_service.ts b/src/core/server/core_usage_data/core_usage_data_service.ts index bd5f23b1c09bc7..e57d8d90a02dcc 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.ts @@ -226,7 +226,7 @@ export class CoreUsageDataService implements CoreService acc.add(a.kind), new Set()) + .reduce((acc, a) => acc.add(a.type), new Set()) .values() ), loggersConfiguredCount: this.loggingConfig?.loggers.length ?? 0, diff --git a/src/core/server/http/base_path_proxy_server.test.ts b/src/core/server/http/base_path_proxy_server.test.ts index 8f3a63058a8ae0..80c03a2af9031b 100644 --- a/src/core/server/http/base_path_proxy_server.test.ts +++ b/src/core/server/http/base_path_proxy_server.test.ts @@ -705,12 +705,8 @@ describe('BasePathProxyServer', () => { options: { body: { output: 'stream' } }, }, (_, req, res) => { - try { - expect(req.body).toBeInstanceOf(Readable); - return res.ok({ body: req.route.options.body }); - } catch (err) { - return res.internalError({ body: err.message }); - } + expect(req.body).toBeInstanceOf(Readable); + return res.ok({ body: req.route.options.body }); } ); registerRouter(router); @@ -740,15 +736,11 @@ describe('BasePathProxyServer', () => { }, }, (_, req, res) => { - try { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } catch (err) { - return res.internalError({ body: err.message }); - } + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); } ); registerRouter(router); @@ -779,15 +771,11 @@ describe('BasePathProxyServer', () => { }, }, (context, req, res) => { - try { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } catch (err) { - return res.internalError({ body: err.message }); - } + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); } ); registerRouter(router); @@ -815,15 +803,11 @@ describe('BasePathProxyServer', () => { }, }, (_, req, res) => { - try { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } catch (err) { - return res.internalError({ body: err.message }); - } + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); } ); registerRouter(router); @@ -851,15 +835,11 @@ describe('BasePathProxyServer', () => { }, }, (_, req, res) => { - try { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } catch (err) { - return res.internalError({ body: err.message }); - } + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); } ); registerRouter(router); diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts index 71452cce246dfa..52dab28accb33b 100644 --- a/src/core/server/http/http_server.mocks.ts +++ b/src/core/server/http/http_server.mocks.ts @@ -151,7 +151,6 @@ const createResponseFactoryMock = (): jest.Mocked => ({ forbidden: jest.fn(), notFound: jest.fn(), conflict: jest.fn(), - internalError: jest.fn(), customError: jest.fn(), }); @@ -162,7 +161,6 @@ const createLifecycleResponseFactoryMock = (): jest.Mocked { options: { body: { parse: false } }, }, (context, req, res) => { - try { - expect(req.body).toBeInstanceOf(Buffer); - expect(req.body.toString()).toBe(JSON.stringify({ test: 1 })); - return res.ok({ body: req.route.options.body }); - } catch (err) { - return res.internalError({ body: err.message }); - } + expect(req.body).toBeInstanceOf(Buffer); + expect(req.body.toString()).toBe(JSON.stringify({ test: 1 })); + return res.ok({ body: req.route.options.body }); } ); registerRouter(router); @@ -1053,15 +1049,11 @@ describe('timeout options', () => { }, }, (context, req, res) => { - try { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } catch (err) { - return res.internalError({ body: err.message }); - } + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); } ); registerRouter(router); @@ -1091,15 +1083,11 @@ describe('timeout options', () => { }, }, (context, req, res) => { - try { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } catch (err) { - return res.internalError({ body: err.message }); - } + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); } ); registerRouter(router); @@ -1128,15 +1116,11 @@ describe('timeout options', () => { }, }, (context, req, res) => { - try { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } catch (err) { - return res.internalError({ body: err.message }); - } + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); } ); registerRouter(router); @@ -1165,15 +1149,11 @@ describe('timeout options', () => { }, }, (context, req, res) => { - try { - return res.ok({ - body: { - timeout: req.route.options.timeout, - }, - }); - } catch (err) { - return res.internalError({ body: err.message }); - } + return res.ok({ + body: { + timeout: req.route.options.timeout, + }, + }); } ); registerRouter(router); @@ -1294,12 +1274,8 @@ test('should return a stream in the body', async () => { options: { body: { output: 'stream' } }, }, (context, req, res) => { - try { - expect(req.body).toBeInstanceOf(Readable); - return res.ok({ body: req.route.options.body }); - } catch (err) { - return res.internalError({ body: err.message }); - } + expect(req.body).toBeInstanceOf(Readable); + return res.ok({ body: req.route.options.body }); } ); registerRouter(router); diff --git a/src/core/server/http/integration_tests/logging.test.ts b/src/core/server/http/integration_tests/logging.test.ts index ba265c1ff61bc2..fcf2cd2ba3372d 100644 --- a/src/core/server/http/integration_tests/logging.test.ts +++ b/src/core/server/http/integration_tests/logging.test.ts @@ -50,16 +50,16 @@ describe('request logging', () => { silent: true, appenders: { 'test-console': { - kind: 'console', + type: 'console', layout: { - kind: 'pattern', + type: 'pattern', pattern: '%level|%logger|%message|%meta', }, }, }, loggers: [ { - context: 'http.server.response', + name: 'http.server.response', appenders: ['test-console'], level: 'debug', }, @@ -96,16 +96,16 @@ describe('request logging', () => { silent: true, appenders: { 'test-console': { - kind: 'console', + type: 'console', layout: { - kind: 'pattern', + type: 'pattern', pattern: '%level|%logger|%message|%meta', }, }, }, loggers: [ { - context: 'http.server.response', + name: 'http.server.response', appenders: ['test-console'], level: 'debug', }, diff --git a/src/core/server/http/integration_tests/router.test.ts b/src/core/server/http/integration_tests/router.test.ts index c9c4410171b34a..6c54067435405a 100644 --- a/src/core/server/http/integration_tests/router.test.ts +++ b/src/core/server/http/integration_tests/router.test.ts @@ -1672,7 +1672,11 @@ describe('Response factory', () => { const result = await supertest(innerServer.listener).get('/').expect(500); - expect(result.body.message).toBe('reason'); + expect(result.body).toEqual({ + error: 'Internal Server Error', + message: 'reason', + statusCode: 500, + }); expect(loggingSystemMock.collect(logger).error).toHaveLength(0); }); diff --git a/src/core/server/http/router/response.ts b/src/core/server/http/router/response.ts index f4e09fa1cc08ea..e2babf719f67e0 100644 --- a/src/core/server/http/router/response.ts +++ b/src/core/server/http/router/response.ts @@ -177,15 +177,6 @@ const errorResponseFactory = { conflict: (options: ErrorHttpResponseOptions = {}) => new KibanaResponse(409, options.body || 'Conflict', options), - // Server error - /** - * The server encountered an unexpected condition that prevented it from fulfilling the request. - * Status code: `500`. - * @param options - {@link HttpResponseOptions} configures HTTP response headers, error message and other error details to pass to the client - */ - internalError: (options: ErrorHttpResponseOptions = {}) => - new KibanaResponse(500, options.body || 'Internal Error', options), - /** * Creates an error response with defined status code and payload. * @param options - {@link CustomHttpResponseOptions} configures HTTP response headers, error message and other error details to pass to the client diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index acce209751e325..85eab7c0892e88 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -314,6 +314,8 @@ type RequestHandlerEnhanced = WithoutHeadAr /** * A function executed when route path matched requested resource path. * Request handler is expected to return a result of one of {@link KibanaResponseFactory} functions. + * If anything else is returned, or an error is thrown, the HTTP service will automatically log the error + * and respond `500 - Internal Server Error`. * @param context {@link RequestHandlerContext} - the core context exposed for this request. * @param request {@link KibanaRequest} - object containing information about requested resource, * such as path, method, headers, parameters, query, body, etc. diff --git a/src/core/server/legacy/integration_tests/logging.test.ts b/src/core/server/legacy/integration_tests/logging.test.ts index 321eb81708f1e7..88c45962ce4a68 100644 --- a/src/core/server/legacy/integration_tests/logging.test.ts +++ b/src/core/server/legacy/integration_tests/logging.test.ts @@ -29,16 +29,16 @@ function createRoot(legacyLoggingConfig: LegacyLoggingConfig = {}) { // platform config appenders: { 'test-console': { - kind: 'console', + type: 'console', layout: { highlight: false, - kind: 'pattern', + type: 'pattern', }, }, }, loggers: [ { - context: 'test-file', + name: 'test-file', appenders: ['test-console'], level: 'info', }, diff --git a/src/core/server/legacy/logging/appenders/legacy_appender.test.ts b/src/core/server/legacy/logging/appenders/legacy_appender.test.ts index 1b76b6748a5bb6..9213403d72d07a 100644 --- a/src/core/server/legacy/logging/appenders/legacy_appender.test.ts +++ b/src/core/server/legacy/logging/appenders/legacy_appender.test.ts @@ -16,13 +16,13 @@ afterEach(() => (LegacyLoggingServer as any).mockClear()); test('`configSchema` creates correct schema.', () => { const appenderSchema = LegacyAppender.configSchema; - const validConfig = { kind: 'legacy-appender', legacyLoggingConfig: { verbose: true } }; + const validConfig = { type: 'legacy-appender', legacyLoggingConfig: { verbose: true } }; expect(appenderSchema.validate(validConfig)).toEqual({ - kind: 'legacy-appender', + type: 'legacy-appender', legacyLoggingConfig: { verbose: true }, }); - const wrongConfig = { kind: 'not-legacy-appender' }; + const wrongConfig = { type: 'not-legacy-appender' }; expect(() => appenderSchema.validate(wrongConfig)).toThrow(); }); diff --git a/src/core/server/legacy/logging/appenders/legacy_appender.ts b/src/core/server/legacy/logging/appenders/legacy_appender.ts index 83e43999eeebf1..a89441a5671b55 100644 --- a/src/core/server/legacy/logging/appenders/legacy_appender.ts +++ b/src/core/server/legacy/logging/appenders/legacy_appender.ts @@ -12,7 +12,7 @@ import { DisposableAppender, LogRecord } from '@kbn/logging'; import { LegacyVars } from '../../types'; export interface LegacyAppenderConfig { - kind: 'legacy-appender'; + type: 'legacy-appender'; legacyLoggingConfig?: any; } @@ -22,7 +22,7 @@ export interface LegacyAppenderConfig { */ export class LegacyAppender implements DisposableAppender { public static configSchema = schema.object({ - kind: schema.literal('legacy-appender'), + type: schema.literal('legacy-appender'), legacyLoggingConfig: schema.any(), }); diff --git a/src/core/server/logging/README.md b/src/core/server/logging/README.md index 9e3da1f3e0d715..385d1fd91a5d75 100644 --- a/src/core/server/logging/README.md +++ b/src/core/server/logging/README.md @@ -24,7 +24,7 @@ Kibana logging system has three main components: _loggers_, _appenders_ and _lay messages according to message type and level, and to control how these messages are formatted and where the final logs will be displayed or stored. -__Loggers__ define what logging settings should be applied at the particular context. +__Loggers__ define what logging settings should be applied at the particular context name. __Appenders__ define where log messages are displayed (eg. stdout or console) and stored (eg. file on the disk). @@ -33,17 +33,17 @@ __Layouts__ define how log messages are formatted and what type of information t ## Logger hierarchy -Every logger has its unique name or context that follows hierarchical naming rule. The logger is considered to be an +Every logger has its unique context name that follows hierarchical naming rule. The logger is considered to be an ancestor of another logger if its name followed by a `.` is a prefix of the descendant logger name. For example logger -with `a.b` context is an ancestor of logger with `a.b.c` context. All top-level loggers are descendants of special -logger with `root` context that resides at the top of the logger hierarchy. This logger always exists and +with `a.b` context name is an ancestor of logger with `a.b.c` context name. All top-level loggers are descendants of special +logger with `root` context name that resides at the top of the logger hierarchy. This logger always exists and fully configured. -Developer can configure _log level_ and _appenders_ that should be used within particular context. If logger configuration +Developer can configure _log level_ and _appenders_ that should be used within particular context name. If logger configuration specifies only _log level_ then _appenders_ configuration will be inherited from the ancestor logger. __Note:__ in the current implementation log messages are only forwarded to appenders configured for a particular logger -context or to appenders of the closest ancestor if current logger doesn't have any appenders configured. That means that +context name or to appenders of the closest ancestor if current logger doesn't have any appenders configured. That means that we __don't support__ so called _appender additivity_ when log messages are forwarded to _every_ distinct appender within ancestor chain including `root`. @@ -55,7 +55,7 @@ A log record is being logged by the logger if its level is higher than or equal the log record is ignored. The _all_ and _off_ levels can be used only in configuration and are just handy shortcuts that allow developer to log every -log record or disable logging entirely for the specific context. +log record or disable logging entirely for the specific context name. ## Layouts @@ -129,7 +129,7 @@ Example of `%date` output: Outputs the process ID. ### JSON layout -With `json` layout log messages will be formatted as JSON strings that include timestamp, log level, context, message +With `json` layout log messages will be formatted as JSON strings that include timestamp, log level, context name, message text and any other metadata that may be associated with the log message itself. ## Appenders @@ -153,15 +153,15 @@ This policy will rotate the file when it reaches a predetermined size. logging: appenders: rolling-file: - kind: rolling-file - path: /var/logs/kibana.log + type: rolling-file + fileName: /var/logs/kibana.log policy: - kind: size-limit + type: size-limit size: 50mb strategy: //... layout: - kind: pattern + type: pattern ``` The options are: @@ -180,16 +180,16 @@ This policy will rotate the file every given interval of time. logging: appenders: rolling-file: - kind: rolling-file - path: /var/logs/kibana.log + type: rolling-file + fileName: /var/logs/kibana.log policy: - kind: time-interval + type: time-interval interval: 10s modulate: true strategy: //... layout: - kind: pattern + type: pattern ``` The options are: @@ -225,16 +225,16 @@ and will retains a fixed amount of rolled files. logging: appenders: rolling-file: - kind: rolling-file - path: /var/logs/kibana.log + type: rolling-file + fileName: /var/logs/kibana.log policy: // ... strategy: - kind: numeric + type: numeric pattern: '-%i' max: 2 layout: - kind: pattern + type: pattern ``` For example, with this configuration: @@ -253,7 +253,7 @@ The options are: The suffix to append to the file path when rolling. Must include `%i`, as this is the value that will be converted to the file index. -for example, with `path: /var/logs/kibana.log` and `pattern: '-%i'`, the created rolling files +for example, with `fileName: /var/logs/kibana.log` and `pattern: '-%i'`, the created rolling files will be `/var/logs/kibana-1.log`, `/var/logs/kibana-2.log`, and so on. The default value is `-%i` @@ -278,49 +278,49 @@ Here is the configuration example that can be used to configure _loggers_, _appe logging: appenders: console: - kind: console + type: console layout: - kind: pattern + type: pattern highlight: true file: - kind: file - path: /var/log/kibana.log + type: file + fileName: /var/log/kibana.log layout: - kind: pattern + type: pattern custom: - kind: console + type: console layout: - kind: pattern + type: pattern pattern: "[%date][%level] %message" json-file-appender: - kind: file - path: /var/log/kibana-json.log + type: file + fileName: /var/log/kibana-json.log root: appenders: [console, file] level: error loggers: - - context: plugins + - name: plugins appenders: [custom] level: warn - - context: plugins.myPlugin + - name: plugins.myPlugin level: info - - context: server + - name: server level: fatal - - context: optimize + - name: optimize appenders: [console] - - context: telemetry + - name: telemetry level: all appenders: [json-file-appender] - - context: metrics.ops + - name: metrics.ops level: debug appenders: [console] ``` Here is what we get with the config above: -| Context | Appenders | Level | +| Context name | Appenders | Level | | ---------------- |:------------------------:| -----:| | root | console, file | error | | plugins | custom | warn | @@ -331,7 +331,7 @@ Here is what we get with the config above: | metrics.ops | console | debug | -The `root` logger has a dedicated configuration node since this context is special and should always exist. By +The `root` logger has a dedicated configuration node since this context name is special and should always exist. By default `root` is configured with `info` level and `default` appender that is also always available. This is the configuration that all custom loggers will use unless they're re-configured explicitly. @@ -391,7 +391,7 @@ The message contains some high-level information, and the corresponding log meta ## Usage -Usage is very straightforward, one should just get a logger for a specific context and use it to log messages with +Usage is very straightforward, one should just get a logger for a specific context name and use it to log messages with different log level. ```typescript @@ -409,7 +409,7 @@ loggerWithNestedContext.trace('Message with `trace` log level.'); loggerWithNestedContext.debug('Message with `debug` log level.'); ``` -And assuming logger for `server` context with `console` appender and `trace` level was used, console output will look like this: +And assuming logger for `server` name with `console` appender and `trace` level was used, console output will look like this: ```bash [2017-07-25T11:54:41.639-07:00][TRACE][server] Message with `trace` log level. [2017-07-25T11:54:41.639-07:00][DEBUG][server] Message with `debug` log level. @@ -422,7 +422,7 @@ And assuming logger for `server` context with `console` appender and `trace` lev [2017-07-25T11:54:41.639-07:00][DEBUG][server.http] Message with `debug` log level. ``` -The log will be less verbose with `warn` level for the `server` context: +The log will be less verbose with `warn` level for the `server` context name: ```bash [2017-07-25T11:54:41.639-07:00][WARN ][server] Message with `warn` log level. [2017-07-25T11:54:41.639-07:00][ERROR][server] Message with `error` log level. @@ -433,7 +433,7 @@ The log will be less verbose with `warn` level for the `server` context: Compatibility with the legacy logging system is assured until the end of the `v7` version. All log messages handled by `root` context are forwarded to the legacy logging service. If you re-write root appenders, make sure that it contains `default` appender to provide backward compatibility. -**Note**: If you define an appender for a context, the log messages aren't handled by the +**Note**: If you define an appender for a context name, the log messages aren't handled by the `root` context anymore and not forwarded to the legacy logging service. #### logging.dest @@ -442,21 +442,21 @@ define a custom one. ```yaml logging: loggers: - - context: plugins.myPlugin + - name: plugins.myPlugin appenders: [console] ``` -Logs in a *file* if given file path. You should define a custom appender with `kind: file` +Logs in a *file* if given file path. You should define a custom appender with `type: file` ```yaml logging: appenders: file: - kind: file - path: /var/log/kibana.log + type: file + fileName: /var/log/kibana.log layout: - kind: pattern + type: pattern loggers: - - context: plugins.myPlugin + - name: plugins.myPlugin appenders: [file] ``` #### logging.json @@ -468,7 +468,7 @@ Suppresses all logging output other than error messages. With new logging, confi with adjusting minimum required [logging level](#log-level). ```yaml loggers: - - context: plugins.myPlugin + - name: plugins.myPlugin appenders: [console] level: error # or for all output @@ -494,32 +494,32 @@ to [specify timezone](#date) for `layout: pattern`. Defaults to host timezone wh logging: appenders: custom-console: - kind: console + type: console layout: - kind: pattern + type: pattern highlight: true pattern: "[%level] [%date{ISO8601_TZ}{America/Los_Angeles}][%logger] %message" ``` #### logging.events -Define a custom logger for a specific context. +Define a custom logger for a specific context name. **`logging.events.ops`** outputs sample system and process information at a regular interval. -With the new logging config, these are provided by a dedicated [context](#logger-hierarchy), +With the new logging config, these are provided by a dedicated [context name](#logger-hierarchy), and you can enable them by adjusting the minimum required [logging level](#log-level) to `debug`: ```yaml loggers: - - context: metrics.ops + - name: metrics.ops appenders: [console] level: debug ``` **`logging.events.request` and `logging.events.response`** provide logs for each request handled -by the http service. With the new logging config, these are provided by a dedicated [context](#logger-hierarchy), +by the http service. With the new logging config, these are provided by a dedicated [context name](#logger-hierarchy), and you can enable them by adjusting the minimum required [logging level](#log-level) to `debug`: ```yaml loggers: - - context: http.server.response + - name: http.server.response appenders: [console] level: debug ``` @@ -532,7 +532,7 @@ TBD | Parameter | Platform log record in **pattern** format | Legacy Platform log record **text** format | | --------------- | ------------------------------------------ | ------------------------------------------ | | @timestamp | ISO8601_TZ `2012-01-31T23:33:22.011-05:00` | Absolute `23:33:22.011` | -| context | `parent.child` | `['parent', 'child']` | +| context name | `parent.child` | `['parent', 'child']` | | level | `DEBUG` | `['debug']` | | meta | stringified JSON object `{"to": "v8"}` | N/A | | pid | can be configured as `%pid` | N/A | @@ -540,9 +540,9 @@ TBD | Parameter | Platform log record in **json** format | Legacy Platform log record **json** format | | --------------- | ------------------------------------------ | -------------------------------------------- | | @timestamp | ISO8601_TZ `2012-01-31T23:33:22.011-05:00` | ISO8601 `2012-01-31T23:33:22.011Z` | -| context | `context: parent.child` | `tags: ['parent', 'child']` | -| level | `level: DEBUG` | `tags: ['debug']` | +| context name | `log.logger: parent.child` | `tags: ['parent', 'child']` | +| level | `log.level: DEBUG` | `tags: ['debug']` | | meta | separate property `"meta": {"to": "v8"}` | merged in log record `{... "to": "v8"}` | -| pid | `pid: 12345` | `pid: 12345` | +| pid | `process.pid: 12345` | `pid: 12345` | | type | N/A | `type: log` | | error | `{ message, name, stack }` | `{ message, name, stack, code, signal }` | diff --git a/src/core/server/logging/__snapshots__/logging_system.test.ts.snap b/src/core/server/logging/__snapshots__/logging_system.test.ts.snap index 8013aec4a06fd3..81321a3b1fe44c 100644 --- a/src/core/server/logging/__snapshots__/logging_system.test.ts.snap +++ b/src/core/server/logging/__snapshots__/logging_system.test.ts.snap @@ -84,7 +84,7 @@ Object { } `; -exports[`uses \`root\` logger if context is not specified. 1`] = ` +exports[`uses \`root\` logger if context name is not specified. 1`] = ` Array [ Array [ "[2012-01-31T03:33:22.011-05:00][INFO ][root] This message goes to a root context.", diff --git a/src/core/server/logging/appenders/appenders.test.mocks.ts b/src/core/server/logging/appenders/appenders.test.mocks.ts index 85a86ff9306c7b..1427cd7220de71 100644 --- a/src/core/server/logging/appenders/appenders.test.mocks.ts +++ b/src/core/server/logging/appenders/appenders.test.mocks.ts @@ -12,7 +12,7 @@ jest.mock('../layouts/layouts', () => { const { schema } = require('@kbn/config-schema'); return { Layouts: { - configSchema: schema.object({ kind: schema.literal('mock') }), + configSchema: schema.object({ type: schema.literal('mock') }), create: mockCreateLayout, }, }; diff --git a/src/core/server/logging/appenders/appenders.test.ts b/src/core/server/logging/appenders/appenders.test.ts index 8e1c18ae3ded66..bd32e4061049bd 100644 --- a/src/core/server/logging/appenders/appenders.test.ts +++ b/src/core/server/logging/appenders/appenders.test.ts @@ -21,33 +21,33 @@ beforeEach(() => { test('`configSchema` creates correct schema.', () => { const appendersSchema = Appenders.configSchema; - const validConfig1 = { kind: 'file', layout: { kind: 'mock' }, path: 'path' }; + const validConfig1 = { type: 'file', layout: { type: 'mock' }, fileName: 'path' }; expect(appendersSchema.validate(validConfig1)).toEqual({ - kind: 'file', - layout: { kind: 'mock' }, - path: 'path', + type: 'file', + layout: { type: 'mock' }, + fileName: 'path', }); - const validConfig2 = { kind: 'console', layout: { kind: 'mock' } }; + const validConfig2 = { type: 'console', layout: { type: 'mock' } }; expect(appendersSchema.validate(validConfig2)).toEqual({ - kind: 'console', - layout: { kind: 'mock' }, + type: 'console', + layout: { type: 'mock' }, }); const wrongConfig1 = { - kind: 'console', - layout: { kind: 'mock' }, - path: 'path', + type: 'console', + layout: { type: 'mock' }, + fileName: 'path', }; expect(() => appendersSchema.validate(wrongConfig1)).toThrow(); - const wrongConfig2 = { kind: 'file', layout: { kind: 'mock' } }; + const wrongConfig2 = { type: 'file', layout: { type: 'mock' } }; expect(() => appendersSchema.validate(wrongConfig2)).toThrow(); const wrongConfig3 = { - kind: 'console', - layout: { kind: 'mock' }, - path: 'path', + type: 'console', + layout: { type: 'mock' }, + fileName: 'path', }; expect(() => appendersSchema.validate(wrongConfig3)).toThrow(); }); @@ -56,31 +56,31 @@ test('`create()` creates correct appender.', () => { mockCreateLayout.mockReturnValue({ format: () => '' }); const consoleAppender = Appenders.create({ - kind: 'console', - layout: { highlight: true, kind: 'pattern', pattern: '' }, + type: 'console', + layout: { highlight: true, type: 'pattern', pattern: '' }, }); expect(consoleAppender).toBeInstanceOf(ConsoleAppender); const fileAppender = Appenders.create({ - kind: 'file', - layout: { highlight: true, kind: 'pattern', pattern: '' }, - path: 'path', + type: 'file', + layout: { highlight: true, type: 'pattern', pattern: '' }, + fileName: 'path', }); expect(fileAppender).toBeInstanceOf(FileAppender); const legacyAppender = Appenders.create({ - kind: 'legacy-appender', + type: 'legacy-appender', legacyLoggingConfig: { verbose: true }, }); expect(legacyAppender).toBeInstanceOf(LegacyAppender); const rollingFileAppender = Appenders.create({ - kind: 'rolling-file', - path: 'path', - layout: { highlight: true, kind: 'pattern', pattern: '' }, - strategy: { kind: 'numeric', max: 5, pattern: '%i' }, - policy: { kind: 'size-limit', size: ByteSizeValue.parse('15b') }, + type: 'rolling-file', + fileName: 'path', + layout: { highlight: true, type: 'pattern', pattern: '' }, + strategy: { type: 'numeric', max: 5, pattern: '%i' }, + policy: { type: 'size-limit', size: ByteSizeValue.parse('15b') }, }); expect(rollingFileAppender).toBeInstanceOf(RollingFileAppender); }); diff --git a/src/core/server/logging/appenders/appenders.ts b/src/core/server/logging/appenders/appenders.ts index 564def5251c132..a41a6a2f68fa1b 100644 --- a/src/core/server/logging/appenders/appenders.ts +++ b/src/core/server/logging/appenders/appenders.ts @@ -52,11 +52,11 @@ export class Appenders { * @returns Fully constructed `Appender` instance. */ public static create(config: AppenderConfigType): DisposableAppender { - switch (config.kind) { + switch (config.type) { case 'console': return new ConsoleAppender(Layouts.create(config.layout)); case 'file': - return new FileAppender(Layouts.create(config.layout), config.path); + return new FileAppender(Layouts.create(config.layout), config.fileName); case 'rolling-file': return new RollingFileAppender(config); case 'legacy-appender': diff --git a/src/core/server/logging/appenders/console/console_appender.test.ts b/src/core/server/logging/appenders/console/console_appender.test.ts index f5ad853775eea3..1e8f742c1ecda6 100644 --- a/src/core/server/logging/appenders/console/console_appender.test.ts +++ b/src/core/server/logging/appenders/console/console_appender.test.ts @@ -12,7 +12,7 @@ jest.mock('../../layouts/layouts', () => { return { Layouts: { configSchema: schema.object({ - kind: schema.literal('mock'), + type: schema.literal('mock'), }), }, }; @@ -23,16 +23,16 @@ import { ConsoleAppender } from './console_appender'; test('`configSchema` creates correct schema.', () => { const appenderSchema = ConsoleAppender.configSchema; - const validConfig = { kind: 'console', layout: { kind: 'mock' } }; + const validConfig = { type: 'console', layout: { type: 'mock' } }; expect(appenderSchema.validate(validConfig)).toEqual({ - kind: 'console', - layout: { kind: 'mock' }, + type: 'console', + layout: { type: 'mock' }, }); - const wrongConfig1 = { kind: 'not-console', layout: { kind: 'mock' } }; + const wrongConfig1 = { type: 'not-console', layout: { type: 'mock' } }; expect(() => appenderSchema.validate(wrongConfig1)).toThrow(); - const wrongConfig2 = { kind: 'file', layout: { kind: 'mock' }, path: 'path' }; + const wrongConfig2 = { type: 'file', layout: { type: 'mock' }, fileName: 'path' }; expect(() => appenderSchema.validate(wrongConfig2)).toThrow(); }); diff --git a/src/core/server/logging/appenders/console/console_appender.ts b/src/core/server/logging/appenders/console/console_appender.ts index 00d26d0836ee38..739068ff0a126c 100644 --- a/src/core/server/logging/appenders/console/console_appender.ts +++ b/src/core/server/logging/appenders/console/console_appender.ts @@ -13,7 +13,7 @@ import { Layouts, LayoutConfigType } from '../../layouts/layouts'; const { literal, object } = schema; export interface ConsoleAppenderConfig { - kind: 'console'; + type: 'console'; layout: LayoutConfigType; } @@ -24,7 +24,7 @@ export interface ConsoleAppenderConfig { */ export class ConsoleAppender implements DisposableAppender { public static configSchema = object({ - kind: literal('console'), + type: literal('console'), layout: Layouts.configSchema, }); diff --git a/src/core/server/logging/appenders/file/file_appender.test.mocks.ts b/src/core/server/logging/appenders/file/file_appender.test.mocks.ts index 0f87829dbbaf16..2c2a2015b6fd38 100644 --- a/src/core/server/logging/appenders/file/file_appender.test.mocks.ts +++ b/src/core/server/logging/appenders/file/file_appender.test.mocks.ts @@ -12,7 +12,7 @@ jest.mock('../../layouts/layouts', () => { return { Layouts: { configSchema: schema.object({ - kind: schema.literal('mock'), + type: schema.literal('mock'), }), }, }; diff --git a/src/core/server/logging/appenders/file/file_appender.test.ts b/src/core/server/logging/appenders/file/file_appender.test.ts index 5ef91b98e92f42..081cb16afd2ff3 100644 --- a/src/core/server/logging/appenders/file/file_appender.test.ts +++ b/src/core/server/logging/appenders/file/file_appender.test.ts @@ -20,24 +20,24 @@ beforeEach(() => { test('`createConfigSchema()` creates correct schema.', () => { const appenderSchema = FileAppender.configSchema; - const validConfig = { kind: 'file', layout: { kind: 'mock' }, path: 'path' }; + const validConfig = { type: 'file', layout: { type: 'mock' }, fileName: 'path' }; expect(appenderSchema.validate(validConfig)).toEqual({ - kind: 'file', - layout: { kind: 'mock' }, - path: 'path', + type: 'file', + layout: { type: 'mock' }, + fileName: 'path', }); const wrongConfig1 = { - kind: 'not-file', - layout: { kind: 'mock' }, - path: 'path', + type: 'not-file', + layout: { type: 'mock' }, + fileName: 'path', }; expect(() => appenderSchema.validate(wrongConfig1)).toThrow(); - const wrongConfig2 = { kind: 'file', layout: { kind: 'mock' } }; + const wrongConfig2 = { type: 'file', layout: { type: 'mock' } }; expect(() => appenderSchema.validate(wrongConfig2)).toThrow(); - const wrongConfig3 = { kind: 'console', layout: { kind: 'mock' } }; + const wrongConfig3 = { type: 'console', layout: { type: 'mock' } }; expect(() => appenderSchema.validate(wrongConfig3)).toThrow(); }); diff --git a/src/core/server/logging/appenders/file/file_appender.ts b/src/core/server/logging/appenders/file/file_appender.ts index 0f1cb71c76e9fb..be46c261dc9965 100644 --- a/src/core/server/logging/appenders/file/file_appender.ts +++ b/src/core/server/logging/appenders/file/file_appender.ts @@ -13,9 +13,9 @@ import { createWriteStream, WriteStream } from 'fs'; import { Layouts, LayoutConfigType } from '../../layouts/layouts'; export interface FileAppenderConfig { - kind: 'file'; + type: 'file'; layout: LayoutConfigType; - path: string; + fileName: string; } /** @@ -24,9 +24,9 @@ export interface FileAppenderConfig { */ export class FileAppender implements DisposableAppender { public static configSchema = schema.object({ - kind: schema.literal('file'), + type: schema.literal('file'), layout: Layouts.configSchema, - path: schema.string(), + fileName: schema.string(), }); /** diff --git a/src/core/server/logging/appenders/rolling_file/policies/index.ts b/src/core/server/logging/appenders/rolling_file/policies/index.ts index 20038d31eee8bf..e3e33c6cbfdef6 100644 --- a/src/core/server/logging/appenders/rolling_file/policies/index.ts +++ b/src/core/server/logging/appenders/rolling_file/policies/index.ts @@ -34,7 +34,7 @@ export type TriggeringPolicyConfig = | TimeIntervalTriggeringPolicyConfig; const defaultPolicy: TimeIntervalTriggeringPolicyConfig = { - kind: 'time-interval', + type: 'time-interval', interval: moment.duration(24, 'hour'), modulate: true, }; @@ -48,7 +48,7 @@ export const createTriggeringPolicy = ( config: TriggeringPolicyConfig, context: RollingFileContext ): TriggeringPolicy => { - switch (config.kind) { + switch (config.type) { case 'size-limit': return new SizeLimitTriggeringPolicy(config, context); case 'time-interval': diff --git a/src/core/server/logging/appenders/rolling_file/policies/size_limit/size_limit_policy.test.ts b/src/core/server/logging/appenders/rolling_file/policies/size_limit/size_limit_policy.test.ts index 3780bb69a8341a..ee9c96de8a940c 100644 --- a/src/core/server/logging/appenders/rolling_file/policies/size_limit/size_limit_policy.test.ts +++ b/src/core/server/logging/appenders/rolling_file/policies/size_limit/size_limit_policy.test.ts @@ -15,7 +15,7 @@ describe('SizeLimitTriggeringPolicy', () => { let context: RollingFileContext; const createPolicy = (size: ByteSizeValue) => - new SizeLimitTriggeringPolicy({ kind: 'size-limit', size }, context); + new SizeLimitTriggeringPolicy({ type: 'size-limit', size }, context); const createLogRecord = (parts: Partial = {}): LogRecord => ({ timestamp: new Date(), diff --git a/src/core/server/logging/appenders/rolling_file/policies/size_limit/size_limit_policy.ts b/src/core/server/logging/appenders/rolling_file/policies/size_limit/size_limit_policy.ts index 77f0a60b0e95c8..82fee352da8df7 100644 --- a/src/core/server/logging/appenders/rolling_file/policies/size_limit/size_limit_policy.ts +++ b/src/core/server/logging/appenders/rolling_file/policies/size_limit/size_limit_policy.ts @@ -12,7 +12,7 @@ import { RollingFileContext } from '../../rolling_file_context'; import { TriggeringPolicy } from '../policy'; export interface SizeLimitTriggeringPolicyConfig { - kind: 'size-limit'; + type: 'size-limit'; /** * The minimum size the file must have to roll over. @@ -21,7 +21,7 @@ export interface SizeLimitTriggeringPolicyConfig { } export const sizeLimitTriggeringPolicyConfigSchema = schema.object({ - kind: schema.literal('size-limit'), + type: schema.literal('size-limit'), size: schema.byteSize({ min: '1b', defaultValue: '100mb' }), }); diff --git a/src/core/server/logging/appenders/rolling_file/policies/time_interval/time_interval_policy.test.ts b/src/core/server/logging/appenders/rolling_file/policies/time_interval/time_interval_policy.test.ts index 25c5cef65c8851..03f457277b7926 100644 --- a/src/core/server/logging/appenders/rolling_file/policies/time_interval/time_interval_policy.test.ts +++ b/src/core/server/logging/appenders/rolling_file/policies/time_interval/time_interval_policy.test.ts @@ -42,7 +42,7 @@ describe('TimeIntervalTriggeringPolicy', () => { interval: string = '15m', modulate: boolean = false ): TimeIntervalTriggeringPolicyConfig => ({ - kind: 'time-interval', + type: 'time-interval', interval: schema.duration().validate(interval), modulate, }); diff --git a/src/core/server/logging/appenders/rolling_file/policies/time_interval/time_interval_policy.ts b/src/core/server/logging/appenders/rolling_file/policies/time_interval/time_interval_policy.ts index 892dd54672f146..7c4d18d929cb0d 100644 --- a/src/core/server/logging/appenders/rolling_file/policies/time_interval/time_interval_policy.ts +++ b/src/core/server/logging/appenders/rolling_file/policies/time_interval/time_interval_policy.ts @@ -15,7 +15,7 @@ import { getNextRollingTime } from './get_next_rolling_time'; import { isValidRolloverInterval } from './utils'; export interface TimeIntervalTriggeringPolicyConfig { - kind: 'time-interval'; + type: 'time-interval'; /** * How often a rollover should occur. @@ -38,7 +38,7 @@ export interface TimeIntervalTriggeringPolicyConfig { } export const timeIntervalTriggeringPolicyConfigSchema = schema.object({ - kind: schema.literal('time-interval'), + type: schema.literal('time-interval'), interval: schema.duration({ defaultValue: '24h', validate: (interval) => { diff --git a/src/core/server/logging/appenders/rolling_file/rolling_file_appender.test.ts b/src/core/server/logging/appenders/rolling_file/rolling_file_appender.test.ts index bc28e9137b2fd7..a95d995885d8b2 100644 --- a/src/core/server/logging/appenders/rolling_file/rolling_file_appender.test.ts +++ b/src/core/server/logging/appenders/rolling_file/rolling_file_appender.test.ts @@ -20,20 +20,20 @@ import { LogLevel, LogRecord } from '@kbn/logging'; import { RollingFileAppender, RollingFileAppenderConfig } from './rolling_file_appender'; const config: RollingFileAppenderConfig = { - kind: 'rolling-file', - path: '/var/log/kibana.log', + type: 'rolling-file', + fileName: '/var/log/kibana.log', layout: { - kind: 'pattern', + type: 'pattern', pattern: '%message', highlight: false, }, policy: { - kind: 'time-interval', + type: 'time-interval', interval: moment.duration(4, 'hour'), modulate: true, }, strategy: { - kind: 'numeric', + type: 'numeric', max: 5, pattern: '-%i', }, @@ -99,7 +99,7 @@ describe('RollingFileAppender', () => { it('constructs its delegates with the correct parameters', () => { expect(RollingFileContextMock).toHaveBeenCalledTimes(1); - expect(RollingFileContextMock).toHaveBeenCalledWith(config.path); + expect(RollingFileContextMock).toHaveBeenCalledWith(config.fileName); expect(RollingFileManagerMock).toHaveBeenCalledTimes(1); expect(RollingFileManagerMock).toHaveBeenCalledWith(context); diff --git a/src/core/server/logging/appenders/rolling_file/rolling_file_appender.ts b/src/core/server/logging/appenders/rolling_file/rolling_file_appender.ts index 748f47504f00ad..452d9493359544 100644 --- a/src/core/server/logging/appenders/rolling_file/rolling_file_appender.ts +++ b/src/core/server/logging/appenders/rolling_file/rolling_file_appender.ts @@ -26,7 +26,7 @@ import { RollingFileManager } from './rolling_file_manager'; import { RollingFileContext } from './rolling_file_context'; export interface RollingFileAppenderConfig { - kind: 'rolling-file'; + type: 'rolling-file'; /** * The layout to use when writing log entries */ @@ -34,7 +34,7 @@ export interface RollingFileAppenderConfig { /** * The absolute path of the file to write to. */ - path: string; + fileName: string; /** * The {@link TriggeringPolicy | policy} to use to determine if a rollover should occur. */ @@ -51,9 +51,9 @@ export interface RollingFileAppenderConfig { */ export class RollingFileAppender implements DisposableAppender { public static configSchema = schema.object({ - kind: schema.literal('rolling-file'), + type: schema.literal('rolling-file'), layout: Layouts.configSchema, - path: schema.string(), + fileName: schema.string(), policy: triggeringPolicyConfigSchema, strategy: rollingStrategyConfigSchema, }); @@ -70,7 +70,7 @@ export class RollingFileAppender implements DisposableAppender { private readonly buffer: BufferAppender; constructor(config: RollingFileAppenderConfig) { - this.context = new RollingFileContext(config.path); + this.context = new RollingFileContext(config.fileName); this.context.refreshFileInfo(); this.fileManager = new RollingFileManager(this.context); this.layout = Layouts.create(config.layout); diff --git a/src/core/server/logging/appenders/rolling_file/strategies/index.ts b/src/core/server/logging/appenders/rolling_file/strategies/index.ts index f63b68e4b92af9..c8364b0e590c67 100644 --- a/src/core/server/logging/appenders/rolling_file/strategies/index.ts +++ b/src/core/server/logging/appenders/rolling_file/strategies/index.ts @@ -19,7 +19,7 @@ export { RollingStrategy } from './strategy'; export type RollingStrategyConfig = NumericRollingStrategyConfig; const defaultStrategy: NumericRollingStrategyConfig = { - kind: 'numeric', + type: 'numeric', pattern: '-%i', max: 7, }; diff --git a/src/core/server/logging/appenders/rolling_file/strategies/numeric/numeric_strategy.test.ts b/src/core/server/logging/appenders/rolling_file/strategies/numeric/numeric_strategy.test.ts index d2e65f3880b87f..b4ca0131156a38 100644 --- a/src/core/server/logging/appenders/rolling_file/strategies/numeric/numeric_strategy.test.ts +++ b/src/core/server/logging/appenders/rolling_file/strategies/numeric/numeric_strategy.test.ts @@ -27,8 +27,8 @@ describe('NumericRollingStrategy', () => { let context: ReturnType; let strategy: NumericRollingStrategy; - const createStrategy = (config: Omit) => - new NumericRollingStrategy({ ...config, kind: 'numeric' }, context); + const createStrategy = (config: Omit) => + new NumericRollingStrategy({ ...config, type: 'numeric' }, context); beforeEach(() => { context = rollingFileAppenderMocks.createContext(logFilePath); diff --git a/src/core/server/logging/appenders/rolling_file/strategies/numeric/numeric_strategy.ts b/src/core/server/logging/appenders/rolling_file/strategies/numeric/numeric_strategy.ts index 5ee75bf6fda52a..13a19a40fa561d 100644 --- a/src/core/server/logging/appenders/rolling_file/strategies/numeric/numeric_strategy.ts +++ b/src/core/server/logging/appenders/rolling_file/strategies/numeric/numeric_strategy.ts @@ -19,10 +19,10 @@ import { } from './rolling_tasks'; export interface NumericRollingStrategyConfig { - kind: 'numeric'; + type: 'numeric'; /** * The suffix pattern to apply when renaming a file. The suffix will be applied - * after the `appender.path` file name, but before the file extension. + * after the `appender.fileName` file name, but before the file extension. * * Must include `%i`, as it is the value that will be converted to the file index * @@ -31,8 +31,8 @@ export interface NumericRollingStrategyConfig { * logging: * appenders: * rolling-file: - * kind: rolling-file - * path: /var/logs/kibana.log + * type: rolling-file + * fileName: /var/logs/kibana.log * strategy: * type: default * pattern: "-%i" @@ -52,7 +52,7 @@ export interface NumericRollingStrategyConfig { } export const numericRollingStrategyConfigSchema = schema.object({ - kind: schema.literal('numeric'), + type: schema.literal('numeric'), pattern: schema.string({ defaultValue: '-%i', validate: (pattern) => { @@ -73,8 +73,8 @@ export const numericRollingStrategyConfigSchema = schema.object({ * logging: * appenders: * rolling-file: - * kind: rolling-file - * path: /kibana.log + * type: rolling-file + * fileName: /kibana.log * strategy: * type: numeric * pattern: "-%i" diff --git a/src/core/server/logging/integration_tests/logging.test.ts b/src/core/server/logging/integration_tests/logging.test.ts index 0af6dbfc8611ec..b4eb98546de21b 100644 --- a/src/core/server/logging/integration_tests/logging.test.ts +++ b/src/core/server/logging/integration_tests/logging.test.ts @@ -17,22 +17,22 @@ function createRoot() { silent: true, // set "true" in kbnTestServer appenders: { 'test-console': { - kind: 'console', + type: 'console', layout: { highlight: false, - kind: 'pattern', + type: 'pattern', pattern: '%level|%logger|%message', }, }, }, loggers: [ { - context: 'parent', + name: 'parent', appenders: ['test-console'], level: 'warn', }, { - context: 'parent.child', + name: 'parent.child', appenders: ['test-console'], level: 'error', }, @@ -42,7 +42,7 @@ function createRoot() { } describe('logging service', () => { - describe('logs according to context hierarchy', () => { + describe('logs according to context name hierarchy', () => { let root: ReturnType; let mockConsoleLog: jest.SpyInstance; beforeAll(async () => { @@ -61,7 +61,7 @@ describe('logging service', () => { await root.shutdown(); }); - it('uses the most specific context', () => { + it('uses the most specific context name', () => { const logger = root.logger.get('parent.child'); logger.error('error from "parent.child" context'); @@ -74,7 +74,7 @@ describe('logging service', () => { ); }); - it('uses parent context', () => { + it('uses parent context name', () => { const logger = root.logger.get('parent.another-child'); logger.error('error from "parent.another-child" context'); @@ -104,31 +104,31 @@ describe('logging service', () => { }); }); - describe('custom context configuration', () => { + describe('custom context name configuration', () => { const CUSTOM_LOGGING_CONFIG: LoggerContextConfigInput = { appenders: { customJsonConsole: { - kind: 'console', + type: 'console', layout: { - kind: 'json', + type: 'json', }, }, customPatternConsole: { - kind: 'console', + type: 'console', layout: { - kind: 'pattern', + type: 'pattern', pattern: 'CUSTOM - PATTERN [%logger][%level] %message', }, }, }, loggers: [ - { context: 'debug_json', appenders: ['customJsonConsole'], level: 'debug' }, - { context: 'debug_pattern', appenders: ['customPatternConsole'], level: 'debug' }, - { context: 'info_json', appenders: ['customJsonConsole'], level: 'info' }, - { context: 'info_pattern', appenders: ['customPatternConsole'], level: 'info' }, + { name: 'debug_json', appenders: ['customJsonConsole'], level: 'debug' }, + { name: 'debug_pattern', appenders: ['customPatternConsole'], level: 'debug' }, + { name: 'info_json', appenders: ['customJsonConsole'], level: 'info' }, + { name: 'info_pattern', appenders: ['customPatternConsole'], level: 'info' }, { - context: 'all', + name: 'all', appenders: ['customJsonConsole', 'customPatternConsole'], level: 'debug', }, diff --git a/src/core/server/logging/integration_tests/rolling_file_appender.test.ts b/src/core/server/logging/integration_tests/rolling_file_appender.test.ts index fb2a714adb687a..b40ce7a4e7b0e3 100644 --- a/src/core/server/logging/integration_tests/rolling_file_appender.test.ts +++ b/src/core/server/logging/integration_tests/rolling_file_appender.test.ts @@ -25,7 +25,7 @@ function createRoot(appenderConfig: any) { }, loggers: [ { - context: 'test.rolling.file', + name: 'test.rolling.file', appenders: ['rolling-file'], level: 'debug', }, @@ -63,18 +63,18 @@ describe('RollingFileAppender', () => { describe('`size-limit` policy with `numeric` strategy', () => { it('rolls the log file in the correct order', async () => { root = createRoot({ - kind: 'rolling-file', - path: logFile, + type: 'rolling-file', + fileName: logFile, layout: { - kind: 'pattern', + type: 'pattern', pattern: '%message', }, policy: { - kind: 'size-limit', + type: 'size-limit', size: '100b', }, strategy: { - kind: 'numeric', + type: 'numeric', max: 5, pattern: '.%i', }, @@ -108,18 +108,18 @@ describe('RollingFileAppender', () => { it('only keep the correct number of files', async () => { root = createRoot({ - kind: 'rolling-file', - path: logFile, + type: 'rolling-file', + fileName: logFile, layout: { - kind: 'pattern', + type: 'pattern', pattern: '%message', }, policy: { - kind: 'size-limit', + type: 'size-limit', size: '60b', }, strategy: { - kind: 'numeric', + type: 'numeric', max: 2, pattern: '-%i', }, @@ -157,19 +157,19 @@ describe('RollingFileAppender', () => { describe('`time-interval` policy with `numeric` strategy', () => { it('rolls the log file at the given interval', async () => { root = createRoot({ - kind: 'rolling-file', - path: logFile, + type: 'rolling-file', + fileName: logFile, layout: { - kind: 'pattern', + type: 'pattern', pattern: '%message', }, policy: { - kind: 'time-interval', + type: 'time-interval', interval: '1s', modulate: true, }, strategy: { - kind: 'numeric', + type: 'numeric', max: 2, pattern: '-%i', }, diff --git a/src/core/server/logging/layouts/json_layout.test.ts b/src/core/server/logging/layouts/json_layout.test.ts index 2504ad476576fb..e55f69daab1100 100644 --- a/src/core/server/logging/layouts/json_layout.test.ts +++ b/src/core/server/logging/layouts/json_layout.test.ts @@ -63,7 +63,7 @@ const records: LogRecord[] = [ test('`createConfigSchema()` creates correct schema.', () => { const layoutSchema = JsonLayout.configSchema; - expect(layoutSchema.validate({ kind: 'json' })).toEqual({ kind: 'json' }); + expect(layoutSchema.validate({ type: 'json' })).toEqual({ type: 'json' }); }); test('`format()` correctly formats record.', () => { diff --git a/src/core/server/logging/layouts/json_layout.ts b/src/core/server/logging/layouts/json_layout.ts index 9e81303bedea07..bb8423f8240af9 100644 --- a/src/core/server/logging/layouts/json_layout.ts +++ b/src/core/server/logging/layouts/json_layout.ts @@ -14,12 +14,12 @@ import { LogRecord, Layout } from '@kbn/logging'; const { literal, object } = schema; const jsonLayoutSchema = object({ - kind: literal('json'), + type: literal('json'), }); /** @internal */ export interface JsonLayoutConfigType { - kind: 'json'; + type: 'json'; } /** diff --git a/src/core/server/logging/layouts/layouts.test.ts b/src/core/server/logging/layouts/layouts.test.ts index df91994564da17..3ff2fe23aae343 100644 --- a/src/core/server/logging/layouts/layouts.test.ts +++ b/src/core/server/logging/layouts/layouts.test.ts @@ -12,43 +12,43 @@ import { PatternLayout } from './pattern_layout'; test('`configSchema` creates correct schema for `pattern` layout.', () => { const layoutsSchema = Layouts.configSchema; - const validConfigWithOptional = { kind: 'pattern' }; + const validConfigWithOptional = { type: 'pattern' }; expect(layoutsSchema.validate(validConfigWithOptional)).toEqual({ highlight: undefined, - kind: 'pattern', + type: 'pattern', pattern: undefined, }); const validConfig = { highlight: true, - kind: 'pattern', + type: 'pattern', pattern: '%message', }; expect(layoutsSchema.validate(validConfig)).toEqual({ highlight: true, - kind: 'pattern', + type: 'pattern', pattern: '%message', }); - const wrongConfig2 = { kind: 'pattern', pattern: 1 }; + const wrongConfig2 = { type: 'pattern', pattern: 1 }; expect(() => layoutsSchema.validate(wrongConfig2)).toThrow(); }); test('`createConfigSchema()` creates correct schema for `json` layout.', () => { const layoutsSchema = Layouts.configSchema; - const validConfig = { kind: 'json' }; - expect(layoutsSchema.validate(validConfig)).toEqual({ kind: 'json' }); + const validConfig = { type: 'json' }; + expect(layoutsSchema.validate(validConfig)).toEqual({ type: 'json' }); }); test('`create()` creates correct layout.', () => { const patternLayout = Layouts.create({ highlight: false, - kind: 'pattern', + type: 'pattern', pattern: '[%date][%level][%logger] %message', }); expect(patternLayout).toBeInstanceOf(PatternLayout); - const jsonLayout = Layouts.create({ kind: 'json' }); + const jsonLayout = Layouts.create({ type: 'json' }); expect(jsonLayout).toBeInstanceOf(JsonLayout); }); diff --git a/src/core/server/logging/layouts/layouts.ts b/src/core/server/logging/layouts/layouts.ts index d6c14f3713b2c6..9abc8cd753f97b 100644 --- a/src/core/server/logging/layouts/layouts.ts +++ b/src/core/server/logging/layouts/layouts.ts @@ -27,7 +27,7 @@ export class Layouts { * @returns Fully constructed `Layout` instance. */ public static create(config: LayoutConfigType): Layout { - switch (config.kind) { + switch (config.type) { case 'json': return new JsonLayout(); diff --git a/src/core/server/logging/layouts/pattern_layout.test.ts b/src/core/server/logging/layouts/pattern_layout.test.ts index 7dd3c7c51f833c..abdc2f4fb929cb 100644 --- a/src/core/server/logging/layouts/pattern_layout.test.ts +++ b/src/core/server/logging/layouts/pattern_layout.test.ts @@ -66,28 +66,28 @@ expect.addSnapshotSerializer(stripAnsiSnapshotSerializer); test('`createConfigSchema()` creates correct schema.', () => { const layoutSchema = PatternLayout.configSchema; - const validConfigWithOptional = { kind: 'pattern' }; + const validConfigWithOptional = { type: 'pattern' }; expect(layoutSchema.validate(validConfigWithOptional)).toEqual({ highlight: undefined, - kind: 'pattern', + type: 'pattern', pattern: undefined, }); const validConfig = { highlight: true, - kind: 'pattern', + type: 'pattern', pattern: '%message', }; expect(layoutSchema.validate(validConfig)).toEqual({ highlight: true, - kind: 'pattern', + type: 'pattern', pattern: '%message', }); - const wrongConfig1 = { kind: 'json' }; + const wrongConfig1 = { type: 'json' }; expect(() => layoutSchema.validate(wrongConfig1)).toThrow(); - const wrongConfig2 = { kind: 'pattern', pattern: 1 }; + const wrongConfig2 = { type: 'pattern', pattern: 1 }; expect(() => layoutSchema.validate(wrongConfig2)).toThrow(); }); diff --git a/src/core/server/logging/layouts/pattern_layout.ts b/src/core/server/logging/layouts/pattern_layout.ts index a5e9c0be8409bd..a5dc41786c4400 100644 --- a/src/core/server/logging/layouts/pattern_layout.ts +++ b/src/core/server/logging/layouts/pattern_layout.ts @@ -32,7 +32,7 @@ export const patternSchema = schema.string({ const patternLayoutSchema = schema.object({ highlight: schema.maybe(schema.boolean()), - kind: schema.literal('pattern'), + type: schema.literal('pattern'), pattern: schema.maybe(patternSchema), }); @@ -47,7 +47,7 @@ const conversions: Conversion[] = [ /** @internal */ export interface PatternLayoutConfigType { - kind: 'pattern'; + type: 'pattern'; highlight?: boolean; pattern?: string; } diff --git a/src/core/server/logging/logging_config.test.ts b/src/core/server/logging/logging_config.test.ts index e494ae2413229a..2cb5831a8fb4ce 100644 --- a/src/core/server/logging/logging_config.test.ts +++ b/src/core/server/logging/logging_config.test.ts @@ -51,12 +51,12 @@ test('correctly fills in default config.', () => { expect(configValue.appenders.size).toBe(2); expect(configValue.appenders.get('default')).toEqual({ - kind: 'console', - layout: { kind: 'pattern', highlight: true }, + type: 'console', + layout: { type: 'pattern', highlight: true }, }); expect(configValue.appenders.get('console')).toEqual({ - kind: 'console', - layout: { kind: 'pattern', highlight: true }, + type: 'console', + layout: { type: 'pattern', highlight: true }, }); }); @@ -65,8 +65,8 @@ test('correctly fills in custom `appenders` config.', () => { config.schema.validate({ appenders: { console: { - kind: 'console', - layout: { kind: 'pattern' }, + type: 'console', + layout: { type: 'pattern' }, }, }, }) @@ -75,13 +75,13 @@ test('correctly fills in custom `appenders` config.', () => { expect(configValue.appenders.size).toBe(2); expect(configValue.appenders.get('default')).toEqual({ - kind: 'console', - layout: { kind: 'pattern', highlight: true }, + type: 'console', + layout: { type: 'pattern', highlight: true }, }); expect(configValue.appenders.get('console')).toEqual({ - kind: 'console', - layout: { kind: 'pattern' }, + type: 'console', + layout: { type: 'pattern' }, }); }); @@ -91,7 +91,7 @@ test('correctly fills in default `loggers` config.', () => { expect(configValue.loggers.size).toBe(1); expect(configValue.loggers.get('root')).toEqual({ appenders: ['default'], - context: 'root', + name: 'root', level: 'info', }); }); @@ -101,24 +101,24 @@ test('correctly fills in custom `loggers` config.', () => { config.schema.validate({ appenders: { file: { - kind: 'file', - layout: { kind: 'pattern' }, - path: 'path', + type: 'file', + layout: { type: 'pattern' }, + fileName: 'path', }, }, loggers: [ { appenders: ['file'], - context: 'plugins', + name: 'plugins', level: 'warn', }, { - context: 'plugins.pid', + name: 'plugins.pid', level: 'trace', }, { appenders: ['default'], - context: 'http', + name: 'http', level: 'error', }, ], @@ -128,22 +128,22 @@ test('correctly fills in custom `loggers` config.', () => { expect(configValue.loggers.size).toBe(4); expect(configValue.loggers.get('root')).toEqual({ appenders: ['default'], - context: 'root', + name: 'root', level: 'info', }); expect(configValue.loggers.get('plugins')).toEqual({ appenders: ['file'], - context: 'plugins', + name: 'plugins', level: 'warn', }); expect(configValue.loggers.get('plugins.pid')).toEqual({ appenders: ['file'], - context: 'plugins.pid', + name: 'plugins.pid', level: 'trace', }); expect(configValue.loggers.get('http')).toEqual({ appenders: ['default'], - context: 'http', + name: 'http', level: 'error', }); }); @@ -153,7 +153,7 @@ test('fails if loggers use unknown appenders.', () => { loggers: [ { appenders: ['unknown'], - context: 'some.nested.context', + name: 'some.nested.context', }, ], }); @@ -167,9 +167,9 @@ describe('extend', () => { config.schema.validate({ appenders: { file1: { - kind: 'file', - layout: { kind: 'pattern' }, - path: 'path', + type: 'file', + layout: { type: 'pattern' }, + fileName: 'path', }, }, }) @@ -179,9 +179,9 @@ describe('extend', () => { config.schema.validate({ appenders: { file2: { - kind: 'file', - layout: { kind: 'pattern' }, - path: 'path', + type: 'file', + layout: { type: 'pattern' }, + fileName: 'path', }, }, }) @@ -200,9 +200,9 @@ describe('extend', () => { config.schema.validate({ appenders: { file1: { - kind: 'file', - layout: { kind: 'pattern' }, - path: 'path', + type: 'file', + layout: { type: 'pattern' }, + fileName: 'path', }, }, }) @@ -212,18 +212,18 @@ describe('extend', () => { config.schema.validate({ appenders: { file1: { - kind: 'file', - layout: { kind: 'json' }, - path: 'updatedPath', + type: 'file', + layout: { type: 'json' }, + fileName: 'updatedPath', }, }, }) ); expect(mergedConfigValue.appenders.get('file1')).toEqual({ - kind: 'file', - layout: { kind: 'json' }, - path: 'updatedPath', + type: 'file', + layout: { type: 'json' }, + fileName: 'updatedPath', }); }); @@ -232,7 +232,7 @@ describe('extend', () => { config.schema.validate({ loggers: [ { - context: 'plugins', + name: 'plugins', level: 'warn', }, ], @@ -243,7 +243,7 @@ describe('extend', () => { config.schema.validate({ loggers: [ { - context: 'plugins.pid', + name: 'plugins.pid', level: 'trace', }, ], @@ -258,7 +258,7 @@ describe('extend', () => { config.schema.validate({ loggers: [ { - context: 'plugins', + name: 'plugins', level: 'warn', }, ], @@ -270,7 +270,7 @@ describe('extend', () => { loggers: [ { appenders: ['console'], - context: 'plugins', + name: 'plugins', level: 'trace', }, ], @@ -279,7 +279,7 @@ describe('extend', () => { expect(mergedConfigValue.loggers.get('plugins')).toEqual({ appenders: ['console'], - context: 'plugins', + name: 'plugins', level: 'trace', }); }); diff --git a/src/core/server/logging/logging_config.ts b/src/core/server/logging/logging_config.ts index 5b79b4e8e15d5b..24496289fb4c84 100644 --- a/src/core/server/logging/logging_config.ts +++ b/src/core/server/logging/logging_config.ts @@ -51,7 +51,7 @@ const levelSchema = schema.oneOf( */ export const loggerSchema = schema.object({ appenders: schema.arrayOf(schema.string(), { defaultValue: [] }), - context: schema.string(), + name: schema.string(), level: levelSchema, }); @@ -148,15 +148,15 @@ export class LoggingConfig { [ 'default', { - kind: 'console', - layout: { kind: 'pattern', highlight: true }, + type: 'console', + layout: { type: 'pattern', highlight: true }, } as AppenderConfigType, ], [ 'console', { - kind: 'console', - layout: { kind: 'pattern', highlight: true }, + type: 'console', + layout: { type: 'pattern', highlight: true }, } as AppenderConfigType, ], ]); @@ -182,8 +182,8 @@ export class LoggingConfig { public extend(contextConfig: LoggerContextConfigType) { // Use a Map to de-dupe any loggers for the same context. contextConfig overrides existing config. const mergedLoggers = new Map([ - ...this.configType.loggers.map((l) => [l.context, l] as [string, LoggerConfigType]), - ...contextConfig.loggers.map((l) => [l.context, l] as [string, LoggerConfigType]), + ...this.configType.loggers.map((l) => [l.name, l] as [string, LoggerConfigType]), + ...contextConfig.loggers.map((l) => [l.name, l] as [string, LoggerConfigType]), ]); const mergedConfig: LoggingConfigType = { @@ -204,13 +204,10 @@ export class LoggingConfig { private fillLoggersConfig(loggingConfig: LoggingConfigType) { // Include `root` logger into common logger list so that it can easily be a part // of the logger hierarchy and put all the loggers in map for easier retrieval. - const loggers = [ - { context: ROOT_CONTEXT_NAME, ...loggingConfig.root }, - ...loggingConfig.loggers, - ]; + const loggers = [{ name: ROOT_CONTEXT_NAME, ...loggingConfig.root }, ...loggingConfig.loggers]; const loggerConfigByContext = new Map( - loggers.map((loggerConfig) => toTuple(loggerConfig.context, loggerConfig)) + loggers.map((loggerConfig) => toTuple(loggerConfig.name, loggerConfig)) ); for (const [loggerContext, loggerConfig] of loggerConfigByContext) { @@ -247,7 +244,7 @@ function getAppenders( loggerConfig: LoggerConfigType, loggerConfigByContext: Map ) { - let currentContext = loggerConfig.context; + let currentContext = loggerConfig.name; let appenders = loggerConfig.appenders; while (appenders.length === 0) { diff --git a/src/core/server/logging/logging_service.test.ts b/src/core/server/logging/logging_service.test.ts index 66f1c67f114024..341a04736b87a4 100644 --- a/src/core/server/logging/logging_service.test.ts +++ b/src/core/server/logging/logging_service.test.ts @@ -30,11 +30,11 @@ describe('LoggingService', () => { it('forwards configuration changes to logging system', () => { const config1: LoggerContextConfigType = { appenders: new Map(), - loggers: [{ context: 'subcontext', appenders: ['console'], level: 'warn' }], + loggers: [{ name: 'subcontext', appenders: ['console'], level: 'warn' }], }; const config2: LoggerContextConfigType = { appenders: new Map(), - loggers: [{ context: 'subcontext', appenders: ['default'], level: 'all' }], + loggers: [{ name: 'subcontext', appenders: ['default'], level: 'all' }], }; setup.configure(['test', 'context'], of(config1, config2)); @@ -54,11 +54,11 @@ describe('LoggingService', () => { const updates$ = new Subject(); const config1: LoggerContextConfigType = { appenders: new Map(), - loggers: [{ context: 'subcontext', appenders: ['console'], level: 'warn' }], + loggers: [{ name: 'subcontext', appenders: ['console'], level: 'warn' }], }; const config2: LoggerContextConfigType = { appenders: new Map(), - loggers: [{ context: 'subcontext', appenders: ['default'], level: 'all' }], + loggers: [{ name: 'subcontext', appenders: ['default'], level: 'all' }], }; setup.configure(['test', 'context'], updates$); @@ -78,7 +78,7 @@ describe('LoggingService', () => { const updates$ = new Subject(); const config1: LoggerContextConfigType = { appenders: new Map(), - loggers: [{ context: 'subcontext', appenders: ['console'], level: 'warn' }], + loggers: [{ name: 'subcontext', appenders: ['console'], level: 'warn' }], }; setup.configure(['test', 'context'], updates$); diff --git a/src/core/server/logging/logging_service.ts b/src/core/server/logging/logging_service.ts index f76533dadd5c84..f5a4717fdbfaf4 100644 --- a/src/core/server/logging/logging_service.ts +++ b/src/core/server/logging/logging_service.ts @@ -31,7 +31,7 @@ export interface LoggingServiceSetup { * core.logging.configure( * of({ * appenders: new Map(), - * loggers: [{ context: 'search', appenders: ['default'] }] + * loggers: [{ name: 'search', appenders: ['default'] }] * }) * ) * ``` diff --git a/src/core/server/logging/logging_system.test.ts b/src/core/server/logging/logging_system.test.ts index 4d4ed191e60f8e..f68d6c6a97fbc4 100644 --- a/src/core/server/logging/logging_system.test.ts +++ b/src/core/server/logging/logging_system.test.ts @@ -46,7 +46,7 @@ test('uses default memory buffer logger until config is provided', () => { const logger = system.get('test', 'context'); logger.trace('trace message'); - // We shouldn't create new buffer appender for another context. + // We shouldn't create new buffer appender for another context name. const anotherLogger = system.get('test', 'context2'); anotherLogger.fatal('fatal message', { some: 'value' }); @@ -69,7 +69,7 @@ test('flushes memory buffer logger and switches to real logger once config is pr // Switch to console appender with `info` level, so that `trace` message won't go through. await system.upgrade( config.schema.validate({ - appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, + appenders: { default: { type: 'console', layout: { type: 'json' } } }, root: { level: 'info' }, }) ); @@ -102,12 +102,12 @@ test('appends records via multiple appenders.', async () => { await system.upgrade( config.schema.validate({ appenders: { - default: { kind: 'console', layout: { kind: 'pattern' } }, - file: { kind: 'file', layout: { kind: 'pattern' }, path: 'path' }, + default: { type: 'console', layout: { type: 'pattern' } }, + file: { type: 'file', layout: { type: 'pattern' }, fileName: 'path' }, }, loggers: [ - { appenders: ['file'], context: 'tests', level: 'warn' }, - { context: 'tests.child', level: 'error' }, + { appenders: ['file'], name: 'tests', level: 'warn' }, + { name: 'tests.child', level: 'error' }, ], }) ); @@ -121,10 +121,10 @@ test('appends records via multiple appenders.', async () => { expect(mockStreamWrite.mock.calls[1][0]).toMatchSnapshot('file logs'); }); -test('uses `root` logger if context is not specified.', async () => { +test('uses `root` logger if context name is not specified.', async () => { await system.upgrade( config.schema.validate({ - appenders: { default: { kind: 'console', layout: { kind: 'pattern' } } }, + appenders: { default: { type: 'console', layout: { type: 'pattern' } } }, }) ); @@ -137,7 +137,7 @@ test('uses `root` logger if context is not specified.', async () => { test('`stop()` disposes all appenders.', async () => { await system.upgrade( config.schema.validate({ - appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, + appenders: { default: { type: 'console', layout: { type: 'json' } } }, root: { level: 'info' }, }) ); @@ -156,7 +156,7 @@ test('asLoggerFactory() only allows to create new loggers.', async () => { await system.upgrade( config.schema.validate({ - appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, + appenders: { default: { type: 'console', layout: { type: 'json' } } }, root: { level: 'all' }, }) ); @@ -180,7 +180,7 @@ test('setContextConfig() updates config with relative contexts', async () => { await system.upgrade( config.schema.validate({ - appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, + appenders: { default: { type: 'console', layout: { type: 'json' } } }, root: { level: 'info' }, }) ); @@ -189,10 +189,10 @@ test('setContextConfig() updates config with relative contexts', async () => { appenders: new Map([ [ 'custom', - { kind: 'console', layout: { kind: 'pattern', pattern: '[%level][%logger] %message' } }, + { type: 'console', layout: { type: 'pattern', pattern: '[%level][%logger] %message' } }, ], ]), - loggers: [{ context: 'grandchild', appenders: ['default', 'custom'], level: 'debug' }], + loggers: [{ name: 'grandchild', appenders: ['default', 'custom'], level: 'debug' }], }); testsLogger.warn('tests log to default!'); @@ -235,7 +235,7 @@ test('setContextConfig() updates config for a root context', async () => { await system.upgrade( config.schema.validate({ - appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, + appenders: { default: { type: 'console', layout: { type: 'json' } } }, root: { level: 'info' }, }) ); @@ -244,10 +244,10 @@ test('setContextConfig() updates config for a root context', async () => { appenders: new Map([ [ 'custom', - { kind: 'console', layout: { kind: 'pattern', pattern: '[%level][%logger] %message' } }, + { type: 'console', layout: { type: 'pattern', pattern: '[%level][%logger] %message' } }, ], ]), - loggers: [{ context: '', appenders: ['custom'], level: 'debug' }], + loggers: [{ name: '', appenders: ['custom'], level: 'debug' }], }); testsLogger.warn('tests log to default!'); @@ -273,21 +273,21 @@ test('setContextConfig() updates config for a root context', async () => { ); }); -test('custom context configs are applied on subsequent calls to update()', async () => { +test('custom context name configs are applied on subsequent calls to update()', async () => { await system.setContextConfig(['tests', 'child'], { appenders: new Map([ [ 'custom', - { kind: 'console', layout: { kind: 'pattern', pattern: '[%level][%logger] %message' } }, + { type: 'console', layout: { type: 'pattern', pattern: '[%level][%logger] %message' } }, ], ]), - loggers: [{ context: 'grandchild', appenders: ['default', 'custom'], level: 'debug' }], + loggers: [{ name: 'grandchild', appenders: ['default', 'custom'], level: 'debug' }], }); // Calling upgrade after setContextConfig should not throw away the context-specific config await system.upgrade( config.schema.validate({ - appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, + appenders: { default: { type: 'console', layout: { type: 'json' } } }, root: { level: 'info' }, }) ); @@ -310,10 +310,10 @@ test('custom context configs are applied on subsequent calls to update()', async ); }); -test('subsequent calls to setContextConfig() for the same context override the previous config', async () => { +test('subsequent calls to setContextConfig() for the same context name override the previous config', async () => { await system.upgrade( config.schema.validate({ - appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, + appenders: { default: { type: 'console', layout: { type: 'json' } } }, root: { level: 'info' }, }) ); @@ -322,10 +322,10 @@ test('subsequent calls to setContextConfig() for the same context override the p appenders: new Map([ [ 'custom', - { kind: 'console', layout: { kind: 'pattern', pattern: '[%level][%logger] %message' } }, + { type: 'console', layout: { type: 'pattern', pattern: '[%level][%logger] %message' } }, ], ]), - loggers: [{ context: 'grandchild', appenders: ['default', 'custom'], level: 'debug' }], + loggers: [{ name: 'grandchild', appenders: ['default', 'custom'], level: 'debug' }], }); // Call again, this time with level: 'warn' and a different pattern @@ -334,12 +334,12 @@ test('subsequent calls to setContextConfig() for the same context override the p [ 'custom', { - kind: 'console', - layout: { kind: 'pattern', pattern: '[%level][%logger] second pattern! %message' }, + type: 'console', + layout: { type: 'pattern', pattern: '[%level][%logger] second pattern! %message' }, }, ], ]), - loggers: [{ context: 'grandchild', appenders: ['default', 'custom'], level: 'warn' }], + loggers: [{ name: 'grandchild', appenders: ['default', 'custom'], level: 'warn' }], }); const logger = system.get('tests', 'child', 'grandchild'); @@ -360,10 +360,10 @@ test('subsequent calls to setContextConfig() for the same context override the p ); }); -test('subsequent calls to setContextConfig() for the same context can disable the previous config', async () => { +test('subsequent calls to setContextConfig() for the same context name can disable the previous config', async () => { await system.upgrade( config.schema.validate({ - appenders: { default: { kind: 'console', layout: { kind: 'json' } } }, + appenders: { default: { type: 'console', layout: { type: 'json' } } }, root: { level: 'info' }, }) ); @@ -372,10 +372,10 @@ test('subsequent calls to setContextConfig() for the same context can disable th appenders: new Map([ [ 'custom', - { kind: 'console', layout: { kind: 'pattern', pattern: '[%level][%logger] %message' } }, + { type: 'console', layout: { type: 'pattern', pattern: '[%level][%logger] %message' } }, ], ]), - loggers: [{ context: 'grandchild', appenders: ['default', 'custom'], level: 'debug' }], + loggers: [{ name: 'grandchild', appenders: ['default', 'custom'], level: 'debug' }], }); // Call again, this time no customizations (effectively disabling) diff --git a/src/core/server/logging/logging_system.ts b/src/core/server/logging/logging_system.ts index 9c22cea23720b7..9ae434aff41d3c 100644 --- a/src/core/server/logging/logging_system.ts +++ b/src/core/server/logging/logging_system.ts @@ -79,7 +79,7 @@ export class LoggingSystem implements LoggerFactory { * loggingSystem.setContextConfig( * ['plugins', 'data'], * { - * loggers: [{ context: 'search', appenders: ['default'] }] + * loggers: [{ name: 'search', appenders: ['default'] }] * } * ) * ``` @@ -95,9 +95,7 @@ export class LoggingSystem implements LoggerFactory { // Automatically prepend the base context to the logger sub-contexts loggers: contextConfig.loggers.map((l) => ({ ...l, - context: LoggingConfig.getLoggerContext( - l.context.length > 0 ? [context, l.context] : [context] - ), + name: LoggingConfig.getLoggerContext(l.name.length > 0 ? [context, l.name] : [context]), })), }); diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts index 317bfe33b3a199..95a867934307a4 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts @@ -45,16 +45,16 @@ describe('migration v2', () => { logging: { appenders: { file: { - kind: 'file', - path: join(__dirname, 'migration_test_kibana.log'), + type: 'file', + fileName: join(__dirname, 'migration_test_kibana.log'), layout: { - kind: 'json', + type: 'json', }, }, }, loggers: [ { - context: 'root', + name: 'root', appenders: ['file'], }, ], diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts index 16ba0c855867ce..c26d4593bede19 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts @@ -47,16 +47,16 @@ describe.skip('migration from 7.7.2-xpack with 100k objects', () => { logging: { appenders: { file: { - kind: 'file', - path: join(__dirname, 'migration_test_kibana.log'), + type: 'file', + fileName: join(__dirname, 'migration_test_kibana.log'), layout: { - kind: 'json', + type: 'json', }, }, }, loggers: [ { - context: 'root', + name: 'root', appenders: ['file'], }, ], diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 377cd2bc2068a9..2177da84b2b53d 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1276,7 +1276,6 @@ export const kibanaResponseFactory: { forbidden: (options?: ErrorHttpResponseOptions) => KibanaResponse; notFound: (options?: ErrorHttpResponseOptions) => KibanaResponse; conflict: (options?: ErrorHttpResponseOptions) => KibanaResponse; - internalError: (options?: ErrorHttpResponseOptions) => KibanaResponse; customError: (options: CustomHttpResponseOptions) => KibanaResponse; redirected: (options: RedirectResponseOptions) => KibanaResponse | Buffer | Stream>; ok: (options?: HttpResponseOptions) => KibanaResponse | Buffer | Stream>; @@ -3197,7 +3196,7 @@ export const validBodyOutput: readonly ["data", "stream"]; // Warnings were encountered during analysis: // -// src/core/server/http/router/response.ts:306:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts +// src/core/server/http/router/response.ts:297:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:280:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:280:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts // src/core/server/plugins/types.ts:283:3 - (ae-forgotten-export) The symbol "SavedObjectsConfigType" needs to be exported by the entry point index.d.ts diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh index 2deafaaf35a94d..b9898960135fcd 100644 --- a/src/dev/ci_setup/setup_env.sh +++ b/src/dev/ci_setup/setup_env.sh @@ -56,7 +56,13 @@ export WORKSPACE="${WORKSPACE:-$PARENT_DIR}" nodeVersion="$(cat "$dir/.node-version")" nodeDir="$cacheDir/node/$nodeVersion" nodeBin="$nodeDir/bin" -classifier="x64.tar.gz" +hostArch="$(command uname -m)" +case "${hostArch}" in + x86_64 | amd64) nodeArch="x64" ;; + aarch64) nodeArch="arm64" ;; + *) nodeArch="${hostArch}" ;; +esac +classifier="$nodeArch.tar.gz" UNAME=$(uname) OS="linux" diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index c72c81f489fb9d..f1a3737747573d 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -6,10 +6,12 @@ * Side Public License, v 1. */ +// Please also add new aliases to test/scripts/jenkins_storybook.sh export const storybookAliases = { apm: 'x-pack/plugins/apm/.storybook', canvas: 'x-pack/plugins/canvas/storybook', codeeditor: 'src/plugins/kibana_react/public/code_editor/.storybook', + ci_composite: '.ci/.storybook', url_template_editor: 'src/plugins/kibana_react/public/url_template_editor/.storybook', dashboard: 'src/plugins/dashboard/.storybook', dashboard_enhanced: 'x-pack/plugins/dashboard_enhanced/.storybook', diff --git a/src/dev/typescript/projects.ts b/src/dev/typescript/projects.ts index 29379bbb31ee12..8b306fc9671153 100644 --- a/src/dev/typescript/projects.ts +++ b/src/dev/typescript/projects.ts @@ -17,6 +17,9 @@ export const PROJECTS = [ new Project(resolve(REPO_ROOT, 'x-pack/tsconfig.json')), new Project(resolve(REPO_ROOT, 'x-pack/test/tsconfig.json'), { name: 'x-pack/test' }), new Project(resolve(REPO_ROOT, 'src/core/tsconfig.json')), + new Project(resolve(REPO_ROOT, 'x-pack/plugins/drilldowns/url_drilldown/tsconfig.json'), { + name: 'security_solution/cypress', + }), new Project(resolve(REPO_ROOT, 'x-pack/plugins/security_solution/cypress/tsconfig.json'), { name: 'security_solution/cypress', }), diff --git a/src/plugins/apm_oss/server/tutorial/index_pattern.json b/src/plugins/apm_oss/server/tutorial/index_pattern.json index 93a2393b70fa43..b0a1ac1ccd379b 100644 --- a/src/plugins/apm_oss/server/tutorial/index_pattern.json +++ b/src/plugins/apm_oss/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.cgroup.memory.mem.limit.bytes\":{\"id\":\"bytes\"},\"system.process.cgroup.memory.mem.usage.bytes\":{\"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.build.original\",\"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.subdomain\",\"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\":\"client.user.roles\",\"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.account.name\",\"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.project.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.name\",\"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.subdomain\",\"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\":\"destination.user.roles\",\"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.architecture\",\"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.imphash\",\"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\":false,\"name\":\"error.stack_trace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"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\":false,\"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.reason\",\"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.architecture\",\"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.imphash\",\"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\":\"file.x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"file.x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.version_number\",\"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\":\"host.user.roles\",\"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.mime_type\",\"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.mime_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"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.file.path\",\"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\":false,\"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.architecture\",\"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.imphash\",\"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.pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.product\",\"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.architecture\",\"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.imphash\",\"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.hosts\",\"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.subdomain\",\"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\":\"server.user.roles\",\"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.subdomain\",\"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\":\"source.user.roles\",\"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\":\"threat.technique.subtechnique.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.subtechnique.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.subtechnique.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.subtechnique.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.client.x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"tls.client.x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.version_number\",\"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.server.x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"tls.server.x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.version_number\",\"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\":\"span.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"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\":\"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.subdomain\",\"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.roles\",\"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\":\"x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.version_number\",\"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.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.node.hostname\",\"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.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\":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\":\"transaction.root\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"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\":\"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\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cgroup.memory.mem.limit.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cgroup.memory.mem.usage.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"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\":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\":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\":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.experience.cls\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.fid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.tbt\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.longtask.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.longtask.sum\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.longtask.max\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"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\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.histogram\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"metricset.period\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.response_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.response_time.sum.us\",\"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\"}]", + "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.cgroup.memory.mem.limit.bytes\":{\"id\":\"bytes\"},\"system.process.cgroup.memory.mem.usage.bytes\":{\"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\"}}", + "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.build.original\",\"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.subdomain\",\"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\":\"client.user.roles\",\"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.account.name\",\"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.project.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.project.name\",\"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.subdomain\",\"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\":\"destination.user.roles\",\"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.architecture\",\"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.imphash\",\"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\":false,\"name\":\"error.stack_trace\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"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\":false,\"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.reason\",\"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.architecture\",\"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.imphash\",\"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\":\"file.x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"file.x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"file.x509.version_number\",\"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\":\"host.user.roles\",\"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.mime_type\",\"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.mime_type\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"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.file.path\",\"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\":false,\"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.architecture\",\"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.imphash\",\"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.pe.architecture\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.company\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.description\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.file_version\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.imphash\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.original_file_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"process.parent.pe.product\",\"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.architecture\",\"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.imphash\",\"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.hosts\",\"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.subdomain\",\"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\":\"server.user.roles\",\"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.subdomain\",\"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\":\"source.user.roles\",\"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\":\"threat.technique.subtechnique.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.subtechnique.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":false,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.subtechnique.name.text\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"threat.technique.subtechnique.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.client.x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"tls.client.x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.client.x509.version_number\",\"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.server.x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"tls.server.x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"tls.server.x509.version_number\",\"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\":\"span.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"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\":\"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.subdomain\",\"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.roles\",\"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\":\"x509.alternative_names\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.issuer.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.not_after\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.not_before\",\"scripted\":false,\"searchable\":true,\"type\":\"date\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.public_key_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.public_key_curve\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":false,\"name\":\"x509.public_key_exponent\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.public_key_size\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.serial_number\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.signature_algorithm\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.common_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.country\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.distinguished_name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.locality\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.organization\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.organizational_unit\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.subject.state_or_province\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"x509.version_number\",\"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.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.node.hostname\",\"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.service.selectors.*\",\"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.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\":\"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\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"cloud.service.name\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"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\":\"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\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.root\",\"scripted\":false,\"searchable\":true,\"type\":\"boolean\"},{\"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\":\"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\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cgroup.memory.mem.limit.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"system.process.cgroup.memory.mem.usage.bytes\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":1,\"doc_values\":true,\"indexed\":true,\"name\":\"profile.id\",\"scripted\":false,\"searchable\":true,\"type\":\"string\"},{\"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\":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.wall.us\",\"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\":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\":\"child.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.experience.cls\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.fid\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.tbt\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.longtask.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.longtask.sum\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.experience.longtask.max\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"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\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"transaction.duration.histogram\",\"scripted\":false,\"searchable\":true},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"metricset.period\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.response_time.count\",\"scripted\":false,\"searchable\":true,\"type\":\"number\"},{\"aggregatable\":true,\"analyzed\":false,\"count\":0,\"doc_values\":true,\"indexed\":true,\"name\":\"span.destination.service.response_time.sum.us\",\"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/src/plugins/charts/public/static/utils/transform_click_event.ts b/src/plugins/charts/public/static/utils/transform_click_event.ts index e875967616bbdb..0c303b92bf1a1c 100644 --- a/src/plugins/charts/public/static/utils/transform_click_event.ts +++ b/src/plugins/charts/public/static/utils/transform_click_event.ts @@ -30,9 +30,6 @@ export interface BrushTriggerEvent { type AllSeriesAccessors = Array<[accessor: Accessor | AccessorFn, value: string | number]>; -// TODO: replace when exported from elastic/charts -const DEFAULT_SINGLE_PANEL_SM_VALUE = '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__'; - /** * returns accessor value from string or function accessor * @param datum @@ -97,11 +94,11 @@ function getSplitChartValue({ | string | number | undefined { - if (smHorizontalAccessorValue !== DEFAULT_SINGLE_PANEL_SM_VALUE) { + if (smHorizontalAccessorValue !== undefined) { return smHorizontalAccessorValue; } - if (smVerticalAccessorValue !== DEFAULT_SINGLE_PANEL_SM_VALUE) { + if (smVerticalAccessorValue !== undefined) { return smVerticalAccessorValue; } diff --git a/src/plugins/console/public/application/components/settings_modal.tsx b/src/plugins/console/public/application/components/settings_modal.tsx index f3c8954d01254b..161b67500b47c7 100644 --- a/src/plugins/console/public/application/components/settings_modal.tsx +++ b/src/plugins/console/public/application/components/settings_modal.tsx @@ -22,7 +22,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiSwitch, } from '@elastic/eui'; @@ -151,115 +150,107 @@ export function DevToolsSettingsModal(props: Props) { ) : undefined; return ( - - - - - - - + + + + + + + + + + } + > + { + const val = parseInt(e.target.value, 10); + if (!val) return; + setFontSize(val); + }} + /> + - - + } - > - { - const val = parseInt(e.target.value, 10); - if (!val) return; - setFontSize(val); - }} - /> - + onChange={(e) => setWrapMode(e.target.checked)} + /> + - - - } - onChange={(e) => setWrapMode(e.target.checked)} + - - - + } - > - - } - onChange={(e) => setTripleQuotes(e.target.checked)} - /> - + onChange={(e) => setTripleQuotes(e.target.checked)} + /> + - - } - > - { - const { stateSetter, ...rest } = opts; - return rest; - })} - idToSelectedMap={checkboxIdToSelectedMap} - onChange={(e: any) => { - onAutocompleteChange(e as AutocompleteOptions); - }} + - + } + > + { + const { stateSetter, ...rest } = opts; + return rest; + })} + idToSelectedMap={checkboxIdToSelectedMap} + onChange={(e: any) => { + onAutocompleteChange(e as AutocompleteOptions); + }} + /> + - {pollingFields} - + {pollingFields} + - - - - + + + + - - - - - - + + + + + ); } diff --git a/src/plugins/dashboard/public/application/top_nav/__snapshots__/clone_modal.test.js.snap b/src/plugins/dashboard/public/application/top_nav/__snapshots__/clone_modal.test.js.snap index d289d267a2fd6d..1e029e6960cdfa 100644 --- a/src/plugins/dashboard/public/application/top_nav/__snapshots__/clone_modal.test.js.snap +++ b/src/plugins/dashboard/public/application/top_nav/__snapshots__/clone_modal.test.js.snap @@ -1,65 +1,63 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders DashboardCloneModal 1`] = ` - - - - - - - - - -

- -

-
- - + + + -
- - - - - + + + + +

- - - - +

+
+ + +
+ + + + + + + + +
`; diff --git a/src/plugins/dashboard/public/application/top_nav/__snapshots__/save_modal.test.js.snap b/src/plugins/dashboard/public/application/top_nav/__snapshots__/save_modal.test.js.snap index bc4ed477d9eea0..f8ba1b38685274 100644 --- a/src/plugins/dashboard/public/application/top_nav/__snapshots__/save_modal.test.js.snap +++ b/src/plugins/dashboard/public/application/top_nav/__snapshots__/save_modal.test.js.snap @@ -2,6 +2,7 @@ exports[`renders DashboardSaveModal 1`] = ` { render() { return ( - - - - - - - - - - -

- -

-
- - - - + + + + + - {this.renderDuplicateTitleCallout()} -
- - - + + +

- - - - - - - - +

+
+ + + + + + {this.renderDuplicateTitleCallout()} +
+ + + + + + + + + + +
); } } diff --git a/src/plugins/dashboard/public/application/top_nav/save_modal.tsx b/src/plugins/dashboard/public/application/top_nav/save_modal.tsx index 2d2a6445582a4f..5b1fbe7374dac6 100644 --- a/src/plugins/dashboard/public/application/top_nav/save_modal.tsx +++ b/src/plugins/dashboard/public/application/top_nav/save_modal.tsx @@ -147,6 +147,7 @@ export class DashboardSaveModal extends React.Component { onClose={this.props.onClose} title={this.props.title} showCopyOnSave={this.props.showCopyOnSave} + initialCopyOnSave={this.props.showCopyOnSave} objectType="dashboard" options={this.renderDashboardSaveOptions()} showDescription={false} diff --git a/src/plugins/data/common/index_patterns/constants.ts b/src/plugins/data/common/index_patterns/constants.ts new file mode 100644 index 00000000000000..88309447a8a29c --- /dev/null +++ b/src/plugins/data/common/index_patterns/constants.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; diff --git a/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap b/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap index 4ef61ec0f25571..6b1d01e5ba1429 100644 --- a/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap +++ b/src/plugins/data/common/index_patterns/fields/__snapshots__/index_pattern_field.test.ts.snap @@ -14,7 +14,7 @@ Object { }, "count": 1, "esTypes": Array [ - "text", + "keyword", ], "lang": "lang", "name": "name", @@ -49,7 +49,7 @@ Object { "count": 1, "customLabel": undefined, "esTypes": Array [ - "text", + "keyword", ], "format": Object { "id": "number", diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts index 85e20c5a32662e..48342a9e02a2bd 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.test.ts @@ -26,7 +26,7 @@ describe('Field', function () { script: 'script', lang: 'lang', count: 1, - esTypes: ['text'], + esTypes: ['text'], // note, this will get replaced by the runtime field type aggregatable: true, filterable: true, searchable: true, @@ -71,7 +71,7 @@ describe('Field', function () { }); it('sets type field when _source field', () => { - const field = getField({ name: '_source' }); + const field = getField({ name: '_source', runtimeField: undefined }); expect(field.type).toEqual('_source'); }); diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts index 4a6ee1149d4c6d..e5f4945c9ad6d4 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts @@ -7,7 +7,7 @@ */ import type { RuntimeField } from '../types'; -import { KbnFieldType, getKbnFieldType } from '../../kbn_field_types'; +import { KbnFieldType, getKbnFieldType, castEsToKbnFieldTypeName } from '../../kbn_field_types'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import type { IFieldType } from './types'; import { FieldSpec, IndexPattern } from '../..'; @@ -99,11 +99,13 @@ export class IndexPatternField implements IFieldType { } public get type() { - return this.spec.type; + return this.runtimeField?.type + ? castEsToKbnFieldTypeName(this.runtimeField?.type) + : this.spec.type; } public get esTypes() { - return this.spec.esTypes; + return this.runtimeField?.type ? [this.runtimeField?.type] : this.spec.esTypes; } public get scripted() { diff --git a/src/plugins/data/common/index_patterns/index.ts b/src/plugins/data/common/index_patterns/index.ts index 1cea49bcbecd30..7f6249caceb52e 100644 --- a/src/plugins/data/common/index_patterns/index.ts +++ b/src/plugins/data/common/index_patterns/index.ts @@ -12,3 +12,4 @@ export { IndexPatternsService, IndexPatternsContract } from './index_patterns'; export type { IndexPattern } from './index_patterns'; export * from './errors'; export * from './expressions'; +export * from './constants'; diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap index 4aadddfad3b970..7757e2fdd4584d 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap +++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap @@ -565,7 +565,9 @@ Object { "conflictDescriptions": undefined, "count": 0, "customLabel": undefined, - "esTypes": undefined, + "esTypes": Array [ + "keyword", + ], "format": Object { "id": "number", "params": Object { @@ -587,7 +589,7 @@ Object { "searchable": false, "shortDotsEnable": false, "subType": undefined, - "type": undefined, + "type": "string", }, "script date": Object { "aggregatable": true, diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index ca4fee0416ac41..41ce7ba4bab4a1 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -389,6 +389,8 @@ export class IndexPattern implements IIndexPattern { existingField.runtimeField = undefined; } else { // runtimeField only + this.setFieldCustomLabel(name, null); + this.deleteFieldFormat(name); this.fields.remove(existingField); } } @@ -423,7 +425,6 @@ export class IndexPattern implements IIndexPattern { if (fieldObject) { fieldObject.customLabel = newCustomLabel; - return; } this.setFieldAttrs(fieldName, 'customLabel', newCustomLabel); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 57a452dcb12046..27794094236046 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -415,11 +415,10 @@ export class IndexPatternsService { }, spec.fieldAttrs ); - // APPLY RUNTIME FIELDS + // CREATE RUNTIME FIELDS for (const [key, value] of Object.entries(runtimeFieldMap || {})) { - if (spec.fields[key]) { - spec.fields[key].runtimeField = value; - } else { + // do not create runtime field if mapped field exists + if (!spec.fields[key]) { spec.fields[key] = { name: key, type: castEsToKbnFieldTypeName(value.type), diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index 6d7327e7fb38d7..c906b809b08c4c 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -10,15 +10,16 @@ import { ToastInputFields, ErrorToastOptions } from 'src/core/public/notificatio // eslint-disable-next-line import type { SavedObject } from 'src/core/server'; import { IFieldType } from './fields'; +import { RUNTIME_FIELD_TYPES } from './constants'; import { SerializedFieldFormat } from '../../../expressions/common'; import { KBN_FIELD_TYPES, IndexPatternField, FieldFormat } from '..'; export type FieldFormatMap = Record; -const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; -type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; + +export type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; export interface RuntimeField { type: RuntimeType; - script: { + script?: { source: string; }; } diff --git a/src/plugins/data/common/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts index 9e889e85734ee6..030e620bea34b4 100644 --- a/src/plugins/data/common/search/search_source/search_source.test.ts +++ b/src/plugins/data/common/search/search_source/search_source.test.ts @@ -95,164 +95,6 @@ describe('SearchSource', () => { } `); }); - - test('recurses parents to get the entire filters: plain object filter', () => { - const RECURSE = true; - - const parent = new SearchSource({}, searchSourceDependencies); - parent.setField('filter', [ - { - meta: { - index: 'd180cae0-60c3-11eb-8569-bd1f5ed24bc9', - params: {}, - alias: null, - disabled: false, - negate: false, - }, - query: { - range: { - '@date': { - gte: '2016-01-27T18:11:05.010Z', - lte: '2021-01-27T18:11:05.010Z', - format: 'strict_date_optional_time', - }, - }, - }, - }, - ]); - searchSource.setParent(parent); - searchSource.setField('aggs', 5); - expect(searchSource.getFields(RECURSE)).toMatchInlineSnapshot(` - Object { - "aggs": 5, - "filter": Array [ - Object { - "meta": Object { - "alias": null, - "disabled": false, - "index": "d180cae0-60c3-11eb-8569-bd1f5ed24bc9", - "negate": false, - "params": Object {}, - }, - "query": Object { - "range": Object { - "@date": Object { - "format": "strict_date_optional_time", - "gte": "2016-01-27T18:11:05.010Z", - "lte": "2021-01-27T18:11:05.010Z", - }, - }, - }, - }, - ], - } - `); - - // calling twice gives the same result: no searchSources in the hierarchy were modified - expect(searchSource.getFields(RECURSE)).toMatchInlineSnapshot(` - Object { - "aggs": 5, - "filter": Array [ - Object { - "meta": Object { - "alias": null, - "disabled": false, - "index": "d180cae0-60c3-11eb-8569-bd1f5ed24bc9", - "negate": false, - "params": Object {}, - }, - "query": Object { - "range": Object { - "@date": Object { - "format": "strict_date_optional_time", - "gte": "2016-01-27T18:11:05.010Z", - "lte": "2021-01-27T18:11:05.010Z", - }, - }, - }, - }, - ], - } - `); - }); - - test('recurses parents to get the entire filters: function filter', () => { - const RECURSE = true; - - const parent = new SearchSource({}, searchSourceDependencies); - parent.setField('filter', () => ({ - meta: { - index: 'd180cae0-60c3-11eb-8569-bd1f5ed24bc9', - params: {}, - alias: null, - disabled: false, - negate: false, - }, - query: { - range: { - '@date': { - gte: '2016-01-27T18:11:05.010Z', - lte: '2021-01-27T18:11:05.010Z', - format: 'strict_date_optional_time', - }, - }, - }, - })); - searchSource.setParent(parent); - searchSource.setField('aggs', 5); - expect(searchSource.getFields(RECURSE)).toMatchInlineSnapshot(` - Object { - "aggs": 5, - "filter": Array [ - Object { - "meta": Object { - "alias": null, - "disabled": false, - "index": "d180cae0-60c3-11eb-8569-bd1f5ed24bc9", - "negate": false, - "params": Object {}, - }, - "query": Object { - "range": Object { - "@date": Object { - "format": "strict_date_optional_time", - "gte": "2016-01-27T18:11:05.010Z", - "lte": "2021-01-27T18:11:05.010Z", - }, - }, - }, - }, - ], - } - `); - - // calling twice gives the same result: no double-added filters - expect(searchSource.getFields(RECURSE)).toMatchInlineSnapshot(` - Object { - "aggs": 5, - "filter": Array [ - Object { - "meta": Object { - "alias": null, - "disabled": false, - "index": "d180cae0-60c3-11eb-8569-bd1f5ed24bc9", - "negate": false, - "params": Object {}, - }, - "query": Object { - "range": Object { - "@date": Object { - "format": "strict_date_optional_time", - "gte": "2016-01-27T18:11:05.010Z", - "lte": "2021-01-27T18:11:05.010Z", - }, - }, - }, - }, - ], - } - `); - }); }); describe('#removeField()', () => { @@ -345,8 +187,13 @@ describe('SearchSource', () => { }); test('allows you to override computed fields if you provide a format', async () => { + const indexPatternFields = indexPattern.fields; + indexPatternFields.getByType = (type) => { + return []; + }; searchSource.setField('index', ({ ...indexPattern, + fields: indexPatternFields, getComputedFields: () => ({ storedFields: [], scriptFields: {}, @@ -379,6 +226,11 @@ describe('SearchSource', () => { test('injects a date format for computed docvalue fields while merging other properties', async () => { searchSource.setField('index', ({ ...indexPattern, + fields: { + getByType: () => { + return []; + }, + }, getComputedFields: () => ({ storedFields: [], scriptFields: {}, @@ -625,7 +477,7 @@ describe('SearchSource', () => { searchSource.setField('fields', ['hello', '@timestamp', 'foo-a', 'bar']); const request = await searchSource.getSearchRequestBody(); - expect(request.fields).toEqual(['hello', '@timestamp', 'bar']); + expect(request.fields).toEqual(['hello', '@timestamp', 'bar', 'date']); expect(request.script_fields).toEqual({ hello: {} }); expect(request.stored_fields).toEqual(['@timestamp', 'bar']); }); @@ -681,6 +533,60 @@ describe('SearchSource', () => { }); }); + describe('handling date fields', () => { + test('adds date format to any date field', async () => { + searchSource.setField('index', ({ + ...indexPattern, + getComputedFields: () => ({ + storedFields: [], + scriptFields: {}, + docvalueFields: [{ field: '@timestamp' }], + }), + fields: { + getByType: () => [{ name: '@timestamp', esTypes: ['date_nanos'] }], + }, + getSourceFiltering: () => ({ excludes: [] }), + } as unknown) as IndexPattern); + searchSource.setField('fields', ['*']); + + const request = await searchSource.getSearchRequestBody(); + expect(request.fields).toEqual([ + '*', + { field: '@timestamp', format: 'strict_date_optional_time_nanos' }, + ]); + }); + + test('adds date format to any date field except the one excluded by source filters', async () => { + const indexPatternFields = indexPattern.fields; + // @ts-ignore + indexPatternFields.getByType = (type) => { + return [ + { name: '@timestamp', esTypes: ['date_nanos'] }, + { name: 'custom_date', esTypes: ['date'] }, + ]; + }; + searchSource.setField('index', ({ + ...indexPattern, + getComputedFields: () => ({ + storedFields: [], + scriptFields: {}, + docvalueFields: [{ field: '@timestamp' }, { field: 'custom_date' }], + }), + fields: indexPatternFields, + getSourceFiltering: () => ({ excludes: ['custom_date'] }), + } as unknown) as IndexPattern); + searchSource.setField('fields', ['*']); + + const request = await searchSource.getSearchRequestBody(); + expect(request.fields).toEqual([ + { field: 'foo-bar' }, + { field: 'field1' }, + { field: 'field2' }, + { field: '@timestamp', format: 'strict_date_optional_time_nanos' }, + ]); + }); + }); + describe(`#setField('index')`, () => { describe('auto-sourceFiltering', () => { describe('new index pattern assigned', () => { @@ -911,4 +817,74 @@ describe('SearchSource', () => { expect(request._source).toEqual(['geometry']); }); }); + + describe('getSerializedFields', () => { + const filter = [ + { + query: 'query', + meta: { + alias: 'alias', + disabled: false, + negate: false, + index: '456', + }, + }, + ]; + + test('should return serialized fields', () => { + const indexPattern123 = { id: '123' } as IndexPattern; + searchSource.setField('index', indexPattern123); + searchSource.setField('filter', () => { + return filter; + }); + const serializedFields = searchSource.getSerializedFields(); + expect(serializedFields).toMatchInlineSnapshot( + { index: '123', filter }, + ` + Object { + "filter": Array [ + Object { + "meta": Object { + "alias": "alias", + "disabled": false, + "index": "456", + "negate": false, + }, + "query": "query", + }, + ], + "index": "123", + } + ` + ); + }); + + test('should support nested search sources', () => { + const indexPattern123 = { id: '123' } as IndexPattern; + searchSource.setField('index', indexPattern123); + searchSource.setField('from', 123); + const childSearchSource = searchSource.createChild(); + childSearchSource.setField('timeout', '100'); + const serializedFields = childSearchSource.getSerializedFields(true); + expect(serializedFields).toMatchInlineSnapshot( + { + timeout: '100', + parent: { + index: '123', + from: 123, + }, + }, + ` + Object { + "index": undefined, + "parent": Object { + "from": 123, + "index": "123", + }, + "timeout": "100", + } + ` + ); + }); + }); }); diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts index 2cf0455ae2df8b..118bb04c1742b4 100644 --- a/src/plugins/data/common/search/search_source/search_source.ts +++ b/src/plugins/data/common/search/search_source/search_source.ts @@ -59,7 +59,7 @@ */ import { setWith } from '@elastic/safer-lodash-set'; -import { uniqueId, keyBy, pick, difference, omit, isFunction, isEqual } from 'lodash'; +import { uniqueId, keyBy, pick, difference, isFunction, isEqual, uniqWith } from 'lodash'; import { map, switchMap, tap } from 'rxjs/operators'; import { defer, from } from 'rxjs'; import { isObject } from 'rxjs/internal-compatibility'; @@ -114,8 +114,13 @@ export class SearchSource { private readonly dependencies: SearchSourceDependencies; constructor(fields: SearchSourceFields = {}, dependencies: SearchSourceDependencies) { - this.fields = fields; + const { parent, ...currentFields } = fields; + this.fields = currentFields; this.dependencies = dependencies; + + if (parent) { + this.setParent(new SearchSource(parent, dependencies)); + } } /** *** @@ -173,49 +178,7 @@ export class SearchSource { /** * returns all search source fields */ - getFields(recurse = false): SearchSourceFields { - let thisFilter = this.fields.filter; // type is single value, array, or function - if (thisFilter) { - if (typeof thisFilter === 'function') { - thisFilter = thisFilter() || []; // type is single value or array - } - - if (Array.isArray(thisFilter)) { - thisFilter = [...thisFilter]; - } else { - thisFilter = [thisFilter]; - } - } else { - thisFilter = []; - } - - if (recurse) { - const parent = this.getParent(); - if (parent) { - const parentFields = parent.getFields(recurse); - - let parentFilter = parentFields.filter; // type is single value, array, or function - if (parentFilter) { - if (typeof parentFilter === 'function') { - parentFilter = parentFilter() || []; // type is single value or array - } - - if (Array.isArray(parentFilter)) { - thisFilter.push(...parentFilter); - } else { - thisFilter.push(parentFilter); - } - } - - // add combined filters to the fields - const thisFields = { - ...this.fields, - filter: thisFilter, - }; - - return { ...parentFields, ...thisFields }; - } - } + getFields(): SearchSourceFields { return { ...this.fields }; } @@ -548,6 +511,37 @@ export class SearchSource { })); } + private getFieldFromDocValueFieldsOrIndexPattern( + docvaluesIndex: Record, + fld: SearchFieldValue, + index?: IndexPattern + ) { + if (typeof fld === 'string') { + return fld; + } + const fieldName = this.getFieldName(fld); + const field = { + ...docvaluesIndex[fieldName], + ...fld, + }; + if (!index) { + return field; + } + const { fields } = index; + const dateFields = fields.getByType('date'); + const dateField = dateFields.find((indexPatternField) => indexPatternField.name === fieldName); + if (!dateField) { + return field; + } + const { esTypes } = dateField; + if (esTypes?.includes('date_nanos')) { + field.format = 'strict_date_optional_time_nanos'; + } else if (esTypes?.includes('date')) { + field.format = 'strict_date_optional_time'; + } + return field; + } + private flatten() { const { getConfig } = this.dependencies; const searchRequest = this.mergeProps(); @@ -657,22 +651,25 @@ export class SearchSource { // if items that are in the docvalueFields are provided, we should // inject the format from the computed fields if one isn't given const docvaluesIndex = keyBy(filteredDocvalueFields, 'field'); - body.fields = this.getFieldsWithoutSourceFilters(index, body.fields).map( - (fld: SearchFieldValue) => { - const fieldName = this.getFieldName(fld); - if (Object.keys(docvaluesIndex).includes(fieldName)) { - // either provide the field object from computed docvalues, - // or merge the user-provided field with the one in docvalues - return typeof fld === 'string' - ? docvaluesIndex[fld] - : { - ...docvaluesIndex[fieldName], - ...fld, - }; - } - return fld; + const bodyFields = this.getFieldsWithoutSourceFilters(index, body.fields); + body.fields = uniqWith( + bodyFields.concat(filteredDocvalueFields), + (fld1: SearchFieldValue, fld2: SearchFieldValue) => { + const field1Name = this.getFieldName(fld1); + const field2Name = this.getFieldName(fld2); + return field1Name === field2Name; } - ); + ).map((fld: SearchFieldValue) => { + const fieldName = this.getFieldName(fld); + if (Object.keys(docvaluesIndex).includes(fieldName)) { + // either provide the field object from computed docvalues, + // or merge the user-provided field with the one in docvalues + return typeof fld === 'string' + ? docvaluesIndex[fld] + : this.getFieldFromDocValueFieldsOrIndexPattern(docvaluesIndex, fld, index); + } + return fld; + }); } } else { body.fields = filteredDocvalueFields; @@ -693,9 +690,7 @@ export class SearchSource { * serializes search source fields (which can later be passed to {@link ISearchStartSearchSource}) */ public getSerializedFields(recurse = false) { - const { filter: originalFilters, ...searchSourceFields } = omit(this.getFields(recurse), [ - 'size', - ]); + const { filter: originalFilters, size: omit, ...searchSourceFields } = this.getFields(); let serializedSearchSourceFields: SearchSourceFields = { ...searchSourceFields, index: (searchSourceFields.index ? searchSourceFields.index.id : undefined) as any, @@ -707,6 +702,9 @@ export class SearchSource { filter: filters, }; } + if (recurse && this.getParent()) { + serializedSearchSourceFields.parent = this.getParent()!.getSerializedFields(recurse); + } return serializedSearchSourceFields; } diff --git a/src/plugins/data/common/search/search_source/types.ts b/src/plugins/data/common/search/search_source/types.ts index 61d7165393b099..d06f521640da04 100644 --- a/src/plugins/data/common/search/search_source/types.ts +++ b/src/plugins/data/common/search/search_source/types.ts @@ -99,6 +99,8 @@ export interface SearchSourceFields { searchAfter?: EsQuerySearchAfter; timeout?: string; terminate_after?: number; + + parent?: SearchSourceFields; } export interface SearchSourceOptions { diff --git a/src/plugins/data/public/autocomplete/autocomplete_service.ts b/src/plugins/data/public/autocomplete/autocomplete_service.ts index a99943c6cd8784..6b288c4507f066 100644 --- a/src/plugins/data/public/autocomplete/autocomplete_service.ts +++ b/src/plugins/data/public/autocomplete/autocomplete_service.ts @@ -16,6 +16,8 @@ import { } from './providers/value_suggestion_provider'; import { ConfigSchema } from '../../config'; +import { UsageCollectionSetup } from '../../../usage_collection/public'; +import { createUsageCollector } from './collectors'; export class AutocompleteService { autocompleteConfig: ConfigSchema['autocomplete']; @@ -47,9 +49,17 @@ export class AutocompleteService { private hasQuerySuggestions = (language: string) => this.querySuggestionProviders.has(language); /** @public **/ - public setup(core: CoreSetup, { timefilter }: { timefilter: TimefilterSetup }) { + public setup( + core: CoreSetup, + { + timefilter, + usageCollection, + }: { timefilter: TimefilterSetup; usageCollection?: UsageCollectionSetup } + ) { + const usageCollector = createUsageCollector(core.getStartServices, usageCollection); + this.getValueSuggestions = this.autocompleteConfig.valueSuggestions.enabled - ? setupValueSuggestionProvider(core, { timefilter }) + ? setupValueSuggestionProvider(core, { timefilter, usageCollector }) : getEmptyValueSuggestions; return { diff --git a/src/plugins/data/public/autocomplete/collectors/create_usage_collector.ts b/src/plugins/data/public/autocomplete/collectors/create_usage_collector.ts new file mode 100644 index 00000000000000..fc0cea2fdbc520 --- /dev/null +++ b/src/plugins/data/public/autocomplete/collectors/create_usage_collector.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { first } from 'rxjs/operators'; +import { StartServicesAccessor } from '../../../../../core/public'; +import { METRIC_TYPE, UsageCollectionSetup } from '../../../../usage_collection/public'; +import { AUTOCOMPLETE_EVENT_TYPE, AutocompleteUsageCollector } from './types'; + +export const createUsageCollector = ( + getStartServices: StartServicesAccessor, + usageCollection?: UsageCollectionSetup +): AutocompleteUsageCollector => { + const getCurrentApp = async () => { + const [{ application }] = await getStartServices(); + return application.currentAppId$.pipe(first()).toPromise(); + }; + + return { + trackCall: async () => { + const currentApp = await getCurrentApp(); + return usageCollection?.reportUiCounter( + currentApp!, + METRIC_TYPE.LOADED, + AUTOCOMPLETE_EVENT_TYPE.CALL + ); + }, + trackRequest: async () => { + const currentApp = await getCurrentApp(); + return usageCollection?.reportUiCounter( + currentApp!, + METRIC_TYPE.LOADED, + AUTOCOMPLETE_EVENT_TYPE.REQUEST + ); + }, + trackResult: async () => { + const currentApp = await getCurrentApp(); + return usageCollection?.reportUiCounter( + currentApp!, + METRIC_TYPE.LOADED, + AUTOCOMPLETE_EVENT_TYPE.RESULT + ); + }, + trackError: async () => { + const currentApp = await getCurrentApp(); + return usageCollection?.reportUiCounter( + currentApp!, + METRIC_TYPE.LOADED, + AUTOCOMPLETE_EVENT_TYPE.ERROR + ); + }, + }; +}; diff --git a/src/plugins/data/public/autocomplete/collectors/index.ts b/src/plugins/data/public/autocomplete/collectors/index.ts new file mode 100644 index 00000000000000..5cfaab19787dab --- /dev/null +++ b/src/plugins/data/public/autocomplete/collectors/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { createUsageCollector } from './create_usage_collector'; +export { AUTOCOMPLETE_EVENT_TYPE, AutocompleteUsageCollector } from './types'; diff --git a/src/plugins/data/public/autocomplete/collectors/types.ts b/src/plugins/data/public/autocomplete/collectors/types.ts new file mode 100644 index 00000000000000..60eb9103dc44ee --- /dev/null +++ b/src/plugins/data/public/autocomplete/collectors/types.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export enum AUTOCOMPLETE_EVENT_TYPE { + CALL = 'autocomplete:call', + REQUEST = 'autocomplete:req', + RESULT = 'autocomplete:res', + ERROR = 'autocomplete:err', +} + +export interface AutocompleteUsageCollector { + trackCall: () => Promise; + trackRequest: () => Promise; + trackResult: () => Promise; + trackError: () => Promise; +} diff --git a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts index 23fc9f5405aea6..a7b1bd2c7839de 100644 --- a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts +++ b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.test.ts @@ -18,7 +18,7 @@ describe('FieldSuggestions', () => { beforeEach(() => { const uiSettings = { get: (key: string) => shouldSuggestValues } as IUiSettingsClient; - http = { fetch: jest.fn() }; + http = { fetch: jest.fn().mockResolvedValue([]) }; getValueSuggestions = setupValueSuggestionProvider({ http, uiSettings } as CoreSetup, { timefilter: ({ diff --git a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts index d8c6d16174d143..b8af6ad3a99e58 100644 --- a/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts +++ b/src/plugins/data/public/autocomplete/providers/value_suggestion_provider.ts @@ -11,12 +11,7 @@ import { memoize } from 'lodash'; import { CoreSetup } from 'src/core/public'; import { IIndexPattern, IFieldType, UI_SETTINGS, buildQueryFromFilters } from '../../../common'; import { TimefilterSetup } from '../../query'; - -function resolver(title: string, field: IFieldType, query: string, filters: any[]) { - // Only cache results for a minute - const ttl = Math.floor(Date.now() / 1000 / 60); - return [ttl, query, title, field.name, JSON.stringify(filters)].join('|'); -} +import { AutocompleteUsageCollector } from '../collectors'; export type ValueSuggestionsGetFn = (args: ValueSuggestionsGetFnArgs) => Promise; @@ -47,15 +42,31 @@ export const getEmptyValueSuggestions = (() => Promise.resolve([])) as ValueSugg export const setupValueSuggestionProvider = ( core: CoreSetup, - { timefilter }: { timefilter: TimefilterSetup } + { + timefilter, + usageCollector, + }: { timefilter: TimefilterSetup; usageCollector?: AutocompleteUsageCollector } ): ValueSuggestionsGetFn => { + function resolver(title: string, field: IFieldType, query: string, filters: any[]) { + // Only cache results for a minute + const ttl = Math.floor(Date.now() / 1000 / 60); + return [ttl, query, title, field.name, JSON.stringify(filters)].join('|'); + } + const requestSuggestions = memoize( - (index: string, field: IFieldType, query: string, filters: any = [], signal?: AbortSignal) => - core.http.fetch(`/api/kibana/suggestions/values/${index}`, { - method: 'POST', - body: JSON.stringify({ query, field: field.name, filters }), - signal, - }), + (index: string, field: IFieldType, query: string, filters: any = [], signal?: AbortSignal) => { + usageCollector?.trackRequest(); + return core.http + .fetch(`/api/kibana/suggestions/values/${index}`, { + method: 'POST', + body: JSON.stringify({ query, field: field.name, filters }), + signal, + }) + .then((r) => { + usageCollector?.trackResult(); + return r; + }); + }, resolver ); @@ -85,6 +96,16 @@ export const setupValueSuggestionProvider = ( : undefined; const filterQuery = timeFilter ? buildQueryFromFilters([timeFilter], indexPattern).filter : []; const filters = [...(boolFilter ? boolFilter : []), ...filterQuery]; - return await requestSuggestions(title, field, query, filters, signal); + try { + usageCollector?.trackCall(); + return await requestSuggestions(title, field, query, filters, signal); + } catch (e) { + if (!signal?.aborted) { + usageCollector?.trackError(); + } + // Remove rejected results from memoize cache + requestSuggestions.cache.delete(resolver(title, field, query, filters)); + return []; + } }; }; diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 39d3ca57215b74..862dd63948a224 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -115,7 +115,10 @@ export class DataPublicPlugin ); return { - autocomplete: this.autocomplete.setup(core, { timefilter: queryService.timefilter }), + autocomplete: this.autocomplete.setup(core, { + timefilter: queryService.timefilter, + usageCollection, + }), search: searchService, fieldFormats: this.fieldFormatsService.setup(core), query: queryService, @@ -195,10 +198,7 @@ export class DataPublicPlugin core, data: dataServices, storage: this.storage, - trackUiMetric: this.usageCollection?.reportUiCounter.bind( - this.usageCollection, - 'data_plugin' - ), + usageCollection: this.usageCollection, }); return { diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 0a3e4666da7473..67423295dfe5e3 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -105,7 +105,6 @@ import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; import { UiActionsSetup } from 'src/plugins/ui_actions/public'; import { UiActionsStart } from 'src/plugins/ui_actions/public'; -import { UiCounterMetricType } from '@kbn/analytics'; import { Unit } from '@elastic/datemath'; import { UnregisterCallback } from 'history'; import { URL } from 'url'; @@ -1094,6 +1093,10 @@ export interface IDataPluginServices extends Partial { storage: IStorageWrapper; // (undocumented) uiSettings: CoreStart_2['uiSettings']; + // Warning: (ae-forgotten-export) The symbol "UsageCollectionStart" needs to be exported by the entry point index.d.ts + // + // (undocumented) + usageCollection?: UsageCollectionStart; } // Warning: (ae-missing-release-tag) "IEsSearchRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -2250,8 +2253,8 @@ export const SEARCH_SESSIONS_MANAGEMENT_ID = "search_sessions"; // Warning: (ae-missing-release-tag) "SearchBar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const SearchBar: React.ComponentClass, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "trackUiMetric" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +export const SearchBar: React.ComponentClass, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; }; // Warning: (ae-forgotten-export) The symbol "SearchBarOwnProps" needs to be exported by the entry point index.d.ts @@ -2379,7 +2382,7 @@ export class SearchSource { // @deprecated fetch(options?: ISearchOptions): Promise>; getField(field: K, recurse?: boolean): SearchSourceFields[K]; - getFields(recurse?: boolean): SearchSourceFields; + getFields(): SearchSourceFields; getId(): string; getOwnField(field: K): SearchSourceFields[K]; getParent(): SearchSource | undefined; @@ -2425,6 +2428,8 @@ export interface SearchSourceFields { // (undocumented) index?: IndexPattern; // (undocumented) + parent?: SearchSourceFields; + // (undocumented) query?: Query; // Warning: (ae-forgotten-export) The symbol "EsQuerySearchAfter" needs to be exported by the entry point index.d.ts // diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index acf9a4b084c0f3..8686823ef05683 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -19,7 +19,7 @@ import { ISearchSetup, ISearchStart, SearchEnhancements } from './search'; import { QuerySetup, QueryStart } from './query'; import { IndexPatternsContract } from './index_patterns'; import { IndexPatternSelectProps, StatefulSearchBarProps } from './ui'; -import { UsageCollectionSetup } from '../../usage_collection/public'; +import { UsageCollectionSetup, UsageCollectionStart } from '../../usage_collection/public'; import { Setup as InspectorSetup } from '../../inspector/public'; import { NowProviderPublicContract } from './now_provider'; @@ -120,4 +120,5 @@ export interface IDataPluginServices extends Partial { http: CoreStart['http']; storage: IStorageWrapper; data: DataPublicPluginStart; + usageCollection?: UsageCollectionStart; } diff --git a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx index 5b52a529ae7aa7..9605eba9c1c282 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_bar.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_bar.tsx @@ -11,12 +11,12 @@ import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import classNames from 'classnames'; import React, { useState } from 'react'; -import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics'; +import { METRIC_TYPE } from '@kbn/analytics'; import { FilterEditor } from './filter_editor'; import { FILTER_EDITOR_WIDTH, FilterItem } from './filter_item'; import { FilterOptions } from './filter_options'; import { useKibana } from '../../../../kibana_react/public'; -import { IIndexPattern } from '../..'; +import { IDataPluginServices, IIndexPattern } from '../..'; import { buildEmptyFilter, Filter, @@ -36,17 +36,16 @@ interface Props { indexPatterns: IIndexPattern[]; intl: InjectedIntl; appName: string; - // Track UI Metrics - trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; } function FilterBarUI(props: Props) { const [isAddFilterPopoverOpen, setIsAddFilterPopoverOpen] = useState(false); - const kibana = useKibana(); - - const uiSettings = kibana.services.uiSettings; + const kibana = useKibana(); + const { appName, usageCollection, uiSettings } = kibana.services; if (!uiSettings) return null; + const reportUiCounter = usageCollection?.reportUiCounter.bind(usageCollection, appName); + function onFiltersUpdated(filters: Filter[]) { if (props.onFiltersUpdated) { props.onFiltersUpdated(filters); @@ -119,66 +118,65 @@ function FilterBarUI(props: Props) { } function onAdd(filter: Filter) { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:added`); setIsAddFilterPopoverOpen(false); - if (props.trackUiMetric) { - props.trackUiMetric(METRIC_TYPE.CLICK, `${props.appName}:filter_added`); - } + const filters = [...props.filters, filter]; onFiltersUpdated(filters); } function onRemove(i: number) { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:removed`); const filters = [...props.filters]; filters.splice(i, 1); onFiltersUpdated(filters); } function onUpdate(i: number, filter: Filter) { - if (props.trackUiMetric) { - props.trackUiMetric(METRIC_TYPE.CLICK, `${props.appName}:filter_edited`); - } + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:edited`); const filters = [...props.filters]; filters[i] = filter; onFiltersUpdated(filters); } function onEnableAll() { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:enable_all`); const filters = props.filters.map(enableFilter); onFiltersUpdated(filters); } function onDisableAll() { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:disable_all`); const filters = props.filters.map(disableFilter); onFiltersUpdated(filters); } function onPinAll() { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:pin_all`); const filters = props.filters.map(pinFilter); onFiltersUpdated(filters); } function onUnpinAll() { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:unpin_all`); const filters = props.filters.map(unpinFilter); onFiltersUpdated(filters); } function onToggleAllNegated() { - if (props.trackUiMetric) { - props.trackUiMetric(METRIC_TYPE.CLICK, `${props.appName}:filter_invertInclusion`); - } + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:invert_all`); const filters = props.filters.map(toggleFilterNegated); onFiltersUpdated(filters); } function onToggleAllDisabled() { - if (props.trackUiMetric) { - props.trackUiMetric(METRIC_TYPE.CLICK, `${props.appName}:filter_toggleAllDisabled`); - } + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:toggle_all`); const filters = props.filters.map(toggleFilterDisabled); onFiltersUpdated(filters); } function onRemoveAll() { + reportUiCounter?.(METRIC_TYPE.CLICK, `filter:remove_all`); onFiltersUpdated([]); } diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index aa42e11d318543..aa2fc9e6314361 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -26,6 +26,7 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { debounce, compact, isEqual, isFunction } from 'lodash'; import { Toast } from 'src/core/public'; +import { METRIC_TYPE } from '@kbn/analytics'; import { IDataPluginServices, IIndexPattern, Query } from '../..'; import { QuerySuggestion, QuerySuggestionTypes } from '../../autocomplete'; @@ -105,6 +106,10 @@ export default class QueryStringInputUI extends Component { private abortController?: AbortController; private fetchIndexPatternsAbortController?: AbortController; private services = this.props.kibana.services; + private reportUiCounter = this.services.usageCollection?.reportUiCounter.bind( + this.services.usageCollection, + this.services.appName + ); private componentIsUnmounting = false; private queryBarInputDivRefInstance: RefObject = createRef(); @@ -178,12 +183,14 @@ export default class QueryStringInputUI extends Component { selectionEnd, signal: this.abortController.signal, })) || []; - return [...suggestions, ...recentSearchSuggestions]; } catch (e) { // TODO: Waiting on https://github.com/elastic/kibana/issues/51406 for a properly typed error // Ignore aborted requests if (e.message === 'The user aborted a request.') return; + + this.reportUiCounter?.(METRIC_TYPE.LOADED, `query_string:suggestions_error`); + throw e; } }; @@ -302,7 +309,7 @@ export default class QueryStringInputUI extends Component { } if (isSuggestionsVisible && index !== null && this.state.suggestions[index]) { event.preventDefault(); - this.selectSuggestion(this.state.suggestions[index]); + this.selectSuggestion(this.state.suggestions[index], index); } else { this.onSubmit(this.props.query); this.setState({ @@ -335,7 +342,7 @@ export default class QueryStringInputUI extends Component { } }; - private selectSuggestion = (suggestion: QuerySuggestion) => { + private selectSuggestion = (suggestion: QuerySuggestion, listIndex: number) => { if (!this.inputRef) { return; } @@ -352,6 +359,17 @@ export default class QueryStringInputUI extends Component { const value = query.substr(0, selectionStart) + query.substr(selectionEnd); const newQueryString = value.substr(0, start) + text + value.substr(end); + this.reportUiCounter?.( + METRIC_TYPE.LOADED, + `query_string:${type}:suggestions_select_position`, + listIndex + ); + this.reportUiCounter?.( + METRIC_TYPE.LOADED, + `query_string:${type}:suggestions_select_q_length`, + end - start + ); + this.onQueryStringChange(newQueryString); this.setState({ @@ -458,6 +476,7 @@ export default class QueryStringInputUI extends Component { const newQuery = { query: '', language }; this.onChange(newQuery); this.onSubmit(newQuery); + this.reportUiCounter?.(METRIC_TYPE.LOADED, `query_string:language:${language}`); }; private onOutsideClick = () => { @@ -480,11 +499,11 @@ export default class QueryStringInputUI extends Component { } }; - private onClickSuggestion = (suggestion: QuerySuggestion) => { + private onClickSuggestion = (suggestion: QuerySuggestion, index: number) => { if (!this.inputRef) { return; } - this.selectSuggestion(suggestion); + this.selectSuggestion(suggestion, index); this.inputRef.focus(); }; @@ -588,6 +607,7 @@ export default class QueryStringInputUI extends Component { if (this.props.onChangeQueryInputFocus) { this.props.onChangeQueryInputFocus(true); } + requestAnimationFrame(() => { this.handleAutoHeight(); }); diff --git a/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx b/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx index 7873886432cbec..077b9ac47286d9 100644 --- a/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx +++ b/src/plugins/data/public/ui/saved_query_form/save_query_form.tsx @@ -9,7 +9,6 @@ import React, { useEffect, useState, useCallback } from 'react'; import { EuiButtonEmpty, - EuiOverlayMask, EuiModal, EuiButton, EuiModalHeader, @@ -208,37 +207,35 @@ export function SaveQueryForm({ ); return ( - - - - - {i18n.translate('data.search.searchBar.savedQueryFormTitle', { - defaultMessage: 'Save query', - })} - - - - {saveQueryForm} - - - - {i18n.translate('data.search.searchBar.savedQueryFormCancelButtonText', { - defaultMessage: 'Cancel', - })} - - - - {i18n.translate('data.search.searchBar.savedQueryFormSaveButtonText', { - defaultMessage: 'Save', - })} - - - - + + + + {i18n.translate('data.search.searchBar.savedQueryFormTitle', { + defaultMessage: 'Save query', + })} + + + + {saveQueryForm} + + + + {i18n.translate('data.search.searchBar.savedQueryFormCancelButtonText', { + defaultMessage: 'Cancel', + })} + + + + {i18n.translate('data.search.searchBar.savedQueryFormSaveButtonText', { + defaultMessage: 'Save', + })} + + + ); } diff --git a/src/plugins/data/public/ui/saved_query_management/saved_query_list_item.tsx b/src/plugins/data/public/ui/saved_query_management/saved_query_list_item.tsx index 47a2d050a9bfa3..b7ba3215eb5aa3 100644 --- a/src/plugins/data/public/ui/saved_query_management/saved_query_list_item.tsx +++ b/src/plugins/data/public/ui/saved_query_management/saved_query_list_item.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { EuiListGroupItem, EuiConfirmModal, EuiOverlayMask, EuiIconTip } from '@elastic/eui'; +import { EuiListGroupItem, EuiConfirmModal, EuiIconTip } from '@elastic/eui'; import React, { Fragment, useState } from 'react'; import classNames from 'classnames'; @@ -114,36 +114,34 @@ export const SavedQueryListItem = ({ /> {showDeletionConfirmationModal && ( - - { - onDelete(savedQuery); - setShowDeletionConfirmationModal(false); - }} - buttonColor="danger" - onCancel={() => { - setShowDeletionConfirmationModal(false); - }} - /> - + { + onDelete(savedQuery); + setShowDeletionConfirmationModal(false); + }} + buttonColor="danger" + onCancel={() => { + setShowDeletionConfirmationModal(false); + }} + /> )} ); diff --git a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx index 8f34ec1912cb44..4aba59442d204f 100644 --- a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx @@ -10,7 +10,6 @@ import _ from 'lodash'; import React, { useEffect, useRef } from 'react'; import { CoreStart } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { UiCounterMetricType } from '@kbn/analytics'; import { KibanaContextProvider } from '../../../../kibana_react/public'; import { QueryStart, SavedQuery } from '../../query'; import { SearchBar, SearchBarOwnProps } from './'; @@ -20,12 +19,13 @@ import { useSavedQuery } from './lib/use_saved_query'; import { DataPublicPluginStart } from '../../types'; import { Filter, Query, TimeRange } from '../../../common'; import { useQueryStringManager } from './lib/use_query_string_manager'; +import { UsageCollectionSetup } from '../../../../usage_collection/public'; interface StatefulSearchBarDeps { core: CoreStart; data: Omit; storage: IStorageWrapper; - trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; + usageCollection?: UsageCollectionSetup; } export type StatefulSearchBarProps = SearchBarOwnProps & { @@ -110,7 +110,7 @@ const overrideDefaultBehaviors = (props: StatefulSearchBarProps) => { return props.useDefaultBehaviors ? {} : props; }; -export function createSearchBar({ core, storage, data, trackUiMetric }: StatefulSearchBarDeps) { +export function createSearchBar({ core, storage, data, usageCollection }: StatefulSearchBarDeps) { // App name should come from the core application service. // Until it's available, we'll ask the user to provide it for the pre-wired component. return (props: StatefulSearchBarProps) => { @@ -161,6 +161,7 @@ export function createSearchBar({ core, storage, data, trackUiMetric }: Stateful appName: props.appName, data, storage, + usageCollection, ...core, }} > @@ -188,7 +189,6 @@ export function createSearchBar({ core, storage, data, trackUiMetric }: Stateful onClearSavedQuery={defaultOnClearSavedQuery(props, clearSavedQuery)} onSavedQueryUpdated={defaultOnSavedQueryUpdated(props, setSavedQuery)} onSaved={defaultOnSavedQueryUpdated(props, setSavedQuery)} - trackUiMetric={trackUiMetric} {...overrideDefaultBehaviors(props)} /> diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index c87fcc3d950410..fe8165a12714a6 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -13,7 +13,7 @@ import React, { Component } from 'react'; import ResizeObserver from 'resize-observer-polyfill'; import { get, isEqual } from 'lodash'; -import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics'; +import { METRIC_TYPE } from '@kbn/analytics'; import { withKibana, KibanaReactContextValue } from '../../../../kibana_react/public'; import QueryBarTopRow from '../query_string_input/query_bar_top_row'; @@ -68,8 +68,6 @@ export interface SearchBarOwnProps { onRefresh?: (payload: { dateRange: TimeRange }) => void; indicateNoData?: boolean; - // Track UI Metrics - trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; } export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps; @@ -323,9 +321,11 @@ class SearchBarUI extends Component { }, }); } - if (this.props.trackUiMetric) { - this.props.trackUiMetric(METRIC_TYPE.CLICK, `${this.services.appName}:query_submitted`); - } + this.services.usageCollection?.reportUiCounter( + this.services.appName, + METRIC_TYPE.CLICK, + 'query_submitted' + ); } ); }; @@ -428,7 +428,6 @@ class SearchBarUI extends Component { onFiltersUpdated={this.props.onFiltersUpdated} indexPatterns={this.props.indexPatterns!} appName={this.services.appName} - trackUiMetric={this.props.trackUiMetric} />
diff --git a/src/plugins/data/public/ui/typeahead/__snapshots__/suggestions_component.test.tsx.snap b/src/plugins/data/public/ui/typeahead/__snapshots__/suggestions_component.test.tsx.snap index 2fa7834872f6b1..9185e6a77d1026 100644 --- a/src/plugins/data/public/ui/typeahead/__snapshots__/suggestions_component.test.tsx.snap +++ b/src/plugins/data/public/ui/typeahead/__snapshots__/suggestions_component.test.tsx.snap @@ -22,6 +22,7 @@ exports[`SuggestionsComponent Passing the index should control which suggestion > { it('Should display the suggestion and use the provided ariaId', () => { const component = shallow( { it('Should make the element active if the selected prop is true', () => { const component = shallow( { mount( { const component = shallow( { component.simulate('click'); expect(mockHandler).toHaveBeenCalledTimes(1); - expect(mockHandler).toHaveBeenCalledWith(mockSuggestion); + expect(mockHandler).toHaveBeenCalledWith(mockSuggestion, 0); }); it('Should call onMouseEnter when user mouses over the element', () => { @@ -100,6 +104,7 @@ describe('SuggestionComponent', () => { const component = shallow( void; + onClick: SuggestionOnClick; onMouseEnter: () => void; selected: boolean; + index: number; suggestion: QuerySuggestion; innerRef: (node: HTMLDivElement) => void; ariaId: string; @@ -48,7 +50,7 @@ export function SuggestionComponent(props: Props) { active: props.selected, })} role="option" - onClick={() => props.onClick(props.suggestion)} + onClick={() => props.onClick(props.suggestion, props.index)} onMouseEnter={props.onMouseEnter} ref={props.innerRef} id={props.ariaId} diff --git a/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx b/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx index ebbdc7fc55e3af..dce8d5bdcfcd14 100644 --- a/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestions_component.test.tsx @@ -115,7 +115,7 @@ describe('SuggestionsComponent', () => { component.find(SuggestionComponent).at(1).simulate('click'); expect(mockCallback).toHaveBeenCalledTimes(1); - expect(mockCallback).toHaveBeenCalledWith(mockSuggestions[1]); + expect(mockCallback).toHaveBeenCalledWith(mockSuggestions[1], 1); }); it('Should call onMouseEnter with the index of the suggestion that was entered', () => { diff --git a/src/plugins/data/public/ui/typeahead/suggestions_component.tsx b/src/plugins/data/public/ui/typeahead/suggestions_component.tsx index fa1f4aa6a8ce8a..6bc91619fe8683 100644 --- a/src/plugins/data/public/ui/typeahead/suggestions_component.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestions_component.tsx @@ -17,11 +17,12 @@ import { SUGGESTIONS_LIST_REQUIRED_TOP_OFFSET, SUGGESTIONS_LIST_REQUIRED_WIDTH, } from './constants'; +import { SuggestionOnClick } from './types'; // @internal export interface SuggestionsComponentProps { index: number | null; - onClick: (suggestion: QuerySuggestion) => void; + onClick: SuggestionOnClick; onMouseEnter: (index: number) => void; show: boolean; suggestions: QuerySuggestion[]; @@ -50,6 +51,7 @@ export default class SuggestionsComponent extends Component (this.childNodes[index] = node)} selected={index === this.props.index} + index={index} suggestion={suggestion} onClick={this.props.onClick} onMouseEnter={() => this.props.onMouseEnter(index)} diff --git a/src/plugins/data/public/ui/typeahead/types.ts b/src/plugins/data/public/ui/typeahead/types.ts new file mode 100644 index 00000000000000..d0be717b2bf9b9 --- /dev/null +++ b/src/plugins/data/public/ui/typeahead/types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { QuerySuggestion } from '../../autocomplete'; + +export type SuggestionOnClick = (suggestion: QuerySuggestion, index: number) => void; diff --git a/src/plugins/data/server/autocomplete/value_suggestions_route.ts b/src/plugins/data/server/autocomplete/value_suggestions_route.ts index 8a633d8d827f4e..489a23eb83897c 100644 --- a/src/plugins/data/server/autocomplete/value_suggestions_route.ts +++ b/src/plugins/data/server/autocomplete/value_suggestions_route.ts @@ -57,17 +57,13 @@ export function registerValueSuggestionsRoute( const field = indexPattern && getFieldByName(fieldName, indexPattern); const body = await getBody(autocompleteSearchOptions, field || fieldName, query, filters); - try { - const result = await client.callAsCurrentUser('search', { index, body }, { signal }); + const result = await client.callAsCurrentUser('search', { index, body }, { signal }); - const buckets: any[] = - get(result, 'aggregations.suggestions.buckets') || - get(result, 'aggregations.nestedSuggestions.suggestions.buckets'); + const buckets: any[] = + get(result, 'aggregations.suggestions.buckets') || + get(result, 'aggregations.nestedSuggestions.suggestions.buckets'); - return response.ok({ body: map(buckets || [], 'key') }); - } catch (error) { - return response.internalError({ body: error }); - } + return response.ok({ body: map(buckets || [], 'key') }); } ); } diff --git a/src/plugins/discover/public/application/angular/context.js b/src/plugins/discover/public/application/angular/context.js index e4128b3e262473..01a28a5c174b6b 100644 --- a/src/plugins/discover/public/application/angular/context.js +++ b/src/plugins/discover/public/application/angular/context.js @@ -65,6 +65,7 @@ function ContextAppRouteController($routeParams, $scope, $route) { storeInSessionStorage: getServices().uiSettings.get('state:storeInSessionStorage'), history: getServices().history(), toasts: getServices().core.notifications.toasts, + uiSettings: getServices().core.uiSettings, }); this.state = { ...appState.getState() }; this.anchorId = $routeParams.id; diff --git a/src/plugins/discover/public/application/angular/context/api/anchor.js b/src/plugins/discover/public/application/angular/context/api/anchor.js index f1074be2ebcf25..83b611cb0d6488 100644 --- a/src/plugins/discover/public/application/angular/context/api/anchor.js +++ b/src/plugins/discover/public/application/angular/context/api/anchor.js @@ -32,7 +32,7 @@ export function fetchAnchorProvider(indexPatterns, searchSource, useNewFieldsApi .setField('sort', sort); if (useNewFieldsApi) { searchSource.removeField('fieldsFromSource'); - searchSource.setField('fields', ['*']); + searchSource.setField('fields', [{ field: '*', include_unmapped: 'true' }]); } const response = await searchSource.fetch(); diff --git a/src/plugins/discover/public/application/angular/context/api/anchor.test.js b/src/plugins/discover/public/application/angular/context/api/anchor.test.js index 5b077f7443c27e..12b9b4ab285567 100644 --- a/src/plugins/discover/public/application/angular/context/api/anchor.test.js +++ b/src/plugins/discover/public/application/angular/context/api/anchor.test.js @@ -154,7 +154,7 @@ describe('context app', function () { const removeFieldsSpy = searchSourceStub.removeField.withArgs('fieldsFromSource'); expect(setFieldsSpy.calledOnce).toBe(true); expect(removeFieldsSpy.calledOnce).toBe(true); - expect(setFieldsSpy.firstCall.args[1]).toEqual(['*']); + expect(setFieldsSpy.firstCall.args[1]).toEqual([{ field: '*', include_unmapped: 'true' }]); }); }); }); diff --git a/src/plugins/discover/public/application/angular/context/api/context.ts b/src/plugins/discover/public/application/angular/context/api/context.ts index 4887a0644b6eb6..43f6e83d286b36 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.ts @@ -114,7 +114,7 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract, useNewFields const searchSource = await data.search.searchSource.create(); if (useNewFieldsApi) { searchSource.removeField('fieldsFromSource'); - searchSource.setField('fields', ['*']); + searchSource.setField('fields', [{ field: '*', include_unmapped: 'true' }]); } return searchSource .setParent(undefined) diff --git a/src/plugins/discover/public/application/angular/context_state.test.ts b/src/plugins/discover/public/application/angular/context_state.test.ts index e7622a13394c13..b49a6546a993d3 100644 --- a/src/plugins/discover/public/application/angular/context_state.test.ts +++ b/src/plugins/discover/public/application/angular/context_state.test.ts @@ -6,10 +6,12 @@ * Side Public License, v 1. */ +import { IUiSettingsClient } from 'kibana/public'; import { getState } from './context_state'; import { createBrowserHistory, History } from 'history'; import { FilterManager, Filter } from '../../../../data/public'; import { coreMock } from '../../../../../core/public/mocks'; +import { SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; const setupMock = coreMock.createSetup(); describe('Test Discover Context State', () => { @@ -23,6 +25,10 @@ describe('Test Discover Context State', () => { defaultStepSize: '4', timeFieldName: 'time', history, + uiSettings: { + get: (key: string) => + ((key === SEARCH_FIELDS_FROM_SOURCE ? true : ['_source']) as unknown) as T, + } as IUiSettingsClient, }); state.startSync(); }); diff --git a/src/plugins/discover/public/application/angular/context_state.ts b/src/plugins/discover/public/application/angular/context_state.ts index d109badbdd1646..0bae006ec1f6e4 100644 --- a/src/plugins/discover/public/application/angular/context_state.ts +++ b/src/plugins/discover/public/application/angular/context_state.ts @@ -8,7 +8,7 @@ import _ from 'lodash'; import { History } from 'history'; -import { NotificationsStart } from 'kibana/public'; +import { NotificationsStart, IUiSettingsClient } from 'kibana/public'; import { createStateContainer, createKbnUrlStateStorage, @@ -17,6 +17,7 @@ import { withNotifyOnErrors, } from '../../../../kibana_utils/public'; import { esFilters, FilterManager, Filter, Query } from '../../../../data/public'; +import { handleSourceColumnState } from './helpers'; export interface AppState { /** @@ -73,6 +74,11 @@ interface GetStateParams { * kbnUrlStateStorage will use it notifying about inner errors */ toasts?: NotificationsStart['toasts']; + + /** + * core ui settings service + */ + uiSettings: IUiSettingsClient; } interface GetStateReturn { @@ -123,6 +129,7 @@ export function getState({ storeInSessionStorage = false, history, toasts, + uiSettings, }: GetStateParams): GetStateReturn { const stateStorage = createKbnUrlStateStorage({ useHash: storeInSessionStorage, @@ -134,7 +141,12 @@ export function getState({ const globalStateContainer = createStateContainer(globalStateInitial); const appStateFromUrl = stateStorage.get(APP_STATE_URL_KEY) as AppState; - const appStateInitial = createInitialAppState(defaultStepSize, timeFieldName, appStateFromUrl); + const appStateInitial = createInitialAppState( + defaultStepSize, + timeFieldName, + appStateFromUrl, + uiSettings + ); const appStateContainer = createStateContainer(appStateInitial); const { start, stop } = syncStates([ @@ -257,7 +269,8 @@ function getFilters(state: AppState | GlobalState): Filter[] { function createInitialAppState( defaultSize: string, timeFieldName: string, - urlState: AppState + urlState: AppState, + uiSettings: IUiSettingsClient ): AppState { const defaultState = { columns: ['_source'], @@ -270,8 +283,11 @@ function createInitialAppState( return defaultState; } - return { - ...defaultState, - ...urlState, - }; + return handleSourceColumnState( + { + ...defaultState, + ...urlState, + }, + uiSettings + ); } diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 3733e866989588..78ad40e48fd965 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -110,7 +110,7 @@ app.config(($routeProvider) => { const history = getHistory(); const savedSearchId = $route.current.params.id; return data.indexPatterns.ensureDefaultIndexPattern(history).then(() => { - const { appStateContainer } = getState({ history }); + const { appStateContainer } = getState({ history, uiSettings: config }); const { index } = appStateContainer.getState(); return Promise.props({ ip: loadIndexPattern(index, data.indexPatterns, config), @@ -169,10 +169,10 @@ function discoverController($route, $scope, Promise) { let inspectorRequest; let isChangingIndexPattern = false; const savedSearch = $route.current.locals.savedObjects.savedSearch; - $scope.searchSource = savedSearch.searchSource; + const persistentSearchSource = savedSearch.searchSource; $scope.indexPattern = resolveIndexPattern( $route.current.locals.savedObjects.ip, - $scope.searchSource, + persistentSearchSource, toastNotifications ); $scope.useNewFieldsApi = !config.get(SEARCH_FIELDS_FROM_SOURCE); @@ -195,6 +195,7 @@ function discoverController($route, $scope, Promise) { storeInSessionStorage: config.get('state:storeInSessionStorage'), history, toasts: core.notifications.toasts, + uiSettings: config, }); const { @@ -370,25 +371,19 @@ function discoverController($route, $scope, Promise) { }); }; - $scope.searchSource - .setField('index', $scope.indexPattern) - .setField('highlightAll', true) - .setField('version', true); - - // Even when searching rollups, we want to use the default strategy so that we get back a - // document-like response. - $scope.searchSource.setPreferredSearchStrategyId('default'); + persistentSearchSource.setField('index', $scope.indexPattern); // searchSource which applies time range - const timeRangeSearchSource = savedSearch.searchSource.create(); + const volatileSearchSource = savedSearch.searchSource.create(); if (isDefaultType($scope.indexPattern)) { - timeRangeSearchSource.setField('filter', () => { + volatileSearchSource.setField('filter', () => { return timefilter.createFilter($scope.indexPattern); }); } - $scope.searchSource.setParent(timeRangeSearchSource); + volatileSearchSource.setParent(persistentSearchSource); + $scope.volatileSearchSource = volatileSearchSource; const pageTitleSuffix = savedSearch.id && savedSearch.title ? `: ${savedSearch.title}` : ''; chrome.docTitle.change(`Discover${pageTitleSuffix}`); @@ -403,7 +398,8 @@ function discoverController($route, $scope, Promise) { } function getStateDefaults() { - const query = $scope.searchSource.getField('query') || data.query.queryString.getDefaultQuery(); + const query = + persistentSearchSource.getField('query') || data.query.queryString.getDefaultQuery(); const sort = getSortArray(savedSearch.sort, $scope.indexPattern); const columns = getDefaultColumns(); @@ -415,7 +411,7 @@ function discoverController($route, $scope, Promise) { columns, index: $scope.indexPattern.id, interval: 'auto', - filters: _.cloneDeep($scope.searchSource.getOwnField('filter')), + filters: _.cloneDeep(persistentSearchSource.getOwnField('filter')), }; if (savedSearch.grid) { defaultState.grid = savedSearch.grid; @@ -556,7 +552,7 @@ function discoverController($route, $scope, Promise) { .then(function () { $scope.fetchStatus = fetchStatuses.LOADING; logInspectorRequest({ searchSessionId }); - return $scope.searchSource.fetch({ + return $scope.volatileSearchSource.fetch({ abortSignal: abortController.signal, sessionId: searchSessionId, }); @@ -603,11 +599,13 @@ function discoverController($route, $scope, Promise) { } function onResults(resp) { - inspectorRequest.stats(getResponseInspectorStats(resp, $scope.searchSource)).ok({ json: resp }); + inspectorRequest + .stats(getResponseInspectorStats(resp, $scope.volatileSearchSource)) + .ok({ json: resp }); if (getTimeField() && !$scope.state.hideChart) { const tabifiedData = tabifyAggResponse($scope.opts.chartAggConfigs, resp); - $scope.searchSource.rawResponse = resp; + $scope.volatileSearchSource.rawResponse = resp; $scope.histogramData = discoverResponseHandler( tabifiedData, getDimensions($scope.opts.chartAggConfigs.aggs, $scope.timeRange) @@ -635,8 +633,8 @@ function discoverController($route, $scope, Promise) { defaultMessage: 'This request queries Elasticsearch to fetch the data for the search.', }); inspectorRequest = inspectorAdapters.requests.start(title, { description, searchSessionId }); - inspectorRequest.stats(getRequestInspectorStats($scope.searchSource)); - $scope.searchSource.getSearchRequestBody().then((body) => { + inspectorRequest.stats(getRequestInspectorStats($scope.volatileSearchSource)); + $scope.volatileSearchSource.getSearchRequestBody().then((body) => { inspectorRequest.json(body); }); } @@ -693,9 +691,11 @@ function discoverController($route, $scope, Promise) { }; $scope.updateDataSource = () => { - const { indexPattern, searchSource, useNewFieldsApi } = $scope; + const { indexPattern, useNewFieldsApi } = $scope; const { columns, sort } = $scope.state; - updateSearchSource(searchSource, { + updateSearchSource({ + persistentSearchSource, + volatileSearchSource: $scope.volatileSearchSource, indexPattern, services, sort, @@ -731,12 +731,12 @@ function discoverController($route, $scope, Promise) { visStateAggs ); - $scope.searchSource.onRequestStart((searchSource, options) => { + $scope.volatileSearchSource.onRequestStart((searchSource, options) => { if (!$scope.opts.chartAggConfigs) return; return $scope.opts.chartAggConfigs.onSearchRequestStart(searchSource, options); }); - $scope.searchSource.setField('aggs', function () { + $scope.volatileSearchSource.setField('aggs', function () { if (!$scope.opts.chartAggConfigs) return; return $scope.opts.chartAggConfigs.toDsl(); }); diff --git a/src/plugins/discover/public/application/angular/discover_datagrid.html b/src/plugins/discover/public/application/angular/discover_datagrid.html index e59ebbb0fafd03..42218568a838db 100644 --- a/src/plugins/discover/public/application/angular/discover_datagrid.html +++ b/src/plugins/discover/public/application/angular/discover_datagrid.html @@ -17,7 +17,7 @@ reset-query="resetQuery" result-state="resultState" rows="rows" - search-source="searchSource" + search-source="volatileSearchSource" set-index-pattern="setIndexPattern" show-save-query="showSaveQuery" state="state" diff --git a/src/plugins/discover/public/application/angular/discover_legacy.html b/src/plugins/discover/public/application/angular/discover_legacy.html index 501496494106af..a01f285b1a1506 100644 --- a/src/plugins/discover/public/application/angular/discover_legacy.html +++ b/src/plugins/discover/public/application/angular/discover_legacy.html @@ -13,7 +13,7 @@ reset-query="resetQuery" result-state="resultState" rows="rows" - search-source="searchSource" + search-source="volatileSearchSource" state="state" time-range="timeRange" top-nav-menu="topNavMenu" diff --git a/src/plugins/discover/public/application/angular/discover_state.test.ts b/src/plugins/discover/public/application/angular/discover_state.test.ts index d3b7ed53e421af..e7322a85886311 100644 --- a/src/plugins/discover/public/application/angular/discover_state.test.ts +++ b/src/plugins/discover/public/application/angular/discover_state.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { IUiSettingsClient } from 'kibana/public'; import { getState, GetStateReturn, @@ -14,11 +15,17 @@ import { import { createBrowserHistory, History } from 'history'; import { dataPluginMock } from '../../../../data/public/mocks'; import { SavedSearch } from '../../saved_searches'; +import { SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; let history: History; let state: GetStateReturn; const getCurrentUrl = () => history.createHref(history.location); +const uiSettingsMock = { + get: (key: string) => + ((key === SEARCH_FIELDS_FROM_SOURCE ? true : ['_source']) as unknown) as T, +} as IUiSettingsClient; + describe('Test discover state', () => { beforeEach(async () => { history = createBrowserHistory(); @@ -26,6 +33,7 @@ describe('Test discover state', () => { state = getState({ getStateDefaults: () => ({ index: 'test' }), history, + uiSettings: uiSettingsMock, }); await state.replaceUrlAppState({}); await state.startSync(); @@ -81,6 +89,7 @@ describe('Test discover state with legacy migration', () => { state = getState({ getStateDefaults: () => ({ index: 'test' }), history, + uiSettings: uiSettingsMock, }); expect(state.appStateContainer.getState()).toMatchInlineSnapshot(` Object { @@ -106,6 +115,7 @@ describe('createSearchSessionRestorationDataProvider', () => { data: mockDataPlugin, appStateContainer: getState({ history: createBrowserHistory(), + uiSettings: uiSettingsMock, }).appStateContainer, getSavedSearch: () => mockSavedSearch, }); diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover/public/application/angular/discover_state.ts index 93fc49b65cbc92..e7d5ed469525f7 100644 --- a/src/plugins/discover/public/application/angular/discover_state.ts +++ b/src/plugins/discover/public/application/angular/discover_state.ts @@ -9,7 +9,7 @@ import { isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; import { History } from 'history'; -import { NotificationsStart } from 'kibana/public'; +import { NotificationsStart, IUiSettingsClient } from 'kibana/public'; import { createKbnUrlStateStorage, createStateContainer, @@ -30,6 +30,7 @@ import { migrateLegacyQuery } from '../helpers/migrate_legacy_query'; import { DiscoverGridSettings } from '../components/discover_grid/types'; import { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from '../../url_generator'; import { SavedSearch } from '../../saved_searches'; +import { handleSourceColumnState } from './helpers'; export interface AppState { /** @@ -90,6 +91,11 @@ interface GetStateParams { * kbnUrlStateStorage will use it notifying about inner errors */ toasts?: NotificationsStart['toasts']; + + /** + * core ui settings service + */ + uiSettings: IUiSettingsClient; } export interface GetStateReturn { @@ -149,6 +155,7 @@ export function getState({ storeInSessionStorage = false, history, toasts, + uiSettings, }: GetStateParams): GetStateReturn { const defaultAppState = getStateDefaults ? getStateDefaults() : {}; const stateStorage = createKbnUrlStateStorage({ @@ -163,10 +170,14 @@ export function getState({ appStateFromUrl.query = migrateLegacyQuery(appStateFromUrl.query); } - let initialAppState = { - ...defaultAppState, - ...appStateFromUrl, - }; + let initialAppState = handleSourceColumnState( + { + ...defaultAppState, + ...appStateFromUrl, + }, + uiSettings + ); + // todo filter source depending on fields fetchinbg flag (if no columns remain and source fetching is enabled, use default columns) let previousAppState: AppState; const appStateContainer = createStateContainer(initialAppState); diff --git a/src/plugins/discover/public/application/angular/helpers/index.ts b/src/plugins/discover/public/application/angular/helpers/index.ts index a4ab9e575e4217..3d4893268fdee8 100644 --- a/src/plugins/discover/public/application/angular/helpers/index.ts +++ b/src/plugins/discover/public/application/angular/helpers/index.ts @@ -8,3 +8,4 @@ export { buildPointSeriesData } from './point_series'; export { formatRow } from './row_formatter'; +export { handleSourceColumnState } from './state_helpers'; diff --git a/src/plugins/discover/public/application/angular/helpers/state_helpers.ts b/src/plugins/discover/public/application/angular/helpers/state_helpers.ts new file mode 100644 index 00000000000000..ec5009dcf48392 --- /dev/null +++ b/src/plugins/discover/public/application/angular/helpers/state_helpers.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IUiSettingsClient } from 'src/core/public'; +import { SEARCH_FIELDS_FROM_SOURCE, DEFAULT_COLUMNS_SETTING } from '../../../../common'; + +/** + * Makes sure the current state is not referencing the source column when using the fields api + * @param state + * @param uiSettings + */ +export function handleSourceColumnState( + state: TState, + uiSettings: IUiSettingsClient +): TState { + if (!state.columns) { + return state; + } + const useNewFieldsApi = !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); + const defaultColumns = uiSettings.get(DEFAULT_COLUMNS_SETTING); + if (useNewFieldsApi) { + // if fields API is used, filter out the source column + return { + ...state, + columns: state.columns.filter((column) => column !== '_source'), + }; + } else if (state.columns.length === 0) { + // if _source fetching is used and there are no column, switch back to default columns + // this can happen if the fields API was previously used + return { + ...state, + columns: [...defaultColumns], + }; + } + + return state; +} diff --git a/src/plugins/discover/public/application/components/discover.tsx b/src/plugins/discover/public/application/components/discover.tsx index 71650a4a38472e..1d183aa75cf3a5 100644 --- a/src/plugins/discover/public/application/components/discover.tsx +++ b/src/plugins/discover/public/application/components/discover.tsx @@ -206,6 +206,7 @@ export function Discover({ query={state.query} savedQuery={state.savedQuery} updateQuery={updateQuery} + searchSource={searchSource} />

diff --git a/src/plugins/discover/public/application/components/discover_grid/constants.ts b/src/plugins/discover/public/application/components/discover_grid/constants.ts index 03e5740793396e..015d0b65246f2e 100644 --- a/src/plugins/discover/public/application/components/discover_grid/constants.ts +++ b/src/plugins/discover/public/application/components/discover_grid/constants.ts @@ -8,7 +8,6 @@ // data types export const kibanaJSON = 'kibana-json'; -export const geoPoint = 'geo-point'; export const gridStyle = { border: 'all', fontSize: 's', diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx index a4e59a50a2f6c7..fcaea63f2a77c4 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx @@ -315,7 +315,8 @@ export const DiscoverGrid = ({ { - return {children}; - }, [kibanaJSON]: ({ children }) => { return ( diff --git a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx index 45f30a9d26f935..cfcdbec475eda4 100644 --- a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx @@ -7,7 +7,6 @@ */ import React, { Fragment, useContext, useEffect } from 'react'; -import { i18n } from '@kbn/i18n'; import themeLight from '@elastic/eui/dist/eui_theme_light.json'; import themeDark from '@elastic/eui/dist/eui_theme_dark.json'; @@ -83,21 +82,6 @@ export const getRenderCellValueFn = ( return {JSON.stringify(rowFlattened[columnId])}; } - if (field?.type === 'geo_point' && rowFlattened && rowFlattened[columnId]) { - const valueFormatted = rowFlattened[columnId] as { lat: number; lon: number }; - return ( -
- {i18n.translate('discover.latitudeAndLongitude', { - defaultMessage: 'Lat: {lat} Lon: {lon}', - values: { - lat: valueFormatted?.lat, - lon: valueFormatted?.lon, - }, - })} -
- ); - } - const valueFormatted = indexPattern.formatField(row, columnId); if (typeof valueFormatted === 'undefined') { return -; diff --git a/src/plugins/discover/public/application/components/discover_topnav.test.tsx b/src/plugins/discover/public/application/components/discover_topnav.test.tsx index 50c9bcfb5d2557..891dc63c92c7c6 100644 --- a/src/plugins/discover/public/application/components/discover_topnav.test.tsx +++ b/src/plugins/discover/public/application/components/discover_topnav.test.tsx @@ -20,7 +20,7 @@ import { SavedObject } from '../../../../../core/types'; import { DiscoverTopNav, DiscoverTopNavProps } from './discover_topnav'; import { RequestAdapter } from '../../../../inspector/common/adapters/request'; import { TopNavMenu } from '../../../../navigation/public'; -import { Query } from '../../../../data/common'; +import { ISearchSource, Query } from '../../../../data/common'; import { DiscoverSearchSessionManager } from '../angular/discover_search_session'; import { Subject } from 'rxjs'; @@ -61,6 +61,7 @@ function getProps(): DiscoverTopNavProps { savedQuery: '', updateQuery: jest.fn(), onOpenInspector: jest.fn(), + searchSource: {} as ISearchSource, }; } diff --git a/src/plugins/discover/public/application/components/discover_topnav.tsx b/src/plugins/discover/public/application/components/discover_topnav.tsx index fd2aba22aa41db..ee59ee13583bdd 100644 --- a/src/plugins/discover/public/application/components/discover_topnav.tsx +++ b/src/plugins/discover/public/application/components/discover_topnav.tsx @@ -10,7 +10,7 @@ import { DiscoverProps } from './types'; import { getTopNavLinks } from './top_nav/get_top_nav_links'; import { Query, TimeRange } from '../../../../data/common/query'; -export type DiscoverTopNavProps = Pick & { +export type DiscoverTopNavProps = Pick & { onOpenInspector: () => void; query?: Query; savedQuery?: string; @@ -24,6 +24,7 @@ export const DiscoverTopNav = ({ query, savedQuery, updateQuery, + searchSource, }: DiscoverTopNavProps) => { const showDatePicker = useMemo(() => indexPattern.isTimeBased(), [indexPattern]); const { TopNavMenu } = opts.services.navigation.ui; @@ -38,8 +39,9 @@ export const DiscoverTopNav = ({ services: opts.services, state: opts.stateContainer, onOpenInspector, + searchSource, }), - [indexPattern, opts, onOpenInspector] + [indexPattern, opts, onOpenInspector, searchSource] ); const updateSavedQueryId = (newSavedQueryId: string | undefined) => { diff --git a/src/plugins/discover/public/application/components/doc/doc.test.tsx b/src/plugins/discover/public/application/components/doc/doc.test.tsx index 623d30d5f53ecf..deaaa1853ae9da 100644 --- a/src/plugins/discover/public/application/components/doc/doc.test.tsx +++ b/src/plugins/discover/public/application/components/doc/doc.test.tsx @@ -13,6 +13,7 @@ import { mountWithIntl } from '@kbn/test/jest'; import { ReactWrapper } from 'enzyme'; import { findTestSubject } from '@elastic/eui/lib/test'; import { Doc, DocProps } from './doc'; +import { SEARCH_FIELDS_FROM_SOURCE as mockSearchFieldsFromSource } from '../../../../common'; const mockSearchApi = jest.fn(); @@ -36,6 +37,13 @@ jest.mock('../../../kibana_services', () => { }, }, }, + uiSettings: { + get: (key: string) => { + if (key === mockSearchFieldsFromSource) { + return false; + } + }, + }, }), getDocViewsRegistry: () => ({ addDocView(view: any) { diff --git a/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx b/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx index 6cb0bf1288d3a8..dc0b6416a7f576 100644 --- a/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx +++ b/src/plugins/discover/public/application/components/doc/use_es_doc_search.test.tsx @@ -10,6 +10,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { buildSearchBody, useEsDocSearch, ElasticRequestState } from './use_es_doc_search'; import { DocProps } from './doc'; import { Observable } from 'rxjs'; +import { SEARCH_FIELDS_FROM_SOURCE as mockSearchFieldsFromSource } from '../../../../common'; const mockSearchResult = new Observable(); @@ -22,19 +23,55 @@ jest.mock('../../../kibana_services', () => ({ }), }, }, + uiSettings: { + get: (key: string) => { + if (key === mockSearchFieldsFromSource) { + return false; + } + }, + }, }), })); describe('Test of helper / hook', () => { - test('buildSearchBody', () => { + test('buildSearchBody given useNewFieldsApi is false', () => { const indexPattern = { getComputedFields: () => ({ storedFields: [], scriptFields: [], docvalueFields: [] }), } as any; - const actual = buildSearchBody('1', indexPattern); + const actual = buildSearchBody('1', indexPattern, false); expect(actual).toMatchInlineSnapshot(` Object { "_source": true, "docvalue_fields": Array [], + "fields": undefined, + "query": Object { + "ids": Object { + "values": Array [ + "1", + ], + }, + }, + "script_fields": Array [], + "stored_fields": Array [], + } + `); + }); + + test('buildSearchBody useNewFieldsApi is true', () => { + const indexPattern = { + getComputedFields: () => ({ storedFields: [], scriptFields: [], docvalueFields: [] }), + } as any; + const actual = buildSearchBody('1', indexPattern, true); + expect(actual).toMatchInlineSnapshot(` + Object { + "_source": false, + "docvalue_fields": Array [], + "fields": Array [ + Object { + "field": "*", + "include_unmapped": "true", + }, + ], "query": Object { "ids": Object { "values": Array [ diff --git a/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts b/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts index 2a63a62650ca90..703a1e557e801a 100644 --- a/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts +++ b/src/plugins/discover/public/application/components/doc/use_es_doc_search.ts @@ -6,10 +6,11 @@ * Side Public License, v 1. */ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useMemo } from 'react'; import { IndexPattern, getServices } from '../../../kibana_services'; import { DocProps } from './doc'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../common'; export enum ElasticRequestState { Loading, @@ -23,7 +24,11 @@ export enum ElasticRequestState { * helper function to build a query body for Elasticsearch * https://www.elastic.co/guide/en/elasticsearch/reference/current//query-dsl-ids-query.html */ -export function buildSearchBody(id: string, indexPattern: IndexPattern): Record { +export function buildSearchBody( + id: string, + indexPattern: IndexPattern, + useNewFieldsApi: boolean +): Record { const computedFields = indexPattern.getComputedFields(); return { @@ -33,7 +38,8 @@ export function buildSearchBody(id: string, indexPattern: IndexPattern): Record< }, }, stored_fields: computedFields.storedFields, - _source: true, + _source: !useNewFieldsApi, + fields: useNewFieldsApi ? [{ field: '*', include_unmapped: 'true' }] : undefined, script_fields: computedFields.scriptFields, docvalue_fields: computedFields.docvalueFields, }; @@ -51,6 +57,8 @@ export function useEsDocSearch({ const [indexPattern, setIndexPattern] = useState(null); const [status, setStatus] = useState(ElasticRequestState.Loading); const [hit, setHit] = useState(null); + const { data, uiSettings } = useMemo(() => getServices(), []); + const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]); useEffect(() => { async function requestData() { @@ -58,11 +66,11 @@ export function useEsDocSearch({ const indexPatternEntity = await indexPatternService.get(indexPatternId); setIndexPattern(indexPatternEntity); - const { rawResponse } = await getServices() - .data.search.search({ + const { rawResponse } = await data.search + .search({ params: { index, - body: buildSearchBody(id, indexPatternEntity), + body: buildSearchBody(id, indexPatternEntity, useNewFieldsApi), }, }) .toPromise(); @@ -86,6 +94,6 @@ export function useEsDocSearch({ } } requestData(); - }, [id, index, indexPatternId, indexPatternService]); + }, [id, index, indexPatternId, indexPatternService, data.search, useNewFieldsApi]); return [status, hit, indexPattern]; } diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx index 62e19b83b016e4..6afa7f89371f98 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx +++ b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer.test.tsx @@ -16,6 +16,11 @@ import { DocViewRenderProps } from '../../doc_views/doc_views_types'; jest.mock('../../../kibana_services', () => { let registry: any[] = []; return { + getServices: () => ({ + uiSettings: { + get: jest.fn(), + }, + }), getDocViewsRegistry: () => ({ addDocView(view: any) { registry.push(view); diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_render_error.tsx b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_render_error.tsx index 571af94f293728..b9b068ce4bd073 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_render_error.tsx +++ b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_render_error.tsx @@ -7,20 +7,18 @@ */ import React from 'react'; -import { EuiCallOut, EuiCodeBlock } from '@elastic/eui'; -import { formatMsg, formatStack } from '../../../../../kibana_legacy/public'; +import { EuiErrorBoundary } from '@elastic/eui'; interface Props { error: Error | string; } -export function DocViewerError({ error }: Props) { - const errMsg = formatMsg(error); - const errStack = typeof error === 'object' ? formatStack(error) : ''; +const DocViewerErrorWrapper = ({ error }: Props) => { + throw error; +}; - return ( - - {errStack && {errStack}} - - ); -} +export const DocViewerError = ({ error }: Props) => ( + + + +); diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx index 1da75b45239105..25454a3bad38ab 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx +++ b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.tsx @@ -11,6 +11,8 @@ import { I18nProvider } from '@kbn/i18n/react'; import { DocViewRenderTab } from './doc_viewer_render_tab'; import { DocViewerError } from './doc_viewer_render_error'; import { DocViewRenderFn, DocViewRenderProps } from '../../doc_views/doc_views_types'; +import { getServices } from '../../../kibana_services'; +import { KibanaContextProvider } from '../../../../../kibana_react/public'; interface Props { component?: React.ComponentType; @@ -72,7 +74,9 @@ export class DocViewerTab extends React.Component { const Component = component as any; return ( - + + + ); } diff --git a/src/plugins/discover/public/application/components/json_code_block/__snapshots__/json_code_block.test.tsx.snap b/src/plugins/discover/public/application/components/json_code_block/__snapshots__/json_code_block.test.tsx.snap deleted file mode 100644 index d6f48a9b3c7745..00000000000000 --- a/src/plugins/discover/public/application/components/json_code_block/__snapshots__/json_code_block.test.tsx.snap +++ /dev/null @@ -1,20 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`returns the \`JsonCodeEditor\` component 1`] = ` - - { - "_index": "test", - "_type": "doc", - "_id": "foo", - "_score": 1, - "_source": { - "test": 123 - } -} - -`; diff --git a/src/plugins/discover/public/application/components/json_code_block/json_code_block.tsx b/src/plugins/discover/public/application/components/json_code_block/json_code_block.tsx deleted file mode 100644 index 7dab2b199b018f..00000000000000 --- a/src/plugins/discover/public/application/components/json_code_block/json_code_block.tsx +++ /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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { EuiCodeBlock } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { DocViewRenderProps } from '../../doc_views/doc_views_types'; - -export function JsonCodeBlock({ hit }: DocViewRenderProps) { - const label = i18n.translate('discover.docViews.json.codeEditorAriaLabel', { - defaultMessage: 'Read only JSON view of an elasticsearch document', - }); - return ( - - {JSON.stringify(hit, null, 2)} - - ); -} diff --git a/src/plugins/discover/public/application/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap b/src/plugins/discover/public/application/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap new file mode 100644 index 00000000000000..4f27158eee04f1 --- /dev/null +++ b/src/plugins/discover/public/application/components/json_code_editor/__snapshots__/json_code_editor.test.tsx.snap @@ -0,0 +1,71 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`returns the \`JsonCodeEditor\` component 1`] = ` + + + +
+ + + +
+
+ + + +
+`; diff --git a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.scss b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.scss new file mode 100644 index 00000000000000..5521df5b363acd --- /dev/null +++ b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.scss @@ -0,0 +1,3 @@ +.dscJsonCodeEditor { + width: 100% +} diff --git a/src/plugins/discover/public/application/components/json_code_block/json_code_block.test.tsx b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.test.tsx similarity index 53% rename from src/plugins/discover/public/application/components/json_code_block/json_code_block.test.tsx rename to src/plugins/discover/public/application/components/json_code_editor/json_code_editor.test.tsx index dd56a1077f1ac6..4ccb3010d5a2b9 100644 --- a/src/plugins/discover/public/application/components/json_code_block/json_code_block.test.tsx +++ b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.test.tsx @@ -8,17 +8,15 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { JsonCodeBlock } from './json_code_block'; -import { IndexPattern } from '../../../../../data/public'; +import { JsonCodeEditor } from './json_code_editor'; it('returns the `JsonCodeEditor` component', () => { - const props = { - hit: { _index: 'test', _type: 'doc', _id: 'foo', _score: 1, _source: { test: 123 } }, - columns: [], - indexPattern: {} as IndexPattern, - filter: jest.fn(), - onAddColumn: jest.fn(), - onRemoveColumn: jest.fn(), + const value = { + _index: 'test', + _type: 'doc', + _id: 'foo', + _score: 1, + _source: { test: 123 }, }; - expect(shallow()).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); diff --git a/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx new file mode 100644 index 00000000000000..85d6aad7552502 --- /dev/null +++ b/src/plugins/discover/public/application/components/json_code_editor/json_code_editor.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import './json_code_editor.scss'; + +import React, { useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { monaco, XJsonLang } from '@kbn/monaco'; +import { EuiButtonEmpty, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { CodeEditor } from '../../../../../kibana_react/public'; +import { DocViewRenderProps } from '../../../application/doc_views/doc_views_types'; + +const codeEditorAriaLabel = i18n.translate('discover.json.codeEditorAriaLabel', { + defaultMessage: 'Read only JSON view of an elasticsearch document', +}); +const copyToClipboardLabel = i18n.translate('discover.json.copyToClipboardLabel', { + defaultMessage: 'Copy to clipboard', +}); + +export const JsonCodeEditor = ({ hit }: DocViewRenderProps) => { + const jsonValue = JSON.stringify(hit, null, 2); + + // setting editor height based on lines height and count to stretch and fit its content + const setEditorCalculatedHeight = useCallback((editor) => { + const editorElement = editor.getDomNode(); + + if (!editorElement) { + return; + } + + const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight); + const lineCount = editor.getModel()?.getLineCount() || 1; + const height = editor.getTopForLineNumber(lineCount + 1) + lineHeight; + + editorElement.style.height = `${height}px`; + editor.layout(); + }, []); + + return ( + + + +
+ + {(copy) => ( + + {copyToClipboardLabel} + + )} + +
+
+ + {}} + editorDidMount={setEditorCalculatedHeight} + aria-label={codeEditorAriaLabel} + options={{ + automaticLayout: true, + fontSize: 12, + minimap: { + enabled: false, + }, + overviewRulerBorder: false, + readOnly: true, + scrollbar: { + alwaysConsumeMouseWheel: false, + }, + scrollBeyondLastLine: false, + wordWrap: 'on', + wrappingIndent: 'indent', + }} + /> + +
+ ); +}; diff --git a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts index 89980f7fd0f54a..9792e98ba84c7d 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.test.ts @@ -47,6 +47,7 @@ const fieldCounts = { category: 1, currency: 1, customer_birth_date: 1, + unknown_field: 1, }; describe('group_fields', function () { @@ -232,7 +233,7 @@ describe('group_fields', function () { const actual = groupFields( fieldsWithUnmappedField as IndexPatternField[], - ['customer_birth_date', 'currency', 'unknown'], + ['customer_birth_date', 'currency'], 5, fieldCounts, fieldFilterState, @@ -241,4 +242,30 @@ describe('group_fields', function () { ); expect(actual.unpopular).toEqual([]); }); + + it('includes unmapped fields when reading from source', function () { + const fieldFilterState = getDefaultFieldFilter(); + const fieldsWithUnmappedField = [...fields]; + fieldsWithUnmappedField.push({ + name: 'unknown_field', + type: 'unknown', + esTypes: ['unknown'], + count: 0, + scripted: false, + searchable: false, + aggregatable: false, + readFromDocValues: false, + }); + + const actual = groupFields( + fieldsWithUnmappedField as IndexPatternField[], + ['customer_birth_date', 'currency'], + 5, + fieldCounts, + fieldFilterState, + false, + undefined + ); + expect(actual.unpopular.map((field) => field.name)).toEqual(['unknown_field']); + }); }); diff --git a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx index c7242a8518b553..eefb96b78aac66 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx +++ b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx @@ -64,7 +64,9 @@ export function groupFields( } else if (field.type !== '_source') { // do not show unmapped fields unless explicitly specified // do not add subfields to this list - if ((field.type !== 'unknown' || showUnmappedFields) && !isSubfield) { + if (useNewFieldsApi && (field.type !== 'unknown' || showUnmappedFields) && !isSubfield) { + result.unpopular.push(field); + } else if (!useNewFieldsApi) { result.unpopular.push(field); } } diff --git a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.test.ts b/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.test.ts index 89cb60700074d1..30edb102c420aa 100644 --- a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.test.ts +++ b/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { ISearchSource } from 'src/plugins/data/public'; import { getTopNavLinks } from './get_top_nav_links'; import { inspectorPluginMock } from '../../../../../inspector/public/mocks'; import { indexPatternMock } from '../../../__mocks__/index_pattern'; @@ -33,6 +34,7 @@ test('getTopNavLinks result', () => { savedSearch: savedSearchMock, services, state, + searchSource: {} as ISearchSource, }); expect(topNavLinks).toMatchInlineSnapshot(` Array [ diff --git a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.ts b/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.ts index 513508c478aa95..a1215836f9c5fc 100644 --- a/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.ts +++ b/src/plugins/discover/public/application/components/top_nav/get_top_nav_links.ts @@ -15,7 +15,7 @@ import { Adapters } from '../../../../../inspector/common/adapters'; import { SavedSearch } from '../../../saved_searches'; import { onSaveSearch } from './on_save_search'; import { GetStateReturn } from '../../angular/discover_state'; -import { IndexPattern } from '../../../kibana_services'; +import { IndexPattern, ISearchSource } from '../../../kibana_services'; /** * Helper function to build the top nav links @@ -29,6 +29,7 @@ export const getTopNavLinks = ({ services, state, onOpenInspector, + searchSource, }: { getFieldCounts: () => Promise>; indexPattern: IndexPattern; @@ -38,6 +39,7 @@ export const getTopNavLinks = ({ services: DiscoverServices; state: GetStateReturn; onOpenInspector: () => void; + searchSource: ISearchSource; }) => { const newSearch = { id: 'new', @@ -93,7 +95,7 @@ export const getTopNavLinks = ({ return; } const sharingData = await getSharingData( - savedSearch.searchSource, + searchSource, state.appStateContainer.getState(), services.uiSettings, getFieldCounts diff --git a/src/plugins/discover/public/application/helpers/get_sharing_data.ts b/src/plugins/discover/public/application/helpers/get_sharing_data.ts index 85acd575138f71..31de1f2f6ed66a 100644 --- a/src/plugins/discover/public/application/helpers/get_sharing_data.ts +++ b/src/plugins/discover/public/application/helpers/get_sharing_data.ts @@ -9,7 +9,7 @@ import { Capabilities, IUiSettingsClient } from 'kibana/public'; import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../common'; import { getSortForSearchSource } from '../angular/doc_table'; -import { SearchSource } from '../../../../data/common'; +import { ISearchSource } from '../../../../data/common'; import { AppState } from '../angular/discover_state'; import { SortOrder } from '../../saved_searches/types'; @@ -39,7 +39,7 @@ const getSharingDataFields = async ( * Preparing data to share the current state as link or CSV/Report */ export async function getSharingData( - currentSearchSource: SearchSource, + currentSearchSource: ISearchSource, state: AppState, config: IUiSettingsClient, getFieldCounts: () => Promise> diff --git a/src/plugins/discover/public/application/helpers/persist_saved_search.ts b/src/plugins/discover/public/application/helpers/persist_saved_search.ts index 06e90c93bc77c9..2e4ab90ee58e54 100644 --- a/src/plugins/discover/public/application/helpers/persist_saved_search.ts +++ b/src/plugins/discover/public/application/helpers/persist_saved_search.ts @@ -35,7 +35,8 @@ export async function persistSavedSearch( state: AppState; } ) { - updateSearchSource(savedSearch.searchSource, { + updateSearchSource({ + persistentSearchSource: savedSearch.searchSource, indexPattern, services, sort: state.sort as SortOrder[], diff --git a/src/plugins/discover/public/application/helpers/update_search_source.test.ts b/src/plugins/discover/public/application/helpers/update_search_source.test.ts index 51586a6bccc234..97e2de3541d354 100644 --- a/src/plugins/discover/public/application/helpers/update_search_source.test.ts +++ b/src/plugins/discover/public/application/helpers/update_search_source.test.ts @@ -17,9 +17,12 @@ import { SortOrder } from '../../saved_searches/types'; describe('updateSearchSource', () => { test('updates a given search source', async () => { - const searchSourceMock = createSearchSourceMock({}); + const persistentSearchSourceMock = createSearchSourceMock({}); + const volatileSearchSourceMock = createSearchSourceMock({}); const sampleSize = 250; - const result = updateSearchSource(searchSourceMock, { + updateSearchSource({ + persistentSearchSource: persistentSearchSourceMock, + volatileSearchSource: volatileSearchSourceMock, indexPattern: indexPatternMock, services: ({ data: dataPluginMock.createStartContract(), @@ -36,15 +39,18 @@ describe('updateSearchSource', () => { columns: [], useNewFieldsApi: false, }); - expect(result.getField('index')).toEqual(indexPatternMock); - expect(result.getField('size')).toEqual(sampleSize); - expect(result.getField('fields')).toBe(undefined); + expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock); + expect(volatileSearchSourceMock.getField('size')).toEqual(sampleSize); + expect(volatileSearchSourceMock.getField('fields')).toBe(undefined); }); test('updates a given search source with the usage of the new fields api', async () => { - const searchSourceMock = createSearchSourceMock({}); + const persistentSearchSourceMock = createSearchSourceMock({}); + const volatileSearchSourceMock = createSearchSourceMock({}); const sampleSize = 250; - const result = updateSearchSource(searchSourceMock, { + updateSearchSource({ + persistentSearchSource: persistentSearchSourceMock, + volatileSearchSource: volatileSearchSourceMock, indexPattern: indexPatternMock, services: ({ data: dataPluginMock.createStartContract(), @@ -61,16 +67,19 @@ describe('updateSearchSource', () => { columns: [], useNewFieldsApi: true, }); - expect(result.getField('index')).toEqual(indexPatternMock); - expect(result.getField('size')).toEqual(sampleSize); - expect(result.getField('fields')).toEqual([{ field: '*' }]); - expect(result.getField('fieldsFromSource')).toBe(undefined); + expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock); + expect(volatileSearchSourceMock.getField('size')).toEqual(sampleSize); + expect(volatileSearchSourceMock.getField('fields')).toEqual([{ field: '*' }]); + expect(volatileSearchSourceMock.getField('fieldsFromSource')).toBe(undefined); }); test('requests unmapped fields when the flag is provided, using the new fields api', async () => { - const searchSourceMock = createSearchSourceMock({}); + const persistentSearchSourceMock = createSearchSourceMock({}); + const volatileSearchSourceMock = createSearchSourceMock({}); const sampleSize = 250; - const result = updateSearchSource(searchSourceMock, { + updateSearchSource({ + persistentSearchSource: persistentSearchSourceMock, + volatileSearchSource: volatileSearchSourceMock, indexPattern: indexPatternMock, services: ({ data: dataPluginMock.createStartContract(), @@ -88,16 +97,21 @@ describe('updateSearchSource', () => { useNewFieldsApi: true, showUnmappedFields: true, }); - expect(result.getField('index')).toEqual(indexPatternMock); - expect(result.getField('size')).toEqual(sampleSize); - expect(result.getField('fields')).toEqual([{ field: '*', include_unmapped: 'true' }]); - expect(result.getField('fieldsFromSource')).toBe(undefined); + expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock); + expect(volatileSearchSourceMock.getField('size')).toEqual(sampleSize); + expect(volatileSearchSourceMock.getField('fields')).toEqual([ + { field: '*', include_unmapped: 'true' }, + ]); + expect(volatileSearchSourceMock.getField('fieldsFromSource')).toBe(undefined); }); test('updates a given search source when showUnmappedFields option is set to true', async () => { - const searchSourceMock = createSearchSourceMock({}); + const persistentSearchSourceMock = createSearchSourceMock({}); + const volatileSearchSourceMock = createSearchSourceMock({}); const sampleSize = 250; - const result = updateSearchSource(searchSourceMock, { + updateSearchSource({ + persistentSearchSource: persistentSearchSourceMock, + volatileSearchSource: volatileSearchSourceMock, indexPattern: indexPatternMock, services: ({ data: dataPluginMock.createStartContract(), @@ -115,9 +129,11 @@ describe('updateSearchSource', () => { useNewFieldsApi: true, showUnmappedFields: true, }); - expect(result.getField('index')).toEqual(indexPatternMock); - expect(result.getField('size')).toEqual(sampleSize); - expect(result.getField('fields')).toEqual([{ field: '*', include_unmapped: 'true' }]); - expect(result.getField('fieldsFromSource')).toBe(undefined); + expect(persistentSearchSourceMock.getField('index')).toEqual(indexPatternMock); + expect(volatileSearchSourceMock.getField('size')).toEqual(sampleSize); + expect(volatileSearchSourceMock.getField('fields')).toEqual([ + { field: '*', include_unmapped: 'true' }, + ]); + expect(volatileSearchSourceMock.getField('fieldsFromSource')).toBe(undefined); }); }); diff --git a/src/plugins/discover/public/application/helpers/update_search_source.ts b/src/plugins/discover/public/application/helpers/update_search_source.ts index 55d2b05a29b690..ba5ac0e8227965 100644 --- a/src/plugins/discover/public/application/helpers/update_search_source.ts +++ b/src/plugins/discover/public/application/helpers/update_search_source.ts @@ -15,24 +15,25 @@ import { DiscoverServices } from '../../build_services'; /** * Helper function to update the given searchSource before fetching/sharing/persisting */ -export function updateSearchSource( - searchSource: ISearchSource, - { - indexPattern, - services, - sort, - columns, - useNewFieldsApi, - showUnmappedFields, - }: { - indexPattern: IndexPattern; - services: DiscoverServices; - sort: SortOrder[]; - columns: string[]; - useNewFieldsApi: boolean; - showUnmappedFields?: boolean; - } -) { +export function updateSearchSource({ + indexPattern, + services, + sort, + columns, + useNewFieldsApi, + showUnmappedFields, + persistentSearchSource, + volatileSearchSource, +}: { + indexPattern: IndexPattern; + services: DiscoverServices; + sort: SortOrder[]; + columns: string[]; + useNewFieldsApi: boolean; + showUnmappedFields?: boolean; + persistentSearchSource: ISearchSource; + volatileSearchSource?: ISearchSource; +}) { const { uiSettings, data } = services; const usedSort = getSortForSearchSource( sort, @@ -40,23 +41,32 @@ export function updateSearchSource( uiSettings.get(SORT_DEFAULT_ORDER_SETTING) ); - searchSource + persistentSearchSource .setField('index', indexPattern) - .setField('size', uiSettings.get(SAMPLE_SIZE_SETTING)) - .setField('sort', usedSort) .setField('query', data.query.queryString.getQuery() || null) .setField('filter', data.query.filterManager.getFilters()); - if (useNewFieldsApi) { - searchSource.removeField('fieldsFromSource'); - const fields: Record = { field: '*' }; - if (showUnmappedFields) { - fields.include_unmapped = 'true'; + + if (volatileSearchSource) { + volatileSearchSource + .setField('size', uiSettings.get(SAMPLE_SIZE_SETTING)) + .setField('sort', usedSort) + .setField('highlightAll', true) + .setField('version', true) + // Even when searching rollups, we want to use the default strategy so that we get back a + // document-like response. + .setPreferredSearchStrategyId('default'); + + if (useNewFieldsApi) { + volatileSearchSource.removeField('fieldsFromSource'); + const fields: Record = { field: '*' }; + if (showUnmappedFields) { + fields.include_unmapped = 'true'; + } + volatileSearchSource.setField('fields', [fields]); + } else { + volatileSearchSource.removeField('fields'); + const fieldNames = indexPattern.fields.map((field) => field.name); + volatileSearchSource.setField('fieldsFromSource', fieldNames); } - searchSource.setField('fields', [fields]); - } else { - searchSource.removeField('fields'); - const fieldNames = indexPattern.fields.map((field) => field.name); - searchSource.setField('fieldsFromSource', fieldNames); } - return searchSource; } diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index c53dfaff24112f..47161c2b8298e1 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -36,7 +36,7 @@ import { UrlGeneratorState } from '../../share/public'; import { DocViewInput, DocViewInputFn } from './application/doc_views/doc_views_types'; import { DocViewsRegistry } from './application/doc_views/doc_views_registry'; import { DocViewTable } from './application/components/table/table'; -import { JsonCodeBlock } from './application/components/json_code_block/json_code_block'; +import { JsonCodeEditor } from './application/components/json_code_editor/json_code_editor'; import { setDocViewsRegistry, setUrlTracker, @@ -187,7 +187,7 @@ export class DiscoverPlugin defaultMessage: 'JSON', }), order: 20, - component: JsonCodeBlock, + component: JsonCodeEditor, }); const { diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts index b645b62c863d5d..4a45cff0b96049 100644 --- a/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts +++ b/src/plugins/es_ui_shared/__packages_do_not_import__/errors/handle_es_error.ts @@ -38,5 +38,5 @@ export const handleEsError = ({ }); } // Case: default - return response.internalError({ body: error }); + throw error; }; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts index 40f44f3671f312..181bd9959c1bbd 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -211,11 +211,12 @@ export function useForm( // ---------------------------------- const addField: FormHook['__addField'] = useCallback( (field) => { + const fieldExists = fieldsRefs.current[field.path] !== undefined; fieldsRefs.current[field.path] = field; updateFormDataAt(field.path, field.value); - if (!field.isValidated) { + if (!fieldExists && !field.isValidated) { setIsValid(undefined); // When we submit the form (and set "isSubmitted" to "true"), we validate **all fields**. diff --git a/src/plugins/home/server/services/sample_data/routes/install.ts b/src/plugins/home/server/services/sample_data/routes/install.ts index 7c00a46602e26e..a20c3e350222f3 100644 --- a/src/plugins/home/server/services/sample_data/routes/install.ts +++ b/src/plugins/home/server/services/sample_data/routes/install.ts @@ -136,8 +136,7 @@ export function createInstallRoute( (counts as any)[index] = count; } catch (err) { const errMsg = `sample_data install errors while loading data. Error: ${err}`; - logger.warn(errMsg); - return res.internalError({ body: errMsg }); + throw new Error(errMsg); } } @@ -157,8 +156,7 @@ export function createInstallRoute( ); } catch (err) { const errMsg = `bulkCreate failed, error: ${err.message}`; - logger.warn(errMsg); - return res.internalError({ body: errMsg }); + throw new Error(errMsg); } const errors = createResults.saved_objects.filter((savedObjectCreateResult) => { return Boolean(savedObjectCreateResult.error); diff --git a/src/plugins/index_pattern_field_editor/README.md b/src/plugins/index_pattern_field_editor/README.md new file mode 100644 index 00000000000000..10949954cef38f --- /dev/null +++ b/src/plugins/index_pattern_field_editor/README.md @@ -0,0 +1,69 @@ +# Index pattern field editor + +The reusable field editor across Kibana! + +This editor can be used to + +* create or edit a runtime field inside an index pattern. +* edit concrete (mapped) fields. In this case certain functionalities will be disabled like the possibility to change the field _type_ or to set the field _value_. + +## How to use + +You first need to add in your kibana.json the "`indexPatternFieldEditor`" plugin as a required dependency of your plugin. + +You will then receive in the start contract of the indexPatternFieldEditor plugin the following API: + +### `openEditor(options: OpenFieldEditorOptions): CloseEditor` + +Use this method to open the index pattern field editor to either create (runtime) or edit (concrete | runtime) a field. + +#### `options` + +`ctx: FieldEditorContext` (**required**) + +This is the only required option. You need to provide the context in which the editor is being consumed. This object has the following properties: + +- `indexPattern: IndexPattern`: the index pattern you want to create/edit the field into. + +`onSave(field: IndexPatternField): void` (optional) + +You can provide an optional `onSave` handler to be notified when the field has being created/updated. This handler is called after the field has been persisted to the saved object. + +`fieldName: string` (optional) + +You can optionally pass the name of a field to edit. Leave empty to create a new runtime field based field. + +### `userPermissions.editIndexPattern(): boolean` + +Convenience method that uses the `core.application.capabilities` api to determine whether the user can edit the index pattern. + +### `` + +This children func React component provides a handler to delete one or multiple runtime fields. + +#### Props + +* `indexPattern: IndexPattern`: the current index pattern. (**required**) + +```js + +const { DeleteRuntimeFieldProvider } = indexPatternFieldEditor; + +// Single field + + {(deleteField) => ( + deleteField('myField')}> + Delete + + )} + + +// Multiple fields + + {(deleteFields) => ( + deleteFields(['field1', 'field2', 'field3'])}> + Delete + + )} + +``` diff --git a/src/plugins/index_pattern_field_editor/jest.config.js b/src/plugins/index_pattern_field_editor/jest.config.js new file mode 100644 index 00000000000000..fc358c37116c98 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/jest.config.js @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/index_pattern_field_editor'], +}; diff --git a/src/plugins/index_pattern_field_editor/kibana.json b/src/plugins/index_pattern_field_editor/kibana.json new file mode 100644 index 00000000000000..1e44b43ab36390 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "indexPatternFieldEditor", + "version": "kibana", + "server": false, + "ui": true, + "requiredPlugins": ["data"], + "optionalPlugins": ["usageCollection"], + "requiredBundles": ["kibanaReact", "esUiShared", "usageCollection"] +} diff --git a/src/plugins/index_pattern_field_editor/public/assets/icons/LICENSE.txt b/src/plugins/index_pattern_field_editor/public/assets/icons/LICENSE.txt new file mode 100644 index 00000000000000..1a86627c4a6b8c --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/assets/icons/LICENSE.txt @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Steven Skelton + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/plugins/index_pattern_field_editor/public/assets/icons/cv.png b/src/plugins/index_pattern_field_editor/public/assets/icons/cv.png new file mode 100644 index 00000000000000..8f2ff8432e6bd2 Binary files /dev/null and b/src/plugins/index_pattern_field_editor/public/assets/icons/cv.png differ diff --git a/src/plugins/index_pattern_field_editor/public/assets/icons/de.png b/src/plugins/index_pattern_field_editor/public/assets/icons/de.png new file mode 100644 index 00000000000000..78279117b4d101 Binary files /dev/null and b/src/plugins/index_pattern_field_editor/public/assets/icons/de.png differ diff --git a/src/plugins/index_pattern_field_editor/public/assets/icons/go.png b/src/plugins/index_pattern_field_editor/public/assets/icons/go.png new file mode 100644 index 00000000000000..34c317db5adf34 Binary files /dev/null and b/src/plugins/index_pattern_field_editor/public/assets/icons/go.png differ diff --git a/src/plugins/index_pattern_field_editor/public/assets/icons/ne.png b/src/plugins/index_pattern_field_editor/public/assets/icons/ne.png new file mode 100644 index 00000000000000..d331209e179988 Binary files /dev/null and b/src/plugins/index_pattern_field_editor/public/assets/icons/ne.png differ diff --git a/src/plugins/index_pattern_field_editor/public/assets/icons/ni.png b/src/plugins/index_pattern_field_editor/public/assets/icons/ni.png new file mode 100644 index 00000000000000..e5bdb0b668d415 Binary files /dev/null and b/src/plugins/index_pattern_field_editor/public/assets/icons/ni.png differ diff --git a/src/plugins/index_pattern_field_editor/public/assets/icons/stop.png b/src/plugins/index_pattern_field_editor/public/assets/icons/stop.png new file mode 100644 index 00000000000000..4bf65fc96f59fd Binary files /dev/null and b/src/plugins/index_pattern_field_editor/public/assets/icons/stop.png differ diff --git a/src/plugins/index_pattern_field_editor/public/assets/icons/us.png b/src/plugins/index_pattern_field_editor/public/assets/icons/us.png new file mode 100644 index 00000000000000..f30f21f85d06a0 Binary files /dev/null and b/src/plugins/index_pattern_field_editor/public/assets/icons/us.png differ diff --git a/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/delete_field_provider.tsx b/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/delete_field_provider.tsx new file mode 100644 index 00000000000000..a42e1c18c1a614 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/delete_field_provider.tsx @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; + +type DeleteFieldFunc = (fieldName: string | string[]) => void; + +export interface Props { + children: (deleteFieldHandler: DeleteFieldFunc) => React.ReactNode; + onConfirmDelete: (fieldsToDelete: string[]) => Promise; +} + +interface State { + isModalOpen: boolean; + fieldsToDelete: string[]; +} + +const geti18nTexts = (fieldsToDelete?: string[]) => { + let modalTitle = ''; + if (fieldsToDelete) { + const isSingle = fieldsToDelete.length === 1; + + modalTitle = isSingle + ? i18n.translate( + 'indexPatternFieldEditor.deleteRuntimeField.confirmModal.deleteSingleTitle', + { + defaultMessage: `Remove field '{name}'?`, + values: { name: fieldsToDelete[0] }, + } + ) + : i18n.translate( + 'indexPatternFieldEditor.deleteRuntimeField.confirmModal.deleteMultipleTitle', + { + defaultMessage: `Remove {count} fields?`, + values: { count: fieldsToDelete.length }, + } + ); + } + + return { + modalTitle, + confirmButtonText: i18n.translate( + 'indexPatternFieldEditor.deleteRuntimeField.confirmationModal.removeButtonLabel', + { + defaultMessage: 'Remove', + } + ), + cancelButtonText: i18n.translate( + 'indexPatternFieldEditor.deleteRuntimeField.confirmationModal.cancelButtonLabel', + { + defaultMessage: 'Cancel', + } + ), + warningMultipleFields: i18n.translate( + 'indexPatternFieldEditor.deleteRuntimeField.confirmModal.multipleDeletionDescription', + { + defaultMessage: 'You are about to remove these runtime fields:', + } + ), + }; +}; + +export const DeleteRuntimeFieldProvider = ({ children, onConfirmDelete }: Props) => { + const [state, setState] = useState({ isModalOpen: false, fieldsToDelete: [] }); + + const { isModalOpen, fieldsToDelete } = state; + const i18nTexts = geti18nTexts(fieldsToDelete); + const { modalTitle, confirmButtonText, cancelButtonText, warningMultipleFields } = i18nTexts; + const isMultiple = Boolean(fieldsToDelete.length > 1); + + const deleteField: DeleteFieldFunc = useCallback((fieldNames) => { + setState({ + isModalOpen: true, + fieldsToDelete: Array.isArray(fieldNames) ? fieldNames : [fieldNames], + }); + }, []); + + const closeModal = useCallback(() => { + setState({ isModalOpen: false, fieldsToDelete: [] }); + }, []); + + const confirmDelete = useCallback(async () => { + try { + await onConfirmDelete(fieldsToDelete); + closeModal(); + } catch (e) { + // silently fail as "onConfirmDelete" is responsible + // to show a toast message if there is an error + } + }, [closeModal, onConfirmDelete, fieldsToDelete]); + + return ( + <> + {children(deleteField)} + + {isModalOpen && ( + + + {isMultiple && ( + <> +

{warningMultipleFields}

+
    + {fieldsToDelete.map((fieldName) => ( +
  • {fieldName}
  • + ))} +
+ + )} +
+
+ )} + + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/get_delete_provider.tsx b/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/get_delete_provider.tsx new file mode 100644 index 00000000000000..c8f1ad90357617 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/get_delete_provider.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { NotificationsStart } from 'src/core/public'; +import { IndexPattern, UsageCollectionStart } from '../../shared_imports'; +import { pluginName } from '../../constants'; +import { DeleteRuntimeFieldProvider, Props as DeleteProviderProps } from './delete_field_provider'; +import { DataPublicPluginStart } from '../../../../data/public'; + +export interface Props extends Omit { + indexPattern: IndexPattern; + onDelete?: (fieldNames: string[]) => void; +} + +export const getDeleteProvider = ( + indexPatternService: DataPublicPluginStart['indexPatterns'], + usageCollection: UsageCollectionStart, + notifications: NotificationsStart +): React.FunctionComponent => { + return React.memo(({ indexPattern, children, onDelete }: Props) => { + const deleteFields = useCallback( + async (fieldNames: string[]) => { + fieldNames.forEach((fieldName) => { + indexPattern.removeRuntimeField(fieldName); + }); + + try { + usageCollection.reportUiCounter( + pluginName, + usageCollection.METRIC_TYPE.COUNT, + 'delete_runtime' + ); + // eslint-disable-next-line no-empty + } catch {} + + try { + await indexPatternService.updateSavedObject(indexPattern); + } catch (e) { + const title = i18n.translate('indexPatternFieldEditor.save.deleteErrorTitle', { + defaultMessage: 'Failed to save field removal', + }); + notifications.toasts.addError(e, { title }); + } + + if (onDelete) { + onDelete(fieldNames); + } + }, + [onDelete, indexPattern] + ); + + return ; + }); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/index.ts b/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/index.ts new file mode 100644 index 00000000000000..b93b7b92560ecf --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/delete_field_provider/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { getDeleteProvider, Props as DeleteProviderProps } from './get_delete_provider'; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/advanced_parameters_section.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/advanced_parameters_section.tsx new file mode 100644 index 00000000000000..26504eee28ddbd --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/advanced_parameters_section.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { EuiButtonEmpty, EuiSpacer } from '@elastic/eui'; + +interface Props { + children: React.ReactNode; +} + +export const AdvancedParametersSection = ({ children }: Props) => { + const [isVisible, setIsVisible] = useState(false); + + const toggleIsVisible = () => { + setIsVisible(!isVisible); + }; + + return ( + <> + + {isVisible + ? i18n.translate('indexPatternFieldEditor.editor.form.advancedSettings.hideButtonLabel', { + defaultMessage: 'Hide advanced settings', + }) + : i18n.translate('indexPatternFieldEditor.editor.form.advancedSettings.showButtonLabel', { + defaultMessage: 'Show advanced settings', + })} + + +
+ + {/* We ned to wrap the children inside a "div" to have our css :first-child rule */} +
{children}
+
+ + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/constants.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor/constants.ts new file mode 100644 index 00000000000000..82711f707fa199 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/constants.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { EuiComboBoxOptionOption } from '@elastic/eui'; +import { RuntimeType } from '../../shared_imports'; + +export const RUNTIME_FIELD_OPTIONS: Array> = [ + { + label: 'Keyword', + value: 'keyword', + }, + { + label: 'Long', + value: 'long', + }, + { + label: 'Double', + value: 'double', + }, + { + label: 'Date', + value: 'date', + }, + { + label: 'IP', + value: 'ip', + }, + { + label: 'Boolean', + value: 'boolean', + }, +]; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.test.tsx new file mode 100644 index 00000000000000..562f15301590bb --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.test.tsx @@ -0,0 +1,212 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { act } from 'react-dom/test-utils'; + +import '../../test_utils/setup_environment'; +import { registerTestBed, TestBed, getCommonActions } from '../../test_utils'; +import { Field } from '../../types'; +import { FieldEditor, Props, FieldEditorFormState } from './field_editor'; + +const defaultProps: Props = { + onChange: jest.fn(), + links: { + runtimePainless: 'https://elastic.co', + }, + ctx: { + existingConcreteFields: [], + namesNotAllowed: [], + fieldTypeToProcess: 'runtime', + }, + indexPattern: { fields: [] } as any, + fieldFormatEditors: { + getAll: () => [], + getById: () => undefined, + }, + fieldFormats: {} as any, + uiSettings: {} as any, + syntaxError: { + error: null, + clear: () => {}, + }, +}; + +const setup = (props?: Partial) => { + const testBed = registerTestBed(FieldEditor, { + memoryRouter: { + wrapComponent: false, + }, + })({ ...defaultProps, ...props }) as TestBed; + + const actions = { + ...getCommonActions(testBed), + }; + + return { + ...testBed, + actions, + }; +}; + +describe('', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + let testBed: TestBed & { actions: ReturnType }; + let onChange: jest.Mock = jest.fn(); + + const lastOnChangeCall = (): FieldEditorFormState[] => + onChange.mock.calls[onChange.mock.calls.length - 1]; + + const getLastStateUpdate = () => lastOnChangeCall()[0]; + + const submitFormAndGetData = async (state: FieldEditorFormState) => { + let formState: + | { + data: Field; + isValid: boolean; + } + | undefined; + + let promise: ReturnType; + + await act(async () => { + // We can't await for the promise here as the validation for the + // "script" field has a setTimeout which is mocked by jest. If we await + // we don't have the chance to call jest.advanceTimersByTime and thus the + // test times out. + promise = state.submit(); + }); + + await act(async () => { + // The painless syntax validation has a timeout set to 600ms + // we give it a bit more time just to be on the safe side + jest.advanceTimersByTime(1000); + }); + + await act(async () => { + promise.then((response) => { + formState = response; + }); + }); + + return formState!; + }; + + beforeEach(() => { + onChange = jest.fn(); + }); + + test('initial state should have "set custom label", "set value" and "set format" turned off', () => { + testBed = setup(); + + ['customLabel', 'value', 'format'].forEach((row) => { + const testSubj = `${row}Row.toggle`; + const toggle = testBed.find(testSubj); + const isOn = toggle.props()['aria-checked']; + + try { + expect(isOn).toBe(false); + } catch (e) { + e.message = `"${row}" row toggle expected to be 'off' but was 'on'. \n${e.message}`; + throw e; + } + }); + }); + + test('should accept a defaultValue and onChange prop to forward the form state', async () => { + const field = { + name: 'foo', + type: 'date', + script: { source: 'emit("hello")' }, + }; + + testBed = setup({ onChange, field }); + + expect(onChange).toHaveBeenCalled(); + + let lastState = getLastStateUpdate(); + expect(lastState.isValid).toBe(undefined); + expect(lastState.isSubmitted).toBe(false); + expect(lastState.submit).toBeDefined(); + + const { data: formData } = await submitFormAndGetData(lastState); + expect(formData).toEqual(field); + + // Make sure that both isValid and isSubmitted state are now "true" + lastState = getLastStateUpdate(); + expect(lastState.isValid).toBe(true); + expect(lastState.isSubmitted).toBe(true); + }); + + describe('validation', () => { + test('should accept an optional list of existing fields and prevent creating duplicates', async () => { + const existingFields = ['myRuntimeField']; + testBed = setup({ + onChange, + ctx: { + namesNotAllowed: existingFields, + existingConcreteFields: [], + fieldTypeToProcess: 'runtime', + }, + }); + + const { form, component, actions } = testBed; + + await act(async () => { + actions.toggleFormRow('value'); + }); + + await act(async () => { + form.setInputValue('nameField.input', existingFields[0]); + form.setInputValue('scriptField', 'echo("hello")'); + }); + + await act(async () => { + jest.advanceTimersByTime(1000); // Make sure our debounced error message is in the DOM + }); + + const lastState = getLastStateUpdate(); + await submitFormAndGetData(lastState); + component.update(); + expect(getLastStateUpdate().isValid).toBe(false); + expect(form.getErrorsMessages()).toEqual(['A field with this name already exists.']); + }); + + test('should not count the default value as a duplicate', async () => { + const existingRuntimeFieldNames = ['myRuntimeField']; + const field: Field = { + name: 'myRuntimeField', + type: 'boolean', + script: { source: 'emit("hello"' }, + }; + + testBed = setup({ + field, + onChange, + ctx: { + namesNotAllowed: existingRuntimeFieldNames, + existingConcreteFields: [], + fieldTypeToProcess: 'runtime', + }, + }); + + const { form, component } = testBed; + const lastState = getLastStateUpdate(); + await submitFormAndGetData(lastState); + component.update(); + expect(getLastStateUpdate().isValid).toBe(true); + expect(form.getErrorsMessages()).toEqual([]); + }); + }); +}); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx new file mode 100644 index 00000000000000..afb87bd1e73344 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.tsx @@ -0,0 +1,286 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiComboBoxOptionOption, + EuiCode, +} from '@elastic/eui'; +import type { CoreStart } from 'src/core/public'; + +import { + Form, + useForm, + FormHook, + UseField, + TextField, + RuntimeType, + IndexPattern, + DataPublicPluginStart, +} from '../../shared_imports'; +import { Field, InternalFieldType, PluginStart } from '../../types'; + +import { RUNTIME_FIELD_OPTIONS } from './constants'; +import { schema } from './form_schema'; +import { getNameFieldConfig } from './lib'; +import { + TypeField, + CustomLabelField, + ScriptField, + FormatField, + PopularityField, + ScriptSyntaxError, +} from './form_fields'; +import { FormRow } from './form_row'; +import { AdvancedParametersSection } from './advanced_parameters_section'; + +export interface FieldEditorFormState { + isValid: boolean | undefined; + isSubmitted: boolean; + submit: FormHook['submit']; +} + +export interface FieldFormInternal extends Omit { + type: Array>; + __meta__: { + isCustomLabelVisible: boolean; + isValueVisible: boolean; + isFormatVisible: boolean; + isPopularityVisible: boolean; + }; +} + +export interface Props { + /** Link URLs to our doc site */ + links: { + runtimePainless: string; + }; + /** Optional field to edit */ + field?: Field; + /** Handler to receive state changes updates */ + onChange?: (state: FieldEditorFormState) => void; + indexPattern: IndexPattern; + fieldFormatEditors: PluginStart['fieldFormatEditors']; + fieldFormats: DataPublicPluginStart['fieldFormats']; + uiSettings: CoreStart['uiSettings']; + /** Context object */ + ctx: { + /** The internal field type we are dealing with (concrete|runtime)*/ + fieldTypeToProcess: InternalFieldType; + /** + * An array of field names not allowed. + * e.g we probably don't want a user to give a name of an existing + * runtime field (for that the user should edit the existing runtime field). + */ + namesNotAllowed: string[]; + /** + * An array of existing concrete fields. If the user gives a name to the runtime + * field that matches one of the concrete fields, a callout will be displayed + * to indicate that this runtime field will shadow the concrete field. + * It is also used to provide the list of field autocomplete suggestions to the code editor. + */ + existingConcreteFields: Array<{ name: string; type: string }>; + }; + syntaxError: ScriptSyntaxError; +} + +const geti18nTexts = (): { + [key: string]: { title: string; description: JSX.Element | string }; +} => ({ + customLabel: { + title: i18n.translate('indexPatternFieldEditor.editor.form.customLabelTitle', { + defaultMessage: 'Set custom label', + }), + description: i18n.translate('indexPatternFieldEditor.editor.form.customLabelDescription', { + defaultMessage: `Create a label to display in place of the field name in Discover, Maps, and Visualize. Useful for shortening a long field name. Queries and filters use the original field name.`, + }), + }, + value: { + title: i18n.translate('indexPatternFieldEditor.editor.form.valueTitle', { + defaultMessage: 'Set value', + }), + description: ( + {'_source'}, + }} + /> + ), + }, + format: { + title: i18n.translate('indexPatternFieldEditor.editor.form.formatTitle', { + defaultMessage: 'Set format', + }), + description: i18n.translate('indexPatternFieldEditor.editor.form.formatDescription', { + defaultMessage: `Set your preferred format for displaying the value. Changing the format can affect the value and prevent highlighting in Discover.`, + }), + }, + popularity: { + title: i18n.translate('indexPatternFieldEditor.editor.form.popularityTitle', { + defaultMessage: 'Set popularity', + }), + description: i18n.translate('indexPatternFieldEditor.editor.form.popularityDescription', { + defaultMessage: `Adjust the popularity to make the field appear higher or lower in the fields list. By default, Discover orders fields from most selected to least selected.`, + }), + }, +}); + +const formDeserializer = (field: Field): FieldFormInternal => { + let fieldType: Array>; + if (!field.type) { + fieldType = [RUNTIME_FIELD_OPTIONS[0]]; + } else { + const label = RUNTIME_FIELD_OPTIONS.find(({ value }) => value === field.type)?.label; + fieldType = [{ label: label ?? field.type, value: field.type as RuntimeType }]; + } + + return { + ...field, + type: fieldType, + __meta__: { + isCustomLabelVisible: field.customLabel !== undefined, + isValueVisible: field.script !== undefined, + isFormatVisible: field.format !== undefined, + isPopularityVisible: field.popularity !== undefined, + }, + }; +}; + +const formSerializer = (field: FieldFormInternal): Field => { + const { __meta__, type, ...rest } = field; + return { + type: type[0].value!, + ...rest, + }; +}; + +const FieldEditorComponent = ({ + field, + onChange, + links, + indexPattern, + fieldFormatEditors, + fieldFormats, + uiSettings, + syntaxError, + ctx: { fieldTypeToProcess, namesNotAllowed, existingConcreteFields }, +}: Props) => { + const { form } = useForm({ + defaultValue: field, + schema, + deserializer: formDeserializer, + serializer: formSerializer, + }); + const { submit, isValid: isFormValid, isSubmitted } = form; + + const nameFieldConfig = getNameFieldConfig(namesNotAllowed, field); + const i18nTexts = geti18nTexts(); + + useEffect(() => { + if (onChange) { + onChange({ isValid: isFormValid, isSubmitted, submit }); + } + }, [onChange, isFormValid, isSubmitted, submit]); + + return ( +
+ + {/* Name */} + + + path="name" + config={nameFieldConfig} + component={TextField} + data-test-subj="nameField" + componentProps={{ + euiFieldProps: { + disabled: fieldTypeToProcess === 'concrete', + 'aria-label': i18n.translate('indexPatternFieldEditor.editor.form.nameAriaLabel', { + defaultMessage: 'Name field', + }), + }, + }} + /> + + + {/* Type */} + + + + + + + + {/* Set custom label */} + + + + + {/* Set value */} + {fieldTypeToProcess === 'runtime' && ( + + + + )} + + {/* Set custom format */} + + + + + {/* Advanced settings */} + + + + + + + ); +}; + +export const FieldEditor = React.memo(FieldEditorComponent); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/custom_label_field.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/custom_label_field.tsx new file mode 100644 index 00000000000000..313137de463032 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/custom_label_field.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +import { UseField, TextField } from '../../../shared_imports'; + +export const CustomLabelField = () => { + return ; +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/format_field.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/format_field.tsx new file mode 100644 index 00000000000000..db98e4a1591625 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/format_field.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React, { useState, useEffect, useRef } from 'react'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; + +import { UseField, useFormData, ES_FIELD_TYPES, useFormContext } from '../../../shared_imports'; +import { FormatSelectEditor, FormatSelectEditorProps } from '../../field_format_editor'; +import { FieldFormInternal } from '../field_editor'; +import { FieldFormatConfig } from '../../../types'; + +export const FormatField = ({ + indexPattern, + fieldFormatEditors, + fieldFormats, + uiSettings, +}: Omit) => { + const isMounted = useRef(false); + const [{ type }] = useFormData({ watch: ['name', 'type'] }); + const { getFields, isSubmitted } = useFormContext(); + const [formatError, setFormatError] = useState(); + // convert from combobox type to values + const typeValue = type.reduce((collector, item) => { + if (item.value !== undefined) { + collector.push(item.value as ES_FIELD_TYPES); + } + return collector; + }, [] as ES_FIELD_TYPES[]); + + useEffect(() => { + if (formatError === undefined) { + getFields().format.setErrors([]); + } else { + getFields().format.setErrors([{ message: formatError }]); + } + }, [formatError, getFields]); + + useEffect(() => { + if (isMounted.current) { + getFields().format.reset(); + } + isMounted.current = true; + }, [type, getFields]); + + return ( + path="format"> + {({ setValue, errors, value }) => { + return ( + <> + {isSubmitted && errors.length > 0 && ( + <> + err.message)} + color="danger" + iconType="cross" + data-test-subj="formFormatError" + /> + + + )} + + + + ); + }} + + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/index.ts new file mode 100644 index 00000000000000..e958e1362bb054 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { TypeField } from './type_field'; + +export { CustomLabelField } from './custom_label_field'; + +export { PopularityField } from './popularity_field'; + +export { ScriptField, ScriptSyntaxError } from './script_field'; + +export { FormatField } from './format_field'; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/popularity_field.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/popularity_field.tsx new file mode 100644 index 00000000000000..44f83138fe1d31 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/popularity_field.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +import { UseField, NumericField } from '../../../shared_imports'; + +export const PopularityField = () => { + return ( + + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/script_field.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/script_field.tsx new file mode 100644 index 00000000000000..d15445f3e10ae6 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/script_field.tsx @@ -0,0 +1,227 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState, useEffect, useMemo, useRef } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFormRow, EuiLink, EuiCode, EuiCodeBlock, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { PainlessLang, PainlessContext } from '@kbn/monaco'; + +import { + UseField, + useFormData, + RuntimeType, + FieldConfig, + CodeEditor, +} from '../../../shared_imports'; +import { RuntimeFieldPainlessError } from '../../../lib'; +import { schema } from '../form_schema'; +import type { FieldFormInternal } from '../field_editor'; + +interface Props { + links: { runtimePainless: string }; + existingConcreteFields?: Array<{ name: string; type: string }>; + syntaxError: ScriptSyntaxError; +} + +export interface ScriptSyntaxError { + error: RuntimeFieldPainlessError | null; + clear: () => void; +} + +const mapReturnTypeToPainlessContext = (runtimeType: RuntimeType): PainlessContext => { + switch (runtimeType) { + case 'keyword': + return 'string_script_field_script_field'; + case 'long': + return 'long_script_field_script_field'; + case 'double': + return 'double_script_field_script_field'; + case 'date': + return 'date_script_field'; + case 'ip': + return 'ip_script_field_script_field'; + case 'boolean': + return 'boolean_script_field_script_field'; + default: + return 'string_script_field_script_field'; + } +}; + +export const ScriptField = React.memo(({ existingConcreteFields, links, syntaxError }: Props) => { + const editorValidationTimeout = useRef>(); + + const [painlessContext, setPainlessContext] = useState( + mapReturnTypeToPainlessContext(schema.type.defaultValue[0].value!) + ); + + const [editorId, setEditorId] = useState(); + + const suggestionProvider = PainlessLang.getSuggestionProvider( + painlessContext, + existingConcreteFields + ); + + const [{ type, script: { source } = { source: '' } }] = useFormData({ + watch: ['type', 'script.source'], + }); + + const { clear: clearSyntaxError } = syntaxError; + + const sourceFieldConfig: FieldConfig = useMemo(() => { + return { + ...schema.script.source, + validations: [ + ...schema.script.source.validations, + { + validator: () => { + if (editorValidationTimeout.current) { + clearTimeout(editorValidationTimeout.current); + } + + return new Promise((resolve) => { + // monaco waits 500ms before validating, so we also add a delay + // before checking if there are any syntax errors + editorValidationTimeout.current = setTimeout(() => { + const painlessSyntaxErrors = PainlessLang.getSyntaxErrors(); + // It is possible for there to be more than one editor in a view, + // so we need to get the syntax errors based on the editor (aka model) ID + const editorHasSyntaxErrors = editorId && painlessSyntaxErrors[editorId].length > 0; + + if (editorHasSyntaxErrors) { + return resolve({ + message: i18n.translate( + 'indexPatternFieldEditor.editor.form.scriptEditorValidationMessage', + { + defaultMessage: 'Invalid Painless syntax.', + } + ), + }); + } + + resolve(undefined); + }, 600); + }); + }, + }, + ], + }; + }, [editorId]); + + useEffect(() => { + setPainlessContext(mapReturnTypeToPainlessContext(type[0]!.value!)); + }, [type]); + + useEffect(() => { + // Whenever the source changes we clear potential syntax errors + clearSyntaxError(); + }, [source, clearSyntaxError]); + + return ( + path="script.source" config={sourceFieldConfig}> + {({ value, setValue, label, isValid, getErrorsMessages }) => { + let errorMessage: string | null = ''; + if (syntaxError.error !== null) { + errorMessage = syntaxError.error.reason ?? syntaxError.error.message; + } else { + errorMessage = getErrorsMessages(); + } + + return ( + <> + + {i18n.translate( + 'indexPatternFieldEditor.editor.form.script.learnMoreLinkText', + { + defaultMessage: 'Learn about script syntax.', + } + )} + + ), + source: {'_source'}, + }} + /> + } + fullWidth + > + setEditorId(editor.getModel()?.id)} + options={{ + fontSize: 12, + minimap: { + enabled: false, + }, + scrollBeyondLastLine: false, + wordWrap: 'on', + wrappingIndent: 'indent', + automaticLayout: true, + suggest: { + snippetsPreventQuickSuggestions: false, + }, + }} + data-test-subj="scriptField" + aria-label={i18n.translate( + 'indexPatternFieldEditor.editor.form.scriptEditorAriaLabel', + { + defaultMessage: 'Script editor', + } + )} + /> + + + {/* Help the user debug the error by showing where it failed in the script */} + {syntaxError.error !== null && ( + <> + + +

+ {i18n.translate( + 'indexPatternFieldEditor.editor.form.scriptEditor.debugErrorMessage', + { + defaultMessage: 'Syntax error detail', + } + )} +

+
+ + + {syntaxError.error.scriptStack.join('\n')} + + + )} + + ); + }} +
+ ); +}); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/type_field.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/type_field.tsx new file mode 100644 index 00000000000000..36428579a30e86 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_fields/type_field.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +import { EuiFormRow, EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; + +import { UseField, RuntimeType } from '../../../shared_imports'; +import { RUNTIME_FIELD_OPTIONS } from '../constants'; + +interface Props { + isDisabled?: boolean; +} + +export const TypeField = ({ isDisabled = false }: Props) => { + return ( + >> path="type"> + {({ label, value, setValue }) => { + if (value === undefined) { + return null; + } + return ( + <> + + { + if (newValue.length === 0) { + // Don't allow clearing the type. One must always be selected + return; + } + setValue(newValue); + }} + isClearable={false} + isDisabled={isDisabled} + data-test-subj="typeField" + aria-label={i18n.translate( + 'indexPatternFieldEditor.editor.form.typeSelectAriaLabel', + { + defaultMessage: 'Type select', + } + )} + fullWidth + /> + + + ); + }} + + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_row.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_row.tsx new file mode 100644 index 00000000000000..66f5af09c8b2f4 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_row.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { get } from 'lodash'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiText, + EuiHorizontalRule, + EuiSpacer, +} from '@elastic/eui'; + +import { UseField, ToggleField, useFormData } from '../../shared_imports'; + +interface Props { + title: string; + formFieldPath: string; + children: React.ReactNode; + description?: string | JSX.Element; + withDividerRule?: boolean; + 'data-test-subj'?: string; +} + +export const FormRow = ({ + title, + description, + children, + formFieldPath, + withDividerRule = false, + 'data-test-subj': dataTestSubj, +}: Props) => { + const [formData] = useFormData({ watch: formFieldPath }); + const isContentVisible = Boolean(get(formData, formFieldPath)); + + return ( + <> + + + + + + +
+ {/* Title */} + +

{title}

+
+ + + {/* Description */} + + {description} + + + {/* Content */} + {isContentVisible && ( + <> + + {children} + + )} +
+
+
+ + {withDividerRule && } + + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/form_schema.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_schema.ts new file mode 100644 index 00000000000000..a722f277b8e237 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/form_schema.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { fieldValidators } from '../../shared_imports'; + +import { RUNTIME_FIELD_OPTIONS } from './constants'; + +const { emptyField, numberGreaterThanField } = fieldValidators; + +export const schema = { + name: { + label: i18n.translate('indexPatternFieldEditor.editor.form.nameLabel', { + defaultMessage: 'Name', + }), + validations: [ + { + validator: emptyField( + i18n.translate( + 'indexPatternFieldEditor.editor.form.validations.nameIsRequiredErrorMessage', + { + defaultMessage: 'A name is required.', + } + ) + ), + }, + ], + }, + type: { + label: i18n.translate('indexPatternFieldEditor.editor.form.runtimeTypeLabel', { + defaultMessage: 'Type', + }), + defaultValue: [RUNTIME_FIELD_OPTIONS[0]], + }, + script: { + source: { + label: i18n.translate('indexPatternFieldEditor.editor.form.defineFieldLabel', { + defaultMessage: 'Define script', + }), + validations: [ + { + validator: emptyField( + i18n.translate( + 'indexPatternFieldEditor.editor.form.validations.scriptIsRequiredErrorMessage', + { + defaultMessage: 'A script is required to set the field value.', + } + ) + ), + }, + ], + }, + }, + customLabel: { + label: i18n.translate('indexPatternFieldEditor.editor.form.customLabelLabel', { + defaultMessage: 'Custom label', + }), + validations: [ + { + validator: emptyField( + i18n.translate( + 'indexPatternFieldEditor.editor.form.validations.customLabelIsRequiredErrorMessage', + { + defaultMessage: 'Give a label to the field.', + } + ) + ), + }, + ], + }, + popularity: { + label: i18n.translate('indexPatternFieldEditor.editor.form.popularityLabel', { + defaultMessage: 'Popularity', + }), + validations: [ + { + validator: emptyField( + i18n.translate( + 'indexPatternFieldEditor.editor.form.validations.popularityIsRequiredErrorMessage', + { + defaultMessage: 'Give a popularity to the field.', + } + ) + ), + }, + { + validator: numberGreaterThanField({ + than: 0, + allowEquality: true, + message: i18n.translate( + 'indexPatternFieldEditor.editor.form.validations.popularityGreaterThan0ErrorMessage', + { + defaultMessage: 'The popularity must be zero or greater.', + } + ), + }), + }, + ], + }, + __meta__: { + isCustomLabelVisible: { + defaultValue: false, + }, + isValueVisible: { + defaultValue: false, + }, + isFormatVisible: { + defaultValue: false, + }, + isPopularityVisible: { + defaultValue: false, + }, + }, +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor/index.ts new file mode 100644 index 00000000000000..db7c05fa7ff7a4 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { FieldEditor } from './field_editor'; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/lib.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor/lib.ts new file mode 100644 index 00000000000000..2d324804c9e43d --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/lib.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +import { ValidationFunc, FieldConfig } from '../../shared_imports'; +import { Field } from '../../types'; +import { schema } from './form_schema'; +import { Props } from './field_editor'; + +const createNameNotAllowedValidator = ( + namesNotAllowed: string[] +): ValidationFunc<{}, string, string> => ({ value }) => { + if (namesNotAllowed.includes(value)) { + return { + message: i18n.translate( + 'indexPatternFieldEditor.editor.runtimeFieldsEditor.existRuntimeFieldNamesValidationErrorMessage', + { + defaultMessage: 'A field with this name already exists.', + } + ), + }; + } +}; + +/** + * Dynamically retrieve the config for the "name" field, adding + * a validator to avoid duplicated runtime fields to be created. + * + * @param namesNotAllowed Array of names not allowed for the field "name" + * @param field Initial value of the form + */ +export const getNameFieldConfig = ( + namesNotAllowed?: string[], + field?: Props['field'] +): FieldConfig => { + const nameFieldConfig = schema.name as FieldConfig; + + if (!namesNotAllowed) { + return nameFieldConfig; + } + + // Add validation to not allow duplicates + return { + ...nameFieldConfig!, + validations: [ + ...(nameFieldConfig.validations ?? []), + { + validator: createNameNotAllowedValidator( + namesNotAllowed.filter((name) => name !== field?.name) + ), + }, + ], + }; +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/shadowing_field_warning.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/shadowing_field_warning.tsx new file mode 100644 index 00000000000000..4343b13db9a5af --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/shadowing_field_warning.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiCallOut } from '@elastic/eui'; + +export const ShadowingFieldWarning = () => { + return ( + +
+ {i18n.translate('indexPatternFieldEditor.editor.form.fieldShadowingCalloutDescription', { + defaultMessage: + 'This field shares the name of a mapped field. Values for this field will be returned in search results.', + })} +
+
+ ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts new file mode 100644 index 00000000000000..e943dbdda998df --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts @@ -0,0 +1,198 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { act } from 'react-dom/test-utils'; + +import '../test_utils/setup_environment'; +import { registerTestBed, TestBed, noop, docLinks, getCommonActions } from '../test_utils'; + +import { FieldEditor } from './field_editor'; +import { FieldEditorFlyoutContent, Props } from './field_editor_flyout_content'; + +const defaultProps: Props = { + onSave: noop, + onCancel: noop, + docLinks, + FieldEditor, + indexPattern: { fields: [] } as any, + uiSettings: {} as any, + fieldFormats: {} as any, + fieldFormatEditors: {} as any, + fieldTypeToProcess: 'runtime', + runtimeFieldValidator: () => Promise.resolve(null), + isSavingField: false, +}; + +const setup = (props: Props = defaultProps) => { + const testBed = registerTestBed(FieldEditorFlyoutContent, { + memoryRouter: { wrapComponent: false }, + })(props) as TestBed; + + const actions = { + ...getCommonActions(testBed), + }; + + return { + ...testBed, + actions, + }; +}; + +describe('', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + test('should have the correct title', () => { + const { exists, find } = setup(); + expect(exists('flyoutTitle')).toBe(true); + expect(find('flyoutTitle').text()).toBe('Create field'); + }); + + test('should allow a field to be provided', () => { + const field = { + name: 'foo', + type: 'ip', + script: { + source: 'emit("hello world")', + }, + }; + + const { find } = setup({ ...defaultProps, field }); + + expect(find('flyoutTitle').text()).toBe(`Edit ${field.name} field`); + expect(find('nameField.input').props().value).toBe(field.name); + expect(find('typeField').props().value).toBe(field.type); + expect(find('scriptField').props().value).toBe(field.script.source); + }); + + test('should accept an "onSave" prop', async () => { + const field = { + name: 'foo', + type: 'date', + script: { source: 'test=123' }, + }; + const onSave: jest.Mock = jest.fn(); + + const { find } = setup({ ...defaultProps, onSave, field }); + + await act(async () => { + find('fieldSaveButton').simulate('click'); + }); + + await act(async () => { + // The painless syntax validation has a timeout set to 600ms + // we give it a bit more time just to be on the safe side + jest.advanceTimersByTime(1000); + }); + + expect(onSave).toHaveBeenCalled(); + const fieldReturned = onSave.mock.calls[onSave.mock.calls.length - 1][0]; + expect(fieldReturned).toEqual(field); + }); + + test('should accept an onCancel prop', () => { + const onCancel = jest.fn(); + const { find } = setup({ ...defaultProps, onCancel }); + + find('closeFlyoutButton').simulate('click'); + + expect(onCancel).toHaveBeenCalled(); + }); + + describe('validation', () => { + test('should validate the fields and prevent saving invalid form', async () => { + const onSave: jest.Mock = jest.fn(); + + const { find, exists, form, component } = setup({ ...defaultProps, onSave }); + + expect(find('fieldSaveButton').props().disabled).toBe(false); + + await act(async () => { + find('fieldSaveButton').simulate('click'); + }); + + await act(async () => { + jest.advanceTimersByTime(1000); + }); + + component.update(); + + expect(onSave).toHaveBeenCalledTimes(0); + expect(find('fieldSaveButton').props().disabled).toBe(true); + expect(form.getErrorsMessages()).toEqual(['A name is required.']); + expect(exists('formError')).toBe(true); + expect(find('formError').text()).toBe('Fix errors in form before continuing.'); + }); + + test('should forward values from the form', async () => { + const onSave: jest.Mock = jest.fn(); + + const { + find, + component, + form, + actions: { toggleFormRow }, + } = setup({ ...defaultProps, onSave }); + + act(() => { + form.setInputValue('nameField.input', 'someName'); + toggleFormRow('value'); + }); + component.update(); + + await act(async () => { + form.setInputValue('scriptField', 'echo("hello")'); + }); + + await act(async () => { + // Let's make sure that validation has finished running + jest.advanceTimersByTime(1000); + }); + + await act(async () => { + find('fieldSaveButton').simulate('click'); + }); + + expect(onSave).toHaveBeenCalled(); + + let fieldReturned = onSave.mock.calls[onSave.mock.calls.length - 1][0]; + + expect(fieldReturned).toEqual({ + name: 'someName', + type: 'keyword', // default to keyword + script: { source: 'echo("hello")' }, + }); + + // Change the type and make sure it is forwarded + act(() => { + find('typeField').simulate('change', [ + { + label: 'Other type', + value: 'other_type', + }, + ]); + }); + + await act(async () => { + find('fieldSaveButton').simulate('click'); + }); + + fieldReturned = onSave.mock.calls[onSave.mock.calls.length - 1][0]; + + expect(fieldReturned).toEqual({ + name: 'someName', + type: 'other_type', + script: { source: 'echo("hello")' }, + }); + }); + }); +}); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx new file mode 100644 index 00000000000000..1511836da85e73 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.tsx @@ -0,0 +1,253 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useState, useCallback, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlyoutHeader, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, + EuiCallOut, + EuiSpacer, +} from '@elastic/eui'; + +import { DocLinksStart, CoreStart } from 'src/core/public'; + +import { Field, InternalFieldType, PluginStart, EsRuntimeField } from '../types'; +import { getLinks, RuntimeFieldPainlessError } from '../lib'; +import type { IndexPattern, DataPublicPluginStart } from '../shared_imports'; +import type { Props as FieldEditorProps, FieldEditorFormState } from './field_editor/field_editor'; + +const geti18nTexts = (field?: Field) => { + return { + flyoutTitle: field + ? i18n.translate('indexPatternFieldEditor.editor.flyoutEditFieldTitle', { + defaultMessage: 'Edit {fieldName} field', + values: { + fieldName: field.name, + }, + }) + : i18n.translate('indexPatternFieldEditor.editor.flyoutDefaultTitle', { + defaultMessage: 'Create field', + }), + closeButtonLabel: i18n.translate('indexPatternFieldEditor.editor.flyoutCloseButtonLabel', { + defaultMessage: 'Close', + }), + saveButtonLabel: i18n.translate('indexPatternFieldEditor.editor.flyoutSaveButtonLabel', { + defaultMessage: 'Save', + }), + formErrorsCalloutTitle: i18n.translate('indexPatternFieldEditor.editor.validationErrorTitle', { + defaultMessage: 'Fix errors in form before continuing.', + }), + }; +}; + +export interface Props { + /** + * Handler for the "save" footer button + */ + onSave: (field: Field) => void; + /** + * Handler for the "cancel" footer button + */ + onCancel: () => void; + /** + * The docLinks start service from core + */ + docLinks: DocLinksStart; + /** + * The Field editor component that contains the form to create or edit a field + */ + FieldEditor: React.ComponentType | null; + /** The internal field type we are dealing with (concrete|runtime)*/ + fieldTypeToProcess: InternalFieldType; + /** Handler to validate the script */ + runtimeFieldValidator: (field: EsRuntimeField) => Promise; + /** Optional field to process */ + field?: Field; + + indexPattern: IndexPattern; + fieldFormatEditors: PluginStart['fieldFormatEditors']; + fieldFormats: DataPublicPluginStart['fieldFormats']; + uiSettings: CoreStart['uiSettings']; + isSavingField: boolean; +} + +const FieldEditorFlyoutContentComponent = ({ + field, + onSave, + onCancel, + FieldEditor, + docLinks, + indexPattern, + fieldFormatEditors, + fieldFormats, + uiSettings, + fieldTypeToProcess, + runtimeFieldValidator, + isSavingField, +}: Props) => { + const i18nTexts = geti18nTexts(field); + + const [formState, setFormState] = useState({ + isSubmitted: false, + isValid: field ? true : undefined, + submit: field + ? async () => ({ isValid: true, data: field }) + : async () => ({ isValid: false, data: {} as Field }), + }); + + const [painlessSyntaxError, setPainlessSyntaxError] = useState( + null + ); + + const [isValidating, setIsValidating] = useState(false); + + const { submit, isValid: isFormValid, isSubmitted } = formState; + const { fields } = indexPattern; + const isSaveButtonDisabled = isFormValid === false || painlessSyntaxError !== null; + + const clearSyntaxError = useCallback(() => setPainlessSyntaxError(null), []); + + const syntaxError = useMemo( + () => ({ + error: painlessSyntaxError, + clear: clearSyntaxError, + }), + [painlessSyntaxError, clearSyntaxError] + ); + + const onClickSave = useCallback(async () => { + const { isValid, data } = await submit(); + + if (isValid) { + if (data.script) { + setIsValidating(true); + + const error = await runtimeFieldValidator({ + type: data.type, + script: data.script, + }); + + setIsValidating(false); + setPainlessSyntaxError(error); + + if (error) { + return; + } + } + + onSave(data); + } + }, [onSave, submit, runtimeFieldValidator]); + + const namesNotAllowed = useMemo(() => fields.map((fld) => fld.name), [fields]); + + const existingConcreteFields = useMemo(() => { + const existing: Array<{ name: string; type: string }> = []; + + fields + .filter((fld) => { + const isFieldBeingEdited = field?.name === fld.name; + return !isFieldBeingEdited && fld.isMapped; + }) + .forEach((fld) => { + existing.push({ + name: fld.name, + type: (fld.esTypes && fld.esTypes[0]) || '', + }); + }); + + return existing; + }, [fields, field]); + + const ctx = useMemo( + () => ({ + fieldTypeToProcess, + namesNotAllowed, + existingConcreteFields, + }), + [fieldTypeToProcess, namesNotAllowed, existingConcreteFields] + ); + + return ( + <> + + +

{i18nTexts.flyoutTitle}

+
+
+ + + {FieldEditor && ( + + )} + + + + {FieldEditor && ( + <> + {isSubmitted && isSaveButtonDisabled && ( + <> + + + + )} + + + + {i18nTexts.closeButtonLabel} + + + + + + {i18nTexts.saveButtonLabel} + + + + + )} + + + ); +}; + +export const FieldEditorFlyoutContent = React.memo(FieldEditorFlyoutContentComponent); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content_container.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content_container.tsx new file mode 100644 index 00000000000000..ade25424c22509 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content_container.tsx @@ -0,0 +1,205 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback, useEffect, useState, useMemo } from 'react'; +import { DocLinksStart, NotificationsStart, CoreStart } from 'src/core/public'; +import { i18n } from '@kbn/i18n'; + +import { + IndexPatternField, + IndexPattern, + DataPublicPluginStart, + RuntimeType, + UsageCollectionStart, +} from '../shared_imports'; +import { Field, PluginStart, InternalFieldType } from '../types'; +import { pluginName } from '../constants'; +import { deserializeField, getRuntimeFieldValidator } from '../lib'; +import { Props as FieldEditorProps } from './field_editor/field_editor'; +import { FieldEditorFlyoutContent } from './field_editor_flyout_content'; + +export interface FieldEditorContext { + indexPattern: IndexPattern; + /** + * The Kibana field type of the field to create or edit + * Default: "runtime" + */ + fieldTypeToProcess: InternalFieldType; + /** The search service from the data plugin */ + search: DataPublicPluginStart['search']; +} + +export interface Props { + /** + * Handler for the "save" footer button + */ + onSave: (field: IndexPatternField) => void; + /** + * Handler for the "cancel" footer button + */ + onCancel: () => void; + /** + * The docLinks start service from core + */ + docLinks: DocLinksStart; + /** + * The context object specific to where the editor is currently being consumed + */ + ctx: FieldEditorContext; + /** + * Optional field to edit + */ + field?: IndexPatternField; + /** + * Services + */ + indexPatternService: DataPublicPluginStart['indexPatterns']; + notifications: NotificationsStart; + fieldFormatEditors: PluginStart['fieldFormatEditors']; + fieldFormats: DataPublicPluginStart['fieldFormats']; + uiSettings: CoreStart['uiSettings']; + usageCollection: UsageCollectionStart; +} + +/** + * The container component will be in charge of the communication with the index pattern service + * to retrieve/save the field in the saved object. + * The component is the presentational component that won't know + * anything about where a field comes from and where it should be persisted. + */ + +export const FieldEditorFlyoutContentContainer = ({ + field, + onSave, + onCancel, + docLinks, + indexPatternService, + ctx: { indexPattern, fieldTypeToProcess, search }, + notifications, + fieldFormatEditors, + fieldFormats, + uiSettings, + usageCollection, +}: Props) => { + const fieldToEdit = deserializeField(indexPattern, field); + const [Editor, setEditor] = useState | null>(null); + const [isSaving, setIsSaving] = useState(false); + + const saveField = useCallback( + async (updatedField: Field) => { + setIsSaving(true); + + const { script } = updatedField; + + if (fieldTypeToProcess === 'runtime') { + try { + usageCollection.reportUiCounter( + pluginName, + usageCollection.METRIC_TYPE.COUNT, + 'save_runtime' + ); + // eslint-disable-next-line no-empty + } catch {} + // rename an existing runtime field + if (field?.name && field.name !== updatedField.name) { + indexPattern.removeRuntimeField(field.name); + } + + indexPattern.addRuntimeField(updatedField.name, { + type: updatedField.type as RuntimeType, + script, + }); + } else { + try { + usageCollection.reportUiCounter( + pluginName, + usageCollection.METRIC_TYPE.COUNT, + 'save_concrete' + ); + // eslint-disable-next-line no-empty + } catch {} + } + + const editedField = indexPattern.getFieldByName(updatedField.name); + + try { + if (!editedField) { + throw new Error( + `Unable to find field named '${updatedField.name}' on index pattern '${indexPattern.title}'` + ); + } + + indexPattern.setFieldCustomLabel(updatedField.name, updatedField.customLabel); + editedField.count = updatedField.popularity || 0; + if (updatedField.format) { + indexPattern.setFieldFormat(updatedField.name, updatedField.format); + } else { + indexPattern.deleteFieldFormat(updatedField.name); + } + + await indexPatternService.updateSavedObject(indexPattern).then(() => { + const message = i18n.translate('indexPatternFieldEditor.deleteField.savedHeader', { + defaultMessage: "Saved '{fieldName}'", + values: { fieldName: updatedField.name }, + }); + notifications.toasts.addSuccess(message); + setIsSaving(false); + onSave(editedField); + }); + } catch (e) { + const title = i18n.translate('indexPatternFieldEditor.save.errorTitle', { + defaultMessage: 'Failed to save field changes', + }); + notifications.toasts.addError(e, { title }); + setIsSaving(false); + } + }, + [ + onSave, + indexPattern, + indexPatternService, + notifications, + fieldTypeToProcess, + field?.name, + usageCollection, + ] + ); + + const validateRuntimeField = useMemo(() => getRuntimeFieldValidator(indexPattern.title, search), [ + search, + indexPattern, + ]); + + const loadEditor = useCallback(async () => { + const { FieldEditor } = await import('./field_editor'); + + setEditor(() => FieldEditor); + }, []); + + useEffect(() => { + // On mount: load the editor asynchronously + loadEditor(); + }, [loadEditor]); + + return ( + + ); +}; diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/__snapshots__/format_editor.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/__snapshots__/format_editor.test.tsx.snap new file mode 100644 index 00000000000000..82d21eb5d30ada --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/__snapshots__/format_editor.test.tsx.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FieldFormatEditor should render normally 1`] = ` + + + +`; + +exports[`FieldFormatEditor should render nothing if there is no editor for the format 1`] = ` + + + +`; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap similarity index 92% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap index 69ea6c481d49b1..0f35267e1fb387 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/__snapshots__/bytes.test.tsx.snap @@ -16,7 +16,7 @@ exports[`BytesFormatEditor should render normally 1`] = ` >   @@ -30,7 +30,7 @@ exports[`BytesFormatEditor should render normally 1`] = ` label={ diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/bytes.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/bytes.test.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/bytes.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/bytes.test.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/bytes.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/bytes.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/bytes.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/bytes.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/bytes/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/bytes/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap similarity index 87% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap index c66e7789aa511e..c33bb57bfeac89 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap @@ -9,7 +9,7 @@ exports[`ColorFormatEditor should render multiple colors 1`] = ` "field": "regex", "name": , "render": [Function], @@ -18,7 +18,7 @@ exports[`ColorFormatEditor should render multiple colors 1`] = ` "field": "text", "name": , "render": [Function], @@ -27,7 +27,7 @@ exports[`ColorFormatEditor should render multiple colors 1`] = ` "field": "background", "name": , "render": [Function], @@ -35,7 +35,7 @@ exports[`ColorFormatEditor should render multiple colors 1`] = ` Object { "name": , "render": [Function], @@ -89,7 +89,7 @@ exports[`ColorFormatEditor should render multiple colors 1`] = ` > @@ -108,7 +108,7 @@ exports[`ColorFormatEditor should render other type normally (range field) 1`] = "field": "range", "name": , "render": [Function], @@ -117,7 +117,7 @@ exports[`ColorFormatEditor should render other type normally (range field) 1`] = "field": "text", "name": , "render": [Function], @@ -126,7 +126,7 @@ exports[`ColorFormatEditor should render other type normally (range field) 1`] = "field": "background", "name": , "render": [Function], @@ -134,7 +134,7 @@ exports[`ColorFormatEditor should render other type normally (range field) 1`] = Object { "name": , "render": [Function], @@ -181,7 +181,7 @@ exports[`ColorFormatEditor should render other type normally (range field) 1`] = > @@ -200,7 +200,7 @@ exports[`ColorFormatEditor should render string type normally (regex field) 1`] "field": "regex", "name": , "render": [Function], @@ -209,7 +209,7 @@ exports[`ColorFormatEditor should render string type normally (regex field) 1`] "field": "text", "name": , "render": [Function], @@ -218,7 +218,7 @@ exports[`ColorFormatEditor should render string type normally (regex field) 1`] "field": "background", "name": , "render": [Function], @@ -226,7 +226,7 @@ exports[`ColorFormatEditor should render string type normally (regex field) 1`] Object { "name": , "render": [Function], @@ -273,7 +273,7 @@ exports[`ColorFormatEditor should render string type normally (regex field) 1`] > diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/color.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/color.test.tsx similarity index 96% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/color.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/color.test.tsx index f0e7d4aea42c84..1026012f3b8878 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/color.test.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/color.test.tsx @@ -11,7 +11,7 @@ import { shallowWithI18nProvider } from '@kbn/test/jest'; import { FieldFormat } from 'src/plugins/data/public'; import { ColorFormatEditor } from './color'; -import { fieldFormats } from '../../../../../../../../data/public'; +import { fieldFormats } from '../../../../../../data/public'; const fieldType = 'string'; const format = { diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/color.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/color.tsx similarity index 89% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/color.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/color.tsx index b169624fce9080..1e899a7179554f 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/color.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/color.tsx @@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { DefaultFormatEditor, FormatEditorProps } from '../default'; -import { fieldFormats } from '../../../../../../../../../plugins/data/public'; +import { fieldFormats } from '../../../../../../data/public'; interface Color { range?: string; @@ -86,7 +86,7 @@ export class ColorFormatEditor extends DefaultFormatEditor ), @@ -110,7 +110,7 @@ export class ColorFormatEditor extends DefaultFormatEditor ), @@ -134,7 +134,7 @@ export class ColorFormatEditor extends DefaultFormatEditor ), @@ -158,7 +158,7 @@ export class ColorFormatEditor extends DefaultFormatEditor ), @@ -181,7 +181,7 @@ export class ColorFormatEditor extends DefaultFormatEditor ), @@ -200,15 +200,15 @@ export class ColorFormatEditor extends DefaultFormatEditor { @@ -229,7 +229,7 @@ export class ColorFormatEditor extends DefaultFormatEditor diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/color/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/__snapshots__/date.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/__snapshots__/date.test.tsx.snap similarity index 93% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/__snapshots__/date.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/__snapshots__/date.test.tsx.snap index 48a7c3e013b1af..4560904c9b4c41 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/__snapshots__/date.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/__snapshots__/date.test.tsx.snap @@ -16,7 +16,7 @@ exports[`DateFormatEditor should render normally 1`] = ` >   @@ -30,7 +30,7 @@ exports[`DateFormatEditor should render normally 1`] = ` label={ diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/date.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/date.test.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/date.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/date.test.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/date.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/date.tsx similarity index 94% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/date.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/date.tsx index ae29c7a1236f57..62fb08855ce93e 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/date.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/date.tsx @@ -41,7 +41,7 @@ export class DateFormatEditor extends DefaultFormatEditor{defaultPattern}, @@ -54,7 +54,7 @@ export class DateFormatEditor extends DefaultFormatEditor   diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.tsx.snap similarity index 93% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.tsx.snap index 540c8ece9e35b9..0d0962a281950c 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/__snapshots__/date_nanos.test.tsx.snap @@ -16,7 +16,7 @@ exports[`DateFormatEditor should render normally 1`] = ` >   @@ -30,7 +30,7 @@ exports[`DateFormatEditor should render normally 1`] = ` label={ diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/date_nanos.test.tsx similarity index 95% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/date_nanos.test.tsx index 3f66dc59ab5697..4e8d56f91c6eb1 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.test.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/date_nanos.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { FieldFormat } from '../../../../../../../../data/public'; +import type { FieldFormat } from 'src/plugins/data/public'; import { DateNanosFormatEditor } from './date_nanos'; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/date_nanos.tsx similarity index 94% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/date_nanos.tsx index fab96322f0a16d..d9ee099aaef36a 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/date_nanos.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/date_nanos.tsx @@ -40,7 +40,7 @@ export class DateNanosFormatEditor extends DefaultFormatEditor{defaultPattern}, @@ -53,7 +53,7 @@ export class DateNanosFormatEditor extends DefaultFormatEditor   diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/date_nanos/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/date_nanos/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/__snapshots__/default.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/__snapshots__/default.test.tsx.snap similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/__snapshots__/default.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/__snapshots__/default.test.tsx.snap diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/default.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/default.test.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/default.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/default.test.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/default.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/default.tsx similarity index 92% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/default.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/default.tsx index 1bfc2c2fa340e1..06f3b318b6e935 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/default.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/default.tsx @@ -10,8 +10,8 @@ import React, { PureComponent, ReactText } from 'react'; import { i18n } from '@kbn/i18n'; import { FieldFormat, FieldFormatsContentType } from 'src/plugins/data/public'; -import { Sample } from '../../../../types'; -import { FieldFormatEditorProps } from '../../field_format_editor'; +import { Sample } from '../../types'; +import { FormatSelectEditorProps } from '../../field_format_editor'; export type ConverterParams = string | number | Array; @@ -30,7 +30,7 @@ export const convertSampleInput = ( }; }); } catch (e) { - error = i18n.translate('indexPatternManagement.defaultErrorMessage', { + error = i18n.translate('indexPatternFieldEditor.defaultErrorMessage', { defaultMessage: 'An error occurred while trying to use this format configuration: {message}', values: { message: e.message }, }); @@ -51,7 +51,7 @@ export interface FormatEditorProps

{ format: FieldFormat; formatParams: { type?: string } & P; onChange: (newParams: Record) => void; - onError: FieldFormatEditorProps['onError']; + onError: FormatSelectEditorProps['onError']; } export interface FormatEditorState { diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/default/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/default/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap similarity index 93% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap index c617c3b43039bf..cb7949deda64f6 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap @@ -12,7 +12,7 @@ exports[`DurationFormatEditor should render human readable output normally 1`] = label={ } @@ -42,7 +42,7 @@ exports[`DurationFormatEditor should render human readable output normally 1`] = label={ } @@ -124,7 +124,7 @@ exports[`DurationFormatEditor should render non-human readable output normally 1 label={ } @@ -154,7 +154,7 @@ exports[`DurationFormatEditor should render non-human readable output normally 1 label={ } @@ -189,7 +189,7 @@ exports[`DurationFormatEditor should render non-human readable output normally 1 label={ } @@ -216,7 +216,7 @@ exports[`DurationFormatEditor should render non-human readable output normally 1 label={ } diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/duration.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/duration.test.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/duration.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/duration.test.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/duration.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/duration.tsx similarity index 93% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/duration.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/duration.tsx index 4842c7066a2ef8..de413d02c5011c 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/duration.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/duration.tsx @@ -65,7 +65,7 @@ export class DurationFormatEditor extends DefaultFormatEditor< !(nextProps.format as DurationFormat).isHuman() && nextProps.formatParams.outputPrecision > 20 ) { - error = i18n.translate('indexPatternManagement.durationErrorMessage', { + error = i18n.translate('indexPatternFieldEditor.durationErrorMessage', { defaultMessage: 'Decimal places must be between 0 and 20', }); nextProps.onError(error); @@ -91,7 +91,7 @@ export class DurationFormatEditor extends DefaultFormatEditor< } @@ -115,7 +115,7 @@ export class DurationFormatEditor extends DefaultFormatEditor< } @@ -140,7 +140,7 @@ export class DurationFormatEditor extends DefaultFormatEditor< } @@ -163,7 +163,7 @@ export class DurationFormatEditor extends DefaultFormatEditor< } diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/index.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/index.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/duration/index.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/index.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap similarity index 92% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap index c73b5e7186547f..3cac3850548351 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/__snapshots__/number.test.tsx.snap @@ -16,7 +16,7 @@ exports[`NumberFormatEditor should render normally 1`] = ` >   @@ -30,7 +30,7 @@ exports[`NumberFormatEditor should render normally 1`] = ` label={ diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/number.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/number.test.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/number.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/number.test.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/number.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/number.tsx similarity index 94% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/number.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/number.tsx index 250bbe570a9c4f..2aeb90373bfaba 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/number/number.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/number/number.tsx @@ -36,7 +36,7 @@ export class NumberFormatEditor extends DefaultFormatEditor{defaultPattern} }} /> @@ -45,7 +45,7 @@ export class NumberFormatEditor extends DefaultFormatEditor   diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap similarity index 92% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap index 16ce8ca9643ef2..f6af1f0dff7fe3 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/__snapshots__/percent.test.tsx.snap @@ -16,7 +16,7 @@ exports[`PercentFormatEditor should render normally 1`] = ` >   @@ -30,7 +30,7 @@ exports[`PercentFormatEditor should render normally 1`] = ` label={ diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/percent.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/percent.test.tsx similarity index 95% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/percent.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/percent.test.tsx index 6eff2fd279cbb5..072dc0caeb3c8b 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/percent.test.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/percent.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { FieldFormat } from '../../../../../../../../data/public'; +import { FieldFormat } from 'src/plugins/data/public'; import { PercentFormatEditor } from './percent'; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/percent.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/percent.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/percent/percent.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/percent/percent.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap similarity index 88% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap index 46267b0c3c0e98..c5697cb699eb7a 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap @@ -9,7 +9,7 @@ exports[`StaticLookupFormatEditor should render multiple lookup entries and unkn "field": "key", "name": , "render": [Function], @@ -18,7 +18,7 @@ exports[`StaticLookupFormatEditor should render multiple lookup entries and unkn "field": "value", "name": , "render": [Function], @@ -73,7 +73,7 @@ exports[`StaticLookupFormatEditor should render multiple lookup entries and unkn > @@ -89,7 +89,7 @@ exports[`StaticLookupFormatEditor should render multiple lookup entries and unkn label={ } @@ -116,7 +116,7 @@ exports[`StaticLookupFormatEditor should render normally 1`] = ` "field": "key", "name": , "render": [Function], @@ -125,7 +125,7 @@ exports[`StaticLookupFormatEditor should render normally 1`] = ` "field": "value", "name": , "render": [Function], @@ -174,7 +174,7 @@ exports[`StaticLookupFormatEditor should render normally 1`] = ` > @@ -190,7 +190,7 @@ exports[`StaticLookupFormatEditor should render normally 1`] = ` label={ } diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/static_lookup.test.tsx similarity index 96% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/static_lookup.test.tsx index e24b656267d1aa..8d9cb17b33a403 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.test.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/static_lookup.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { shallowWithI18nProvider } from '@kbn/test/jest'; import { StaticLookupFormatEditorFormatParams } from './static_lookup'; -import { FieldFormat } from '../../../../../../../../data/public'; +import { FieldFormat } from 'src/plugins/data/public'; import { StaticLookupFormatEditor } from './static_lookup'; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/static_lookup.tsx similarity index 88% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/static_lookup.tsx index 8c49615c99f6c2..8ac03bb23bd25a 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/static_lookup/static_lookup.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/static_lookup.tsx @@ -72,7 +72,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor ), @@ -96,7 +96,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor ), @@ -118,15 +118,15 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor { @@ -148,7 +148,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor @@ -156,7 +156,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor } @@ -164,7 +164,7 @@ export class StaticLookupFormatEditor extends DefaultFormatEditor } diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/string/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/string/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/string/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/string/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/string/string.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/string/string.test.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/string/string.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/string/string.test.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/string/string.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/string/string.tsx similarity index 96% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/string/string.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/string/string.tsx index 6f9e0e10e188a4..e86a62775cebcb 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/string/string.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/string/string.tsx @@ -47,7 +47,7 @@ export class StringFormatEditor extends DefaultFormatEditor } diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/__snapshots__/truncate.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/__snapshots__/truncate.test.tsx.snap similarity index 96% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/__snapshots__/truncate.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/__snapshots__/truncate.test.tsx.snap index 2d1ee496d2786b..f982632bba5235 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/__snapshots__/truncate.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/__snapshots__/truncate.test.tsx.snap @@ -12,7 +12,7 @@ exports[`TruncateFormatEditor should render normally 1`] = ` label={ } diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/sample.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/sample.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/sample.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/sample.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/truncate.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/truncate.test.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/truncate.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/truncate.test.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/truncate.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/truncate.tsx similarity index 96% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/truncate.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/truncate.tsx index 4b24d33e58f3ce..03b7d6e0573cc5 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/truncate/truncate.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/truncate/truncate.tsx @@ -37,7 +37,7 @@ export class TruncateFormatEditor extends DefaultFormatEditor } diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap similarity index 92% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap index b627dbe0576ee2..bc5efb8f5eda42 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap @@ -166,14 +166,26 @@ exports[`UrlFormatEditor should render normally 1`] = ` class="euiFormHelpText euiFormRow__text" id="generated-id-help" > - + + + (opens in a new tab or window) + +

@@ -217,14 +229,26 @@ exports[`UrlFormatEditor should render normally 1`] = ` class="euiFormHelpText euiFormRow__text" id="generated-id-help" > - + + + (opens in a new tab or window) + +
diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/url.test.tsx similarity index 75% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/url.test.tsx index 5c86abc3b4a9c1..9f299a433aab1a 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url.test.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/url.test.tsx @@ -10,8 +10,8 @@ import React from 'react'; import { FieldFormat } from 'src/plugins/data/public'; import { IntlProvider } from 'react-intl'; import { UrlFormatEditor } from './url'; -import { coreMock } from '../../../../../../../../../core/public/mocks'; -import { createKibanaReactContext } from '../../../../../../../../kibana_react/public'; +import { coreMock } from 'src/core/public/mocks'; +import { createKibanaReactContext } from '../../../../../../kibana_react/public'; import { render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -76,38 +76,6 @@ describe('UrlFormatEditor', () => { expect(container).toMatchSnapshot(); }); - it('should render url template help', async () => { - const { getByText, getByTestId } = renderWithContext( - - ); - - getByText('URL template help'); - userEvent.click(getByText('URL template help')); - expect(getByTestId('urlTemplateFlyoutTestSubj')).toBeVisible(); - }); - - it('should render label template help', async () => { - const { getByText, getByTestId } = renderWithContext( - - ); - - getByText('Label template help'); - userEvent.click(getByText('Label template help')); - expect(getByTestId('labelTemplateFlyoutTestSubj')).toBeVisible(); - }); - it('should render width and height fields if image', async () => { const { getByLabelText } = renderWithContext( { static contextType = contextType; static formatId = 'url'; - // TODO: @kbn/optimizer can't compile this - // declare context: IndexPatternManagmentContextValue; - context: IndexPatternManagmentContextValue | undefined; private get sampleIconPath() { const sampleIconPath = `/plugins/indexPatternManagement/assets/icons/{{value}}.png`; return this.context?.services.http @@ -110,32 +103,6 @@ export class UrlFormatEditor extends DefaultFormatEditor< this.onChange(params); }; - showUrlTemplateHelp = () => { - this.setState({ - showLabelTemplateHelp: false, - showUrlTemplateHelp: true, - }); - }; - - hideUrlTemplateHelp = () => { - this.setState({ - showUrlTemplateHelp: false, - }); - }; - - showLabelTemplateHelp = () => { - this.setState({ - showLabelTemplateHelp: true, - showUrlTemplateHelp: false, - }); - }; - - hideLabelTemplateHelp = () => { - this.setState({ - showLabelTemplateHelp: false, - }); - }; - renderWidthHeightParameters = () => { const width = this.sanitizeNumericValue(this.props.formatParams.width); const height = this.sanitizeNumericValue(this.props.formatParams.height); @@ -143,7 +110,7 @@ export class UrlFormatEditor extends DefaultFormatEditor< + } > + } > - - + } > } @@ -217,9 +179,12 @@ export class UrlFormatEditor extends DefaultFormatEditor< + ) : ( - + ) } checked={!formatParams.openLinkInCurrentTab} @@ -233,14 +198,17 @@ export class UrlFormatEditor extends DefaultFormatEditor< } helpText={ - + @@ -260,14 +228,17 @@ export class UrlFormatEditor extends DefaultFormatEditor< } helpText={ - + diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/field_format_editor.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/field_format_editor.tsx new file mode 100644 index 00000000000000..1f3e87e69fd4c2 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/field_format_editor.tsx @@ -0,0 +1,186 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { PureComponent } from 'react'; +import { EuiCode, EuiFormRow, EuiSelect } from '@elastic/eui'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { + FieldFormatInstanceType, + IndexPattern, + KBN_FIELD_TYPES, + ES_FIELD_TYPES, + DataPublicPluginStart, + FieldFormat, +} from 'src/plugins/data/public'; +import { CoreStart } from 'src/core/public'; +import { castEsToKbnFieldTypeName } from '../../../../data/public'; +import { FormatEditor } from './format_editor'; +import { FormatEditorServiceStart } from '../../service'; +import { FieldFormatConfig } from '../../types'; + +export interface FormatSelectEditorProps { + esTypes: ES_FIELD_TYPES[]; + indexPattern: IndexPattern; + fieldFormatEditors: FormatEditorServiceStart['fieldFormatEditors']; + fieldFormats: DataPublicPluginStart['fieldFormats']; + uiSettings: CoreStart['uiSettings']; + onChange: (change?: FieldFormatConfig) => void; + onError: (error?: string) => void; + value?: FieldFormatConfig; +} + +interface FieldTypeFormat { + id: string; + title: string; +} + +export interface FormatSelectEditorState { + fieldTypeFormats: FieldTypeFormat[]; + fieldFormatId?: string; + fieldFormatParams?: { [key: string]: unknown }; + format: FieldFormat; + kbnType: KBN_FIELD_TYPES; +} + +interface InitialFieldTypeFormat extends FieldTypeFormat { + defaultFieldFormat: FieldFormatInstanceType; +} + +const getFieldTypeFormatsList = ( + fieldType: KBN_FIELD_TYPES, + defaultFieldFormat: FieldFormatInstanceType, + fieldFormats: DataPublicPluginStart['fieldFormats'] +) => { + const formatsByType = fieldFormats.getByFieldType(fieldType).map(({ id, title }) => ({ + id, + title, + })); + + return [ + { + id: '', + defaultFieldFormat, + title: i18n.translate('indexPatternFieldEditor.defaultFormatDropDown', { + defaultMessage: '- Default -', + }), + }, + ...formatsByType, + ]; +}; + +export class FormatSelectEditor extends PureComponent< + FormatSelectEditorProps, + FormatSelectEditorState +> { + constructor(props: FormatSelectEditorProps) { + super(props); + const { fieldFormats, esTypes, value } = props; + const kbnType = castEsToKbnFieldTypeName(esTypes[0] || 'keyword'); + + // get current formatter for field, provides default if none exists + const format = value?.id + ? fieldFormats.getInstance(value?.id, value?.params) + : fieldFormats.getDefaultInstance(kbnType, esTypes); + + this.state = { + fieldTypeFormats: getFieldTypeFormatsList( + kbnType, + fieldFormats.getDefaultType(kbnType, esTypes) as FieldFormatInstanceType, + fieldFormats + ), + format, + kbnType, + }; + } + onFormatChange = (formatId: string, params?: any) => { + const { fieldTypeFormats } = this.state; + const { fieldFormats, uiSettings } = this.props; + + const FieldFormatClass = fieldFormats.getType( + formatId || (fieldTypeFormats[0] as InitialFieldTypeFormat).defaultFieldFormat.id + ) as FieldFormatInstanceType; + + const newFormat = new FieldFormatClass(params, (key: string) => uiSettings.get(key)); + + this.setState( + { + fieldFormatId: formatId, + fieldFormatParams: params, + format: newFormat, + }, + () => { + this.props.onChange( + formatId + ? { + id: formatId, + params: params || {}, + } + : undefined + ); + } + ); + }; + onFormatParamsChange = (newParams: { fieldType: string; [key: string]: any }) => { + const { fieldFormatId } = this.state; + this.onFormatChange(fieldFormatId as string, newParams); + }; + + render() { + const { fieldFormatEditors, onError, value } = this.props; + const fieldFormatId = value?.id; + const fieldFormatParams = value?.params; + const { kbnType } = this.state; + + const { fieldTypeFormats, format } = this.state; + + const defaultFormat = (fieldTypeFormats[0] as InitialFieldTypeFormat).defaultFieldFormat.title; + + const label = defaultFormat ? ( + {defaultFormat}, + }} + /> + ) : ( + + ); + return ( + <> + + { + return { value: fmt.id || '', text: fmt.title }; + })} + data-test-subj="editorSelectedFormatId" + onChange={(e) => { + this.onFormatChange(e.target.value); + }} + /> + + {fieldFormatId ? ( + { + this.onFormatChange(fieldFormatId, params); + }} + onError={onError} + /> + ) : null} + + ); + } +} diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.test.tsx new file mode 100644 index 00000000000000..5514baf43ecfc0 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.test.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { PureComponent } from 'react'; +import { shallow } from 'enzyme'; +import { FormatEditor } from './format_editor'; + +class TestEditor extends PureComponent { + render() { + if (this.props) { + return null; + } + return
Test editor
; + } +} + +const formatEditors = { + byFormatId: { + ip: TestEditor, + number: TestEditor, + }, + getById: jest.fn(() => TestEditor as any), + getAll: jest.fn(), +}; + +describe('FieldFormatEditor', () => { + it('should render normally', async () => { + const component = shallow( + {}} + onError={() => {}} + /> + ); + + expect(component).toMatchSnapshot(); + }); + + it('should render nothing if there is no editor for the format', async () => { + const component = shallow( + {}} + onError={() => {}} + /> + ); + + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.tsx new file mode 100644 index 00000000000000..043a911e69812a --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/format_editor.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { PureComponent, Fragment } from 'react'; +import { FieldFormat } from 'src/plugins/data/public'; + +export interface FormatEditorProps { + fieldType: string; + fieldFormat: FieldFormat; + fieldFormatId: string; + fieldFormatParams: { [key: string]: unknown }; + fieldFormatEditors: any; + onChange: (change: { fieldType: string; [key: string]: any }) => void; + onError: (error?: string) => void; +} + +interface EditorComponentProps { + fieldType: FormatEditorProps['fieldType']; + format: FormatEditorProps['fieldFormat']; + formatParams: FormatEditorProps['fieldFormatParams']; + onChange: FormatEditorProps['onChange']; + onError: FormatEditorProps['onError']; +} + +interface FormatEditorState { + EditorComponent: React.FC; + fieldFormatId?: string; +} + +export class FormatEditor extends PureComponent { + constructor(props: FormatEditorProps) { + super(props); + this.state = { + EditorComponent: props.fieldFormatEditors.getById(props.fieldFormatId), + }; + } + + static getDerivedStateFromProps(nextProps: FormatEditorProps) { + return { + EditorComponent: nextProps.fieldFormatEditors.getById(nextProps.fieldFormatId) || null, + }; + } + + render() { + const { EditorComponent } = this.state; + const { fieldType, fieldFormat, fieldFormatParams, onChange, onError } = this.props; + + return ( + + {EditorComponent ? ( + + ) : null} + + ); + } +} diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/index.ts new file mode 100644 index 00000000000000..34619f53e9eed6 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { FormatSelectEditor, FormatSelectEditorProps } from './field_format_editor'; +export * from './editors'; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap similarity index 96% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap index ce8c9e70433c84..1a0b96c14fe359 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap @@ -10,7 +10,7 @@ exports[`FormatEditorSamples should render normally 1`] = ` label={ } diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/index.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/index.ts rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/index.ts diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/samples.scss b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/samples.scss similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/samples.scss rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/samples.scss diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/samples.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/samples.test.tsx similarity index 100% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/samples.test.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/samples.test.tsx diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/samples.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/samples.tsx similarity index 87% rename from src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/samples.tsx rename to src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/samples.tsx index 536cb3567017a9..73727119f10777 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/samples/samples.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/samples.tsx @@ -14,7 +14,7 @@ import { EuiBasicTable, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Sample } from '../../../types'; +import { Sample } from '../types'; interface FormatEditorSamplesProps { samples: Sample[]; @@ -32,7 +32,7 @@ export class FormatEditorSamples extends PureComponent const columns = [ { field: 'input', - name: i18n.translate('indexPatternManagement.samples.inputHeader', { + name: i18n.translate('indexPatternFieldEditor.samples.inputHeader', { defaultMessage: 'Input', }), render: (input: {} | string) => { @@ -41,7 +41,7 @@ export class FormatEditorSamples extends PureComponent }, { field: 'output', - name: i18n.translate('indexPatternManagement.samples.outputHeader', { + name: i18n.translate('indexPatternFieldEditor.samples.outputHeader', { defaultMessage: 'Output', }), render: (output: string) => { @@ -63,7 +63,7 @@ export class FormatEditorSamples extends PureComponent return samples.length ? ( + } > diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/types.ts b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/types.ts new file mode 100644 index 00000000000000..11c0b8a6259070 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/types.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ReactText } from 'react'; + +export interface Sample { + input: ReactText | ReactText[]; + output: string; +} diff --git a/src/plugins/index_pattern_field_editor/public/components/index.ts b/src/plugins/index_pattern_field_editor/public/components/index.ts new file mode 100644 index 00000000000000..0fbb574a6f0a41 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/components/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { + FieldEditorFlyoutContent, + Props as FieldEditorFlyoutContentProps, +} from './field_editor_flyout_content'; + +export { + FieldEditorFlyoutContentContainer, + Props as FieldEditorFlyoutContentContainerProps, + FieldEditorContext, +} from './field_editor_flyout_content_container'; + +export * from './field_format_editor'; diff --git a/src/plugins/index_pattern_field_editor/public/constants.ts b/src/plugins/index_pattern_field_editor/public/constants.ts new file mode 100644 index 00000000000000..69d231f3758486 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/constants.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const pluginName = 'index_pattern_field_editor'; diff --git a/src/plugins/index_pattern_field_editor/public/index.ts b/src/plugins/index_pattern_field_editor/public/index.ts new file mode 100644 index 00000000000000..38735013d576d5 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * Management Plugin - public + * + * This is the entry point for the entire client-side public contract of the plugin. + * If something is not explicitly exported here, you can safely assume it is private + * to the plugin and not considered stable. + * + * All stateful contracts will be injected by the platform at runtime, and are defined + * in the setup/start interfaces in `plugin.ts`. The remaining items exported here are + * either types, or static code. + */ + +import { IndexPatternFieldEditorPlugin } from './plugin'; + +export { PluginStart as IndexPatternFieldEditorStart } from './types'; +export { DefaultFormatEditor } from './components'; + +export function plugin() { + return new IndexPatternFieldEditorPlugin(); +} + +// Expose types +export type { OpenFieldEditorOptions } from './open_editor'; +export type { FieldEditorContext } from './components/field_editor_flyout_content_container'; diff --git a/src/plugins/index_pattern_field_editor/public/lib/documentation.ts b/src/plugins/index_pattern_field_editor/public/lib/documentation.ts new file mode 100644 index 00000000000000..9577f25184ba0a --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/lib/documentation.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DocLinksStart } from 'src/core/public'; + +export const getLinks = (docLinks: DocLinksStart) => { + const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } = docLinks; + const docsBase = `${ELASTIC_WEBSITE_URL}guide/en`; + const esDocsBase = `${docsBase}/elasticsearch/reference/${DOC_LINK_VERSION}`; + const painlessDocsBase = `${docsBase}/elasticsearch/painless/${DOC_LINK_VERSION}`; + + return { + runtimePainless: `${esDocsBase}/runtime.html#runtime-mapping-fields`, + painlessSyntax: `${painlessDocsBase}/painless-lang-spec.html`, + }; +}; diff --git a/src/plugins/index_pattern_field_editor/public/lib/index.ts b/src/plugins/index_pattern_field_editor/public/lib/index.ts new file mode 100644 index 00000000000000..5d5b3d881e9769 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/lib/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { deserializeField } from './serialization'; + +export { getLinks } from './documentation'; + +export { getRuntimeFieldValidator, RuntimeFieldPainlessError } from './runtime_field_validation'; diff --git a/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.test.ts b/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.test.ts new file mode 100644 index 00000000000000..b25d47b3d0d151 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.test.ts @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { dataPluginMock } from '../../../data/public/mocks'; +import { getRuntimeFieldValidator } from './runtime_field_validation'; + +const dataStart = dataPluginMock.createStartContract(); +const { search } = dataStart; + +const runtimeField = { + type: 'keyword', + script: { + source: 'emit("hello")', + }, +}; + +const spy = jest.fn(); + +search.search = () => + ({ + toPromise: spy, + } as any); + +const validator = getRuntimeFieldValidator('myIndex', search); + +describe('Runtime field validation', () => { + const expectedError = { + message: 'Error compiling the painless script', + position: { offset: 4, start: 0, end: 18 }, + reason: 'cannot resolve symbol [emit]', + scriptStack: ["emit.some('value')", ' ^---- HERE'], + }; + + [ + { + title: 'should return null when there are no errors', + response: {}, + status: 200, + expected: null, + }, + { + title: 'should return the error in the first failed shard', + response: { + attributes: { + type: 'status_exception', + reason: 'error while executing search', + caused_by: { + failed_shards: [ + { + shard: 0, + index: 'kibana_sample_data_logs', + node: 'gVwk20UWSdO6VyuNOc_6UA', + reason: { + type: 'script_exception', + script_stack: ["emit.some('value')", ' ^---- HERE'], + position: { offset: 4, start: 0, end: 18 }, + caused_by: { + type: 'illegal_argument_exception', + reason: 'cannot resolve symbol [emit]', + }, + }, + }, + ], + }, + }, + }, + status: 400, + expected: expectedError, + }, + { + title: 'should return the error in the third failed shard', + response: { + attributes: { + type: 'status_exception', + reason: 'error while executing search', + caused_by: { + failed_shards: [ + { + shard: 0, + index: 'kibana_sample_data_logs', + node: 'gVwk20UWSdO6VyuNOc_6UA', + reason: { + type: 'foo', + }, + }, + { + shard: 1, + index: 'kibana_sample_data_logs', + node: 'gVwk20UWSdO6VyuNOc_6UA', + reason: { + type: 'bar', + }, + }, + { + shard: 2, + index: 'kibana_sample_data_logs', + node: 'gVwk20UWSdO6VyuNOc_6UA', + reason: { + type: 'script_exception', + script_stack: ["emit.some('value')", ' ^---- HERE'], + position: { offset: 4, start: 0, end: 18 }, + caused_by: { + type: 'illegal_argument_exception', + reason: 'cannot resolve symbol [emit]', + }, + }, + }, + ], + }, + }, + }, + status: 400, + expected: expectedError, + }, + { + title: 'should have default values if an error prop is not found', + response: { + attributes: { + type: 'status_exception', + reason: 'error while executing search', + caused_by: { + failed_shards: [ + { + shard: 0, + index: 'kibana_sample_data_logs', + node: 'gVwk20UWSdO6VyuNOc_6UA', + reason: { + // script_stack, position and caused_by are missing + type: 'script_exception', + caused_by: { + type: 'illegal_argument_exception', + }, + }, + }, + ], + }, + }, + }, + status: 400, + expected: { + message: 'Error compiling the painless script', + position: null, + reason: null, + scriptStack: [], + }, + }, + ].map(({ title, response, status, expected }) => { + test(title, async () => { + if (status !== 200) { + spy.mockRejectedValueOnce(response); + } else { + spy.mockResolvedValueOnce(response); + } + + const result = await validator(runtimeField); + + expect(result).toEqual(expected); + }); + }); +}); diff --git a/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.ts b/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.ts new file mode 100644 index 00000000000000..f1a6fd7f9e8aa8 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/lib/runtime_field_validation.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { i18n } from '@kbn/i18n'; + +import { DataPublicPluginStart } from '../shared_imports'; +import { EsRuntimeField } from '../types'; + +export interface RuntimeFieldPainlessError { + message: string; + reason: string; + position: { + offset: number; + start: number; + end: number; + } | null; + scriptStack: string[]; +} + +type Error = Record; + +/** + * We are only interested in "script_exception" error type + */ +const getScriptExceptionErrorOnShard = (error: Error): Error | null => { + if (error.type === 'script_exception') { + return error; + } + + if (!error.caused_by) { + return null; + } + + // Recursively try to get a script exception error + return getScriptExceptionErrorOnShard(error.caused_by); +}; + +/** + * We get the first script exception error on any failing shard. + * The UI can only display one error at the time so there is no need + * to look any further. + */ +const getScriptExceptionError = (error: Error): Error | null => { + if (error === undefined || !Array.isArray(error.failed_shards)) { + return null; + } + + let scriptExceptionError = null; + for (const err of error.failed_shards) { + scriptExceptionError = getScriptExceptionErrorOnShard(err.reason); + + if (scriptExceptionError !== null) { + break; + } + } + return scriptExceptionError; +}; + +const parseEsError = (error?: Error): RuntimeFieldPainlessError | null => { + if (error === undefined) { + return null; + } + + const scriptError = getScriptExceptionError(error.caused_by); + + if (scriptError === null) { + return null; + } + + return { + message: i18n.translate( + 'indexPatternFieldEditor.editor.form.scriptEditor.compileErrorMessage', + { + defaultMessage: 'Error compiling the painless script', + } + ), + position: scriptError.position ?? null, + scriptStack: scriptError.script_stack ?? [], + reason: scriptError.caused_by?.reason ?? null, + }; +}; + +/** + * Handler to validate the painless script for syntax and semantic errors. + * This is a temporary solution. In a future work we will have a dedicate + * ES API to debug the script. + */ +export const getRuntimeFieldValidator = ( + index: string, + searchService: DataPublicPluginStart['search'] +) => async (runtimeField: EsRuntimeField) => { + return await searchService + .search({ + params: { + index, + body: { + runtime_mappings: { + temp: runtimeField, + }, + size: 0, + query: { + match_none: {}, + }, + }, + }, + }) + .toPromise() + .then(() => null) + .catch((e) => { + return parseEsError(e.attributes); + }); +}; diff --git a/src/plugins/index_pattern_field_editor/public/lib/serialization.ts b/src/plugins/index_pattern_field_editor/public/lib/serialization.ts new file mode 100644 index 00000000000000..9000a34b23cbea --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/lib/serialization.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IndexPatternField, IndexPattern } from '../shared_imports'; +import { Field } from '../types'; + +export const deserializeField = ( + indexPattern: IndexPattern, + field?: IndexPatternField +): Field | undefined => { + if (field === undefined) { + return undefined; + } + + return { + name: field.name, + type: field?.esTypes ? field.esTypes[0] : 'keyword', + script: field.runtimeField ? field.runtimeField.script : undefined, + customLabel: field.customLabel, + popularity: field.count, + format: indexPattern.getFormatterForFieldNoDefault(field.name)?.toJSON(), + }; +}; diff --git a/src/plugins/index_pattern_field_editor/public/mocks.ts b/src/plugins/index_pattern_field_editor/public/mocks.ts new file mode 100644 index 00000000000000..23bd4c385ca3b2 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/mocks.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { IndexPatternFieldEditorPlugin } from './plugin'; + +export type Start = jest.Mocked< + Omit, 'DeleteRuntimeFieldProvider'> +> & { + DeleteRuntimeFieldProvider: ReturnType< + IndexPatternFieldEditorPlugin['start'] + >['DeleteRuntimeFieldProvider']; +}; + +export type Setup = jest.Mocked>; + +const createSetupContract = (): Setup => { + return { + fieldFormatEditors: { + register: jest.fn(), + } as any, + }; +}; + +const createStartContract = (): Start => { + return { + openEditor: jest.fn(), + fieldFormatEditors: { + getAll: jest.fn(), + getById: jest.fn(), + } as any, + userPermissions: { + editIndexPattern: jest.fn(), + }, + DeleteRuntimeFieldProvider: ({ children }) => children(jest.fn()) as JSX.Element, + }; +}; + +export const indexPatternFieldEditorPluginMock = { + createSetupContract, + createStartContract, +}; diff --git a/src/plugins/index_pattern_field_editor/public/open_editor.tsx b/src/plugins/index_pattern_field_editor/public/open_editor.tsx new file mode 100644 index 00000000000000..d4d1f71433ea74 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/open_editor.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { CoreStart, OverlayRef } from 'src/core/public'; +import { i18n } from '@kbn/i18n'; + +import { + createKibanaReactContext, + toMountPoint, + IndexPatternField, + DataPublicPluginStart, + IndexPattern, + UsageCollectionStart, +} from './shared_imports'; + +import { InternalFieldType } from './types'; +import { FieldEditorFlyoutContentContainer } from './components/field_editor_flyout_content_container'; + +import { PluginStart } from './types'; + +export interface OpenFieldEditorOptions { + ctx: { + indexPattern: IndexPattern; + }; + onSave?: (field: IndexPatternField) => void; + fieldName?: string; +} + +type CloseEditor = () => void; +interface Dependencies { + core: CoreStart; + /** The search service from the data plugin */ + search: DataPublicPluginStart['search']; + indexPatternService: DataPublicPluginStart['indexPatterns']; + fieldFormats: DataPublicPluginStart['fieldFormats']; + fieldFormatEditors: PluginStart['fieldFormatEditors']; + usageCollection: UsageCollectionStart; +} + +export const getFieldEditorOpener = ({ + core, + indexPatternService, + fieldFormats, + fieldFormatEditors, + search, + usageCollection, +}: Dependencies) => (options: OpenFieldEditorOptions): CloseEditor => { + const { uiSettings, overlays, docLinks, notifications } = core; + const { Provider: KibanaReactContextProvider } = createKibanaReactContext({ + uiSettings, + docLinks, + http: core.http, + }); + + let overlayRef: OverlayRef | null = null; + + const openEditor = ({ onSave, fieldName, ctx }: OpenFieldEditorOptions): CloseEditor => { + const closeEditor = () => { + if (overlayRef) { + overlayRef.close(); + overlayRef = null; + } + }; + + const onSaveField = (updatedField: IndexPatternField) => { + closeEditor(); + + if (onSave) { + onSave(updatedField); + } + }; + + const field = fieldName ? ctx.indexPattern.getFieldByName(fieldName) : undefined; + + if (fieldName && !field) { + const err = i18n.translate('indexPatternFieldEditor.noSuchFieldName', { + defaultMessage: "Field named '{fieldName}' not found on index pattern", + values: { fieldName }, + }); + notifications.toasts.addDanger(err); + return closeEditor; + } + + const isNewRuntimeField = !fieldName; + const isExistingRuntimeField = field && field.runtimeField && !field.isMapped; + const fieldTypeToProcess: InternalFieldType = + isNewRuntimeField || isExistingRuntimeField ? 'runtime' : 'concrete'; + + overlayRef = overlays.openFlyout( + toMountPoint( + + + + ) + ); + + return closeEditor; + }; + + return openEditor(options); +}; diff --git a/src/plugins/index_pattern_field_editor/public/plugin.test.tsx b/src/plugins/index_pattern_field_editor/public/plugin.test.tsx new file mode 100644 index 00000000000000..4870ecc827ab09 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/plugin.test.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; + +jest.mock('../../kibana_react/public', () => { + const original = jest.requireActual('../../kibana_react/public'); + + return { + ...original, + toMountPoint: (node: React.ReactNode) => node, + }; +}); + +import { CoreStart } from 'src/core/public'; +import { coreMock } from 'src/core/public/mocks'; +import { dataPluginMock } from '../../data/public/mocks'; +import { usageCollectionPluginMock } from '../../usage_collection/public/mocks'; + +import { registerTestBed } from './test_utils'; + +import { FieldEditorFlyoutContentContainer } from './components/field_editor_flyout_content_container'; +import { IndexPatternFieldEditorPlugin } from './plugin'; + +const noop = () => {}; + +describe('IndexPatternFieldEditorPlugin', () => { + const coreStart: CoreStart = coreMock.createStart(); + const pluginStart = { + data: dataPluginMock.createStartContract(), + usageCollection: usageCollectionPluginMock.createSetupContract(), + }; + + let plugin: IndexPatternFieldEditorPlugin; + + beforeEach(() => { + plugin = new IndexPatternFieldEditorPlugin(); + }); + + test('should expose a handler to open the indexpattern field editor', async () => { + const startApi = await plugin.start(coreStart, pluginStart); + expect(startApi.openEditor).toBeDefined(); + }); + + test('should call core.overlays.openFlyout when opening the editor', async () => { + const openFlyout = jest.fn(); + const onSaveSpy = jest.fn(); + + const coreStartMocked = { + ...coreStart, + overlays: { + ...coreStart.overlays, + openFlyout, + }, + }; + const { openEditor } = await plugin.start(coreStartMocked, pluginStart); + + openEditor({ onSave: onSaveSpy, ctx: { indexPattern: {} as any } }); + + expect(openFlyout).toHaveBeenCalled(); + + const [[arg]] = openFlyout.mock.calls; + expect(arg.props.children.type).toBe(FieldEditorFlyoutContentContainer); + + // We force call the "onSave" prop from the component + // and make sure that the the spy is being called. + // Note: we are testing implementation details, if we change or rename the "onSave" prop on + // the component, we will need to update this test accordingly. + expect(arg.props.children.props.onSave).toBeDefined(); + arg.props.children.props.onSave(); + expect(onSaveSpy).toHaveBeenCalled(); + }); + + test('should return a handler to close the flyout', async () => { + const { openEditor } = await plugin.start(coreStart, pluginStart); + + const closeEditorHandler = openEditor({ onSave: noop, ctx: { indexPattern: {} as any } }); + expect(typeof closeEditorHandler).toBe('function'); + }); + + test('should expose a render props component to delete runtime fields', async () => { + const { DeleteRuntimeFieldProvider } = await plugin.start(coreStart, pluginStart); + + const TestComponent = ({ callback }: { callback: (...args: any[]) => void }) => { + return ( + + {(...args) => { + // Forward arguments passed down to children to our spy callback + callback(args); + return null; + }} + + ); + }; + + const setup = registerTestBed(TestComponent, { + memoryRouter: { wrapComponent: false }, + }); + + const spy = jest.fn(); + // Mount our dummy component and pass it the spy + setup({ callback: spy }); + + expect(spy).toHaveBeenCalled(); + const argumentsFromRenderProps = spy.mock.calls[0][0]; + + expect(argumentsFromRenderProps.length).toBe(1); + expect(typeof argumentsFromRenderProps[0]).toBe('function'); + }); +}); diff --git a/src/plugins/index_pattern_field_editor/public/plugin.ts b/src/plugins/index_pattern_field_editor/public/plugin.ts new file mode 100644 index 00000000000000..c3736b50c344cb --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/plugin.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Plugin, CoreSetup, CoreStart } from 'src/core/public'; + +import { PluginSetup, PluginStart, SetupPlugins, StartPlugins } from './types'; +import { getFieldEditorOpener } from './open_editor'; +import { FormatEditorService } from './service'; +import { getDeleteProvider } from './components/delete_field_provider'; + +export class IndexPatternFieldEditorPlugin + implements Plugin { + private readonly formatEditorService = new FormatEditorService(); + + public setup(core: CoreSetup, plugins: SetupPlugins): PluginSetup { + const { fieldFormatEditors } = this.formatEditorService.setup(); + + return { + fieldFormatEditors, + }; + } + + public start(core: CoreStart, plugins: StartPlugins) { + const { fieldFormatEditors } = this.formatEditorService.start(); + const { + application: { capabilities }, + } = core; + const { data, usageCollection } = plugins; + return { + fieldFormatEditors, + openEditor: getFieldEditorOpener({ + core, + indexPatternService: data.indexPatterns, + fieldFormats: data.fieldFormats, + fieldFormatEditors, + search: data.search, + usageCollection, + }), + userPermissions: { + editIndexPattern: () => { + return capabilities.management.kibana.indexPatterns; + }, + }, + DeleteRuntimeFieldProvider: getDeleteProvider( + data.indexPatterns, + usageCollection, + core.notifications + ), + }; + } + + public stop() { + return {}; + } +} diff --git a/src/plugins/index_pattern_management/public/service/field_format_editors/field_format_editors.ts b/src/plugins/index_pattern_field_editor/public/service/field_format_editors/field_format_editors.ts similarity index 89% rename from src/plugins/index_pattern_management/public/service/field_format_editors/field_format_editors.ts rename to src/plugins/index_pattern_field_editor/public/service/field_format_editors/field_format_editors.ts index d5335cdf0f06e7..fdc54a39c8c2a6 100644 --- a/src/plugins/index_pattern_management/public/service/field_format_editors/field_format_editors.ts +++ b/src/plugins/index_pattern_field_editor/public/service/field_format_editors/field_format_editors.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { DefaultFormatEditor } from '../../components/field_editor/components/field_format_editor'; +import { DefaultFormatEditor } from '../../components/field_format_editor'; export class FieldFormatEditors { private editors: Array = []; diff --git a/src/plugins/index_pattern_management/public/service/field_format_editors/index.ts b/src/plugins/index_pattern_field_editor/public/service/field_format_editors/index.ts similarity index 100% rename from src/plugins/index_pattern_management/public/service/field_format_editors/index.ts rename to src/plugins/index_pattern_field_editor/public/service/field_format_editors/index.ts diff --git a/src/plugins/index_pattern_field_editor/public/service/format_editor_service.ts b/src/plugins/index_pattern_field_editor/public/service/format_editor_service.ts new file mode 100644 index 00000000000000..67064e8a9cdbf9 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/service/format_editor_service.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FieldFormatEditors } from './field_format_editors'; + +import { + BytesFormatEditor, + ColorFormatEditor, + DateFormatEditor, + DateNanosFormatEditor, + DurationFormatEditor, + NumberFormatEditor, + PercentFormatEditor, + StaticLookupFormatEditor, + StringFormatEditor, + TruncateFormatEditor, + UrlFormatEditor, +} from '../components'; + +/** + * Index patterns management service + * + * @internal + */ +export class FormatEditorService { + fieldFormatEditors: FieldFormatEditors; + + constructor() { + this.fieldFormatEditors = new FieldFormatEditors(); + } + + public setup() { + const defaultFieldFormatEditors = [ + BytesFormatEditor, + ColorFormatEditor, + DateFormatEditor, + DateNanosFormatEditor, + DurationFormatEditor, + NumberFormatEditor, + PercentFormatEditor, + StaticLookupFormatEditor, + StringFormatEditor, + TruncateFormatEditor, + UrlFormatEditor, + ]; + + const fieldFormatEditorsSetup = this.fieldFormatEditors.setup(defaultFieldFormatEditors); + + return { + fieldFormatEditors: fieldFormatEditorsSetup, + }; + } + + public start() { + return { + fieldFormatEditors: this.fieldFormatEditors.start(), + }; + } + + public stop() { + // nothing to do here yet. + } +} + +/** @internal */ +export type FormatEditorServiceSetup = ReturnType; +export type FormatEditorServiceStart = ReturnType; diff --git a/src/plugins/index_pattern_field_editor/public/service/index.ts b/src/plugins/index_pattern_field_editor/public/service/index.ts new file mode 100644 index 00000000000000..700d79459327c1 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/service/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './format_editor_service'; diff --git a/src/plugins/index_pattern_field_editor/public/shared_imports.ts b/src/plugins/index_pattern_field_editor/public/shared_imports.ts new file mode 100644 index 00000000000000..9caa5e093a96f1 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/shared_imports.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { + IndexPattern, + IndexPatternField, + DataPublicPluginStart, + FieldFormat, +} from '../../data/public'; + +export { UsageCollectionStart } from '../../usage_collection/public'; + +export { RuntimeType, RuntimeField, KBN_FIELD_TYPES, ES_FIELD_TYPES } from '../../data/common'; + +export { createKibanaReactContext, toMountPoint, CodeEditor } from '../../kibana_react/public'; + +export { + useForm, + useFormData, + useFormContext, + Form, + FormSchema, + UseField, + FormHook, + ValidationFunc, + FieldConfig, +} from '../../es_ui_shared/static/forms/hook_form_lib'; + +export { fieldValidators } from '../../es_ui_shared/static/forms/helpers'; + +export { TextField, ToggleField, NumericField } from '../../es_ui_shared/static/forms/components'; diff --git a/src/plugins/index_pattern_field_editor/public/test_utils/helpers.ts b/src/plugins/index_pattern_field_editor/public/test_utils/helpers.ts new file mode 100644 index 00000000000000..295c32cf28e78d --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/test_utils/helpers.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { TestBed } from './test_utils'; + +export const getCommonActions = (testBed: TestBed) => { + const toggleFormRow = (row: 'customLabel' | 'value' | 'format', value: 'on' | 'off' = 'on') => { + const testSubj = `${row}Row.toggle`; + const toggle = testBed.find(testSubj); + const isOn = toggle.props()['aria-checked']; + + if ((value === 'on' && isOn) || (value === 'off' && isOn === false)) { + return; + } + + testBed.form.toggleEuiSwitch(testSubj); + }; + + return { + toggleFormRow, + }; +}; diff --git a/src/plugins/index_pattern_field_editor/public/test_utils/index.ts b/src/plugins/index_pattern_field_editor/public/test_utils/index.ts new file mode 100644 index 00000000000000..b5d943281cd79b --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/test_utils/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './test_utils'; + +export * from './mocks'; + +export * from './helpers'; diff --git a/src/plugins/index_pattern_field_editor/public/test_utils/mocks.ts b/src/plugins/index_pattern_field_editor/public/test_utils/mocks.ts new file mode 100644 index 00000000000000..c6bc24f1768588 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/test_utils/mocks.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DocLinksStart } from 'src/core/public'; + +export const noop = () => {}; + +export const docLinks: DocLinksStart = { + ELASTIC_WEBSITE_URL: 'htts://jestTest.elastic.co', + DOC_LINK_VERSION: 'jest', + links: {} as any, +}; + +// TODO check how we can better stub an index pattern format +export const fieldFormats = { + getDefaultInstance: () => ({ + convert: (val: any) => val, + }), +} as any; diff --git a/src/plugins/index_pattern_field_editor/public/test_utils/setup_environment.tsx b/src/plugins/index_pattern_field_editor/public/test_utils/setup_environment.tsx new file mode 100644 index 00000000000000..885bcc87f89df9 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/test_utils/setup_environment.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +const EDITOR_ID = 'testEditor'; + +jest.mock('../../../kibana_react/public', () => { + const original = jest.requireActual('../../../kibana_react/public'); + + /** + * We mock the CodeEditor because it requires the + * with the uiSettings passed down. Let's use a simple in our tests. + */ + const CodeEditorMock = (props: any) => { + // Forward our deterministic ID to the consumer + // We need below for the PainlessLang.getSyntaxErrors mock + props.editorDidMount({ + getModel() { + return { + id: EDITOR_ID, + }; + }, + }); + + return ( + ) => { + props.onChange(e.target.value); + }} + /> + ); + }; + + return { + ...original, + CodeEditor: CodeEditorMock, + }; +}); + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + EuiComboBox: (props: any) => ( + { + props.onChange([syntheticEvent['0']]); + }} + /> + ), + }; +}); + +jest.mock('@kbn/monaco', () => { + const original = jest.requireActual('@kbn/monaco'); + + return { + ...original, + PainlessLang: { + ID: 'painless', + getSuggestionProvider: () => undefined, + getSyntaxErrors: () => ({ + [EDITOR_ID]: [], + }), + }, + }; +}); diff --git a/src/plugins/index_pattern_field_editor/public/test_utils/test_utils.ts b/src/plugins/index_pattern_field_editor/public/test_utils/test_utils.ts new file mode 100644 index 00000000000000..c8e4aedc264716 --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/test_utils/test_utils.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { getRandomString } from '@kbn/test/jest'; + +export { registerTestBed, TestBed } from '@kbn/test/jest'; diff --git a/src/plugins/index_pattern_field_editor/public/types.ts b/src/plugins/index_pattern_field_editor/public/types.ts new file mode 100644 index 00000000000000..363af9ceb20fbb --- /dev/null +++ b/src/plugins/index_pattern_field_editor/public/types.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FunctionComponent } from 'react'; + +import { + DataPublicPluginStart, + RuntimeField, + RuntimeType, + UsageCollectionStart, +} from './shared_imports'; +import { OpenFieldEditorOptions } from './open_editor'; +import { FormatEditorServiceSetup, FormatEditorServiceStart } from './service'; +import { DeleteProviderProps } from './components/delete_field_provider'; + +export interface PluginSetup { + fieldFormatEditors: FormatEditorServiceSetup['fieldFormatEditors']; +} + +export interface PluginStart { + openEditor(options: OpenFieldEditorOptions): () => void; + fieldFormatEditors: FormatEditorServiceStart['fieldFormatEditors']; + userPermissions: { + editIndexPattern: () => boolean; + }; + DeleteRuntimeFieldProvider: FunctionComponent; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SetupPlugins {} + +export interface StartPlugins { + data: DataPublicPluginStart; + usageCollection: UsageCollectionStart; +} + +export type InternalFieldType = 'concrete' | 'runtime'; + +export interface Field { + name: string; + type: RuntimeField['type'] | string; + script?: RuntimeField['script']; + customLabel?: string; + popularity?: number; + format?: FieldFormatConfig; +} + +export interface FieldFormatConfig { + id: string; + params?: { [key: string]: any }; +} + +export interface EsRuntimeField { + type: RuntimeType | string; + script?: { + source: string; + }; +} diff --git a/src/plugins/index_pattern_field_editor/tsconfig.json b/src/plugins/index_pattern_field_editor/tsconfig.json new file mode 100644 index 00000000000000..559b1aaf0fc26c --- /dev/null +++ b/src/plugins/index_pattern_field_editor/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "public/**/*", + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../data/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../es_ui_shared/tsconfig.json" }, + ] +} diff --git a/src/plugins/index_pattern_management/kibana.json b/src/plugins/index_pattern_management/kibana.json index 6c3025485bbd7c..60e382fb395f77 100644 --- a/src/plugins/index_pattern_management/kibana.json +++ b/src/plugins/index_pattern_management/kibana.json @@ -3,6 +3,6 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["management", "data", "urlForwarding"], + "requiredPlugins": ["management", "data", "urlForwarding", "indexPatternFieldEditor"], "requiredBundles": ["kibanaReact", "kibanaUtils"] } diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap index 0e5fc0582f72c5..70b638d5d0b8d4 100644 --- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap @@ -18,6 +18,8 @@ exports[`CreateIndexPatternWizard renders index pattern step when there are indi
{indexPattern.title} }} + defaultMessage="View and edit fields in {indexPatternTitle}. Field attributes, such as type and searchability, are based on {mappingAPILink} in Elasticsearch." + values={{ + indexPatternTitle: {indexPattern.title}, + mappingAPILink: ( + + {mappingAPILink} + + ), + }} />{' '} - - {mappingAPILink} -

{conflictedFields.length > 0 && ( @@ -203,6 +207,9 @@ export const EditIndexPattern = withRouter( fields={fields} history={history} location={location} + refreshFields={() => { + setFields(indexPattern.getNonScriptedFields()); + }} />
diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap index 8e7fac9c6c1483..6e5e652b8d0eb3 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/__snapshots__/indexed_fields_table.test.tsx.snap @@ -3,6 +3,7 @@ exports[`IndexedFieldsTable should filter based on the query bar 1`] = `
`; diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx index c5ef40be9c0657..9c154ce1b0e7ba 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.test.tsx @@ -23,99 +23,84 @@ const items: IndexedFieldItem[] = [ searchable: true, info: [], type: 'name', + kbnType: 'string', excluded: false, format: '', + isMapped: true, }, { name: 'timestamp', displayName: 'timestamp', type: 'date', + kbnType: 'date', info: [], excluded: false, format: 'YYYY-MM-DD', + isMapped: true, }, { name: 'conflictingField', displayName: 'conflictingField', - type: 'conflict', + type: 'text, long', + kbnType: 'conflict', info: [], excluded: false, format: '', + isMapped: true, }, ]; +const renderTable = ( + { editField } = { + editField: () => {}, + } +) => + shallow( +
{}} /> + ); + describe('Table', () => { test('should render normally', () => { - const component = shallow( -
{}} /> - ); - - expect(component).toMatchSnapshot(); + expect(renderTable()).toMatchSnapshot(); }); test('should render normal field name', () => { - const component = shallow( -
{}} /> - ); - - const tableCell = shallow(component.prop('columns')[0].render('Elastic', items[0])); + const tableCell = shallow(renderTable().prop('columns')[0].render('Elastic', items[0])); expect(tableCell).toMatchSnapshot(); }); test('should render timestamp field name', () => { - const component = shallow( -
{}} /> - ); - - const tableCell = shallow(component.prop('columns')[0].render('timestamp', items[1])); + const tableCell = shallow(renderTable().prop('columns')[0].render('timestamp', items[1])); expect(tableCell).toMatchSnapshot(); }); test('should render the boolean template (true)', () => { - const component = shallow( -
{}} /> - ); - - const tableCell = shallow(component.prop('columns')[3].render(true)); + const tableCell = shallow(renderTable().prop('columns')[3].render(true)); expect(tableCell).toMatchSnapshot(); }); test('should render the boolean template (false)', () => { - const component = shallow( -
{}} /> - ); - - const tableCell = shallow(component.prop('columns')[3].render(false, items[2])); + const tableCell = shallow(renderTable().prop('columns')[3].render(false, items[2])); expect(tableCell).toMatchSnapshot(); }); test('should render normal type', () => { - const component = shallow( -
{}} /> - ); - - const tableCell = shallow(component.prop('columns')[1].render('string')); + const tableCell = shallow(renderTable().prop('columns')[1].render('string', {})); expect(tableCell).toMatchSnapshot(); }); test('should render conflicting type', () => { - const component = shallow( -
{}} /> + const tableCell = shallow( + renderTable().prop('columns')[1].render('conflict', { kbnType: 'conflict' }) ); - - const tableCell = shallow(component.prop('columns')[1].render('conflict', true)); expect(tableCell).toMatchSnapshot(); }); test('should allow edits', () => { const editField = jest.fn(); - const component = shallow( -
- ); - // Click the edit button - component.prop('columns')[6].actions[0].onClick(); + renderTable({ editField }).prop('columns')[6].actions[0].onClick(); expect(editField).toBeCalled(); }); }); diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx index 58080722d1bced..4e9a2bb6451125 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx @@ -140,6 +140,18 @@ const editDescription = i18n.translate( { defaultMessage: 'Edit' } ); +const deleteLabel = i18n.translate( + 'indexPatternManagement.editIndexPattern.fields.table.deleteLabel', + { + defaultMessage: 'Delete', + } +); + +const deleteDescription = i18n.translate( + 'indexPatternManagement.editIndexPattern.fields.table.deleteDescription', + { defaultMessage: 'Delete' } +); + const labelDescription = i18n.translate( 'indexPatternManagement.editIndexPattern.fields.table.customLabelTooltip', { defaultMessage: 'A custom label for the field.' } @@ -149,6 +161,7 @@ interface IndexedFieldProps { indexPattern: IIndexPattern; items: IndexedFieldItem[]; editField: (field: IndexedFieldItem) => void; + deleteField: (fieldName: string) => void; } export class Table extends PureComponent { @@ -221,7 +234,7 @@ export class Table extends PureComponent { } render() { - const { items, editField } = this.props; + const { items, editField, deleteField } = this.props; const pagination = { initialPageSize: 10, @@ -245,8 +258,8 @@ export class Table extends PureComponent { name: typeHeader, dataType: 'string', sortable: true, - render: (value: string) => { - return this.renderFieldType(value, value === 'conflict'); + render: (value: string, field: IndexedFieldItem) => { + return this.renderFieldType(value, field.kbnType === 'conflict'); }, 'data-test-subj': 'indexedFieldType', }, @@ -294,10 +307,30 @@ export class Table extends PureComponent { ], width: '40px', }, + { + name: '', + actions: [ + { + name: deleteLabel, + description: deleteDescription, + icon: 'trash', + onClick: (field) => deleteField(field.name), + type: 'icon', + 'data-test-subj': 'deleteField', + available: (field) => !field.isMapped, + }, + ], + width: '40px', + }, ]; return ( - + ); } } diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx index 8786f2f4e8deca..e587ada6695cb4 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.test.tsx @@ -26,7 +26,8 @@ jest.mock('./components/table', () => ({ })); const helpers = { - redirectToRoute: (obj: any) => {}, + editField: (fieldName: string) => {}, + deleteField: (fieldName: string) => {}, getFieldInfo: () => [], }; @@ -35,7 +36,9 @@ const indexPattern = ({ getFormatterForFieldNoDefault: () => ({ params: () => ({}) }), } as unknown) as IndexPattern; -const mockFieldToIndexPatternField = (spec: Record) => { +const mockFieldToIndexPatternField = ( + spec: Record +) => { return new IndexPatternField((spec as unknown) as IndexPatternField['spec']); }; @@ -44,10 +47,10 @@ const fields = [ name: 'Elastic', displayName: 'Elastic', searchable: true, - type: 'string', + esTypes: ['keyword'], }, - { name: 'timestamp', displayName: 'timestamp', type: 'date' }, - { name: 'conflictingField', displayName: 'conflictingField', type: 'conflict' }, + { name: 'timestamp', displayName: 'timestamp', esTypes: ['date'] }, + { name: 'conflictingField', displayName: 'conflictingField', esTypes: ['keyword', 'long'] }, ].map(mockFieldToIndexPatternField); describe('IndexedFieldsTable', () => { diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx index 95458b55dbf2a7..c703a882d38d6a 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx @@ -18,7 +18,8 @@ interface IndexedFieldsTableProps { fieldFilter?: string; indexedFieldTypeFilter?: string; helpers: { - redirectToRoute: (obj: any) => void; + editField: (fieldName: string) => void; + deleteField: (fieldName: string) => void; getFieldInfo: (indexPattern: IndexPattern, field: IFieldType) => string[]; }; fieldWildcardMatcher: (filters: any[]) => (val: any) => boolean; @@ -60,10 +61,13 @@ export class IndexedFieldsTable extends Component< fields.map((field) => { return { ...field.spec, + type: field.esTypes?.join(', ') || '', + kbnType: field.type, displayName: field.displayName, format: indexPattern.getFormatterForFieldNoDefault(field.name)?.type?.title || '', excluded: fieldWildcardMatch ? fieldWildcardMatch(field.name) : false, info: helpers.getFieldInfo && helpers.getFieldInfo(indexPattern, field), + isMapped: !!field.isMapped, }; })) || [] @@ -102,7 +106,8 @@ export class IndexedFieldsTable extends Component<
this.props.helpers.redirectToRoute(field)} + editField={(field) => this.props.helpers.editField(field.name)} + deleteField={(fieldName) => this.props.helpers.deleteField(fieldName)} /> ); diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/types.ts b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/types.ts index dee8f5b0d775f9..47ae84b4d2fd8f 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/types.ts +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/types.ts @@ -11,4 +11,6 @@ import { IFieldType } from '../../../../../../plugins/data/public'; export interface IndexedFieldItem extends IFieldType { info: string[]; excluded: boolean; + kbnType: string; + isMapped: boolean; } diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap b/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap index 2b320782cb1634..eaaccdb499b0b4 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap @@ -1,14 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`DeleteScritpedFieldConfirmationModal should render normally 1`] = ` - - - + `; diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/confirmation_modal/confirmation_modal.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/confirmation_modal/confirmation_modal.tsx index 5fbd3118b800bb..36069f408f3543 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/confirmation_modal/confirmation_modal.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/confirmation_modal/confirmation_modal.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EUI_MODAL_CONFIRM_BUTTON, EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EUI_MODAL_CONFIRM_BUTTON, EuiConfirmModal } from '@elastic/eui'; import { ScriptedFieldItem } from '../../types'; @@ -42,15 +42,13 @@ export const DeleteScritpedFieldConfirmationModal = ({ ); return ( - - - + ); }; diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap index 9d92a3689b6983..736dbb611dbbdf 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/__snapshots__/confirmation_modal.test.tsx.snap @@ -1,37 +1,35 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Header should render normally 1`] = ` - - - } - confirmButtonText={ - - } - defaultFocusedButton="confirm" - onCancel={[Function]} - onConfirm={[Function]} - title={ - + } + confirmButtonText={ + + } + defaultFocusedButton="confirm" + onCancel={[Function]} + onConfirm={[Function]} + title={ + - } - /> - + } + /> + } +/> `; diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.tsx index 6715d7a6780ae4..fb8d4a38bfe63e 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/confirmation_modal/confirmation_modal.tsx @@ -10,7 +10,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiOverlayMask, EuiConfirmModal, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; +import { EuiConfirmModal, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; interface DeleteFilterConfirmationModalProps { filterToDeleteValue: string; @@ -26,35 +26,33 @@ export const DeleteFilterConfirmationModal = ({ onDeleteFilter, }: DeleteFilterConfirmationModalProps) => { return ( - - - } - onCancel={onCancelConfirmationModal} - onConfirm={onDeleteFilter} - cancelButtonText={ - - } - buttonColor="danger" - confirmButtonText={ - - } - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - /> - + + } + onCancel={onCancelConfirmationModal} + onConfirm={onDeleteFilter} + cancelButtonText={ + + } + buttonColor="danger" + confirmButtonText={ + + } + defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + /> ); }; diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx index ac57c6ffd78ed9..7771c5d54f4157 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useState, useCallback, useEffect, Fragment, useMemo } from 'react'; +import React, { useState, useCallback, useEffect, Fragment, useMemo, useRef } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { EuiFlexGroup, @@ -17,6 +17,7 @@ import { EuiFieldSearch, EuiSelect, EuiSelectOption, + EuiButton, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { fieldWildcardMatcher } from '../../../../../kibana_utils/public'; @@ -39,6 +40,7 @@ interface TabsProps extends Pick { indexPattern: IndexPattern; fields: IndexPatternField[]; saveIndexPattern: DataPublicPluginStart['indexPatterns']['updateSavedObject']; + refreshFields: () => void; } const searchAriaLabel = i18n.translate( @@ -62,11 +64,26 @@ const filterPlaceholder = i18n.translate( } ); -export function Tabs({ indexPattern, saveIndexPattern, fields, history, location }: TabsProps) { +const addFieldButtonLabel = i18n.translate( + 'indexPatternManagement.editIndexPattern.fields.addFieldButtonLabel', + { + defaultMessage: 'Add field', + } +); + +export function Tabs({ + indexPattern, + saveIndexPattern, + fields, + history, + location, + refreshFields, +}: TabsProps) { const { uiSettings, indexPatternManagementStart, docLinks, + indexPatternFieldEditor, } = useKibana().services; const [fieldFilter, setFieldFilter] = useState(''); const [indexedFieldTypeFilter, setIndexedFieldTypeFilter] = useState(''); @@ -76,6 +93,8 @@ export function Tabs({ indexPattern, saveIndexPattern, fields, history, location const [syncingStateFunc, setSyncingStateFunc] = useState({ getCurrentTab: () => TAB_INDEXED_FIELDS, }); + const closeEditorHandler = useRef<() => void | undefined>(); + const { DeleteRuntimeFieldProvider } = indexPatternFieldEditor; const refreshFilters = useCallback(() => { const tempIndexedFieldTypes: string[] = []; @@ -86,7 +105,9 @@ export function Tabs({ indexPattern, saveIndexPattern, fields, history, location tempScriptedFieldLanguages.push(field.lang); } } else { - tempIndexedFieldTypes.push(field.type); + if (field.esTypes) { + tempIndexedFieldTypes.push(field.esTypes?.join(', ')); + } } }); @@ -96,10 +117,36 @@ export function Tabs({ indexPattern, saveIndexPattern, fields, history, location ); }, [indexPattern]); + const closeFieldEditor = useCallback(() => { + if (closeEditorHandler.current) { + closeEditorHandler.current(); + } + }, []); + + const openFieldEditor = useCallback( + (fieldName?: string) => { + closeEditorHandler.current = indexPatternFieldEditor.openEditor({ + ctx: { + indexPattern, + }, + onSave: refreshFields, + fieldName, + }); + }, + [indexPatternFieldEditor, indexPattern, refreshFields] + ); + useEffect(() => { refreshFilters(); }, [indexPattern, indexPattern.fields, refreshFilters]); + useEffect(() => { + return () => { + // When the component unmounts, make sure to close the field editor + closeFieldEditor(); + }; + }, [closeFieldEditor]); + const fieldWildcardMatcherDecorated = useCallback( (filters: string[]) => fieldWildcardMatcher(filters, uiSettings.get(UI_SETTINGS.META_FIELDS)), [uiSettings] @@ -120,15 +167,22 @@ export function Tabs({ indexPattern, saveIndexPattern, fields, history, location /> {type === TAB_INDEXED_FIELDS && indexedFieldTypes.length > 0 && ( - - setIndexedFieldTypeFilter(e.target.value)} - data-test-subj="indexedFieldTypeFilterDropdown" - aria-label={filterAriaLabel} - /> - + <> + + setIndexedFieldTypeFilter(e.target.value)} + data-test-subj="indexedFieldTypeFilterDropdown" + aria-label={filterAriaLabel} + /> + + + openFieldEditor()}> + {addFieldButtonLabel} + + + )} {type === TAB_SCRIPTED_FIELDS && scriptedFieldLanguages.length > 0 && ( @@ -149,6 +203,7 @@ export function Tabs({ indexPattern, saveIndexPattern, fields, history, location indexedFieldTypes, scriptedFieldLanguageFilter, scriptedFieldLanguages, + openFieldEditor, ] ); @@ -161,19 +216,22 @@ export function Tabs({ indexPattern, saveIndexPattern, fields, history, location {getFilterSection(type)} - { - history.push(getPath(field, indexPattern)); - }, - getFieldInfo: indexPatternManagementStart.list.getFieldInfo, - }} - /> + + {(deleteField) => ( + + )} + ); case TAB_SCRIPTED_FIELDS: @@ -227,6 +285,9 @@ export function Tabs({ indexPattern, saveIndexPattern, fields, history, location refreshFilters, scriptedFieldLanguageFilter, saveIndexPattern, + openFieldEditor, + DeleteRuntimeFieldProvider, + refreshFields, ] ); diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap deleted file mode 100644 index 38f630358d064c..00000000000000 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/label_template_flyout.test.tsx.snap +++ /dev/null @@ -1,109 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LabelTemplateFlyout should not render if not visible 1`] = `""`; - -exports[`LabelTemplateFlyout should render normally 1`] = ` - - - -

- -

-

- - {{ }} - , - } - } - /> -

-
    -
  • - - value - - —  - -
  • -
  • - - url - - —  - -
  • -
-

- -

- User #1234", - "urlTemplate": "http://company.net/profiles?user_id={{value}}", - }, - Object { - "input": "/assets/main.css", - "labelTemplate": "View Asset", - "output": "View Asset", - "urlTemplate": "http://site.com{{rawValue}}", - }, - ] - } - noItemsMessage="No items found" - responsive={true} - tableLayout="fixed" - /> -
-
-
-`; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap deleted file mode 100644 index 83e815dd72661c..00000000000000 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/__snapshots__/url_template_flyout.test.tsx.snap +++ /dev/null @@ -1,114 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`UrlTemplateFlyout should not render if not visible 1`] = `""`; - -exports[`UrlTemplateFlyout should render normally 1`] = ` - - - -

- -

-

- - {{ }} - , - "strongUrlTemplate": - - , - } - } - /> -

-
    -
  • - - value - - —  - -
  • -
  • - - rawValue - - —  - -
  • -
-

- -

- -
-
-
-`; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.test.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.test.tsx deleted file mode 100644 index 0af6ba062e86b8..00000000000000 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { shallowWithI18nProvider } from '@kbn/test/jest'; - -import { LabelTemplateFlyout } from './label_template_flyout'; - -describe('LabelTemplateFlyout', () => { - it('should render normally', async () => { - const component = shallowWithI18nProvider(); - expect(component).toMatchSnapshot(); - }); - - it('should not render if not visible', async () => { - const component = shallowWithI18nProvider(); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.tsx deleted file mode 100644 index 0ce1858a5cc44c..00000000000000 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/label_template_flyout.tsx +++ /dev/null @@ -1,142 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; - -import { EuiBasicTable, EuiCode, EuiFlyout, EuiFlyoutBody, EuiText } from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -interface LabelTemplateExampleItem { - input: string | number; - urlTemplate: string; - labelTemplate: string; - output: string; -} - -const items: LabelTemplateExampleItem[] = [ - { - input: 1234, - urlTemplate: 'http://company.net/profiles?user_id={{value}}', - labelTemplate: i18n.translate('indexPatternManagement.labelTemplate.example.idLabel', { - defaultMessage: 'User #{value}', - values: { value: '{{value}}' }, - }), - output: - '' + - i18n.translate('indexPatternManagement.labelTemplate.example.output.idLabel', { - defaultMessage: 'User', - }) + - ' #1234', - }, - { - input: '/assets/main.css', - urlTemplate: 'http://site.com{{rawValue}}', - labelTemplate: i18n.translate('indexPatternManagement.labelTemplate.example.pathLabel', { - defaultMessage: 'View Asset', - }), - output: - '' + - i18n.translate('indexPatternManagement.labelTemplate.example.output.pathLabel', { - defaultMessage: 'View Asset', - }) + - '', - }, -]; - -export const LabelTemplateFlyout = ({ isVisible = false, onClose = () => {} }) => { - return isVisible ? ( - - - -

- -

-

- {'{{ }}'} }} - /> -

-
    -
  • - value —  - -
  • -
  • - url —  - -
  • -
-

- -

- - items={items} - columns={[ - { - field: 'input', - name: i18n.translate('indexPatternManagement.labelTemplate.inputHeader', { - defaultMessage: 'Input', - }), - width: '160px', - }, - { - field: 'urlTemplate', - name: i18n.translate('indexPatternManagement.labelTemplate.urlHeader', { - defaultMessage: 'URL Template', - }), - }, - { - field: 'labelTemplate', - name: i18n.translate('indexPatternManagement.labelTemplate.labelHeader', { - defaultMessage: 'Label Template', - }), - }, - { - field: 'output', - name: i18n.translate('indexPatternManagement.labelTemplate.outputHeader', { - defaultMessage: 'Output', - }), - render: (value: LabelTemplateExampleItem['output']) => { - return ( - - ); - }, - }, - ]} - /> -
-
-
- ) : null; -}; - -LabelTemplateFlyout.displayName = 'LabelTemplateFlyout'; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.test.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.test.tsx deleted file mode 100644 index bbdb18da901d1f..00000000000000 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { shallowWithI18nProvider } from '@kbn/test/jest'; - -import { UrlTemplateFlyout } from './url_template_flyout'; - -describe('UrlTemplateFlyout', () => { - it('should render normally', async () => { - const component = shallowWithI18nProvider(); - expect(component).toMatchSnapshot(); - }); - - it('should not render if not visible', async () => { - const component = shallowWithI18nProvider(); - expect(component).toMatchSnapshot(); - }); -}); diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.tsx deleted file mode 100644 index fc2b8d72536ec6..00000000000000 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/editors/url/url_template_flyout.tsx +++ /dev/null @@ -1,112 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; - -import { EuiBasicTable, EuiCode, EuiFlyout, EuiFlyoutBody, EuiText } from '@elastic/eui'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export const UrlTemplateFlyout = ({ isVisible = false, onClose = () => {} }) => { - return isVisible ? ( - - - -

- -

-

- {'{{ }}'}, - strongUrlTemplate: ( - - - - ), - }} - /> -

-
    -
  • - value —  - -
  • -
  • - rawValue —  - -
  • -
-

- -

- -
-
-
- ) : null; -}; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/field_format_editor.test.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/field_format_editor.test.tsx index d9352f18e96731..78dc87f7a8027a 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/field_format_editor.test.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/field_format_editor.test.tsx @@ -10,7 +10,7 @@ import React, { PureComponent } from 'react'; import { shallow } from 'enzyme'; import { FieldFormatEditor } from './field_format_editor'; -import { DefaultFormatEditor } from './editors/default'; +import type { DefaultFormatEditor } from 'src/plugins/index_pattern_field_editor/public'; class TestEditor extends PureComponent { render() { diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/field_format_editor.tsx b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/field_format_editor.tsx index 81abf2b5b1d203..60107e19170c77 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/field_format_editor.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/field_format_editor.tsx @@ -7,7 +7,7 @@ */ import React, { PureComponent, Fragment } from 'react'; -import { DefaultFormatEditor } from '../../components/field_format_editor/editors/default'; +import type { DefaultFormatEditor } from 'src/plugins/index_pattern_field_editor/public'; export interface FieldFormatEditorProps { fieldType: string; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/index.ts b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/index.ts index 7eea994a3e2d2f..66d9760b24c657 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/index.ts +++ b/src/plugins/index_pattern_management/public/components/field_editor/components/field_format_editor/index.ts @@ -7,4 +7,3 @@ */ export { FieldFormatEditor } from './field_format_editor'; -export * from './editors'; diff --git a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx index f22981de857497..f0da57a5f9b6f3 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx @@ -25,7 +25,6 @@ import { EuiFormRow, EuiIcon, EuiLink, - EuiOverlayMask, EuiSelect, EuiSpacer, EuiText, @@ -488,7 +487,7 @@ export class FieldEditor extends PureComponent @@ -643,42 +642,40 @@ export class FieldEditor extends PureComponent - { - this.hideDeleteModal(); - this.deleteField(); - }} - cancelButtonText={i18n.translate('indexPatternManagement.deleteField.cancelButton', { - defaultMessage: 'Cancel', - })} - confirmButtonText={i18n.translate('indexPatternManagement.deleteField.deleteButton', { - defaultMessage: 'Delete', - })} - buttonColor="danger" - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - > -

- -
-
- - ), - }} - /> -

-
- + { + this.hideDeleteModal(); + this.deleteField(); + }} + cancelButtonText={i18n.translate('indexPatternManagement.deleteField.cancelButton', { + defaultMessage: 'Cancel', + })} + confirmButtonText={i18n.translate('indexPatternManagement.deleteField.deleteButton', { + defaultMessage: 'Delete', + })} + buttonColor="danger" + defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + > +

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

+
) : null; }; diff --git a/src/plugins/index_pattern_management/public/index.ts b/src/plugins/index_pattern_management/public/index.ts index 27e405a4113de9..94611705a93908 100644 --- a/src/plugins/index_pattern_management/public/index.ts +++ b/src/plugins/index_pattern_management/public/index.ts @@ -31,6 +31,4 @@ export { IndexPatternListConfig, } from './service'; -export { DefaultFormatEditor } from './components/field_editor/components/field_format_editor'; - export { MlCardState } from './types'; diff --git a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx index 45941969dbed12..e47f60ad6fcdd6 100644 --- a/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx +++ b/src/plugins/index_pattern_management/public/management_app/mount_management_section.tsx @@ -42,7 +42,7 @@ export async function mountManagementSection( ) { const [ { chrome, application, savedObjects, uiSettings, notifications, overlays, http, docLinks }, - { data }, + { data, indexPatternFieldEditor }, indexPatternManagementStart, ] = await getStartServices(); const canSave = Boolean(application.capabilities.indexPatterns.save); @@ -61,9 +61,11 @@ export async function mountManagementSection( http, docLinks, data, + indexPatternFieldEditor, indexPatternManagementStart: indexPatternManagementStart as IndexPatternManagementStart, setBreadcrumbs: params.setBreadcrumbs, getMlCardState, + fieldFormatEditors: indexPatternFieldEditor.fieldFormatEditors, }; ReactDOM.render( diff --git a/src/plugins/index_pattern_management/public/mocks.ts b/src/plugins/index_pattern_management/public/mocks.ts index 974b1ae1bd8630..309d5a5611cd62 100644 --- a/src/plugins/index_pattern_management/public/mocks.ts +++ b/src/plugins/index_pattern_management/public/mocks.ts @@ -11,11 +11,13 @@ import { coreMock } from '../../../core/public/mocks'; import { managementPluginMock } from '../../management/public/mocks'; import { urlForwardingPluginMock } from '../../url_forwarding/public/mocks'; import { dataPluginMock } from '../../data/public/mocks'; +import { indexPatternFieldEditorPluginMock } from '../../index_pattern_field_editor/public/mocks'; import { IndexPatternManagementSetup, IndexPatternManagementStart, IndexPatternManagementPlugin, } from './plugin'; +import { IndexPatternManagmentContext } from './types'; const createSetupContract = (): IndexPatternManagementSetup => ({ creation: { @@ -24,10 +26,6 @@ const createSetupContract = (): IndexPatternManagementSetup => ({ list: { addListConfig: jest.fn(), } as any, - fieldFormatEditors: { - getAll: jest.fn(), - getById: jest.fn(), - } as any, environment: { update: jest.fn(), }, @@ -43,10 +41,6 @@ const createStartContract = (): IndexPatternManagementStart => ({ getFieldInfo: jest.fn(), areScriptedFieldsEnabled: jest.fn(), } as any, - fieldFormatEditors: { - getAll: jest.fn(), - getById: jest.fn(), - } as any, }); const createInstance = async () => { @@ -59,6 +53,7 @@ const createInstance = async () => { const doStart = () => plugin.start(coreMock.createStart(), { data: dataPluginMock.createStartContract(), + indexPatternFieldEditor: indexPatternFieldEditorPluginMock.createStartContract(), }); return { @@ -69,13 +64,17 @@ const createInstance = async () => { }; const docLinks = { + ELASTIC_WEBSITE_URL: 'htts://jestTest.elastic.co', + DOC_LINK_VERSION: 'jest', links: { indexPatterns: {}, scriptedFields: {}, - }, + } as any, }; -const createIndexPatternManagmentContext = () => { +const createIndexPatternManagmentContext = (): { + [key in keyof IndexPatternManagmentContext]: any; +} => { const { chrome, application, @@ -86,6 +85,7 @@ const createIndexPatternManagmentContext = () => { } = coreMock.createStart(); const { http } = coreMock.createSetup(); const data = dataPluginMock.createStartContract(); + const indexPatternFieldEditor = indexPatternFieldEditorPluginMock.createStartContract(); return { chrome, @@ -97,8 +97,11 @@ const createIndexPatternManagmentContext = () => { http, docLinks, data, + indexPatternFieldEditor, indexPatternManagementStart: createStartContract(), setBreadcrumbs: () => {}, + getMlCardState: () => 2, + fieldFormatEditors: indexPatternFieldEditor.fieldFormatEditors, }; }; diff --git a/src/plugins/index_pattern_management/public/plugin.ts b/src/plugins/index_pattern_management/public/plugin.ts index bc4ef83de5012f..ed92172c8b91ca 100644 --- a/src/plugins/index_pattern_management/public/plugin.ts +++ b/src/plugins/index_pattern_management/public/plugin.ts @@ -17,6 +17,7 @@ import { } from './service'; import { ManagementSetup } from '../../management/public'; +import { IndexPatternFieldEditorStart } from '../../index_pattern_field_editor/public'; export interface IndexPatternManagementSetupDependencies { management: ManagementSetup; @@ -25,6 +26,7 @@ export interface IndexPatternManagementSetupDependencies { export interface IndexPatternManagementStartDependencies { data: DataPublicPluginStart; + indexPatternFieldEditor: IndexPatternFieldEditorStart; } export type IndexPatternManagementSetup = IndexPatternManagementServiceSetup; diff --git a/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts b/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts index a686891c980149..15be7f11892e49 100644 --- a/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts +++ b/src/plugins/index_pattern_management/public/service/index_pattern_management_service.ts @@ -9,23 +9,7 @@ import { HttpSetup } from '../../../../core/public'; import { IndexPatternCreationManager, IndexPatternCreationConfig } from './creation'; import { IndexPatternListManager, IndexPatternListConfig } from './list'; -import { FieldFormatEditors } from './field_format_editors'; import { EnvironmentService } from './environment'; - -import { - BytesFormatEditor, - ColorFormatEditor, - DateFormatEditor, - DateNanosFormatEditor, - DurationFormatEditor, - NumberFormatEditor, - PercentFormatEditor, - StaticLookupFormatEditor, - StringFormatEditor, - TruncateFormatEditor, - UrlFormatEditor, -} from '../components/field_editor/components/field_format_editor'; - interface SetupDependencies { httpClient: HttpSetup; } @@ -38,13 +22,11 @@ interface SetupDependencies { export class IndexPatternManagementService { indexPatternCreationManager: IndexPatternCreationManager; indexPatternListConfig: IndexPatternListManager; - fieldFormatEditors: FieldFormatEditors; environmentService: EnvironmentService; constructor() { this.indexPatternCreationManager = new IndexPatternCreationManager(); this.indexPatternListConfig = new IndexPatternListManager(); - this.fieldFormatEditors = new FieldFormatEditors(); this.environmentService = new EnvironmentService(); } @@ -55,26 +37,9 @@ export class IndexPatternManagementService { const indexPatternListConfigSetup = this.indexPatternListConfig.setup(); indexPatternListConfigSetup.addListConfig(IndexPatternListConfig); - const defaultFieldFormatEditors = [ - BytesFormatEditor, - ColorFormatEditor, - DateFormatEditor, - DateNanosFormatEditor, - DurationFormatEditor, - NumberFormatEditor, - PercentFormatEditor, - StaticLookupFormatEditor, - StringFormatEditor, - TruncateFormatEditor, - UrlFormatEditor, - ]; - - const fieldFormatEditorsSetup = this.fieldFormatEditors.setup(defaultFieldFormatEditors); - return { creation: creationManagerSetup, list: indexPatternListConfigSetup, - fieldFormatEditors: fieldFormatEditorsSetup, environment: this.environmentService.setup(), }; } @@ -83,7 +48,6 @@ export class IndexPatternManagementService { return { creation: this.indexPatternCreationManager.start(), list: this.indexPatternListConfig.start(), - fieldFormatEditors: this.fieldFormatEditors.start(), }; } diff --git a/src/plugins/index_pattern_management/public/types.ts b/src/plugins/index_pattern_management/public/types.ts index 84e8ae007b99c8..62ee18ababc0bd 100644 --- a/src/plugins/index_pattern_management/public/types.ts +++ b/src/plugins/index_pattern_management/public/types.ts @@ -20,6 +20,7 @@ import { DataPublicPluginStart } from 'src/plugins/data/public'; import { ManagementAppMountParams } from '../../management/public'; import { IndexPatternManagementStart } from './index'; import { KibanaReactContextValue } from '../../kibana_react/public'; +import { IndexPatternFieldEditorStart } from '../../index_pattern_field_editor/public'; export interface IndexPatternManagmentContext { chrome: ChromeStart; @@ -31,9 +32,11 @@ export interface IndexPatternManagmentContext { http: HttpSetup; docLinks: DocLinksStart; data: DataPublicPluginStart; + indexPatternFieldEditor: IndexPatternFieldEditorStart; indexPatternManagementStart: IndexPatternManagementStart; setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; getMlCardState: () => MlCardState; + fieldFormatEditors: IndexPatternFieldEditorStart['fieldFormatEditors']; } export type IndexPatternManagmentContextValue = KibanaReactContextValue; diff --git a/src/plugins/index_pattern_management/tsconfig.json b/src/plugins/index_pattern_management/tsconfig.json index 4dca1634fddb69..37bd3e4aa5bbb9 100644 --- a/src/plugins/index_pattern_management/tsconfig.json +++ b/src/plugins/index_pattern_management/tsconfig.json @@ -18,5 +18,7 @@ { "path": "../url_forwarding/tsconfig.json" }, { "path": "../kibana_react/tsconfig.json" }, { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../es_ui_shared/tsconfig.json" }, + { "path": "../index_pattern_field_editor/tsconfig.json" }, ] } diff --git a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx index 7b3322f5f6c2d0..fa0a32fc3d542a 100644 --- a/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx +++ b/src/plugins/kibana_react/public/table_list_view/table_list_view.tsx @@ -21,7 +21,6 @@ import { EuiFlexItem, EuiButton, EuiSpacer, - EuiOverlayMask, EuiConfirmModal, EuiCallOut, EuiBasicTableColumn, @@ -238,42 +237,40 @@ class TableListView extends React.Component - - } - buttonColor="danger" - onCancel={this.closeDeleteModal} - onConfirm={this.deleteSelectedItems} - cancelButtonText={ - - } - confirmButtonText={deleteButton} - defaultFocusedButton="cancel" - > -

- -

-
- + + } + buttonColor="danger" + onCancel={this.closeDeleteModal} + onConfirm={this.deleteSelectedItems} + cancelButtonText={ + + } + confirmButtonText={deleteButton} + defaultFocusedButton="cancel" + > +

+ +

+
); } diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index 5d611c75cdb992..d632c3ad61a800 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -127,5 +127,4 @@ export const stackManagementSchema: MakeSchemaFrom = { 'securitySolution:rulesTableRefresh': { type: 'text' }, 'apm:enableSignificantTerms': { type: 'boolean' }, 'apm:enableServiceOverview': { type: 'boolean' }, - 'apm:enableCorrelations': { type: 'boolean' }, }; diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index ee602fd51c32e3..698e7e7115295c 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -30,7 +30,6 @@ export interface UsageStats { 'securitySolution:rulesTableRefresh': string; 'apm:enableSignificantTerms': boolean; 'apm:enableServiceOverview': boolean; - 'apm:enableCorrelations': boolean; 'visualize:enableLabs': boolean; 'visualization:heatmap:maxBuckets': number; 'visualization:colorMapping': string; diff --git a/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap b/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap index f88039fbda9bad..1f05ed6b944051 100644 --- a/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap +++ b/src/plugins/saved_objects/public/save_modal/__snapshots__/saved_object_save_modal.test.tsx.snap @@ -1,407 +1,399 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`SavedObjectSaveModal should render matching snapshot 1`] = ` - +
- - - - - - - - - + + + - + + + + + - - + - } - labelType="label" - > - + + - - - - - - - - - Save - - - + + + + + + + + + Save + + -
+ `; exports[`SavedObjectSaveModal should render matching snapshot when custom isValid is set 1`] = ` - +
- - - - + + - - - - - - } - labelType="label" - > - + + + + + - - + - } - labelType="label" - > - + + - - - - - - - - - Save - - - + + + + + + + + + Save + + -
+ `; exports[`SavedObjectSaveModal should render matching snapshot when custom isValid is set 2`] = ` - +
- - - - + + - - - - - - } - labelType="label" - > - + + + + + - - + - } - labelType="label" - > - + + - - - - - - - - - Save - - - + + + + + + + + + Save + + -
+ `; exports[`SavedObjectSaveModal should render matching snapshot when given options 1`] = ` - +
- - - - + + - - - - - - - - } - labelType="label" - > - + + + + + + + - - + - } - labelType="label" - > - + + - -
- Hello! Main options -
-
- -
- Hey there! Options on the right -
-
-
-
-
- - - - - - Save - - -
+ } + labelType="label" + > + + +
+ Hello! Main options +
+
+ +
+ Hey there! Options on the right +
+
+ + + + + + + + + Save + + - + `; diff --git a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx index 39c87c9da60c2b..e476d62a0e793b 100644 --- a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx +++ b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx @@ -21,7 +21,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiSpacer, EuiSwitch, EuiSwitchEvent, @@ -123,52 +122,48 @@ export class SavedObjectSaveModal extends React.Component ); return ( - +
- - - - - - - - - {this.renderDuplicateTitleCallout(duplicateWarningId)} - - - {!this.props.showDescription && this.props.description && ( - - {this.props.description} - - )} - {formBody} - {this.renderCopyOnSave()} - - - - - - - - - {this.renderConfirmButton()} - - + + + + + + + + {this.renderDuplicateTitleCallout(duplicateWarningId)} + + + {!this.props.showDescription && this.props.description && ( + + {this.props.description} + + )} + {formBody} + {this.renderCopyOnSave()} + + + + + + + + + {this.renderConfirmButton()} + -
+ ); } diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.tsx index f67e7bd0b568cd..f6f00c95d9bf19 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.tsx @@ -53,90 +53,88 @@ export const DeleteConfirmModal: FC = ({ // can't use `EuiConfirmModal` here as the confirm modal body is wrapped // inside a `

` element, causing UI glitches with the table. return ( - - - - - - - - -

- -

- - ( - - - - ), - }, - { - field: 'id', - name: i18n.translate( - 'savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.idColumnName', - { defaultMessage: 'Id' } - ), - }, - { - field: 'meta.title', - name: i18n.translate( - 'savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.titleColumnName', - { defaultMessage: 'Title' } - ), - }, - ]} - pagination={true} - sorting={false} + + + + - - - - - - - - - - - - - - - - - - - - - + + + +

+ +

+ + ( + + + + ), + }, + { + field: 'id', + name: i18n.translate( + 'savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.idColumnName', + { defaultMessage: 'Id' } + ), + }, + { + field: 'meta.title', + name: i18n.translate( + 'savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.titleColumnName', + { defaultMessage: 'Title' } + ), + }, + ]} + pagination={true} + sorting={false} + /> +
+ + + + + + + + + + + + + + + + + + + ); }; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/export_modal.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/export_modal.tsx index 693fe00ffedccb..0699f77f575219 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/export_modal.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/export_modal.tsx @@ -8,7 +8,6 @@ import React, { FC } from 'react'; import { - EuiOverlayMask, EuiModal, EuiModalHeader, EuiModalHeaderTitle, @@ -47,80 +46,78 @@ export const ExportModal: FC = ({ onIncludeReferenceChange, }) => { return ( - - - - + + + + + + + + - - - - - } - labelType="legend" - > - { - onSelectedOptionsChange({ - ...selectedOptions, - ...{ - [optionId]: !selectedOptions[optionId], - }, - }); - }} + id="savedObjectsManagement.objectsTable.exportObjectsConfirmModalDescription" + defaultMessage="Select which types to export" /> - - - - } - checked={includeReferences} - onChange={() => onIncludeReferenceChange(!includeReferences)} + } + labelType="legend" + > + { + onSelectedOptionsChange({ + ...selectedOptions, + ...{ + [optionId]: !selectedOptions[optionId], + }, + }); + }} /> - - - - - - - - - - - - - - - - - - - - - + + + + } + checked={includeReferences} + onChange={() => onIncludeReferenceChange(!includeReferences)} + /> + + + + + + + + + + + + + + + + + + + + ); }; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.tsx index 66753e81ccd3ff..cfe0b2be1d3c05 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.tsx @@ -7,13 +7,7 @@ */ import React, { useState, Fragment, ReactNode } from 'react'; -import { - EuiOverlayMask, - EuiConfirmModal, - EUI_MODAL_CONFIRM_BUTTON, - EuiText, - EuiSuperSelect, -} from '@elastic/eui'; +import { EuiConfirmModal, EUI_MODAL_CONFIRM_BUTTON, EuiText, EuiSuperSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; import { FailedImportConflict } from '../../../lib/resolve_import_errors'; @@ -98,29 +92,27 @@ export const OverwriteModal = ({ conflict, onFinish }: OverwriteModalProps) => { } ); return ( - - onFinish(false)} - onConfirm={() => onFinish(true, destinationId)} - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - maxWidth="500px" - > -

{bodyText}

- {selectControl} -
-
+ onFinish(false)} + onConfirm={() => onFinish(true, destinationId)} + defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + maxWidth="500px" + > +

{bodyText}

+ {selectControl} +
); }; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index c129fd006ae156..566d10182b5444 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -4384,9 +4384,6 @@ }, "apm:enableServiceOverview": { "type": "boolean" - }, - "apm:enableCorrelations": { - "type": "boolean" } } }, diff --git a/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts b/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts index 35592d83cf822a..566c9428901502 100644 --- a/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts +++ b/src/plugins/telemetry/server/telemetry_collection/get_kibana.ts @@ -77,5 +77,5 @@ export async function getKibana( kibanaRequest: KibanaRequest | undefined // intentionally `| undefined` to enforce providing the parameter ): Promise { const usage = await usageCollection.bulkFetch(asInternalUser, soClient, kibanaRequest); - return usageCollection.toObject(usage); + return usageCollection.toObject(usage); } diff --git a/src/plugins/usage_collection/public/mocks.tsx b/src/plugins/usage_collection/public/mocks.tsx index ff8740ea48f7b9..72cefaf0264907 100644 --- a/src/plugins/usage_collection/public/mocks.tsx +++ b/src/plugins/usage_collection/public/mocks.tsx @@ -36,9 +36,6 @@ const createSetupContract = (): Setup => { allowTrackUserAgent: jest.fn(), reportUiCounter: jest.fn(), METRIC_TYPE, - __LEGACY: { - appChanged: jest.fn(), - }, }; return setupContract; diff --git a/src/plugins/usage_collection/public/plugin.tsx b/src/plugins/usage_collection/public/plugin.tsx index ac566c6a0d0445..6d1eb751d907a6 100644 --- a/src/plugins/usage_collection/public/plugin.tsx +++ b/src/plugins/usage_collection/public/plugin.tsx @@ -7,17 +7,17 @@ */ import { Reporter, METRIC_TYPE, ApplicationUsageTracker } from '@kbn/analytics'; -import { Subject, merge, Subscription } from 'rxjs'; +import type { Subscription } from 'rxjs'; import React from 'react'; -import { Storage } from '../../kibana_utils/public'; -import { createReporter, trackApplicationUsageChange } from './services'; -import { +import type { PluginInitializerContext, Plugin, CoreSetup, CoreStart, HttpSetup, -} from '../../../core/public'; +} from 'src/core/public'; +import { Storage } from '../../kibana_utils/public'; +import { createReporter, trackApplicationUsageChange } from './services'; import { ApplicationUsageContext } from './components/track_application_view'; export interface PublicConfigType { @@ -39,15 +39,6 @@ export interface UsageCollectionSetup { applicationUsageTracker: IApplicationUsageTracker; reportUiCounter: Reporter['reportUiCounter']; METRIC_TYPE: typeof METRIC_TYPE; - __LEGACY: { - /** - * Legacy handler so we can report the actual app being used inside "kibana#/{appId}". - * To be removed when we get rid of the legacy world - * - * @deprecated - */ - appChanged: (appId: string) => void; - }; } export interface UsageCollectionStart { @@ -65,7 +56,6 @@ export function isUnauthenticated(http: HttpSetup) { } export class UsageCollectionPlugin implements Plugin { - private readonly legacyAppId$ = new Subject(); private applicationUsageTracker?: ApplicationUsageTracker; private trackUserAgent: boolean = true; private subscriptions: Subscription[] = []; @@ -103,9 +93,6 @@ export class UsageCollectionPlugin implements Plugin this.legacyAppId$.next(appId), - }, }; } @@ -118,7 +105,7 @@ export class UsageCollectionPlugin implements Plugin(statsData: Array<{ type: string; result: T }> = []) => { - return statsData.reduce((accumulatedStats, { type, result }) => { - return { - ...accumulatedStats, - [type]: result, - }; - }, {} as Result); - }; - - // rename fields to use api conventions - public toApiFieldNames = (apiData: any): any => { - const getValueOrRecurse = (value: any) => { - if (value == null || typeof value !== 'object') { - return value; - } else { - return this.toApiFieldNames(value); // recurse - } - }; + /** + * Convert an array of fetched stats results into key/object + * @param statsData Array of fetched stats results + */ + public toObject, T = unknown>( + statsData: Array<{ type: string; result: T }> = [] + ): Result { + return Object.fromEntries(statsData.map(({ type, result }) => [type, result])) as Result; + } + /** + * Rename fields to use API conventions + * @param apiData Data to be normalized + */ + public toApiFieldNames( + apiData: Record | unknown[] + ): Record | unknown[] { // handle array and return early, or return a reduced object - if (Array.isArray(apiData)) { - return apiData.map(getValueOrRecurse); + return apiData.map((value) => this.getValueOrRecurse(value)); } - return Object.keys(apiData).reduce((accum, field) => { - const value = apiData[field]; - let newName = field; - newName = snakeCase(newName); - newName = newName.replace(/^(1|5|15)_m/, '$1m'); // os.load.15m, os.load.5m, os.load.1m - newName = newName.replace('_in_bytes', '_bytes'); - newName = newName.replace('_in_millis', '_ms'); + return Object.fromEntries( + Object.entries(apiData).map(([field, value]) => { + let newName = field; + newName = snakeCase(newName); + newName = newName.replace(/^(1|5|15)_m/, '$1m'); // os.load.15m, os.load.5m, os.load.1m + newName = newName.replace('_in_bytes', '_bytes'); + newName = newName.replace('_in_millis', '_ms'); - return { - ...accum, - [newName]: getValueOrRecurse(value), - }; - }, {}); - }; + return [newName, this.getValueOrRecurse(value)]; + }) + ); + } + + private getValueOrRecurse(value: unknown) { + if (Array.isArray(value) || (typeof value === 'object' && value !== null)) { + return this.toApiFieldNames(value as Record | unknown[]); // recurse + } + return value; + } private makeCollectorSetFromArray = (collectors: AnyCollector[]) => { return new CollectorSet({ diff --git a/src/plugins/vis_type_timelion/server/routes/run.ts b/src/plugins/vis_type_timelion/server/routes/run.ts index bae25d6f918e37..b3ab3c61c15d89 100644 --- a/src/plugins/vis_type_timelion/server/routes/run.ts +++ b/src/plugins/vis_type_timelion/server/routes/run.ts @@ -77,46 +77,32 @@ export function runRoute( }, }, router.handleLegacyErrors(async (context, request, response) => { - try { - const [, { data }] = await core.getStartServices(); - const uiSettings = await context.core.uiSettings.client.getAll(); - const indexPatternsService = await data.indexPatterns.indexPatternsServiceFactory( - context.core.savedObjects.client, - context.core.elasticsearch.client.asCurrentUser - ); + const [, { data }] = await core.getStartServices(); + const uiSettings = await context.core.uiSettings.client.getAll(); + const indexPatternsService = await data.indexPatterns.indexPatternsServiceFactory( + context.core.savedObjects.client, + context.core.elasticsearch.client.asCurrentUser + ); - const tlConfig = getTlConfig({ - context, - request, - settings: _.defaults(uiSettings, timelionDefaults), // Just in case they delete some setting. - getFunction, - getIndexPatternsService: () => indexPatternsService, - getStartServices: core.getStartServices, - allowedGraphiteUrls: configManager.getGraphiteUrls(), - esShardTimeout: configManager.getEsShardTimeout(), - }); - const chainRunner = chainRunnerFn(tlConfig); - const sheet = await Bluebird.all(chainRunner.processRequest(request.body)); + const tlConfig = getTlConfig({ + context, + request, + settings: _.defaults(uiSettings, timelionDefaults), // Just in case they delete some setting. + getFunction, + getIndexPatternsService: () => indexPatternsService, + getStartServices: core.getStartServices, + allowedGraphiteUrls: configManager.getGraphiteUrls(), + esShardTimeout: configManager.getEsShardTimeout(), + }); + const chainRunner = chainRunnerFn(tlConfig); + const sheet = await Bluebird.all(chainRunner.processRequest(request.body)); - return response.ok({ - body: { - sheet, - stats: chainRunner.getStats(), - }, - }); - } catch (err) { - logger.error(`${err.toString()}: ${err.stack}`); - // TODO Maybe we should just replace everywhere we throw with Boom? Probably. - if (err.isBoom) { - throw err; - } else { - return response.internalError({ - body: { - message: err.toString(), - }, - }); - } - } + return response.ok({ + body: { + sheet, + stats: chainRunner.getStats(), + }, + }); }) ); } diff --git a/src/plugins/vis_type_timeseries/common/metric_types.ts b/src/plugins/vis_type_timeseries/common/metric_types.ts index 52b31b67743715..17352f0f9da258 100644 --- a/src/plugins/vis_type_timeseries/common/metric_types.ts +++ b/src/plugins/vis_type_timeseries/common/metric_types.ts @@ -20,6 +20,8 @@ export enum METRIC_TYPES { VALUE_COUNT = 'value_count', AVERAGE = 'avg', SUM = 'sum', + MIN = 'min', + MAX = 'max', } // We should probably use BUCKET_TYPES from data plugin in future. diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.test.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.test.tsx index d82a16f1186219..579205ae048444 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.test.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.test.tsx @@ -87,6 +87,14 @@ describe('TSVB AggSelect', () => { "label": "Count", "value": "count", }, + Object { + "label": "Max", + "value": "max", + }, + Object { + "label": "Min", + "value": "min", + }, Object { "label": "Sum", "value": "sum", diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx index 0b8fad4d780f64..00d088025bf253 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg_select.tsx @@ -225,7 +225,7 @@ const FILTER_RATIO_AGGS = [ 'value_count', ]; -const HISTOGRAM_AGGS = ['avg', 'count', 'sum', 'value_count']; +const HISTOGRAM_AGGS = ['avg', 'count', 'sum', 'min', 'max', 'value_count']; const allAggOptions = [...metricAggs, ...pipelineAggs, ...siblingAggs, ...specialAggs]; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.test.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.test.js index dd85e2490d70a7..5648a8d3e7133a 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.test.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/filter_ratio.test.js @@ -51,26 +51,13 @@ describe('TSVB Filter Ratio', () => { field: 'histogram_value', }; const wrapper = setup(metric); - expect(wrapper.find(EuiComboBox).at(1).props().options).toMatchInlineSnapshot(` - Array [ - Object { - "label": "Average", - "value": "avg", - }, - Object { - "label": "Count", - "value": "count", - }, - Object { - "label": "Sum", - "value": "sum", - }, - Object { - "label": "Value Count", - "value": "value_count", - }, - ] - `); + expect( + wrapper + .find(EuiComboBox) + .at(1) + .props() + .options.map(({ value }) => value) + ).toEqual(['avg', 'count', 'max', 'min', 'sum', 'value_count']); }); const shouldNotHaveHistogramField = (agg) => { it(`should not have histogram fields for ${agg}`, () => { @@ -80,23 +67,19 @@ describe('TSVB Filter Ratio', () => { field: '', }; const wrapper = setup(metric); - expect(wrapper.find(EuiComboBox).at(2).props().options).toMatchInlineSnapshot(` - Array [ - Object { - "label": "number", - "options": Array [ - Object { - "label": "system.cpu.user.pct", - "value": "system.cpu.user.pct", - }, - ], - }, - ] - `); + expect(wrapper.find(EuiComboBox).at(2).props().options).toEqual([ + { + label: 'number', + options: [ + { + label: 'system.cpu.user.pct', + value: 'system.cpu.user.pct', + }, + ], + }, + ]); }); }; - shouldNotHaveHistogramField('max'); - shouldNotHaveHistogramField('min'); shouldNotHaveHistogramField('positive_rate'); it(`should not have histogram fields for cardinality`, () => { diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/histogram_support.test.js b/src/plugins/vis_type_timeseries/public/application/components/aggs/histogram_support.test.js index 5a5f37db2b7b5e..c536856327f283 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/histogram_support.test.js +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/histogram_support.test.js @@ -65,6 +65,8 @@ describe('Histogram Types', () => { }; shouldHaveHistogramSupport('avg'); shouldHaveHistogramSupport('sum'); + shouldHaveHistogramSupport('min'); + shouldHaveHistogramSupport('max'); shouldHaveHistogramSupport('value_count'); shouldHaveHistogramSupport('percentile'); shouldHaveHistogramSupport('percentile_rank'); @@ -81,8 +83,6 @@ describe('Histogram Types', () => { ); }; shouldNotHaveHistogramSupport('cardinality'); - shouldNotHaveHistogramSupport('max'); - shouldNotHaveHistogramSupport('min'); shouldNotHaveHistogramSupport('variance'); shouldNotHaveHistogramSupport('sum_of_squares'); shouldNotHaveHistogramSupport('std_deviation'); diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js index ba3b330ed8eef2..61226e4e8dcbe5 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.js @@ -17,6 +17,8 @@ export function getSupportedFieldsByMetricType(type) { return Object.values(KBN_FIELD_TYPES); case METRIC_TYPES.AVERAGE: case METRIC_TYPES.SUM: + case METRIC_TYPES.MIN: + case METRIC_TYPES.MAX: return [KBN_FIELD_TYPES.NUMBER, KBN_FIELD_TYPES.HISTOGRAM]; default: return [KBN_FIELD_TYPES.NUMBER]; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.test.js index 10258effbb8836..c009146abb7bd1 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.test.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/get_supported_fields_by_metric_type.test.js @@ -26,11 +26,11 @@ describe('getSupportedFieldsByMetricType', () => { shouldSupportAllFieldTypes('value_count'); shouldHaveHistogramAndNumbers('avg'); shouldHaveHistogramAndNumbers('sum'); + shouldHaveHistogramAndNumbers('min'); + shouldHaveHistogramAndNumbers('max'); shouldHaveOnlyNumbers('positive_rate'); shouldHaveOnlyNumbers('std_deviation'); - shouldHaveOnlyNumbers('max'); - shouldHaveOnlyNumbers('min'); it(`should return everything but histogram for cardinality`, () => { expect(getSupportedFieldsByMetricType('cardinality')).not.toContain('histogram'); diff --git a/src/plugins/vis_type_timeseries/server/routes/vis.ts b/src/plugins/vis_type_timeseries/server/routes/vis.ts index 7c314228dad246..15890011d75bb9 100644 --- a/src/plugins/vis_type_timeseries/server/routes/vis.ts +++ b/src/plugins/vis_type_timeseries/server/routes/vis.ts @@ -42,18 +42,12 @@ export const visDataRoutes = (router: VisTypeTimeseriesRouter, framework: Framew ); } - try { - const results = await getVisData( - requestContext, - request as KibanaRequest<{}, {}, GetVisDataOptions>, - framework - ); - return response.ok({ body: results }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + const results = await getVisData( + requestContext, + request as KibanaRequest<{}, {}, GetVisDataOptions>, + framework + ); + return response.ok({ body: results }); } ); }; diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/handler.js b/src/plugins/vis_type_vislib/public/vislib/lib/handler.js index 1f1d971a445462..f3201f60007f06 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/handler.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/handler.js @@ -9,6 +9,7 @@ import d3 from 'd3'; import _ from 'lodash'; import MarkdownIt from 'markdown-it'; +import moment from 'moment'; import { dispatchRenderComplete } from '../../../../kibana_utils/public'; @@ -26,6 +27,10 @@ const markdownIt = new MarkdownIt({ linkify: true, }); +const convertToTimestamp = (date) => { + return parseInt(moment(date).format('x')); +}; + /** * Handles building all the components of the visualization * @@ -80,11 +85,13 @@ export class Handler { case 'brush': const xRaw = _.get(eventPayload.data, 'series[0].values[0].xRaw'); if (!xRaw) return; // not sure if this is possible? + const [start, end] = eventPayload.range; + const range = [convertToTimestamp(start), convertToTimestamp(end)]; return self.vis.emit(eventType, { name: 'brush', data: { table: xRaw.table, - range: eventPayload.range, + range, column: xRaw.column, }, }); diff --git a/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx b/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx index 0c1ab262755a73..c9ed82fcf58e55 100644 --- a/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx +++ b/src/plugins/vis_type_xy/public/components/detailed_tooltip.tsx @@ -27,9 +27,6 @@ interface TooltipData { value: string; } -// TODO: replace when exported from elastic/charts -const DEFAULT_SINGLE_PANEL_SM_VALUE = '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__'; - export const getTooltipData = ( aspects: Aspects, header: TooltipValue | null, @@ -81,7 +78,7 @@ export const getTooltipData = ( if ( aspects.splitColumn && valueSeries.smHorizontalAccessorValue !== undefined && - valueSeries.smHorizontalAccessorValue !== DEFAULT_SINGLE_PANEL_SM_VALUE + valueSeries.smHorizontalAccessorValue !== undefined ) { data.push({ label: aspects.splitColumn.title, @@ -92,7 +89,7 @@ export const getTooltipData = ( if ( aspects.splitRow && valueSeries.smVerticalAccessorValue !== undefined && - valueSeries.smVerticalAccessorValue !== DEFAULT_SINGLE_PANEL_SM_VALUE + valueSeries.smVerticalAccessorValue !== undefined ) { data.push({ label: aspects.splitRow.title, diff --git a/src/plugins/vis_type_xy/public/utils/get_legend_actions.tsx b/src/plugins/vis_type_xy/public/utils/get_legend_actions.tsx index 9f557806cf1427..5c28ca77da0c4a 100644 --- a/src/plugins/vis_type_xy/public/utils/get_legend_actions.tsx +++ b/src/plugins/vis_type_xy/public/utils/get_legend_actions.tsx @@ -20,7 +20,7 @@ export const getLegendActions = ( onFilter: (data: ClickTriggerEvent, negate?: any) => void, getSeriesName: (series: XYChartSeriesIdentifier) => SeriesName ): LegendAction => { - return ({ series: xySeries }) => { + return ({ series: [xySeries] }) => { const [popoverOpen, setPopoverOpen] = useState(false); const [isfilterable, setIsfilterable] = useState(false); const series = xySeries as XYChartSeriesIdentifier; diff --git a/src/plugins/vis_type_xy/public/utils/use_color_picker.tsx b/src/plugins/vis_type_xy/public/utils/use_color_picker.tsx index 80e7e12adf799d..5028bc379c375d 100644 --- a/src/plugins/vis_type_xy/public/utils/use_color_picker.tsx +++ b/src/plugins/vis_type_xy/public/utils/use_color_picker.tsx @@ -36,7 +36,7 @@ export const useColorPicker = ( getSeriesName: (series: XYChartSeriesIdentifier) => SeriesName ): LegendColorPicker => useMemo( - () => ({ anchor, color, onClose, onChange, seriesIdentifier }) => { + () => ({ anchor, color, onClose, onChange, seriesIdentifiers: [seriesIdentifier] }) => { const seriesName = getSeriesName(seriesIdentifier as XYChartSeriesIdentifier); const handlChange = (newColor: string | null, event: BaseSyntheticEvent) => { if (!seriesName) { diff --git a/src/plugins/vis_type_xy/public/vis_component.tsx b/src/plugins/vis_type_xy/public/vis_component.tsx index c7ba5021196ff7..ab398101bac9d6 100644 --- a/src/plugins/vis_type_xy/public/vis_component.tsx +++ b/src/plugins/vis_type_xy/public/vis_component.tsx @@ -157,17 +157,12 @@ const VisComponent = (props: VisComponentProps) => { ( visData: Datatable, xAccessor: Accessor | AccessorFn, - splitSeriesAccessors: Array, - splitChartAccessor?: Accessor | AccessorFn + splitSeriesAccessors: Array ) => { const splitSeriesAccessorFnMap = getSplitSeriesAccessorFnMap(splitSeriesAccessors); return (series: XYChartSeriesIdentifier): ClickTriggerEvent | null => { if (xAccessor !== null) { - return getFilterFromSeriesFn(visData)( - series, - splitSeriesAccessorFnMap, - splitChartAccessor - ); + return getFilterFromSeriesFn(visData)(series, splitSeriesAccessorFnMap); } return null; @@ -373,12 +368,7 @@ const VisComponent = (props: VisComponentProps) => { config.aspects.series && (config.aspects.series?.length ?? 0) > 0 ? getLegendActions( canFilter, - getFilterEventData( - visData, - xAccessor, - splitSeriesAccessors, - splitChartColumnAccessor ?? splitChartRowAccessor - ), + getFilterEventData(visData, xAccessor, splitSeriesAccessors), handleFilterAction, getSeriesName ) diff --git a/src/plugins/visualizations/public/wizard/new_vis_modal.tsx b/src/plugins/visualizations/public/wizard/new_vis_modal.tsx index ff352003609a99..d36b734f75be2e 100644 --- a/src/plugins/visualizations/public/wizard/new_vis_modal.tsx +++ b/src/plugins/visualizations/public/wizard/new_vis_modal.tsx @@ -8,7 +8,7 @@ import React from 'react'; -import { EuiModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics'; @@ -121,7 +121,7 @@ class NewVisModal extends React.Component ); - return {selectionModal}; + return selectionModal; } private onCloseModal = () => { diff --git a/test/common/fixtures/plugins/newsfeed/server/plugin.ts b/test/common/fixtures/plugins/newsfeed/server/plugin.ts index 732bc6c4732436..49ffa464efac92 100644 --- a/test/common/fixtures/plugins/newsfeed/server/plugin.ts +++ b/test/common/fixtures/plugins/newsfeed/server/plugin.ts @@ -34,7 +34,7 @@ export class NewsFeedSimulatorPlugin implements Plugin { options: { authRequired: false }, }, (context, req, res) => { - return res.internalError({ body: new Error('Internal server error') }); + throw new Error('Internal server error'); } ); } diff --git a/test/functional/apps/discover/_shared_links.ts b/test/functional/apps/discover/_shared_links.ts index 076a53a618c46a..2893102367b04b 100644 --- a/test/functional/apps/discover/_shared_links.ts +++ b/test/functional/apps/discover/_shared_links.ts @@ -77,7 +77,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { '/app/discover?_t=1453775307251#' + '/?_g=(filters:!(),refreshInterval:(pause:!t,value:0),time' + ":(from:'2015-09-19T06:31:44.000Z',to:'2015-09" + - "-23T18:31:44.000Z'))&_a=(columns:!(_source),filters:!(),index:'logstash-" + + "-23T18:31:44.000Z'))&_a=(columns:!(),filters:!(),index:'logstash-" + "*',interval:auto,query:(language:kuery,query:'')" + ",sort:!(!('@timestamp',desc)))"; const actualUrl = await PageObjects.share.getSharedUrl(); diff --git a/test/functional/apps/home/_navigation.ts b/test/functional/apps/home/_navigation.ts index 3a59f45cef8fa8..401f33b789c859 100644 --- a/test/functional/apps/home/_navigation.ts +++ b/test/functional/apps/home/_navigation.ts @@ -15,8 +15,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const appsMenu = getService('appsMenu'); const esArchiver = getService('esArchiver'); - // Failing: See https://github.com/elastic/kibana/issues/88826 - describe.skip('Kibana browser back navigation should work', function describeIndexTests() { + describe('Kibana browser back navigation should work', function describeIndexTests() { before(async () => { await esArchiver.loadIfNeeded('discover'); await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/test/functional/apps/management/_handle_version_conflict.js b/test/functional/apps/management/_handle_version_conflict.js index 61b143e9874714..16c427e9bbe20b 100644 --- a/test/functional/apps/management/_handle_version_conflict.js +++ b/test/functional/apps/management/_handle_version_conflict.js @@ -18,6 +18,7 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { + const testSubjects = getService('testSubjects'); const esArchiver = getService('esArchiver'); const browser = getService('browser'); const es = getService('legacyEs'); @@ -65,6 +66,11 @@ export default function ({ getService, getPageObjects }) { log.debug('Starting openControlsByName (' + fieldName + ')'); await PageObjects.settings.openControlsByName(fieldName); log.debug('controls are open'); + await ( + await (await testSubjects.find('formatRow')).findAllByCssSelector( + '[data-test-subj="toggle"]' + ) + )[0].click(); await PageObjects.settings.setFieldFormat('url'); const response = await es.update({ index: '.kibana', diff --git a/test/functional/apps/management/_import_objects.ts b/test/functional/apps/management/_import_objects.ts index e2a056359b48e2..a3daaf86294939 100644 --- a/test/functional/apps/management/_import_objects.ts +++ b/test/functional/apps/management/_import_objects.ts @@ -23,8 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const log = getService('log'); - // FLAKY: https://github.com/elastic/kibana/issues/89478 - describe.skip('import objects', function describeIndexTests() { + describe('import objects', function describeIndexTests() { describe('.ndjson file', () => { beforeEach(async function () { await esArchiver.load('management'); diff --git a/test/functional/apps/management/_index_pattern_filter.js b/test/functional/apps/management/_index_pattern_filter.js index eeb0b224d5f0ca..261ba29410a09d 100644 --- a/test/functional/apps/management/_index_pattern_filter.js +++ b/test/functional/apps/management/_index_pattern_filter.js @@ -35,23 +35,23 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.clickIndexPatternLogstash(); await PageObjects.settings.getFieldTypes(); - await PageObjects.settings.setFieldTypeFilter('string'); + await PageObjects.settings.setFieldTypeFilter('keyword'); await retry.try(async function () { const fieldTypes = await PageObjects.settings.getFieldTypes(); expect(fieldTypes.length).to.be.above(0); for (const fieldType of fieldTypes) { - expect(fieldType).to.be('string'); + expect(fieldType).to.be('keyword'); } }); - await PageObjects.settings.setFieldTypeFilter('number'); + await PageObjects.settings.setFieldTypeFilter('long'); await retry.try(async function () { const fieldTypes = await PageObjects.settings.getFieldTypes(); expect(fieldTypes.length).to.be.above(0); for (const fieldType of fieldTypes) { - expect(fieldType).to.be('number'); + expect(fieldType).to.be('long'); } }); }); diff --git a/test/functional/apps/management/_index_pattern_popularity.js b/test/functional/apps/management/_index_pattern_popularity.js index 231617d7084e96..0618dd79e272ec 100644 --- a/test/functional/apps/management/_index_pattern_popularity.js +++ b/test/functional/apps/management/_index_pattern_popularity.js @@ -10,6 +10,7 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); const log = getService('log'); const PageObjects = getPageObjects(['settings', 'common']); @@ -27,11 +28,12 @@ export default function ({ getService, getPageObjects }) { log.debug('Starting openControlsByName (' + fieldName + ')'); await PageObjects.settings.openControlsByName(fieldName); log.debug('increasePopularity'); + await testSubjects.click('toggleAdvancedSetting'); await PageObjects.settings.increasePopularity(); }); afterEach(async () => { - await PageObjects.settings.controlChangeCancel(); + await testSubjects.click('closeFlyoutButton'); await PageObjects.settings.removeIndexPattern(); // Cancel saving the popularity change (we didn't make a change in this case, just checking the value) }); @@ -44,12 +46,12 @@ export default function ({ getService, getPageObjects }) { it('should be reset on cancel', async function () { // Cancel saving the popularity change - await PageObjects.settings.controlChangeCancel(); + await testSubjects.click('closeFlyoutButton'); await PageObjects.settings.openControlsByName(fieldName); // check that it is 0 (previous increase was cancelled const popularity = await PageObjects.settings.getPopularity(); log.debug('popularity = ' + popularity); - expect(popularity).to.be(''); + expect(popularity).to.be('0'); }); it('can be saved', async function () { diff --git a/test/functional/apps/management/_index_pattern_results_sort.js b/test/functional/apps/management/_index_pattern_results_sort.js index 90af0636bcd486..cedf5ee355b36a 100644 --- a/test/functional/apps/management/_index_pattern_results_sort.js +++ b/test/functional/apps/management/_index_pattern_results_sort.js @@ -17,6 +17,12 @@ export default function ({ getService, getPageObjects }) { before(async function () { // delete .kibana index and then wait for Kibana to re-create it await kibanaServer.uiSettings.replace({}); + await PageObjects.settings.navigateTo(); + await PageObjects.settings.createIndexPattern(); + }); + + after(async function () { + return await PageObjects.settings.removeIndexPattern(); }); const columns = [ @@ -31,8 +37,8 @@ export default function ({ getService, getPageObjects }) { }, { heading: 'Type', - first: '_source', - last: 'string', + first: '', + last: 'text', selector: async function () { const tableRow = await PageObjects.settings.getTableRow(0, 1); return await tableRow.getVisibleText(); @@ -42,16 +48,11 @@ export default function ({ getService, getPageObjects }) { columns.forEach(function (col) { describe('sort by heading - ' + col.heading, function indexPatternCreation() { - before(async function () { - await PageObjects.settings.createIndexPattern(); - }); - - after(async function () { - return await PageObjects.settings.removeIndexPattern(); - }); - it('should sort ascending', async function () { - await PageObjects.settings.sortBy(col.heading); + console.log('col.heading', col.heading); + if (col.heading !== 'Name') { + await PageObjects.settings.sortBy(col.heading); + } const rowText = await col.selector(); expect(rowText).to.be(col.first); }); @@ -65,15 +66,6 @@ export default function ({ getService, getPageObjects }) { }); describe('field list pagination', function () { const EXPECTED_FIELD_COUNT = 86; - - before(async function () { - await PageObjects.settings.createIndexPattern(); - }); - - after(async function () { - return await PageObjects.settings.removeIndexPattern(); - }); - it('makelogs data should have expected number of fields', async function () { await retry.try(async function () { const TabCount = await PageObjects.settings.getFieldsTabCount(); diff --git a/test/functional/apps/saved_objects_management/edit_saved_object.ts b/test/functional/apps/saved_objects_management/edit_saved_object.ts index 81569c5bfc498a..89889088bd73ba 100644 --- a/test/functional/apps/saved_objects_management/edit_saved_object.ts +++ b/test/functional/apps/saved_objects_management/edit_saved_object.ts @@ -55,8 +55,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await button.click(); }; - // Flaky: https://github.com/elastic/kibana/issues/68400 - describe.skip('saved objects edition page', () => { + describe('saved objects edition page', () => { beforeEach(async () => { await esArchiver.load('saved_objects_management/edit_saved_object'); }); diff --git a/test/functional/apps/visualize/_tag_cloud.ts b/test/functional/apps/visualize/_tag_cloud.ts index e619a35fb3d0b7..c7d864e5cfb233 100644 --- a/test/functional/apps/visualize/_tag_cloud.ts +++ b/test/functional/apps/visualize/_tag_cloud.ts @@ -11,6 +11,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); const filterBar = getService('filterBar'); const log = getService('log'); const inspector = getService('inspector'); @@ -145,6 +146,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.settings.clickIndexPatternLogstash(); await PageObjects.settings.filterField(termsField); await PageObjects.settings.openControlsByName(termsField); + await ( + await (await testSubjects.find('formatRow')).findAllByCssSelector( + '[data-test-subj="toggle"]' + ) + )[0].click(); await PageObjects.settings.setFieldFormat('bytes'); await PageObjects.settings.controlChangeSave(); await PageObjects.common.navigateToApp('visualize'); diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 9c571f0f0ef86b..465deed4d9039b 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -425,8 +425,9 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide await this.setStoreTimeWithDashboard(saveOptions.storeTimeWithDashboard); } - if (saveOptions.saveAsNew !== undefined) { - await this.setSaveAsNewCheckBox(saveOptions.saveAsNew); + const saveAsNewCheckboxExists = await testSubjects.exists('saveAsNewCheckbox'); + if (saveAsNewCheckboxExists) { + await this.setSaveAsNewCheckBox(Boolean(saveOptions.saveAsNew)); } if (saveOptions.tags) { diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index f59db345f39ff9..09a05732b791bb 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -249,7 +249,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider await find.clickByCssSelector( `table.euiTable tbody tr.euiTableRow:nth-child(${tableFields.indexOf(name) + 1}) - td:last-child button` + td:nth-last-child(2) button` ); } diff --git a/test/functional/services/remote/remote.ts b/test/functional/services/remote/remote.ts index f731ffade6efc7..5bf99b4bf1136b 100644 --- a/test/functional/services/remote/remote.ts +++ b/test/functional/services/remote/remote.ts @@ -37,14 +37,21 @@ export async function RemoteProvider({ getService }: FtrProviderContext) { }; const writeCoverage = (coverageJson: string) => { - if (!Fs.existsSync(coverageDir)) { - Fs.mkdirSync(coverageDir, { recursive: true }); + // on CI we make hard link clone and run tests from kibana${process.env.CI_GROUP} root path + const re = new RegExp(`kibana${process.env.CI_GROUP}`, 'g'); + const dir = process.env.CI ? coverageDir.replace(re, 'kibana') : coverageDir; + + if (!Fs.existsSync(dir)) { + Fs.mkdirSync(dir, { recursive: true }); } + const id = coverageCounter++; const timestamp = Date.now(); - const path = resolve(coverageDir, `${id}.${timestamp}.coverage.json`); + const path = resolve(dir, `${id}.${timestamp}.coverage.json`); log.info('writing coverage to', path); - Fs.writeFileSync(path, JSON.stringify(JSON.parse(coverageJson), null, 2)); + + const jsonString = process.env.CI ? coverageJson.replace(re, 'kibana') : coverageJson; + Fs.writeFileSync(path, JSON.stringify(JSON.parse(jsonString), null, 2)); }; const browserConfig: BrowserConfig = { diff --git a/test/plugin_functional/plugins/core_plugin_b/server/plugin.ts b/test/plugin_functional/plugins/core_plugin_b/server/plugin.ts index 47644256b05e7c..ae7abdaebba101 100644 --- a/test/plugin_functional/plugins/core_plugin_b/server/plugin.ts +++ b/test/plugin_functional/plugins/core_plugin_b/server/plugin.ts @@ -18,7 +18,7 @@ export class CorePluginBPlugin implements Plugin { public setup(core: CoreSetup, deps: {}) { const router = core.http.createRouter(); router.get({ path: '/core_plugin_b', validate: false }, async (context, req, res) => { - if (!context.pluginA) return res.internalError({ body: 'pluginA is disabled' }); + if (!context.pluginA) throw new Error('pluginA is disabled'); const response = await context.pluginA.ping(); return res.ok({ body: `Pong via plugin A: ${response}` }); }); diff --git a/test/scripts/jenkins_ci_group.sh b/test/scripts/jenkins_ci_group.sh index 4faf645975c778..58dcc9f52c0893 100755 --- a/test/scripts/jenkins_ci_group.sh +++ b/test/scripts/jenkins_ci_group.sh @@ -29,14 +29,6 @@ else echo " -> running tests from the clone folder" node scripts/functional_tests --debug --include-tag "ciGroup$CI_GROUP" --exclude-tag "skipCoverage" || true; - if [[ -d target/kibana-coverage/functional ]]; then - echo " -> replacing kibana${CI_GROUP} with kibana in json files" - sed -i "s|kibana${CI_GROUP}|kibana|g" target/kibana-coverage/functional/*.json - echo " -> copying coverage to the original folder" - mkdir -p ../kibana/target/kibana-coverage/functional - mv target/kibana-coverage/functional/* ../kibana/target/kibana-coverage/functional/ - fi - echo " -> moving junit output, silently fail in case of no report" mkdir -p ../kibana/target/junit mv target/junit/* ../kibana/target/junit/ || echo "copying junit failed" diff --git a/test/scripts/jenkins_storybook.sh b/test/scripts/jenkins_storybook.sh new file mode 100755 index 00000000000000..8ebfc1035fe1f1 --- /dev/null +++ b/test/scripts/jenkins_storybook.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +source src/dev/ci_setup/setup_env.sh + +cd "$XPACK_DIR/plugins/canvas" +node scripts/storybook --dll + +cd "$KIBANA_DIR" + +# yarn storybook --site apm # TODO re-enable after being fixed +yarn storybook --site canvas +yarn storybook --site ci_composite +yarn storybook --site url_template_editor +yarn storybook --site codeeditor +yarn storybook --site dashboard +yarn storybook --site dashboard_enhanced +yarn storybook --site data_enhanced +yarn storybook --site embeddable +yarn storybook --site infra +yarn storybook --site security_solution +yarn storybook --site ui_actions_enhanced +yarn storybook --site observability +yarn storybook --site presentation diff --git a/test/scripts/jenkins_unit.sh b/test/scripts/jenkins_unit.sh index a483f8378b8b41..3300ed2d0884f9 100755 --- a/test/scripts/jenkins_unit.sh +++ b/test/scripts/jenkins_unit.sh @@ -28,7 +28,7 @@ if [[ -z "$CODE_COVERAGE" ]] ; then ./test/scripts/checks/test_hardening.sh else echo " -> Running jest tests with coverage" - node scripts/jest --ci --verbose --maxWorkers=6 --coverage || true; + node scripts/jest --ci --verbose --maxWorkers=8 --coverage || true; echo " -> Running jest integration tests with coverage" node scripts/jest_integration --ci --verbose --coverage || true; diff --git a/test/scripts/jenkins_xpack_ci_group.sh b/test/scripts/jenkins_xpack_ci_group.sh index 648605135b3595..0198a5d0ac5fac 100755 --- a/test/scripts/jenkins_xpack_ci_group.sh +++ b/test/scripts/jenkins_xpack_ci_group.sh @@ -25,14 +25,6 @@ else echo " -> running tests from the clone folder" node scripts/functional_tests --debug --include-tag "ciGroup$CI_GROUP" --exclude-tag "skipCoverage" || true; - if [[ -d ../target/kibana-coverage/functional ]]; then - echo " -> replacing kibana${CI_GROUP} with kibana in json files" - sed -i "s|kibana${CI_GROUP}|kibana|g" ../target/kibana-coverage/functional/*.json - echo " -> copying coverage to the original folder" - mkdir -p ../../kibana/target/kibana-coverage/functional - mv ../target/kibana-coverage/functional/* ../../kibana/target/kibana-coverage/functional/ - fi - echo " -> moving junit output, silently fail in case of no report" mkdir -p ../../kibana/target/junit mv ../target/junit/* ../../kibana/target/junit/ || echo "copying junit failed" diff --git a/tsconfig.json b/tsconfig.json index 48feac3efe4752..f6ce6b92b7e02f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -63,5 +63,6 @@ { "path": "./src/plugins/visualizations/tsconfig.json" }, { "path": "./src/plugins/visualize/tsconfig.json" }, { "path": "./src/plugins/index_pattern_management/tsconfig.json" }, + { "path": "./src/plugins/index_pattern_field_editor/tsconfig.json" }, ] } diff --git a/tsconfig.refs.json b/tsconfig.refs.json index 7806cf93756e57..428460e0bfeb51 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -67,6 +67,7 @@ { "path": "./x-pack/plugins/data_enhanced/tsconfig.json" }, { "path": "./x-pack/plugins/dashboard_mode/tsconfig.json" }, { "path": "./x-pack/plugins/discover_enhanced/tsconfig.json" }, + { "path": "./x-pack/plugins/drilldowns/url_drilldown/tsconfig.json" }, { "path": "./x-pack/plugins/embeddable_enhanced/tsconfig.json" }, { "path": "./x-pack/plugins/encrypted_saved_objects/tsconfig.json" }, { "path": "./x-pack/plugins/enterprise_search/tsconfig.json" }, @@ -84,6 +85,7 @@ { "path": "./x-pack/plugins/lens/tsconfig.json" }, { "path": "./x-pack/plugins/license_management/tsconfig.json" }, { "path": "./x-pack/plugins/licensing/tsconfig.json" }, + { "path": "./x-pack/plugins/logstash/tsconfig.json" }, { "path": "./x-pack/plugins/maps_legacy_licensing/tsconfig.json" }, { "path": "./x-pack/plugins/maps/tsconfig.json" }, { "path": "./x-pack/plugins/ml/tsconfig.json" }, @@ -112,6 +114,7 @@ { "path": "./x-pack/plugins/remote_clusters/tsconfig.json" }, { "path": "./x-pack/plugins/cross_cluster_replication/tsconfig.json" }, { "path": "./x-pack/plugins/index_lifecycle_management/tsconfig.json" }, - { "path": "./x-pack/plugins/uptime/tsconfig.json" } + { "path": "./x-pack/plugins/uptime/tsconfig.json" }, + { "path": "./x-pack/plugins/xpack_legacy/tsconfig.json" }, ] } diff --git a/vars/githubCommitStatus.groovy b/vars/githubCommitStatus.groovy index 248d226169a61e..175dbe0c90542f 100644 --- a/vars/githubCommitStatus.groovy +++ b/vars/githubCommitStatus.groovy @@ -41,13 +41,15 @@ def trackBuild(commit, context, Closure closure) { } // state: error|failure|pending|success -def create(sha, state, description, context) { +def create(sha, state, description, context, targetUrl = null) { + targetUrl = targetUrl ?: env.BUILD_URL + withGithubCredentials { return githubApi.post("repos/elastic/kibana/statuses/${sha}", [ state: state, description: description, context: context, - target_url: env.BUILD_URL + target_url: targetUrl.toString() ]) } } diff --git a/vars/githubPr.groovy b/vars/githubPr.groovy index eead00c082ba75..a2a3a81f253a02 100644 --- a/vars/githubPr.groovy +++ b/vars/githubPr.groovy @@ -169,12 +169,18 @@ def getNextCommentMessage(previousCommentInfo = [:], isFinal = false) { ? getBuildStatusIncludingMetrics() : buildUtils.getBuildStatus() + def storybooksUrl = buildState.get('storybooksUrl') + def storybooksMessage = storybooksUrl ? "* [Storybooks Preview](${storybooksUrl})" : "* Storybooks not built" + if (!isFinal) { + storybooksMessage = storybooksUrl ? storybooksMessage : "* Storybooks not built yet" + def failuresPart = status != 'SUCCESS' ? ', with failures' : '' messages << """ ## :hourglass_flowing_sand: Build in-progress${failuresPart} * [continuous-integration/kibana-ci/pull-request](${env.BUILD_URL}) * Commit: ${getCommitHash()} + ${storybooksMessage} * This comment will update when the build is complete """ } else if (status == 'SUCCESS') { @@ -182,6 +188,7 @@ def getNextCommentMessage(previousCommentInfo = [:], isFinal = false) { ## :green_heart: Build Succeeded * [continuous-integration/kibana-ci/pull-request](${env.BUILD_URL}) * Commit: ${getCommitHash()} + ${storybooksMessage} ${getDocsChangesLink()} """ } else if(status == 'UNSTABLE') { @@ -189,6 +196,7 @@ def getNextCommentMessage(previousCommentInfo = [:], isFinal = false) { ## :yellow_heart: Build succeeded, but was flaky * [continuous-integration/kibana-ci/pull-request](${env.BUILD_URL}) * Commit: ${getCommitHash()} + ${storybooksMessage} ${getDocsChangesLink()} """.stripIndent() @@ -204,6 +212,7 @@ def getNextCommentMessage(previousCommentInfo = [:], isFinal = false) { ## :broken_heart: Build Failed * [continuous-integration/kibana-ci/pull-request](${env.BUILD_URL}) * Commit: ${getCommitHash()} + ${storybooksMessage} * [Pipeline Steps](${env.BUILD_URL}flowGraphTable) (look for red circles / failed steps) * [Interpreting CI Failures](https://www.elastic.co/guide/en/kibana/current/interpreting-ci-failures.html) ${getDocsChangesLink()} @@ -235,6 +244,13 @@ def getNextCommentMessage(previousCommentInfo = [:], isFinal = false) { messages << "To update your PR or re-run it, just comment with:\n`@elasticmachine merge upstream`" + catchErrors { + def assignees = getAssignees() + if (assignees) { + messages << "cc " + assignees.collect { "@${it}"}.join(" ") + } + } + info.builds << [ status: status, url: env.BUILD_URL, @@ -329,3 +345,19 @@ def shouldCheckCiMetricSuccess() { return true } + +def getPR() { + withGithubCredentials { + def path = "repos/elastic/kibana/pulls/${env.ghprbPullId}" + return githubApi.get(path) + } +} + +def getAssignees() { + def pr = getPR() + if (!pr) { + return [] + } + + return pr.assignees.collect { it.login } +} diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 7adf755bfc5834..466a04d9b6b39f 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -425,12 +425,13 @@ def buildXpackPlugins() { runbld('./test/scripts/jenkins_xpack_build_plugins.sh', 'Build X-Pack Plugins') } -def withTasks(Map params = [worker: [:]], Closure closure) { +def withTasks(Map params = [:], Closure closure) { catchErrors { - def config = [name: 'ci-worker', size: 'xxl', ramDisk: true] + (params.worker ?: [:]) + def config = [setupWork: {}, worker: [:], parallel: 24] + params + def workerConfig = [name: 'ci-worker', size: 'xxl', ramDisk: true] + config.worker - workers.ci(config) { - withCiTaskQueue(parallel: 24) { + workers.ci(workerConfig) { + withCiTaskQueue([parallel: config.parallel]) { parallel([ docker: { retry(2) { @@ -443,6 +444,8 @@ def withTasks(Map params = [worker: [:]], Closure closure) { xpackPlugins: { buildXpackPlugins() }, ]) + config.setupWork() + catchErrors { closure() } @@ -460,6 +463,7 @@ def allCiTasks() { tasks.test() tasks.functionalOss() tasks.functionalXpack() + tasks.storybooksCi() } }, jest: { diff --git a/vars/storybooks.groovy b/vars/storybooks.groovy new file mode 100644 index 00000000000000..f3c4a97a7d4364 --- /dev/null +++ b/vars/storybooks.groovy @@ -0,0 +1,83 @@ +def getStorybooksBucket() { + return "ci-artifacts.kibana.dev/storybooks" +} + +def getDestinationDir() { + return env.ghprbPullId ? "pr-${env.ghprbPullId}" : buildState.get('checkoutInfo').branch.replace("/", "__") +} + +def getUrl() { + return "https://${getStorybooksBucket()}/${getDestinationDir()}" +} + +def getUrlLatest() { + return "${getUrl()}/latest" +} + +def getUrlForCommit() { + return "${getUrl()}/${buildState.get('checkoutInfo').commit}" +} + +def upload() { + dir("built_assets/storybook") { + sh "mv ci_composite composite" + + def storybooks = sh( + script: 'ls -1d */', + returnStdout: true + ).trim() + .split('\n') + .collect { it.replace('/', '') } + .findAll { it != 'composite' } + + def listHtml = storybooks.collect { """
  • ${it}
  • """ }.join("\n") + + def html = """ + + +

    Storybooks

    +

    Composite Storybook

    +

    All

    +
      + ${listHtml} +
    + + + """ + + writeFile(file: 'index.html', text: html) + + withGcpServiceAccount.fromVaultSecret('secret/kibana-issues/dev/ci-artifacts-key', 'value') { + kibanaPipeline.bash(""" + gsutil -q -m cp -r -z js,css,html,json,map,txt,svg '*' 'gs://${getStorybooksBucket()}/${getDestinationDir()}/${buildState.get('checkoutInfo').commit}/' + gsutil -h "Cache-Control:no-cache, max-age=0, no-transform" cp -z html 'index.html' 'gs://${getStorybooksBucket()}/${getDestinationDir()}/latest/' + """, "Upload Storybooks to GCS") + } + + buildState.set('storybooksUrl', getUrlForCommit()) + } +} + +def build() { + withEnv(["STORYBOOK_BASE_URL=${getUrlForCommit()}"]) { + kibanaPipeline.bash('test/scripts/jenkins_storybook.sh', 'Build Storybooks') + } +} + +def buildAndUpload() { + def sha = buildState.get('checkoutInfo').commit + def context = 'Build and Publish Storybooks' + + githubCommitStatus.create(sha, 'pending', 'Building Storybooks', context) + + try { + build() + upload() + githubCommitStatus.create(sha, 'success', 'Storybooks built', context, getUrlForCommit()) + } catch(ex) { + githubCommitStatus.create(sha, 'error', 'Building Storybooks failed', context) + throw ex + } +} + +return this diff --git a/vars/tasks.groovy b/vars/tasks.groovy index 7c40966ff5e04c..846eed85fb0762 100644 --- a/vars/tasks.groovy +++ b/vars/tasks.groovy @@ -124,4 +124,10 @@ def functionalXpack(Map params = [:]) { } } +def storybooksCi() { + task { + storybooks.buildAndUpload() + } +} + return this diff --git a/x-pack/plugins/alerts/README.md b/x-pack/plugins/alerts/README.md index 2191b23eec11ee..aab848d4555d2d 100644 --- a/x-pack/plugins/alerts/README.md +++ b/x-pack/plugins/alerts/README.md @@ -20,26 +20,13 @@ Table of Contents - [Example](#example) - [Role Based Access-Control](#role-based-access-control) - [Alert Navigation](#alert-navigation) - - [RESTful API](#restful-api) - - [`POST /api/alerts/alert`: Create alert](#post-apialert-create-alert) - - [`DELETE /api/alerts/alert/{id}`: Delete alert](#delete-apialertid-delete-alert) - - [`GET /api/alerts/_find`: Find alerts](#get-apialertfind-find-alerts) - - [`GET /api/alerts/alert/{id}`: Get alert](#get-apialertid-get-alert) + - [Experimental RESTful API](#restful-api) - [`GET /api/alerts/alert/{id}/state`: Get alert state](#get-apialertidstate-get-alert-state) - [`GET /api/alerts/alert/{id}/_instance_summary`: Get alert instance summary](#get-apialertidstate-get-alert-instance-summary) - - [`GET /api/alerts/list_alert_types`: List alert types](#get-apialerttypes-list-alert-types) - - [`PUT /api/alerts/alert/{id}`: Update alert](#put-apialertid-update-alert) - - [`POST /api/alerts/alert/{id}/_enable`: Enable an alert](#post-apialertidenable-enable-an-alert) - - [`POST /api/alerts/alert/{id}/_disable`: Disable an alert](#post-apialertiddisable-disable-an-alert) - - [`POST /api/alerts/alert/{id}/_mute_all`: Mute all alert instances](#post-apialertidmuteall-mute-all-alert-instances) - - [`POST /api/alerts/alert/{alert_id}/alert_instance/{alert_instance_id}/_mute`: Mute alert instance](#post-apialertalertidalertinstancealertinstanceidmute-mute-alert-instance) - - [`POST /api/alerts/alert/{id}/_unmute_all`: Unmute all alert instances](#post-apialertidunmuteall-unmute-all-alert-instances) - - [`POST /api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute`: Unmute an alert instance](#post-apialertalertidalertinstancealertinstanceidunmute-unmute-an-alert-instance) - [`POST /api/alerts/alert/{id}/_update_api_key`: Update alert API key](#post-apialertidupdateapikey-update-alert-api-key) - - [Schedule Formats](#schedule-formats) - [Alert instance factory](#alert-instance-factory) - [Templating actions](#templating-actions) - - [Examples](#examples) + - [Examples](#examples) ## Terminology @@ -61,7 +48,7 @@ A Kibana alert detects a condition and executes one or more actions when that co 1. Develop and register an alert type (see alert types -> example). 2. Configure feature level privileges using RBAC -3. Create an alert using the RESTful API (see alerts -> create). +3. Create an alert using the RESTful API [Documentation](https://www.elastic.co/guide/en/kibana/master/alerts-api-update.html) (see alerts -> create). ## Limitations @@ -96,6 +83,7 @@ The following table describes the properties of the `options` object. |validate.params|When developing an alert type, you can choose to accept a series of parameters. You may also have the parameters validated before they are passed to the `executor` function or created as an alert saved object. In order to do this, provide a `@kbn/config-schema` schema that we will use to validate the `params` attribute.|@kbn/config-schema| |executor|This is where the code of the alert type lives. This is a function to be called when executing an alert on an interval basis. For full details, see executor section below.|Function| |producer|The id of the application producing this alert type.|string| +|minimumLicenseRequired|The value of a minimum license. Most of the alerts are licensed as "basic".|string| ### Executor @@ -142,13 +130,13 @@ This example receives server and threshold as parameters. It will read the CPU u ```typescript import { schema } from '@kbn/config-schema'; +import { AlertType, AlertExecutorOptions } from '../../../alerts/server'; import { - Alert, - AlertTypeParams, - AlertTypeState, - AlertInstanceState, - AlertInstanceContext -} from 'x-pack/plugins/alerts/common'; + AlertTypeParams, + AlertTypeState, + AlertInstanceState, + AlertInstanceContext, +} from '../../../alerts/common'; ... interface MyAlertTypeParams extends AlertTypeParams { server: string; @@ -156,7 +144,7 @@ interface MyAlertTypeParams extends AlertTypeParams { } interface MyAlertTypeState extends AlertTypeState { - lastChecked: number; + lastChecked: Date; } interface MyAlertTypeInstanceState extends AlertInstanceState { @@ -257,83 +245,6 @@ const myAlertType: AlertType< server.newPlatform.setup.plugins.alerts.registerType(myAlertType); ``` -This example only receives threshold as a parameter. It will read the CPU usage of all the servers and schedule individual actions if the reading for a server is greater than the threshold. This is a better implementation than above as only one query is performed for all the servers instead of one query per server. - -```typescript -server.newPlatform.setup.plugins.alerts.registerType({ - id: 'my-alert-type', - name: 'My alert type', - validate: { - params: schema.object({ - threshold: schema.number({ min: 0, max: 1 }), - }), - }, - actionGroups: [ - { - id: 'default', - name: 'Default', - }, - ], - defaultActionGroupId: 'default', - minimumLicenseRequired: 'basic', - actionVariables: { - context: [ - { name: 'server', description: 'the server' }, - { name: 'hasCpuUsageIncreased', description: 'boolean indicating if the cpu usage has increased' }, - ], - state: [ - { name: 'cpuUsage', description: 'CPU usage' }, - ], - }, - async executor({ - alertId, - startedAt, - previousStartedAt, - services, - params, - state, - }: AlertExecutorOptions) { - const { threshold } = params; // Let's assume params is { threshold: 0.8 } - - // Call a function to get the CPU readings on all the servers. The result will be - // an array of { server, cpuUsage }. - const cpuUsageByServer = await getCpuUsageByServer(); - - for (const { server, cpuUsage: currentCpuUsage } of cpuUsageByServer) { - // Only execute if CPU usage is greater than threshold - if (currentCpuUsage > threshold) { - // The first argument is a unique identifier the alert instance is about. In this scenario - // the provided server will be used. Also, this id will be used to make `getState()` return - // previous state, if any, on matching identifiers. - const alertInstance = services.alertInstanceFactory(server); - - // State from last execution. This will exist if an alert instance was created and executed - // in the previous execution - const { cpuUsage: previousCpuUsage } = alertInstance.getState(); - - // Replace state entirely with new values - alertInstance.replaceState({ - cpuUsage: currentCpuUsage, - }); - - // 'default' refers to the id of a group of actions to be scheduled for execution, see 'actions' in create alert section - alertInstance.scheduleActions('default', { - server, - hasCpuUsageIncreased: currentCpuUsage > previousCpuUsage, - }); - } - } - - // Single object containing state that isn't specific to a server, this will become available - // within the `state` function parameter at the next execution - return { - lastChecked: new Date(), - }; - }, - producer: 'alerting', -}); -``` - ## Role Based Access-Control Once you have registered your AlertType, you need to grant your users privileges to use it. When registering a feature in Kibana you can specify multiple types of privileges which are granted to users when they're assigned certain roles. @@ -387,29 +298,37 @@ It's important to note that any role can be granted a mix of `all` and `read` pr ```typescript features.registerKibanaFeature({ - id: 'my-application-id', - name: 'My Application', - app: [], - privileges: { - all: { - alerting: { - all: [ - 'my-application-id.my-alert-type', - 'my-application-id.my-restricted-alert-type' - ], - }, - }, - read: { - alerting: { - all: [ - 'my-application-id.my-alert-type' - ] - read: [ - 'my-application-id.my-restricted-alert-type' - ], - }, - }, - }, + id: 'my-application-id', + name: 'My Application', + app: [], + privileges: { + all: { + app: ['my-application-id', 'kibana'], + savedObject: { + all: [], + read: [], + }, + ui: [], + api: [], + }, + read: { + app: ['lens', 'kibana'], + alerting: { + all: [ + 'my-application-id.my-alert-type' + ], + read: [ + 'my-application-id.my-restricted-alert-type' + ], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + api: [], + }, + }, }); ``` @@ -494,46 +413,10 @@ The only case in which this handler will not be used to evaluate the navigation You can use the `registerNavigation` api to specify as many AlertType specific handlers as you like, but you can only use it once per AlertType as we wouldn't know which handler to use if you specified two for the same AlertType. For the same reason, you can only use `registerDefaultNavigation` once per plugin, as it covers all cases for your specific plugin. -## RESTful API - -Using an alert type requires you to create an alert that will contain parameters and actions for a given alert type. See below for CRUD operations using the API. +## Experimental RESTful API -### `POST /api/alerts/alert`: Create alert - -Payload: - -|Property|Description|Type| -|---|---|---| -|enabled|Indicate if you want the alert to start executing on an interval basis after it has been created.|boolean| -|name|A name to reference and search in the future.|string| -|tags|A list of keywords to reference and search in the future.|string[]| -|alertTypeId|The id value of the alert type you want to call when the alert is scheduled to execute.|string| -|schedule|The schedule specifying when this alert should be run, using one of the available schedule formats specified under _Schedule Formats_ below|object| -|throttle|A Duration specifying how often this alert should fire the same actions. This will prevent the alert from sending out the same notification over and over. For example, if an alert with a `schedule` of 1 minute stays in a triggered state for 90 minutes, setting a `throttle` of `10m` or `1h` will prevent it from sending 90 notifications over this period.|string| -|params|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object| -|actions|Array of the following:
    - `group` (string): We support grouping actions in the scenario of escalations or different types of alert instances. If you don't need this, feel free to use `default` as a value.
    - `id` (string): The id of the action saved object to execute.
    - `params` (object): The map to the `params` the action type will receive. In order to help apply context to strings, we handle them as mustache templates and pass in a default set of context. (see templating actions).|array| - -### `DELETE /api/alerts/alert/{id}`: Delete alert - -Params: - -|Property|Description|Type| -|---|---|---| -|id|The id of the alert you're trying to delete.|string| - -### `GET /api/alerts/_find`: Find alerts - -Params: - -See the saved objects API documentation for find. All the properties are the same except you cannot pass in `type`. - -### `GET /api/alerts/alert/{id}`: Get alert - -Params: - -|Property|Description|Type| -|---|---|---| -|id|The id of the alert you're trying to get.|string| +Using of the alert type requires you to create an alert that will contain parameters and actions for a given alert type. API description for CRUD operations is a part of the [user documentation](https://www.elastic.co/guide/en/kibana/master/alerts-api-update.html). +API listed below is experimental and could be changed or removed in the future. ### `GET /api/alerts/alert/{id}/state`: Get alert state @@ -560,93 +443,12 @@ Query: |---|---|---| |dateStart|The date to start looking for alert events in the event log. Either an ISO date string, or a duration string indicating time since now.|string| -### `GET /api/alerts/list_alert_types`: List alert types - -No parameters. - -### `PUT /api/alerts/alert/{id}`: Update alert - -Params: - -|Property|Description|Type| -|---|---|---| -|id|The id of the alert you're trying to update.|string| - -Payload: - -|Property|Description|Type| -|---|---|---| -|schedule|The schedule specifying when this alert should be run, using one of the available schedule formats specified under _Schedule Formats_ below|object| -|throttle|A Duration specifying how often this alert should fire the same actions. This will prevent the alert from sending out the same notification over and over. For example, if an alert with a `schedule` of 1 minute stays in a triggered state for 90 minutes, setting a `throttle` of `10m` or `1h` will prevent it from sending 90 notifications over this period.|string| -|name|A name to reference and search in the future.|string| -|tags|A list of keywords to reference and search in the future.|string[]| -|params|The parameters to pass in to the alert type executor `params` value. This will also validate against the alert type params validator if defined.|object| -|actions|Array of the following:
    - `group` (string): We support grouping actions in the scenario of escalations or different types of alert instances. If you don't need this, feel free to use `default` as a value.
    - `id` (string): The id of the action saved object to execute.
    - `params` (object): There map to the `params` the action type will receive. In order to help apply context to strings, we handle them as mustache templates and pass in a default set of context. (see templating actions).|array| - -### `POST /api/alerts/alert/{id}/_enable`: Enable an alert - -Params: - -|Property|Description|Type| -|---|---|---| -|id|The id of the alert you're trying to enable.|string| - -### `POST /api/alerts/alert/{id}/_disable`: Disable an alert - -Params: - -|Property|Description|Type| -|---|---|---| -|id|The id of the alert you're trying to disable.|string| - -### `POST /api/alerts/alert/{id}/_mute_all`: Mute all alert instances - -Params: - -|Property|Description|Type| -|---|---|---| -|id|The id of the alert you're trying to mute all alert instances for.|string| - -### `POST /api/alerts/alert/{alert_id}/alert_instance/{alert_instance_id}/_mute`: Mute alert instance - -Params: - -|Property|Description|Type| -|---|---|---| -|alertId|The id of the alert you're trying to mute an instance for.|string| -|alertInstanceId|The instance id of the alert instance you're trying to mute.|string| - -### `POST /api/alerts/alert/{id}/_unmute_all`: Unmute all alert instances - -Params: - -|Property|Description|Type| -|---|---|---| -|id|The id of the alert you're trying to unmute all alert instances for.|string| - -### `POST /api/alerts/alert/{alertId}/alert_instance/{alertInstanceId}/_unmute`: Unmute an alert instance - -Params: - -|Property|Description|Type| -|---|---|---| -|alertId|The id of the alert you're trying to unmute an instance for.|string| -|alertInstanceId|The instance id of the alert instance you're trying to unmute.|string| - ### `POST /api/alerts/alert/{id}/_update_api_key`: Update alert API key |Property|Description|Type| |---|---|---| |id|The id of the alert you're trying to update the API key for. System will use user in request context to generate an API key for.|string| -## Schedule Formats -A schedule is structured such that the key specifies the format you wish to use and its value specifies the schedule. - -We currently support the _Interval format_ which specifies the interval in seconds, minutes, hours or days at which the alert should execute. -Example: `{ interval: "10s" }`, `{ interval: "5m" }`, `{ interval: "1h" }`, `{ interval: "1d" }`. - -There are plans to support multiple other schedule formats in the near future. - ## Alert instance factory **alertInstanceFactory(id)** @@ -694,7 +496,7 @@ When an alert instance executes, the first argument is the `group` of actions to The templating engine is [mustache]. General definition for the [mustache variable] is a double-brace {{}}. All variables are HTML-escaped by default and if there is a requirement to render unescaped HTML, it should be applied the triple mustache: `{{{name}}}`. Also, can be used `&` to unescape a variable. -## Examples +### Examples The following code would be within an alert type. As you can see `cpuUsage ` will replace the state of the alert instance and `server` is the context for the alert instance to execute. The difference between the two is `cpuUsage ` will be accessible at the next execution. diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index 8dba4453d56827..5a0745d3f00b7d 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -161,6 +161,7 @@ export class AlertingPlugin { private eventLogService?: IEventLogService; private eventLogger?: IEventLogger; private readonly kibanaIndexConfig: Observable<{ kibana: { index: string } }>; + private kibanaBaseUrl: string | undefined; constructor(initializerContext: PluginInitializerContext) { this.config = initializerContext.config.create().pipe(first()).toPromise(); @@ -176,6 +177,7 @@ export class AlertingPlugin { core: CoreSetup, plugins: AlertingPluginsSetup ): PluginSetupContract { + this.kibanaBaseUrl = core.http.basePath.publicBaseUrl; this.licenseState = new LicenseState(plugins.licensing.license$); this.security = plugins.security; @@ -371,6 +373,7 @@ export class AlertingPlugin { eventLogger: this.eventLogger!, internalSavedObjectsRepository: core.savedObjects.createInternalRepository(['alert']), alertTypeRegistry: this.alertTypeRegistry!, + kibanaBaseUrl: this.kibanaBaseUrl, }); this.eventLogService!.registerSavedObjectProvider('alert', (request) => { diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts index 4de53a38958f48..120ab6de296dd8 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.test.ts @@ -72,6 +72,7 @@ const createExecutionHandlerParams: jest.Mocked< alertName: 'name-of-alert', tags: ['tag-A', 'tag-B'], apiKey: 'MTIzOmFiYw==', + kibanaBaseUrl: 'http://localhost:5601', alertType, logger: loggingSystemMock.create().get(), eventLogger: mockEventLogger, diff --git a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts index ad024d7ddd8841..9999ea6a4d3d7c 100644 --- a/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts +++ b/x-pack/plugins/alerts/server/task_runner/create_execution_handler.ts @@ -39,6 +39,7 @@ export interface CreateExecutionHandlerOptions< actions: AlertAction[]; spaceId: string; apiKey: RawAlert['apiKey']; + kibanaBaseUrl: string | undefined; alertType: NormalizedAlertType< Params, State, @@ -82,6 +83,7 @@ export function createExecutionHandler< spaceId, apiKey, alertType, + kibanaBaseUrl, eventLogger, request, alertParams, @@ -126,6 +128,7 @@ export function createExecutionHandler< context, actionParams: action.params, state, + kibanaBaseUrl, alertParams, }), }; diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts index 08b288f293bd77..bb5e0e5830159d 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.test.ts @@ -97,6 +97,7 @@ describe('Task Runner', () => { eventLogger: eventLoggerMock.create(), internalSavedObjectsRepository: savedObjectsRepositoryMock.create(), alertTypeRegistry, + kibanaBaseUrl: 'https://localhost:5601', }; const mockedAlertTypeSavedObject: Alert = { diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index 7e96cf03e06106..744be164519995 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -160,6 +160,7 @@ export class TaskRunner< tags: string[] | undefined, spaceId: string, apiKey: RawAlert['apiKey'], + kibanaBaseUrl: string | undefined, actions: Alert['actions'], alertParams: Params ) { @@ -180,6 +181,7 @@ export class TaskRunner< actions, spaceId, alertType: this.alertType, + kibanaBaseUrl, eventLogger: this.context.eventLogger, request: this.getFakeKibanaRequest(spaceId, apiKey), alertParams, @@ -388,6 +390,7 @@ export class TaskRunner< alert.tags, spaceId, apiKey, + this.context.kibanaBaseUrl, alert.actions, alert.params ); diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts index 175de7384ed467..343dffa0d5e709 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.test.ts @@ -77,6 +77,7 @@ describe('Task Runner Factory', () => { eventLogger: eventLoggerMock.create(), internalSavedObjectsRepository: savedObjectsRepositoryMock.create(), alertTypeRegistry: alertTypeRegistryMock.create(), + kibanaBaseUrl: 'https://localhost:5601', }; beforeEach(() => { diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts index 6d8a3aec92636d..a023776134e9cf 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner_factory.ts @@ -40,6 +40,7 @@ export interface TaskRunnerContext { basePathService: IBasePath; internalSavedObjectsRepository: ISavedObjectsRepository; alertTypeRegistry: AlertTypeRegistry; + kibanaBaseUrl: string | undefined; } export class TaskRunnerFactory { diff --git a/x-pack/plugins/alerts/server/task_runner/transform_action_params.ts b/x-pack/plugins/alerts/server/task_runner/transform_action_params.ts index 7e95fee15e700b..4ce30c46cd9f7e 100644 --- a/x-pack/plugins/alerts/server/task_runner/transform_action_params.ts +++ b/x-pack/plugins/alerts/server/task_runner/transform_action_params.ts @@ -27,6 +27,7 @@ interface TransformActionParamsOptions { actionParams: AlertActionParams; alertParams: AlertTypeParams; state: AlertInstanceState; + kibanaBaseUrl?: string; context: AlertInstanceContext; } @@ -44,6 +45,7 @@ export function transformActionParams({ context, actionParams, state, + kibanaBaseUrl, alertParams, }: TransformActionParamsOptions): AlertActionParams { // when the list of variables we pass in here changes, @@ -61,6 +63,7 @@ export function transformActionParams({ context, date: new Date().toISOString(), state, + kibanaBaseUrl, params: alertParams, }; return actionsPlugin.renderActionParameterTemplates(actionTypeId, actionParams, variables); diff --git a/x-pack/plugins/apm/common/ui_settings_keys.ts b/x-pack/plugins/apm/common/ui_settings_keys.ts index 83d358068905e1..427c30605e71b8 100644 --- a/x-pack/plugins/apm/common/ui_settings_keys.ts +++ b/x-pack/plugins/apm/common/ui_settings_keys.ts @@ -5,5 +5,4 @@ * 2.0. */ -export const enableCorrelations = 'apm:enableCorrelations'; export const enableServiceOverview = 'apm:enableServiceOverview'; diff --git a/x-pack/plugins/apm/common/utils/queries.test.ts b/x-pack/plugins/apm/common/utils/queries.test.ts new file mode 100644 index 00000000000000..546c8627def69c --- /dev/null +++ b/x-pack/plugins/apm/common/utils/queries.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SERVICE_ENVIRONMENT } from '../elasticsearch_fieldnames'; +import { ENVIRONMENT_NOT_DEFINED } from '../environment_filter_values'; +import { environmentQuery } from './queries'; + +describe('environmentQuery', () => { + describe('when environment is undefined', () => { + it('returns an empty query', () => { + expect(environmentQuery()).toEqual([]); + }); + }); + + it('creates a query for a service environment', () => { + expect(environmentQuery('test')).toEqual([ + { + term: { [SERVICE_ENVIRONMENT]: 'test' }, + }, + ]); + }); + + it('creates a query for missing service environments', () => { + expect(environmentQuery(ENVIRONMENT_NOT_DEFINED.value)[0]).toHaveProperty( + ['bool', 'must_not', 'exists', 'field'], + SERVICE_ENVIRONMENT + ); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts b/x-pack/plugins/apm/common/utils/queries.ts similarity index 52% rename from x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts rename to x-pack/plugins/apm/common/utils/queries.ts index 3dea3344085c92..dbbbf324b964a9 100644 --- a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts +++ b/x-pack/plugins/apm/common/utils/queries.ts @@ -5,19 +5,41 @@ * 2.0. */ -import { ESFilter } from '../../../../../../typings/elasticsearch'; +import { ESFilter } from '../../../../typings/elasticsearch'; import { ENVIRONMENT_NOT_DEFINED, ENVIRONMENT_ALL, -} from '../../../../common/environment_filter_values'; -import { SERVICE_ENVIRONMENT } from '../../../../common/elasticsearch_fieldnames'; +} from '../environment_filter_values'; +import { SERVICE_ENVIRONMENT } from '../elasticsearch_fieldnames'; -export function getEnvironmentUiFilterES(environment?: string): ESFilter[] { +type QueryContainer = ESFilter; + +export function environmentQuery(environment?: string): QueryContainer[] { if (!environment || environment === ENVIRONMENT_ALL.value) { return []; } + if (environment === ENVIRONMENT_NOT_DEFINED.value) { return [{ bool: { must_not: { exists: { field: SERVICE_ENVIRONMENT } } } }]; } + return [{ term: { [SERVICE_ENVIRONMENT]: environment } }]; } + +export function rangeQuery( + start: number, + end: number, + field = '@timestamp' +): QueryContainer[] { + return [ + { + range: { + [field]: { + gte: start, + lte: end, + format: 'epoch_millis', + }, + }, + }, + ]; +} diff --git a/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx b/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx index 0c1eacb6e800ca..3cb31aa10c4feb 100644 --- a/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx +++ b/x-pack/plugins/apm/public/application/action_menu/anomaly_detection_setup_link.tsx @@ -30,8 +30,9 @@ export type AnomalyDetectionApiResponse = APIReturnType<'GET /api/apm/settings/a const DEFAULT_DATA = { jobs: [], hasLegacyJobs: false }; export function AnomalyDetectionSetupLink() { - const { uiFilters } = useUrlParams(); - const environment = uiFilters.environment; + const { + urlParams: { environment }, + } = useUrlParams(); const { core } = useApmPluginContext(); const canGetJobs = !!core.application.capabilities.ml?.canGetJobs; const license = useLicenseContext(); diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.stories.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.stories.tsx index 0d44c08355c107..d069d4a11b4942 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.stories.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.stories.tsx @@ -25,7 +25,7 @@ export default { core: { http: { get: (endpoint: string) => { - if (endpoint === '/api/apm/ui_filters/environments') { + if (endpoint === '/api/apm/environments') { return Promise.resolve(['production']); } else { return Promise.resolve({ diff --git a/x-pack/plugins/apm/public/components/app/Correlations/index.tsx b/x-pack/plugins/apm/public/components/app/Correlations/index.tsx deleted file mode 100644 index c5b2f265fac8c8..00000000000000 --- a/x-pack/plugins/apm/public/components/app/Correlations/index.tsx +++ /dev/null @@ -1,108 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState } from 'react'; -import { - EuiButtonEmpty, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiTitle, - EuiPortal, - EuiCode, - EuiLink, - EuiCallOut, - EuiButton, -} from '@elastic/eui'; -import { useHistory } from 'react-router-dom'; -import { EuiSpacer } from '@elastic/eui'; -import { isActivePlatinumLicense } from '../../../../common/license_check'; -import { enableCorrelations } from '../../../../common/ui_settings_keys'; -import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; -import { LatencyCorrelations } from './LatencyCorrelations'; -import { ErrorCorrelations } from './ErrorCorrelations'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; -import { createHref } from '../../shared/Links/url_helpers'; -import { useLicenseContext } from '../../../context/license/use_license_context'; - -export function Correlations() { - const { uiSettings } = useApmPluginContext().core; - const { urlParams } = useUrlParams(); - const license = useLicenseContext(); - const history = useHistory(); - const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); - if ( - !uiSettings.get(enableCorrelations) || - !isActivePlatinumLicense(license) - ) { - return null; - } - - return ( - <> - { - setIsFlyoutVisible(true); - }} - > - View correlations - - - - - {isFlyoutVisible && ( - - setIsFlyoutVisible(false)} - > - - -

    Correlations

    -
    -
    - - {urlParams.kuery ? ( - <> - - Filtering by - {urlParams.kuery} - - Clear - - - - - ) : null} - - -

    - Correlations is an experimental feature and in active - development. Bugs and surprises are to be expected but let us - know your feedback so we can improve it. -

    -
    - - - - - -
    -
    -
    - )} - - ); -} diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx index 3edf21eae7279f..4cd2db43621a8f 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/index.tsx @@ -68,7 +68,7 @@ type ErrorGroupDetailsProps = RouteComponentProps<{ export function ErrorGroupDetails({ location, match }: ErrorGroupDetailsProps) { const { serviceName, groupId } = match.params; const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { environment, start, end } = urlParams; const { data: errorGroupData } = useFetcher( (callApmApi) => { @@ -81,6 +81,7 @@ export function ErrorGroupDetails({ location, match }: ErrorGroupDetailsProps) { groupId, }, query: { + environment, start, end, uiFilters: JSON.stringify(uiFilters), @@ -89,7 +90,7 @@ export function ErrorGroupDetails({ location, match }: ErrorGroupDetailsProps) { }); } }, - [serviceName, start, end, groupId, uiFilters] + [environment, serviceName, start, end, groupId, uiFilters] ); const { errorDistributionData } = useErrorGroupDistributionFetcher({ diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/Metrics.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/Metrics.tsx new file mode 100644 index 00000000000000..3a9100a0712aab --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/Metrics.tsx @@ -0,0 +1,139 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as React from 'react'; +import numeral from '@elastic/numeral'; +import styled from 'styled-components'; +import { useContext, useEffect } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiStat, + EuiToolTip, + EuiIconTip, +} from '@elastic/eui'; +import { useFetcher } from '../../../../hooks/use_fetcher'; +import { I18LABELS } from '../translations'; +import { useUxQuery } from '../hooks/useUxQuery'; +import { formatToSec } from '../UXMetrics/KeyUXMetrics'; +import { CsmSharedContext } from '../CsmSharedContext'; + +const ClFlexGroup = styled(EuiFlexGroup)` + flex-direction: row; + @media only screen and (max-width: 768px) { + flex-direction: row; + justify-content: space-between; + } +`; + +function formatTitle(unit: string, value?: number) { + if (typeof value === 'undefined') return I18LABELS.dataMissing; + return formatToSec(value, unit); +} + +function PageViewsTotalTitle({ pageViews }: { pageViews?: number }) { + if (typeof pageViews === 'undefined') { + return <>{I18LABELS.dataMissing}; + } + return pageViews < 10000 ? ( + <>{numeral(pageViews).format('0,0')} + ) : ( + + <>{numeral(pageViews).format('0 a')} + + ); +} + +export function Metrics() { + const uxQuery = useUxQuery(); + + const { data, status } = useFetcher( + (callApmApi) => { + if (uxQuery) { + return callApmApi({ + endpoint: 'GET /api/apm/rum/client-metrics', + params: { + query: { + ...uxQuery, + }, + }, + }); + } + return Promise.resolve(null); + }, + [uxQuery] + ); + + const { setSharedData } = useContext(CsmSharedContext); + + useEffect(() => { + setSharedData({ totalPageViews: data?.pageViews?.value ?? 0 }); + }, [data, setSharedData]); + + const STAT_STYLE = { minWidth: '150px', maxWidth: '250px' }; + + return ( + + + + {I18LABELS.totalPageLoad} + + + } + isLoading={status !== 'success'} + /> + + + + {I18LABELS.backEnd} + + + } + isLoading={status !== 'success'} + /> + + + + {I18LABELS.frontEnd} + + + } + isLoading={status !== 'success'} + /> + + + } + description={I18LABELS.pageViews} + isLoading={status !== 'success'} + /> + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx index 1d6ce21d198c4a..add6ac1b08b281 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx @@ -6,134 +6,36 @@ */ import * as React from 'react'; -import numeral from '@elastic/numeral'; -import styled from 'styled-components'; -import { useContext, useEffect } from 'react'; import { EuiFlexGroup, EuiFlexItem, - EuiStat, - EuiToolTip, - EuiIconTip, + EuiPanel, + EuiTitle, + EuiSpacer, } from '@elastic/eui'; -import { useFetcher } from '../../../../hooks/use_fetcher'; import { I18LABELS } from '../translations'; -import { useUxQuery } from '../hooks/useUxQuery'; -import { formatToSec } from '../UXMetrics/KeyUXMetrics'; -import { CsmSharedContext } from '../CsmSharedContext'; - -const ClFlexGroup = styled(EuiFlexGroup)` - flex-direction: row; - @media only screen and (max-width: 768px) { - flex-direction: row; - justify-content: space-between; - } -`; - -function formatTitle(unit: string, value?: number) { - if (typeof value === 'undefined') return I18LABELS.dataMissing; - return formatToSec(value, unit); -} - -function PageViewsTotalTitle({ pageViews }: { pageViews?: number }) { - if (typeof pageViews === 'undefined') { - return <>{I18LABELS.dataMissing}; - } - return pageViews < 10000 ? ( - <>{numeral(pageViews).format('0,0')} - ) : ( - - <>{numeral(pageViews).format('0 a')} - - ); -} +import { getPercentileLabel } from '../UXMetrics/translations'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; +import { Metrics } from './Metrics'; export function ClientMetrics() { - const uxQuery = useUxQuery(); - - const { data, status } = useFetcher( - (callApmApi) => { - if (uxQuery) { - return callApmApi({ - endpoint: 'GET /api/apm/rum/client-metrics', - params: { - query: { - ...uxQuery, - }, - }, - }); - } - return Promise.resolve(null); - }, - [uxQuery] - ); - - const { setSharedData } = useContext(CsmSharedContext); - - useEffect(() => { - setSharedData({ totalPageViews: data?.pageViews?.value ?? 0 }); - }, [data, setSharedData]); - - const STAT_STYLE = { width: '240px' }; + const { + urlParams: { percentile }, + } = useUrlParams(); return ( - - - - {I18LABELS.totalPageLoad} - - - } - isLoading={status !== 'success'} - /> - - - - {I18LABELS.backEnd} - - - } - isLoading={status !== 'success'} - /> - - - - {I18LABELS.frontEnd} - - - } - isLoading={status !== 'success'} - /> - - - } - description={I18LABELS.pageViews} - isLoading={status !== 'success'} - /> - - + + + + +

    + {I18LABELS.pageLoad} ({getPercentileLabel(percentile!)}) +

    +
    + + +
    +
    +
    ); } diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx index 4afecb7623f73e..5b0b475e86e038 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/LocalUIFilters/index.tsx @@ -11,12 +11,14 @@ import { EuiSpacer, EuiHorizontalRule, EuiButtonEmpty, + EuiAccordion, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { Filter } from './Filter'; import { useLocalUIFilters } from '../hooks/useLocalUIFilters'; import { LocalUIFilterName } from '../../../../../common/ui_filter'; +import { useBreakPoints } from '../../../../hooks/use_break_points'; interface Props { filterNames: LocalUIFilterName[]; @@ -45,16 +47,20 @@ function LocalUIFilters({ const hasValues = filters.some((filter) => filter.value.length > 0); - return ( + const { isSmall } = useBreakPoints(); + + const title = ( + +

    + {i18n.translate('xpack.apm.localFiltersTitle', { + defaultMessage: 'Filters', + })} +

    +
    + ); + + const content = ( <> - -

    - {i18n.translate('xpack.apm.localFiltersTitle', { - defaultMessage: 'Filters', - })} -

    -
    - {children} {filters.map((filter) => { return ( @@ -90,6 +96,18 @@ function LocalUIFilters({ ) : null} ); + + return isSmall ? ( + + {content} + + ) : ( + <> + {title} + + {content} + + ); } export { LocalUIFilters }; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx index 7d34328d2653dd..e3e2a979c48d35 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Panels/MainFilters.tsx @@ -13,6 +13,7 @@ import { useFetcher } from '../../../../hooks/use_fetcher'; import { RUM_AGENT_NAMES } from '../../../../../common/agent_name'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { UserPercentile } from '../UserPercentile'; +import { useBreakPoints } from '../../../../hooks/use_break_points'; export function MainFilters() { const { @@ -37,6 +38,11 @@ export function MainFilters() { [start, end] ); + const { isSmall } = useBreakPoints(); + + // on mobile we want it to take full width + const envStyle = isSmall ? {} : { maxWidth: 200 }; + return ( <> @@ -45,7 +51,7 @@ export function MainFilters() { serviceNames={data ?? []} /> - + diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx index 50d91ca73c249d..a182de8540f584 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx @@ -5,46 +5,22 @@ * 2.0. */ -import { - EuiFlexGroup, - EuiFlexItem, - EuiTitle, - EuiSpacer, - EuiPanel, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; -import { ClientMetrics } from './ClientMetrics'; -import { I18LABELS } from './translations'; import { UXMetrics } from './UXMetrics'; import { ImpactfulMetrics } from './ImpactfulMetrics'; import { PageLoadAndViews } from './Panels/PageLoadAndViews'; import { VisitorBreakdownsPanel } from './Panels/VisitorBreakdowns'; import { useBreakPoints } from '../../../hooks/use_break_points'; -import { getPercentileLabel } from './UXMetrics/translations'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { ClientMetrics } from './ClientMetrics'; export function RumDashboard() { - const { - urlParams: { percentile }, - } = useUrlParams(); const { isSmall } = useBreakPoints(); return ( - - - - -

    - {I18LABELS.pageLoad} ({getPercentileLabel(percentile!)}) -

    -
    - - -
    -
    -
    +
    diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx index 6e3ed54ed2e780..a0b3781a30b209 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx @@ -20,24 +20,15 @@ export const UX_LABEL = i18n.translate('xpack.apm.ux.title', { export function RumHome() { return ( - +

    {UX_LABEL}

    + - - - - - - +
    diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx index d3a34a1df25f7a..965449b78f3e08 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/SelectableUrlList.tsx @@ -206,6 +206,7 @@ export function SelectableUrlList({ panelRef={setPopoverRef} button={search} closePopover={closePopover} + style={{ minWidth: 200 }} >
    diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx index f2e761128e5cea..b8766e8b5ce67d 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/UXMetrics/KeyUXMetrics.tsx @@ -36,7 +36,7 @@ export function formatToSec( } return (valueInMs / 1000).toFixed(2) + ' s'; } -const STAT_STYLE = { width: '240px' }; +const STAT_STYLE = { width: '200px' }; interface Props { data?: UXMetrics | null; @@ -71,7 +71,7 @@ export function KeyUXMetrics({ data, loading }: Props) { // Note: FCP value is in ms unit return ( - + - +

    diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useLocalUIFilters.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useLocalUIFilters.ts index 3f366300792aca..c40f6ba2b88509 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useLocalUIFilters.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/hooks/useLocalUIFilters.ts @@ -10,11 +10,11 @@ import { useHistory } from 'react-router-dom'; import { LocalUIFilterName } from '../../../../../common/ui_filter'; import { pickKeys } from '../../../../../common/utils/pick_keys'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LocalUIFiltersAPIResponse } from '../../../../../server/lib/ui_filters/local_ui_filters'; +import { LocalUIFiltersAPIResponse } from '../../../../../server/lib/rum_client/ui_filters/local_ui_filters'; import { localUIFilters, // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../server/lib/ui_filters/local_ui_filters/config'; +} from '../../../../../server/lib/rum_client/ui_filters/local_ui_filters/config'; import { fromQuery, toQuery, @@ -72,7 +72,7 @@ export function useLocalUIFilters({ (callApmApi) => { if (shouldFetch && urlParams.start && urlParams.end) { return callApmApi({ - endpoint: `GET /api/apm/ui_filters/local_filters/rumOverview`, + endpoint: 'GET /api/apm/rum/local_filters', params: { query: { uiFilters: JSON.stringify(uiFilters), diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx index 6554f48ea3c2b3..081a3dbc907c5a 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/ConfirmDeleteModal.tsx @@ -6,7 +6,7 @@ */ import React, { useState } from 'react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { NotificationsStart } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { getOptionLabel } from '../../../../../../common/agent_configuration/all_option'; @@ -29,41 +29,39 @@ export function ConfirmDeleteModal({ config, onCancel, onConfirm }: Props) { const { toasts } = useApmPluginContext().core.notifications; return ( - - { + setIsDeleting(true); + await deleteConfig(config, toasts); + setIsDeleting(false); + onConfirm(); + }} + cancelButtonText={i18n.translate( + 'xpack.apm.agentConfig.deleteModal.cancel', + { defaultMessage: `Cancel` } + )} + confirmButtonText={i18n.translate( + 'xpack.apm.agentConfig.deleteModal.confirm', + { defaultMessage: `Delete` } + )} + confirmButtonDisabled={isDeleting} + buttonColor="danger" + defaultFocusedButton="confirm" + > +

    + {i18n.translate('xpack.apm.agentConfig.deleteModal.text', { + defaultMessage: `You are about to delete the configuration for service "{serviceName}" and environment "{environment}".`, + values: { + serviceName: getOptionLabel(config.service.name), + environment: getOptionLabel(config.service.environment), + }, })} - onCancel={onCancel} - onConfirm={async () => { - setIsDeleting(true); - await deleteConfig(config, toasts); - setIsDeleting(false); - onConfirm(); - }} - cancelButtonText={i18n.translate( - 'xpack.apm.agentConfig.deleteModal.cancel', - { defaultMessage: `Cancel` } - )} - confirmButtonText={i18n.translate( - 'xpack.apm.agentConfig.deleteModal.confirm', - { defaultMessage: `Delete` } - )} - confirmButtonDisabled={isDeleting} - buttonColor="danger" - defaultFocusedButton="confirm" - > -

    - {i18n.translate('xpack.apm.agentConfig.deleteModal.text', { - defaultMessage: `You are about to delete the configuration for service "{serviceName}" and environment "{environment}".`, - values: { - serviceName: getOptionLabel(config.service.name), - environment: getOptionLabel(config.service.environment), - }, - })} -

    -
    -
    +

    + ); } diff --git a/x-pack/plugins/apm/public/components/app/Correlations/SignificantTermsTable.tsx b/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx similarity index 51% rename from x-pack/plugins/apm/public/components/app/Correlations/SignificantTermsTable.tsx rename to x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx index f7580cc65c543f..c75c1fb6d96a6b 100644 --- a/x-pack/plugins/apm/public/components/app/Correlations/SignificantTermsTable.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx @@ -5,16 +5,23 @@ * 2.0. */ -import React from 'react'; -import { EuiIcon, EuiLink } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import { debounce } from 'lodash'; +import { + EuiIcon, + EuiLink, + EuiBasicTable, + EuiBasicTableColumn, + EuiToolTip, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { useHistory } from 'react-router-dom'; -import { EuiBasicTable } from '@elastic/eui'; -import { EuiBasicTableColumn } from '@elastic/eui'; -import { EuiCode } from '@elastic/eui'; import { asInteger, asPercent } from '../../../../common/utils/formatters'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { createHref, push } from '../../shared/Links/url_helpers'; +import { ImpactBar } from '../../shared/ImpactBar'; +import { useUiTracker } from '../../../../../observability/public'; type CorrelationsApiResponse = | APIReturnType<'GET /api/apm/correlations/failed_transactions'> @@ -27,49 +34,83 @@ type SignificantTerm = NonNullable< interface Props { significantTerms?: T[]; status: FETCH_STATUS; - cardinalityColumnName: string; + percentageColumnName: string; setSelectedSignificantTerm: (term: T | null) => void; + onFilter: () => void; } -export function SignificantTermsTable({ +export function CorrelationsTable({ significantTerms, status, - cardinalityColumnName, + percentageColumnName, setSelectedSignificantTerm, + onFilter, }: Props) { + const trackApmEvent = useUiTracker({ app: 'apm' }); + const trackSelectSignificantTerm = useCallback( + () => + debounce( + () => trackApmEvent({ metric: 'select_significant_term' }), + 1000 + ), + [trackApmEvent] + ); const history = useHistory(); const columns: Array> = [ { width: '100px', - field: 'score', - name: 'Score', + field: 'impact', + name: i18n.translate( + 'xpack.apm.correlations.correlationsTable.impactLabel', + { defaultMessage: 'Impact' } + ), render: (_: any, term: T) => { - return {Math.round(term.score)}; + return ; }, }, { - field: 'cardinality', - name: cardinalityColumnName, + field: 'percentage', + name: percentageColumnName, render: (_: any, term: T) => { - const matches = asPercent(term.fgCount, term.bgCount); - return `${asInteger(term.fgCount)} (${matches})`; + return ( + + <>{asPercent(term.valueCount, term.fieldCount)} + + ); }, }, { field: 'fieldName', - name: 'Field name', + name: i18n.translate( + 'xpack.apm.correlations.correlationsTable.fieldNameLabel', + { defaultMessage: 'Field name' } + ), }, { field: 'fieldValue', - name: 'Field value', + name: i18n.translate( + 'xpack.apm.correlations.correlationsTable.fieldValueLabel', + { defaultMessage: 'Field value' } + ), render: (_: any, term: T) => String(term.fieldValue).slice(0, 50), }, { width: '100px', actions: [ { - name: 'Focus', - description: 'Focus on this term', + name: i18n.translate( + 'xpack.apm.correlations.correlationsTable.filterLabel', + { defaultMessage: 'Filter' } + ), + description: i18n.translate( + 'xpack.apm.correlations.correlationsTable.filterDescription', + { defaultMessage: 'Filter by value' } + ), icon: 'magnifyWithPlus', type: 'icon', onClick: (term: T) => { @@ -80,11 +121,19 @@ export function SignificantTermsTable({ )}"`, }, }); + onFilter(); + trackApmEvent({ metric: 'correlations_term_include_filter' }); }, }, { - name: 'Exclude', - description: 'Exclude this term', + name: i18n.translate( + 'xpack.apm.correlations.correlationsTable.excludeLabel', + { defaultMessage: 'Exclude' } + ), + description: i18n.translate( + 'xpack.apm.correlations.correlationsTable.excludeDescription', + { defaultMessage: 'Filter out value' } + ), icon: 'magnifyWithMinus', type: 'icon', onClick: (term: T) => { @@ -95,10 +144,15 @@ export function SignificantTermsTable({ )}"`, }, }); + onFilter(); + trackApmEvent({ metric: 'correlations_term_exclude_filter' }); }, }, ], - name: 'Actions', + name: i18n.translate( + 'xpack.apm.correlations.correlationsTable.actionsLabel', + { defaultMessage: 'Actions' } + ), render: (_: any, term: T) => { return ( <> @@ -134,15 +188,30 @@ export function SignificantTermsTable({ return ( { return { - onMouseEnter: () => setSelectedSignificantTerm(term), + onMouseEnter: () => { + setSelectedSignificantTerm(term); + trackSelectSignificantTerm(); + }, onMouseLeave: () => setSelectedSignificantTerm(null), }; }} /> ); } + +const loadingText = i18n.translate( + 'xpack.apm.correlations.correlationsTable.loadingText', + { defaultMessage: 'Loading' } +); + +const noDataText = i18n.translate( + 'xpack.apm.correlations.correlationsTable.noDataText', + { defaultMessage: 'No data' } +); diff --git a/x-pack/plugins/apm/public/components/app/correlations/custom_fields.tsx b/x-pack/plugins/apm/public/components/app/correlations/custom_fields.tsx new file mode 100644 index 00000000000000..9d7da4c0d30836 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/custom_fields.tsx @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiAccordion, + EuiComboBox, + EuiFormRow, + EuiLink, + EuiSelect, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useEffect, useState } from 'react'; +import { useFieldNames } from './use_field_names'; +import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; +import { useUiTracker } from '../../../../../observability/public'; + +interface Props { + fieldNames: string[]; + setFieldNames: (fieldNames: string[]) => void; + setDurationPercentile?: (value: PercentileOption) => void; + showThreshold?: boolean; + durationPercentile?: PercentileOption; +} + +export type PercentileOption = 50 | 75 | 99; +const percentilOptions: PercentileOption[] = [50, 75, 99]; + +export function CustomFields({ + fieldNames, + setFieldNames, + setDurationPercentile = () => {}, + showThreshold = false, + durationPercentile = 75, +}: Props) { + const trackApmEvent = useUiTracker({ app: 'apm' }); + const { defaultFieldNames, getSuggestions } = useFieldNames(); + const [suggestedFieldNames, setSuggestedFieldNames] = useState( + getSuggestions('') + ); + + useEffect(() => { + if (suggestedFieldNames.length) { + return; + } + setSuggestedFieldNames(getSuggestions('')); + }, [getSuggestions, suggestedFieldNames]); + + return ( + + + + {showThreshold && ( + + + ({ + value: percentile, + text: i18n.translate( + 'xpack.apm.correlations.customize.thresholdPercentile', + { + defaultMessage: '{percentile}th percentile', + values: { percentile }, + } + ), + }))} + onChange={(e) => { + setDurationPercentile( + parseInt(e.target.value, 10) as PercentileOption + ); + }} + /> + + + )} + + { + setFieldNames(defaultFieldNames); + }} + > + {i18n.translate( + 'xpack.apm.correlations.customize.fieldHelpTextReset', + { defaultMessage: 'reset' } + )} + + ), + docsLink: ( + + {i18n.translate( + 'xpack.apm.correlations.customize.fieldHelpTextDocsLink', + { + defaultMessage: + 'Learn more about the default fields.', + } + )} + + ), + }} + /> + } + > + ({ label }))} + onChange={(options) => { + const nextFieldNames = options.map((option) => option.label); + setFieldNames(nextFieldNames); + trackApmEvent({ metric: 'customize_correlations_fields' }); + }} + onCreateOption={(term) => { + const nextFieldNames = [...fieldNames, term]; + setFieldNames(nextFieldNames); + }} + onSearchChange={(searchValue) => { + setSuggestedFieldNames(getSuggestions(searchValue)); + }} + options={suggestedFieldNames.map((label) => ({ label }))} + /> + + + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Correlations/ErrorCorrelations.tsx b/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx similarity index 60% rename from x-pack/plugins/apm/public/components/app/Correlations/ErrorCorrelations.tsx rename to x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx index 533373d7e8778f..9b80ee6fc31b85 100644 --- a/x-pack/plugins/apm/public/components/app/Correlations/ErrorCorrelations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/error_correlations.tsx @@ -17,19 +17,19 @@ import { } from '@elastic/charts'; import React, { useState } from 'react'; import { useParams } from 'react-router-dom'; -import { - EuiTitle, - EuiFlexGroup, - EuiFlexItem, - EuiComboBox, - EuiAccordion, -} from '@elastic/eui'; +import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; import { px } from '../../../style/variables'; -import { SignificantTermsTable } from './SignificantTermsTable'; +import { CorrelationsTable } from './correlations_table'; import { ChartContainer } from '../../shared/charts/chart_container'; +import { useTheme } from '../../../hooks/use_theme'; +import { CustomFields } from './custom_fields'; +import { useFieldNames } from './use_field_names'; +import { useLocalStorage } from '../../../hooks/useLocalStorage'; +import { useUiTracker } from '../../../../../observability/public'; type CorrelationsApiResponse = NonNullable< APIReturnType<'GET /api/apm/correlations/failed_transactions'> @@ -39,29 +39,30 @@ type SignificantTerm = NonNullable< CorrelationsApiResponse['significantTerms'] >[0]; -const initialFieldNames = [ - 'transaction.name', - 'user.username', - 'user.id', - 'host.ip', - 'user_agent.name', - 'kubernetes.pod.uuid', - 'kubernetes.pod.name', - 'url.domain', - 'container.id', - 'service.node.name', -].map((label) => ({ label })); +interface Props { + onClose: () => void; +} -export function ErrorCorrelations() { +export function ErrorCorrelations({ onClose }: Props) { const [ selectedSignificantTerm, setSelectedSignificantTerm, ] = useState(null); - const [fieldNames, setFieldNames] = useState(initialFieldNames); const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams, uiFilters } = useUrlParams(); - const { transactionName, transactionType, start, end } = urlParams; + const { + environment, + transactionName, + transactionType, + start, + end, + } = urlParams; + const { defaultFieldNames } = useFieldNames(); + const [fieldNames, setFieldNames] = useLocalStorage( + `apm.correlations.errors.fields:${serviceName}`, + defaultFieldNames + ); const { data, status } = useFetcher( (callApmApi) => { @@ -70,19 +71,21 @@ export function ErrorCorrelations() { endpoint: 'GET /api/apm/correlations/failed_transactions', params: { query: { + environment, serviceName, transactionName, transactionType, start, end, uiFilters: JSON.stringify(uiFilters), - fieldNames: fieldNames.map((field) => field.label).join(','), + fieldNames: fieldNames.join(','), }, }, }); } }, [ + environment, serviceName, start, end, @@ -93,12 +96,29 @@ export function ErrorCorrelations() { ] ); + const trackApmEvent = useUiTracker({ app: 'apm' }); + trackApmEvent({ metric: 'view_errors_correlations' }); + return ( <> - -

    Error rate over time

    + +

    + {i18n.translate('xpack.apm.correlations.error.description', { + defaultMessage: + 'Why are some transactions failing and returning errors? Correlations will help discover a possible culprit in a particular cohort of your data. Either by host, version, or other custom fields.', + })} +

    +
    +
    + + +

    + {i18n.translate('xpack.apm.correlations.error.chart.title', { + defaultMessage: 'Error rate over time', + })} +

    @@ -109,26 +129,20 @@ export function ErrorCorrelations() { /> - - - setFieldNames((names) => [...names, { label: term }]) - } - /> - - - - + + +
    ); @@ -143,6 +157,7 @@ function ErrorTimeseriesChart({ selectedSignificantTerm: SignificantTerm | null; status: FETCH_STATUS; }) { + const theme = useTheme(); const dateFormatter = timeFormatter('HH:mm:ss'); return ( @@ -164,7 +179,10 @@ function ErrorTimeseriesChart({ /> diff --git a/x-pack/plugins/apm/public/components/app/correlations/index.tsx b/x-pack/plugins/apm/public/components/app/correlations/index.tsx new file mode 100644 index 00000000000000..eba7c42490e066 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/index.tsx @@ -0,0 +1,191 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { + EuiButtonEmpty, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, + EuiPortal, + EuiCode, + EuiLink, + EuiCallOut, + EuiButton, + EuiTab, + EuiTabs, + EuiSpacer, + EuiBetaBadge, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useHistory } from 'react-router-dom'; +import { LatencyCorrelations } from './latency_correlations'; +import { ErrorCorrelations } from './error_correlations'; +import { useUrlParams } from '../../../context/url_params_context/use_url_params'; +import { createHref } from '../../shared/Links/url_helpers'; +import { + METRIC_TYPE, + useTrackMetric, +} from '../../../../../observability/public'; +import { isActivePlatinumLicense } from '../../../../common/license_check'; +import { useLicenseContext } from '../../../context/license/use_license_context'; +import { LicensePrompt } from '../../shared/LicensePrompt'; + +const latencyTab = { + key: 'latency', + label: i18n.translate('xpack.apm.correlations.tabs.latencyLabel', { + defaultMessage: 'Latency', + }), + component: LatencyCorrelations, +}; +const errorRateTab = { + key: 'errorRate', + label: i18n.translate('xpack.apm.correlations.tabs.errorRateLabel', { + defaultMessage: 'Error rate', + }), + component: ErrorCorrelations, +}; +const tabs = [latencyTab, errorRateTab]; + +export function Correlations() { + const { urlParams } = useUrlParams(); + const history = useHistory(); + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + const [currentTab, setCurrentTab] = useState(latencyTab.key); + const { component: TabContent } = + tabs.find((tab) => tab.key === currentTab) ?? latencyTab; + + return ( + <> + { + setIsFlyoutVisible(true); + }} + iconType="visTagCloud" + > + {i18n.translate('xpack.apm.correlations.buttonLabel', { + defaultMessage: 'View correlations', + })} + + + {isFlyoutVisible && ( + + setIsFlyoutVisible(false)} + > + + +

    + {CORRELATIONS_TITLE} +   + +

    +
    +
    + + + {urlParams.kuery ? ( + <> + + + {i18n.translate( + 'xpack.apm.correlations.filteringByLabel', + { defaultMessage: 'Filtering by' } + )} + + {urlParams.kuery} + + + {i18n.translate( + 'xpack.apm.correlations.clearFiltersLabel', + { defaultMessage: 'Clear' } + )} + + + + + + ) : null} + + + + {tabs.map(({ key, label }) => ( + { + setCurrentTab(key); + }} + > + {label} + + ))} + + + setIsFlyoutVisible(false)} /> + + +
    +
    + )} + + ); +} + +const CORRELATIONS_TITLE = i18n.translate('xpack.apm.correlations.title', { + defaultMessage: 'Correlations', +}); + +function CorrelationsMetricsLicenseCheck({ + children, +}: { + children: React.ReactNode; +}) { + const license = useLicenseContext(); + const hasActivePlatinumLicense = isActivePlatinumLicense(license); + + const metric = { + app: 'apm' as const, + metric: hasActivePlatinumLicense + ? 'correlations_flyout_view' + : 'correlations_license_prompt', + metricType: METRIC_TYPE.COUNT as METRIC_TYPE.COUNT, + }; + useTrackMetric(metric); + useTrackMetric({ ...metric, delay: 15000 }); + + return ( + <> + {hasActivePlatinumLicense ? ( + children + ) : ( + + )} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx similarity index 61% rename from x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx rename to x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx index 19f6248f56da61..459df99a62f5a2 100644 --- a/x-pack/plugins/apm/public/components/app/Correlations/LatencyCorrelations.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.tsx @@ -15,21 +15,19 @@ import { } from '@elastic/charts'; import React, { useState } from 'react'; import { useParams } from 'react-router-dom'; -import { - EuiTitle, - EuiFlexGroup, - EuiFlexItem, - EuiComboBox, - EuiAccordion, - EuiFormRow, - EuiFieldNumber, -} from '@elastic/eui'; +import { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { getDurationFormatter } from '../../../../common/utils/formatters'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; -import { SignificantTermsTable } from './SignificantTermsTable'; +import { CorrelationsTable } from './correlations_table'; import { ChartContainer } from '../../shared/charts/chart_container'; +import { useTheme } from '../../../hooks/use_theme'; +import { CustomFields, PercentileOption } from './custom_fields'; +import { useFieldNames } from './use_field_names'; +import { useLocalStorage } from '../../../hooks/useLocalStorage'; +import { useUiTracker } from '../../../../../observability/public'; type CorrelationsApiResponse = NonNullable< APIReturnType<'GET /api/apm/correlations/slow_transactions'> @@ -39,29 +37,37 @@ type SignificantTerm = NonNullable< CorrelationsApiResponse['significantTerms'] >[0]; -const initialFieldNames = [ - 'user.username', - 'user.id', - 'host.ip', - 'user_agent.name', - 'kubernetes.pod.uuid', - 'kubernetes.pod.name', - 'url.domain', - 'container.id', - 'service.node.name', -].map((label) => ({ label })); - -export function LatencyCorrelations() { +interface Props { + onClose: () => void; +} + +export function LatencyCorrelations({ onClose }: Props) { const [ selectedSignificantTerm, setSelectedSignificantTerm, ] = useState(null); - const [fieldNames, setFieldNames] = useState(initialFieldNames); - const [durationPercentile, setDurationPercentile] = useState('50'); const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams, uiFilters } = useUrlParams(); - const { transactionName, transactionType, start, end } = urlParams; + const { + environment, + transactionName, + transactionType, + start, + end, + } = urlParams; + const { defaultFieldNames } = useFieldNames(); + const [fieldNames, setFieldNames] = useLocalStorage( + `apm.correlations.latency.fields:${serviceName}`, + defaultFieldNames + ); + const [ + durationPercentile, + setDurationPercentile, + ] = useLocalStorage( + `apm.correlations.latency.threshold:${serviceName}`, + 75 + ); const { data, status } = useFetcher( (callApmApi) => { @@ -70,20 +76,22 @@ export function LatencyCorrelations() { endpoint: 'GET /api/apm/correlations/slow_transactions', params: { query: { + environment, serviceName, transactionName, transactionType, start, end, uiFilters: JSON.stringify(uiFilters), - durationPercentile, - fieldNames: fieldNames.map((field) => field.label).join(','), + durationPercentile: durationPercentile.toString(10), + fieldNames: fieldNames.join(','), }, }, }); } }, [ + environment, serviceName, start, end, @@ -95,14 +103,32 @@ export function LatencyCorrelations() { ] ); + const trackApmEvent = useUiTracker({ app: 'apm' }); + trackApmEvent({ metric: 'view_latency_correlations' }); + return ( <> + + +

    + {i18n.translate('xpack.apm.correlations.latency.description', { + defaultMessage: + 'What is slowing down my service? Correlations will help discover a slower performance in a particular cohort of your data. Either by host, version, or other custom fields.', + })} +

    +
    +
    - -

    Latency distribution

    + +

    + {i18n.translate( + 'xpack.apm.correlations.latency.chart.title', + { defaultMessage: 'Latency distribution' } + )} +

    - - - - - - setDurationPercentile(e.currentTarget.value) - } - /> - - - - - { - setFieldNames((names) => [...names, { label: term }]); - }} - /> - - - - - - - + + +
    @@ -181,6 +187,7 @@ function LatencyDistributionChart({ selectedSignificantTerm: SignificantTerm | null; status: FETCH_STATUS; }) { + const theme = useTheme(); const xMax = Math.max( ...(data?.overall?.distribution.map((p) => p.x ?? 0) ?? []) ); @@ -218,7 +225,10 @@ function LatencyDistributionChart({ /> `${roundFloat(d)}%`} diff --git a/x-pack/plugins/apm/public/components/app/correlations/use_field_names.ts b/x-pack/plugins/apm/public/components/app/correlations/use_field_names.ts new file mode 100644 index 00000000000000..ff88808c51d15a --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/correlations/use_field_names.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { memoize } from 'lodash'; +import { useEffect, useMemo, useState } from 'react'; +import { isRumAgentName } from '../../../../common/agent_name'; +import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context'; +import { useDynamicIndexPatternFetcher } from '../../../hooks/use_dynamic_index_pattern'; + +interface IndexPattern { + fields: Array<{ name: string; esTypes: string[] }>; +} + +export function useFieldNames() { + const { agentName } = useApmServiceContext(); + const isRumAgent = isRumAgentName(agentName); + const { indexPattern } = useDynamicIndexPatternFetcher(); + + const [defaultFieldNames, setDefaultFieldNames] = useState( + getDefaultFieldNames(indexPattern, isRumAgent) + ); + + const getSuggestions = useMemo( + () => + memoize((searchValue: string) => + getMatchingFieldNames(indexPattern, searchValue) + ), + [indexPattern] + ); + + useEffect(() => { + setDefaultFieldNames(getDefaultFieldNames(indexPattern, isRumAgent)); + }, [indexPattern, isRumAgent]); + + return { defaultFieldNames, getSuggestions }; +} + +function getMatchingFieldNames( + indexPattern: IndexPattern | undefined, + inputValue: string +) { + if (!indexPattern) { + return []; + } + return indexPattern.fields + .filter( + ({ name, esTypes }) => + name.startsWith(inputValue) && esTypes[0] === 'keyword' // only show fields of type 'keyword' + ) + .map(({ name }) => name); +} + +function getDefaultFieldNames( + indexPattern: IndexPattern | undefined, + isRumAgent: boolean +) { + const labelFields = getMatchingFieldNames(indexPattern, 'labels.').slice( + 0, + 6 + ); + return isRumAgent + ? [ + ...labelFields, + 'user_agent.name', + 'user_agent.os.name', + 'url.original', + ...getMatchingFieldNames(indexPattern, 'user.').slice(0, 6), + ] + : [...labelFields, 'service.version', 'service.node.name', 'host.ip']; +} diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx index 29bdf6467e5447..bde23eddaa44fa 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/index.tsx @@ -28,7 +28,7 @@ interface ErrorGroupOverviewProps { export function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) { const { urlParams, uiFilters } = useUrlParams(); - const { start, end, sortField, sortDirection } = urlParams; + const { environment, start, end, sortField, sortDirection } = urlParams; const { errorDistributionData } = useErrorGroupDistributionFetcher({ serviceName, groupId: undefined, @@ -46,6 +46,7 @@ export function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) { serviceName, }, query: { + environment, start, end, sortField, @@ -56,7 +57,7 @@ export function ErrorGroupOverview({ serviceName }: ErrorGroupOverviewProps) { }); } }, - [serviceName, start, end, sortField, sortDirection, uiFilters] + [environment, serviceName, start, end, sortField, sortDirection, uiFilters] ); useTrackPageview({ diff --git a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx index 23f699b63d207f..d2d5c9f6f3a9ab 100644 --- a/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx +++ b/x-pack/plugins/apm/public/components/app/service_details/service_detail_tabs.tsx @@ -26,6 +26,7 @@ import { ServiceNodeOverview } from '../service_node_overview'; import { ServiceMetrics } from '../service_metrics'; import { ServiceOverview } from '../service_overview'; import { TransactionOverview } from '../transaction_overview'; +import { Correlations } from '../correlations'; interface Tab { key: string; @@ -137,6 +138,9 @@ export function ServiceDetailTabs({ serviceName, tab }: Props) { {text} ))} +
    + +
    {selectedTab ? selectedTab.render() : null} diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx index 1cb420a8ac1949..cd17ca0ce023d2 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx @@ -39,19 +39,24 @@ function useServicesFetcher() { const { urlParams, uiFilters } = useUrlParams(); const { core } = useApmPluginContext(); const upgradeAssistantHref = useUpgradeAssistantHref(); - const { start, end } = urlParams; + const { environment, start, end } = urlParams; const { data = initialData, status } = useFetcher( (callApmApi) => { if (start && end) { return callApmApi({ endpoint: 'GET /api/apm/services', params: { - query: { start, end, uiFilters: JSON.stringify(uiFilters) }, + query: { + environment, + start, + end, + uiFilters: JSON.stringify(uiFilters), + }, }, }); } }, - [start, end, uiFilters] + [environment, start, end, uiFilters] ); useEffect(() => { @@ -118,7 +123,7 @@ export function ServiceInventory() { return ( <> - + {displayMlCallout ? ( diff --git a/x-pack/plugins/apm/public/components/app/service_overview/get_latency_column_label.ts b/x-pack/plugins/apm/public/components/app/service_overview/get_latency_column_label.ts new file mode 100644 index 00000000000000..fda45db98d0cae --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/get_latency_column_label.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; + +export function getLatencyColumnLabel( + latencyAggregationType?: LatencyAggregationType +) { + switch (latencyAggregationType) { + case LatencyAggregationType.avg: + return i18n.translate('xpack.apm.serviceOverview.latencyColumnAvgLabel', { + defaultMessage: 'Latency (avg.)', + }); + + case LatencyAggregationType.p95: + return i18n.translate('xpack.apm.serviceOverview.latencyColumnP95Label', { + defaultMessage: 'Latency (95th)', + }); + + case LatencyAggregationType.p99: + return i18n.translate('xpack.apm.serviceOverview.latencyColumnP99Label', { + defaultMessage: 'Latency (99th)', + }); + + default: + return i18n.translate( + 'xpack.apm.serviceOverview.latencyColumnDefaultLabel', + { + defaultMessage: 'Latency', + } + ); + } +} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index 8141ecfebb1ff0..a0ea00ae5c3ad8 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -57,7 +57,7 @@ export function ServiceOverview({ return ( - + diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx index baae683e2eba95..a4647bc148b1e2 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx @@ -14,10 +14,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { - ENVIRONMENT_ALL, - getNextEnvironmentUrlParam, -} from '../../../../../common/environment_filter_values'; +import { getNextEnvironmentUrlParam } from '../../../../../common/environment_filter_values'; import { asMillisecondDuration, asPercent, @@ -96,7 +93,7 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { name: i18n.translate( 'xpack.apm.serviceOverview.dependenciesTableColumnLatency', { - defaultMessage: 'Latency', + defaultMessage: 'Latency (avg.)', } ), width: px(unit * 10), @@ -182,7 +179,7 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { query: { start, end, - environment: environment || ENVIRONMENT_ALL.value, + environment, numBuckets: 20, }, }, diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx index 7f4823c13d593a..f7f5db32e986cc 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_errors_table/index.tsx @@ -52,7 +52,7 @@ const DEFAULT_SORT = { export function ServiceOverviewErrorsTable({ serviceName }: Props) { const { - urlParams: { start, end }, + urlParams: { environment, start, end }, uiFilters, } = useUrlParams(); const { transactionType } = useApmServiceContext(); @@ -152,6 +152,7 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { params: { path: { serviceName }, query: { + environment, start, end, uiFilters: JSON.stringify(uiFilters), @@ -178,6 +179,7 @@ export function ServiceOverviewErrorsTable({ serviceName }: Props) { }); }, [ + environment, start, end, serviceName, diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx index e7c1d4442e3b7c..2f2aaf3156b93a 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_chart_and_table.tsx @@ -25,13 +25,13 @@ export function ServiceOverviewInstancesChartAndTable({ const { transactionType } = useApmServiceContext(); const { - urlParams: { start, end }, + urlParams: { environment, latencyAggregationType, start, end }, uiFilters, } = useUrlParams(); const { data = [], status } = useFetcher( (callApmApi) => { - if (!start || !end || !transactionType) { + if (!start || !end || !transactionType || !latencyAggregationType) { return; } @@ -43,6 +43,8 @@ export function ServiceOverviewInstancesChartAndTable({ serviceName, }, query: { + environment, + latencyAggregationType, start, end, transactionType, @@ -52,7 +54,15 @@ export function ServiceOverviewInstancesChartAndTable({ }, }); }, - [start, end, serviceName, transactionType, uiFilters] + [ + environment, + latencyAggregationType, + start, + end, + serviceName, + transactionType, + uiFilters, + ] ); return ( diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx index 62ae4b7bc34462..83ad506e8659b6 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_instances_table/index.tsx @@ -24,6 +24,7 @@ import { asTransactionRate, } from '../../../../../common/utils/formatters'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; +import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { px, unit } from '../../../../style/variables'; @@ -32,6 +33,7 @@ import { MetricOverviewLink } from '../../../shared/Links/apm/MetricOverviewLink import { ServiceNodeMetricOverviewLink } from '../../../shared/Links/apm/ServiceNodeMetricOverviewLink'; import { TableFetchWrapper } from '../../../shared/table_fetch_wrapper'; import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; +import { getLatencyColumnLabel } from '../get_latency_column_label'; import { ServiceOverviewTableContainer } from '../service_overview_table_container'; type ServiceInstanceItem = ValuesType< @@ -50,6 +52,9 @@ export function ServiceOverviewInstancesTable({ status, }: Props) { const { agentName } = useApmServiceContext(); + const { + urlParams: { latencyAggregationType }, + } = useUrlParams(); const columns: Array> = [ { @@ -95,12 +100,7 @@ export function ServiceOverviewInstancesTable({ }, { field: 'latencyValue', - name: i18n.translate( - 'xpack.apm.serviceOverview.instancesTableColumnLatency', - { - defaultMessage: 'Latency', - } - ), + name: getLatencyColumnLabel(latencyAggregationType), width: px(unit * 10), render: (_, { latency }) => { return ( @@ -182,7 +182,7 @@ export function ServiceOverviewInstancesTable({ defaultMessage: 'Memory usage (avg.)', } ), - width: px(unit * 8), + width: px(unit * 9), render: (_, { memoryUsage }) => { return ( (); const { urlParams, uiFilters } = useUrlParams(); const { transactionType } = useApmServiceContext(); - const { start, end, comparisonEnabled, comparisonType } = urlParams; - const comparisonChartTheme = getComparisonChartTheme(theme); - const { - comparisonStart = undefined, - comparisonEnd = undefined, - } = comparisonType - ? getTimeRangeComparison({ - start, - end, - comparisonType, - }) - : {}; + const { environment, start, end } = urlParams; const { data = INITIAL_STATE, status } = useFetcher( (callApmApi) => { @@ -57,26 +42,17 @@ export function ServiceOverviewThroughputChart({ serviceName, }, query: { + environment, start, end, transactionType, uiFilters: JSON.stringify(uiFilters), - comparisonStart, - comparisonEnd, }, }, }); } }, - [ - serviceName, - start, - end, - uiFilters, - transactionType, - comparisonStart, - comparisonEnd, - ] + [environment, serviceName, start, end, uiFilters, transactionType] ); return ( @@ -93,7 +69,6 @@ export function ServiceOverviewThroughputChart({ height={height} showAnnotations={false} fetchStatus={status} - customTheme={comparisonChartTheme} timeseries={[ { data: data.currentPeriod, @@ -104,21 +79,6 @@ export function ServiceOverviewThroughputChart({ { defaultMessage: 'Throughput' } ), }, - ...(comparisonEnabled - ? [ - { - data: data.previousPeriod, - type: 'area', - color: theme.eui.euiColorLightestShade, - title: i18n.translate( - 'xpack.apm.serviceOverview.throughtputChart.previousPeriodLabel', - { - defaultMessage: 'Previous period', - } - ), - }, - ] - : []), ]} yLabelFormat={asTransactionRate} /> diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/get_columns.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/get_columns.tsx index 2ffc0fc9c93a3a..b01ad93f2c9988 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/get_columns.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/get_columns.tsx @@ -9,6 +9,7 @@ import { EuiBasicTableColumn } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { ValuesType } from 'utility-types'; +import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types'; import { asMillisecondDuration, asPercent, @@ -20,6 +21,7 @@ import { SparkPlot } from '../../../shared/charts/spark_plot'; import { ImpactBar } from '../../../shared/ImpactBar'; import { TransactionDetailLink } from '../../../shared/Links/apm/transaction_detail_link'; import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip'; +import { getLatencyColumnLabel } from '../get_latency_column_label'; type TransactionGroupPrimaryStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups/primary_statistics'>; @@ -28,35 +30,13 @@ type ServiceTransactionGroupItem = ValuesType< >; type TransactionGroupComparisonStatistics = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/groups/comparison_statistics'>; -function getLatencyAggregationTypeLabel(latencyAggregationType?: string) { - switch (latencyAggregationType) { - case 'avg': - return i18n.translate( - 'xpack.apm.serviceOverview.transactionsTableColumnLatency.avg', - { defaultMessage: 'Latency (avg.)' } - ); - - case 'p95': - return i18n.translate( - 'xpack.apm.serviceOverview.transactionsTableColumnLatency.p95', - { defaultMessage: 'Latency (95th)' } - ); - - case 'p99': - return i18n.translate( - 'xpack.apm.serviceOverview.transactionsTableColumnLatency.p99', - { defaultMessage: 'Latency (99th)' } - ); - } -} - export function getColumns({ serviceName, latencyAggregationType, transactionGroupComparisonStatistics, }: { serviceName: string; - latencyAggregationType?: string; + latencyAggregationType?: LatencyAggregationType; transactionGroupComparisonStatistics?: TransactionGroupComparisonStatistics; }): Array> { return [ @@ -88,7 +68,7 @@ export function getColumns({ { field: 'latency', sortable: true, - name: getLatencyAggregationTypeLabel(latencyAggregationType), + name: getLatencyColumnLabel(latencyAggregationType), width: px(unit * 10), render: (_, { latency, name }) => { const timeseries = diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx index a0facb2ddbedf6..5529f9028b9dda 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_transactions_table/index.tsx @@ -58,7 +58,7 @@ export function ServiceOverviewTransactionsTable({ serviceName }: Props) { const { transactionType } = useApmServiceContext(); const { uiFilters, - urlParams: { start, end, latencyAggregationType }, + urlParams: { environment, start, end, latencyAggregationType }, } = useUrlParams(); const { data = INITIAL_STATE, status } = useFetcher( @@ -72,6 +72,7 @@ export function ServiceOverviewTransactionsTable({ serviceName }: Props) { params: { path: { serviceName }, query: { + environment, start, end, uiFilters: JSON.stringify(uiFilters), @@ -87,6 +88,7 @@ export function ServiceOverviewTransactionsTable({ serviceName }: Props) { }); }, [ + environment, serviceName, start, end, @@ -125,6 +127,7 @@ export function ServiceOverviewTransactionsTable({ serviceName }: Props) { params: { path: { serviceName }, query: { + environment, start, end, uiFilters: JSON.stringify(uiFilters), diff --git a/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx index d29dad7a7e3dea..8fc9ac12824baa 100644 --- a/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx @@ -5,14 +5,13 @@ * 2.0. */ -import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiPanel } from '@elastic/eui'; +import { EuiFlexGroup, EuiPage, EuiPanel } from '@elastic/eui'; import React from 'react'; import { useTrackPageview } from '../../../../../observability/public'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; import { APIReturnType } from '../../../services/rest/createCallApmApi'; import { SearchBar } from '../../shared/search_bar'; -import { Correlations } from '../Correlations'; import { TraceList } from './TraceList'; type TracesAPIResponse = APIReturnType<'GET /api/apm/traces'>; @@ -24,7 +23,7 @@ const DEFAULT_RESPONSE: TracesAPIResponse = { export function TraceOverview() { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { environment, start, end } = urlParams; const { status, data = DEFAULT_RESPONSE } = useFetcher( (callApmApi) => { if (start && end) { @@ -32,6 +31,7 @@ export function TraceOverview() { endpoint: 'GET /api/apm/traces', params: { query: { + environment, start, end, uiFilters: JSON.stringify(uiFilters), @@ -40,7 +40,7 @@ export function TraceOverview() { }); } }, - [start, end, uiFilters] + [environment, start, end, uiFilters] ); useTrackPageview({ app: 'apm', path: 'traces_overview' }); @@ -48,14 +48,9 @@ export function TraceOverview() { return ( <> - + - - - - - {transactionName}

    - + - - - - - - diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx index 1b8c41344efc57..97be35ec6f5b95 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/index.tsx @@ -29,7 +29,6 @@ import { ElasticDocsLink } from '../../shared/Links/ElasticDocsLink'; import { fromQuery, toQuery } from '../../shared/Links/url_helpers'; import { SearchBar } from '../../shared/search_bar'; import { TransactionTypeSelect } from '../../shared/transaction_type_select'; -import { Correlations } from '../Correlations'; import { TransactionList } from './TransactionList'; import { useRedirect } from './useRedirect'; import { useTransactionListFetcher } from './use_transaction_list'; @@ -83,7 +82,7 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) { return ( <> - + @@ -110,9 +109,6 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) {
    - - -
    diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx index 8fb5166bd8676e..e4fbd075660605 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx @@ -131,7 +131,7 @@ describe('TransactionOverview', () => { }); expect(history.location.search).toEqual( - '?transactionType=secondType&rangeFrom=now-15m&rangeTo=now&comparisonEnabled=true&comparisonType=day' + '?transactionType=secondType&rangeFrom=now-15m&rangeTo=now' ); expect(getByText(container, 'firstType')).toBeInTheDocument(); expect(getByText(container, 'secondType')).toBeInTheDocument(); @@ -142,7 +142,7 @@ describe('TransactionOverview', () => { expect(history.push).toHaveBeenCalled(); expect(history.location.search).toEqual( - '?transactionType=firstType&rangeFrom=now-15m&rangeTo=now&comparisonEnabled=true&comparisonType=day' + '?transactionType=firstType&rangeFrom=now-15m&rangeTo=now' ); }); }); diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/use_transaction_list.ts b/x-pack/plugins/apm/public/components/app/transaction_overview/use_transaction_list.ts index 406ba98b79e256..a63788457b8b5d 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/use_transaction_list.ts +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/use_transaction_list.ts @@ -21,7 +21,7 @@ const DEFAULT_RESPONSE: Partial = { export function useTransactionListFetcher() { const { urlParams, uiFilters } = useUrlParams(); const { serviceName } = useParams<{ serviceName?: string }>(); - const { transactionType, start, end } = urlParams; + const { environment, transactionType, start, end } = urlParams; const { data = DEFAULT_RESPONSE, error, status } = useFetcher( (callApmApi) => { if (serviceName && start && end && transactionType) { @@ -30,6 +30,7 @@ export function useTransactionListFetcher() { params: { path: { serviceName }, query: { + environment, start, end, transactionType, @@ -39,7 +40,7 @@ export function useTransactionListFetcher() { }); } }, - [serviceName, start, end, transactionType, uiFilters] + [environment, serviceName, start, end, transactionType, uiFilters] ); return { diff --git a/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx b/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx index 714645fd749612..59c99463144cbe 100644 --- a/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/EnvironmentFilter/index.tsx @@ -64,10 +64,9 @@ export function EnvironmentFilter() { const history = useHistory(); const location = useLocation(); const { serviceName } = useParams<{ serviceName?: string }>(); - const { uiFilters, urlParams } = useUrlParams(); + const { urlParams } = useUrlParams(); - const { environment } = uiFilters; - const { start, end } = urlParams; + const { environment, start, end } = urlParams; const { environments, status = 'loading' } = useEnvironmentsFetcher({ serviceName, start, diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx index 3bfdb5df61c2ee..c3d418b63426b5 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/MetricOverviewLink.tsx @@ -5,10 +5,9 @@ * 2.0. */ -import { EuiLink } from '@elastic/eui'; import React from 'react'; import { APMQueryParams } from '../url_helpers'; -import { APMLinkExtendProps, useAPMHref } from './APMLink'; +import { APMLink, APMLinkExtendProps, useAPMHref } from './APMLink'; const persistedFilters: Array = [ 'host', @@ -29,6 +28,5 @@ interface Props extends APMLinkExtendProps { } export function MetricOverviewLink({ serviceName, ...rest }: Props) { - const href = useMetricOverviewHref(serviceName); - return ; + return ; } diff --git a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx index 7bfe17e82bf4ae..8009f288d48c0b 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx @@ -24,7 +24,6 @@ import { } from '@elastic/charts'; import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import moment from 'moment'; import React from 'react'; import { useHistory } from 'react-router-dom'; import { useChartTheme } from '../../../../../observability/public'; @@ -32,7 +31,6 @@ import { asAbsoluteDateTime } from '../../../../common/utils/formatters'; import { RectCoordinate, TimeSeries } from '../../../../typings/timeseries'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { useTheme } from '../../../hooks/use_theme'; -import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { useAnnotationsContext } from '../../../context/annotations/use_annotations_context'; import { useChartPointerEventContext } from '../../../context/chart_pointer_event/use_chart_pointer_event_context'; import { unit } from '../../../style/variables'; @@ -78,14 +76,13 @@ export function TimeseriesChart({ const history = useHistory(); const { annotations } = useAnnotationsContext(); const { setPointerEvent, chartRef } = useChartPointerEventContext(); - const { urlParams } = useUrlParams(); const theme = useTheme(); const chartTheme = useChartTheme(); - const { start, end } = urlParams; + const xValues = timeseries.flatMap(({ data }) => data.map(({ x }) => x)); - const min = moment.utc(start).valueOf(); - const max = moment.utc(end).valueOf(); + const min = Math.min(...xValues); + const max = Math.max(...xValues); const xFormatter = niceTimeFormatter([min, max]); diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/use_transaction_breakdown.ts b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/use_transaction_breakdown.ts index 466f201ab3398e..293a1929ca6069 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/use_transaction_breakdown.ts +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/use_transaction_breakdown.ts @@ -13,7 +13,7 @@ import { useApmServiceContext } from '../../../../context/apm_service/use_apm_se export function useTransactionBreakdown() { const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams, uiFilters } = useUrlParams(); - const { start, end, transactionName } = urlParams; + const { environment, start, end, transactionName } = urlParams; const { transactionType } = useApmServiceContext(); const { data = { timeseries: undefined }, error, status } = useFetcher( @@ -25,6 +25,7 @@ export function useTransactionBreakdown() { params: { path: { serviceName }, query: { + environment, start, end, transactionName, @@ -35,7 +36,15 @@ export function useTransactionBreakdown() { }); } }, - [serviceName, start, end, transactionType, transactionName, uiFilters] + [ + environment, + serviceName, + start, + end, + transactionType, + transactionName, + uiFilters, + ] ); return { diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx index df18e7627faedb..a3da8812966f18 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_error_rate_chart/index.tsx @@ -33,7 +33,7 @@ export function TransactionErrorRateChart({ const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams, uiFilters } = useUrlParams(); const { transactionType } = useApmServiceContext(); - const { start, end, transactionName } = urlParams; + const { environment, start, end, transactionName } = urlParams; const { data, status } = useFetcher( (callApmApi) => { @@ -46,6 +46,7 @@ export function TransactionErrorRateChart({ serviceName, }, query: { + environment, start, end, transactionType, @@ -56,7 +57,15 @@ export function TransactionErrorRateChart({ }); } }, - [serviceName, start, end, uiFilters, transactionType, transactionName] + [ + environment, + serviceName, + start, + end, + uiFilters, + transactionType, + transactionName, + ] ); const errorRates = data?.transactionErrorRate || []; diff --git a/x-pack/plugins/apm/public/components/shared/search_bar.tsx b/x-pack/plugins/apm/public/components/shared/search_bar.tsx index 260306abe2c342..2bd3fef8c0e884 100644 --- a/x-pack/plugins/apm/public/components/shared/search_bar.tsx +++ b/x-pack/plugins/apm/public/components/shared/search_bar.tsx @@ -22,15 +22,21 @@ const SearchBarFlexGroup = euiStyled(EuiFlexGroup)` interface Props { prepend?: React.ReactNode | string; showTimeComparison?: boolean; + showCorrelations?: boolean; } function getRowDirection(showColumn: boolean) { return showColumn ? 'column' : 'row'; } -export function SearchBar({ prepend, showTimeComparison = false }: Props) { +export function SearchBar({ + prepend, + showTimeComparison = false, + showCorrelations = false, +}: Props) { const { isMedium, isLarge } = useBreakPoints(); const itemsStyle = { marginBottom: isLarge ? px(unit) : 0 }; + return ( diff --git a/x-pack/plugins/apm/public/context/url_params_context/helpers.test.ts b/x-pack/plugins/apm/public/context/url_params_context/helpers.test.ts index bdce54ad086342..4de68a5bc20362 100644 --- a/x-pack/plugins/apm/public/context/url_params_context/helpers.test.ts +++ b/x-pack/plugins/apm/public/context/url_params_context/helpers.test.ts @@ -13,7 +13,7 @@ describe('url_params_context helpers', () => { describe('getDateRange', () => { describe('with non-rounded dates', () => { describe('one minute', () => { - it('rounds the values', () => { + it('rounds the start value to minute', () => { expect( helpers.getDateRange({ state: {}, @@ -21,13 +21,13 @@ describe('url_params_context helpers', () => { rangeTo: '2021-01-28T05:48:55.304Z', }) ).toEqual({ - start: '2021-01-28T05:47:50.000Z', - end: '2021-01-28T05:49:00.000Z', + start: '2021-01-28T05:47:00.000Z', + end: '2021-01-28T05:48:55.304Z', }); }); }); describe('one day', () => { - it('rounds the values', () => { + it('rounds the start value to minute', () => { expect( helpers.getDateRange({ state: {}, @@ -35,14 +35,14 @@ describe('url_params_context helpers', () => { rangeTo: '2021-01-28T05:46:13.367Z', }) ).toEqual({ - start: '2021-01-27T03:00:00.000Z', - end: '2021-01-28T06:00:00.000Z', + start: '2021-01-27T05:46:00.000Z', + end: '2021-01-28T05:46:13.367Z', }); }); }); describe('one year', () => { - it('rounds the values', () => { + it('rounds the start value to minute', () => { expect( helpers.getDateRange({ state: {}, @@ -50,8 +50,8 @@ describe('url_params_context helpers', () => { rangeTo: '2021-01-28T05:52:39.741Z', }) ).toEqual({ - start: '2020-01-01T00:00:00.000Z', - end: '2021-02-01T00:00:00.000Z', + start: '2020-01-28T05:52:00.000Z', + end: '2021-01-28T05:52:39.741Z', }); }); }); diff --git a/x-pack/plugins/apm/public/context/url_params_context/helpers.ts b/x-pack/plugins/apm/public/context/url_params_context/helpers.ts index f77c4119f46e1a..eae9eba8b3ddad 100644 --- a/x-pack/plugins/apm/public/context/url_params_context/helpers.ts +++ b/x-pack/plugins/apm/public/context/url_params_context/helpers.ts @@ -6,8 +6,8 @@ */ import datemath from '@elastic/datemath'; -import { scaleUtc } from 'd3-scale'; import { compact, pickBy } from 'lodash'; +import moment from 'moment'; import { IUrlParams } from './types'; function getParsedDate(rawDate?: string, options = {}) { @@ -42,13 +42,12 @@ export function getDateRange({ return { start: state.start, end: state.end }; } - // Calculate ticks for the time ranges to produce nicely rounded values. - const ticks = scaleUtc().domain([start, end]).nice().ticks(); + // rounds down start to minute + const roundedStart = moment(start).startOf('minute'); - // Return the first and last tick values. return { - start: ticks[0].toISOString(), - end: ticks[ticks.length - 1].toISOString(), + start: roundedStart.toISOString(), + end: end.toISOString(), }; } diff --git a/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts b/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts index addef74f5b25b0..b6e7330be30cbd 100644 --- a/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts +++ b/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts @@ -6,10 +6,11 @@ */ import { Location } from 'history'; +import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; import { LatencyAggregationType } from '../../../common/latency_aggregation_types'; import { pickKeys } from '../../../common/utils/pick_keys'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { localUIFilterNames } from '../../../server/lib/ui_filters/local_ui_filters/config'; +import { localUIFilterNames } from '../../../server/lib/rum_client/ui_filters/local_ui_filters/config'; import { toQuery } from '../../components/shared/Links/url_helpers'; import { TimeRangeComparisonType } from '../../components/shared/time_comparison/get_time_range_comparison'; import { @@ -66,6 +67,7 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) { refreshInterval: refreshInterval ? toNumber(refreshInterval) : undefined, // query params + environment: toString(environment) || ENVIRONMENT_ALL.value, sortDirection, sortField, page: toNumber(page) || 0, @@ -87,7 +89,6 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) { : undefined, comparisonType: comparisonType as TimeRangeComparisonType | undefined, // ui filters - environment, ...localUIFilters, }); } diff --git a/x-pack/plugins/apm/public/context/url_params_context/url_params_context.test.tsx b/x-pack/plugins/apm/public/context/url_params_context/url_params_context.test.tsx index 66e22f32b26bd5..056aabb10f8784 100644 --- a/x-pack/plugins/apm/public/context/url_params_context/url_params_context.test.tsx +++ b/x-pack/plugins/apm/public/context/url_params_context/url_params_context.test.tsx @@ -52,8 +52,8 @@ describe('UrlParamsContext', () => { const params = getDataFromOutput(wrapper); expect([params.start, params.end]).toEqual([ - '2010-03-15T00:00:00.000Z', - '2010-04-11T00:00:00.000Z', + '2010-03-15T12:00:00.000Z', + '2010-04-10T12:00:00.000Z', ]); }); @@ -71,8 +71,8 @@ describe('UrlParamsContext', () => { const params = getDataFromOutput(wrapper); expect([params.start, params.end]).toEqual([ - '2009-03-15T00:00:00.000Z', - '2009-04-11T00:00:00.000Z', + '2009-03-15T12:00:00.000Z', + '2009-04-10T12:00:00.000Z', ]); }); @@ -92,7 +92,7 @@ describe('UrlParamsContext', () => { expect([params.start, params.end]).toEqual([ '1969-12-31T00:00:00.000Z', - '1970-01-01T00:00:00.000Z', + '1969-12-31T23:59:59.999Z', ]); nowSpy.mockRestore(); @@ -145,8 +145,8 @@ describe('UrlParamsContext', () => { const params = getDataFromOutput(wrapper); expect([params.start, params.end]).toEqual([ - '2005-09-19T00:00:00.000Z', - '2005-10-23T00:00:00.000Z', + '2005-09-20T12:00:00.000Z', + '2005-10-21T12:00:00.000Z', ]); }); @@ -196,7 +196,7 @@ describe('UrlParamsContext', () => { expect([params.start, params.end]).toEqual([ '2000-06-14T00:00:00.000Z', - '2000-06-15T00:00:00.000Z', + '2000-06-14T23:59:59.999Z', ]); }); }); diff --git a/x-pack/plugins/apm/public/context/url_params_context/url_params_context.tsx b/x-pack/plugins/apm/public/context/url_params_context/url_params_context.tsx index 9cc11eef79eef3..90245b9843b013 100644 --- a/x-pack/plugins/apm/public/context/url_params_context/url_params_context.tsx +++ b/x-pack/plugins/apm/public/context/url_params_context/url_params_context.tsx @@ -14,11 +14,10 @@ import React, { useState, } from 'react'; import { withRouter } from 'react-router-dom'; -import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; import { LocalUIFilterName } from '../../../common/ui_filter'; import { pickKeys } from '../../../common/utils/pick_keys'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { localUIFilterNames } from '../../../server/lib/ui_filters/local_ui_filters/config'; +import { localUIFilterNames } from '../../../server/lib/rum_client/ui_filters/local_ui_filters/config'; import { UIFilters } from '../../../typings/ui_filters'; import { useDeepObjectIdentity } from '../../hooks/useDeepObjectIdentity'; import { getDateRange } from './helpers'; @@ -39,7 +38,6 @@ function useUiFilters(params: IUrlParams): UIFilters { return useDeepObjectIdentity({ kuery, - environment: environment || ENVIRONMENT_ALL.value, ...localUiFilters, }); } diff --git a/x-pack/plugins/apm/public/hooks/use_environments_fetcher.tsx b/x-pack/plugins/apm/public/hooks/use_environments_fetcher.tsx index f6d3e848024701..b7dc29a36170d7 100644 --- a/x-pack/plugins/apm/public/hooks/use_environments_fetcher.tsx +++ b/x-pack/plugins/apm/public/hooks/use_environments_fetcher.tsx @@ -36,7 +36,7 @@ export function useEnvironmentsFetcher({ (callApmApi) => { if (start && end) { return callApmApi({ - endpoint: 'GET /api/apm/ui_filters/environments', + endpoint: 'GET /api/apm/environments', params: { query: { start, diff --git a/x-pack/plugins/apm/public/hooks/use_error_group_distribution_fetcher.tsx b/x-pack/plugins/apm/public/hooks/use_error_group_distribution_fetcher.tsx index 834b0cc0527897..9ff179e6af6a06 100644 --- a/x-pack/plugins/apm/public/hooks/use_error_group_distribution_fetcher.tsx +++ b/x-pack/plugins/apm/public/hooks/use_error_group_distribution_fetcher.tsx @@ -16,7 +16,7 @@ export function useErrorGroupDistributionFetcher({ groupId: string | undefined; }) { const { urlParams, uiFilters } = useUrlParams(); - const { start, end } = urlParams; + const { environment, start, end } = urlParams; const { data } = useFetcher( (callApmApi) => { if (start && end) { @@ -25,6 +25,7 @@ export function useErrorGroupDistributionFetcher({ params: { path: { serviceName }, query: { + environment, start, end, groupId, @@ -34,7 +35,7 @@ export function useErrorGroupDistributionFetcher({ }); } }, - [serviceName, start, end, groupId, uiFilters] + [environment, serviceName, start, end, groupId, uiFilters] ); return { errorDistributionData: data }; diff --git a/x-pack/plugins/apm/public/hooks/use_service_metric_charts_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_service_metric_charts_fetcher.ts index ecfa5471189d20..87e10f1e8937b0 100644 --- a/x-pack/plugins/apm/public/hooks/use_service_metric_charts_fetcher.ts +++ b/x-pack/plugins/apm/public/hooks/use_service_metric_charts_fetcher.ts @@ -24,7 +24,7 @@ export function useServiceMetricChartsFetcher({ const { urlParams, uiFilters } = useUrlParams(); const { agentName } = useApmServiceContext(); const { serviceName } = useParams<{ serviceName?: string }>(); - const { start, end } = urlParams; + const { environment, start, end } = urlParams; const { data = INITIAL_DATA, error, status } = useFetcher( (callApmApi) => { if (serviceName && start && end && agentName) { @@ -33,6 +33,7 @@ export function useServiceMetricChartsFetcher({ params: { path: { serviceName }, query: { + environment, serviceNodeName, start, end, @@ -43,7 +44,15 @@ export function useServiceMetricChartsFetcher({ }); } }, - [serviceName, start, end, agentName, serviceNodeName, uiFilters] + [ + environment, + serviceName, + start, + end, + agentName, + serviceNodeName, + uiFilters, + ] ); return { diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts index 66446bf0dfebad..c493a30716aa5e 100644 --- a/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts +++ b/x-pack/plugins/apm/public/hooks/use_transaction_distribution_fetcher.ts @@ -25,6 +25,7 @@ export function useTransactionDistributionFetcher() { const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams, uiFilters } = useUrlParams(); const { + environment, start, end, transactionType, @@ -45,6 +46,7 @@ export function useTransactionDistributionFetcher() { serviceName, }, query: { + environment, start, end, transactionType, @@ -92,7 +94,15 @@ export function useTransactionDistributionFetcher() { }, // the histogram should not be refetched if the transactionId or traceId changes // eslint-disable-next-line react-hooks/exhaustive-deps - [serviceName, start, end, transactionType, transactionName, uiFilters] + [ + environment, + serviceName, + start, + end, + transactionType, + transactionName, + uiFilters, + ] ); return { diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts index d5974ee3543a7e..cca2e99d84dfdf 100644 --- a/x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts +++ b/x-pack/plugins/apm/public/hooks/use_transaction_latency_chart_fetcher.ts @@ -18,7 +18,13 @@ export function useTransactionLatencyChartsFetcher() { const { transactionType } = useApmServiceContext(); const theme = useTheme(); const { - urlParams: { start, end, transactionName, latencyAggregationType }, + urlParams: { + environment, + start, + end, + transactionName, + latencyAggregationType, + }, uiFilters, } = useUrlParams(); @@ -37,6 +43,7 @@ export function useTransactionLatencyChartsFetcher() { params: { path: { serviceName }, query: { + environment, start, end, transactionType, @@ -49,6 +56,7 @@ export function useTransactionLatencyChartsFetcher() { } }, [ + environment, serviceName, start, end, diff --git a/x-pack/plugins/apm/public/hooks/use_transaction_throughput_chart_fetcher.ts b/x-pack/plugins/apm/public/hooks/use_transaction_throughput_chart_fetcher.ts index af9a5fee24877a..55765cd40c04eb 100644 --- a/x-pack/plugins/apm/public/hooks/use_transaction_throughput_chart_fetcher.ts +++ b/x-pack/plugins/apm/public/hooks/use_transaction_throughput_chart_fetcher.ts @@ -18,7 +18,7 @@ export function useTransactionThroughputChartsFetcher() { const { transactionType } = useApmServiceContext(); const theme = useTheme(); const { - urlParams: { start, end, transactionName }, + urlParams: { environment, start, end, transactionName }, uiFilters, } = useUrlParams(); @@ -31,6 +31,7 @@ export function useTransactionThroughputChartsFetcher() { params: { path: { serviceName }, query: { + environment, start, end, transactionType, @@ -41,7 +42,15 @@ export function useTransactionThroughputChartsFetcher() { }); } }, - [serviceName, start, end, transactionName, transactionType, uiFilters] + [ + environment, + serviceName, + start, + end, + transactionName, + transactionType, + uiFilters, + ] ); const memoizedData = useMemo( diff --git a/x-pack/plugins/apm/public/utils/testHelpers.tsx b/x-pack/plugins/apm/public/utils/testHelpers.tsx index aac6196c4253c5..f7f6f7486091b7 100644 --- a/x-pack/plugins/apm/public/utils/testHelpers.tsx +++ b/x-pack/plugins/apm/public/utils/testHelpers.tsx @@ -182,8 +182,8 @@ export async function inspectSearchParams( }, } ) as APMConfig, - uiFilters: { environment: 'test' }, - esFilter: [{ term: { 'service.environment': 'test' } }], + uiFilters: {}, + esFilter: [], indices: { /* eslint-disable @typescript-eslint/naming-convention */ 'apm_oss.sourcemapIndices': 'myIndex', diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js b/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js index f466b7ff72c49f..ae941c1d2de0c9 100644 --- a/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js +++ b/x-pack/plugins/apm/scripts/optimize-tsconfig/optimize.js @@ -16,6 +16,7 @@ const { omit } = require('lodash'); const readFile = promisify(fs.readFile); const writeFile = promisify(fs.writeFile); +const unlink = promisify(fs.unlink); const { xpackRoot, @@ -72,6 +73,10 @@ async function setIgnoreChanges() { } } +async function deleteApmTsConfig() { + await unlink(path.resolve(kibanaRoot, 'x-pack/plugins/apm', 'tsconfig.json')); +} + async function optimizeTsConfig() { await unoptimizeTsConfig(); @@ -79,6 +84,8 @@ async function optimizeTsConfig() { await addApmFilesToXpackTsConfig(); + await deleteApmTsConfig(); + await setIgnoreChanges(); // eslint-disable-next-line no-console console.log( diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js b/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js index 9788de37bdb33c..d697c073fa17a0 100644 --- a/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js +++ b/x-pack/plugins/apm/scripts/optimize-tsconfig/paths.js @@ -16,6 +16,7 @@ const filesToIgnore = [ path.resolve(xpackRoot, 'tsconfig.json'), path.resolve(kibanaRoot, 'tsconfig.json'), path.resolve(kibanaRoot, 'tsconfig.base.json'), + path.resolve(kibanaRoot, 'x-pack/plugins/apm', 'tsconfig.json'), ]; module.exports = { diff --git a/x-pack/plugins/apm/scripts/precommit.js b/x-pack/plugins/apm/scripts/precommit.js index c741be6c0e9a6b..42fe0734d9160d 100644 --- a/x-pack/plugins/apm/scripts/precommit.js +++ b/x-pack/plugins/apm/scripts/precommit.js @@ -11,10 +11,24 @@ const execa = require('execa'); const Listr = require('listr'); const { resolve } = require('path'); +const { argv } = require('yargs'); -const cwd = resolve(__dirname, '../../../..'); +const root = resolve(__dirname, '../../../..'); -const execaOpts = { cwd, stderr: 'inherit' }; +const execaOpts = { cwd: root, stderr: 'pipe' }; + +const useOptimizedTsConfig = !!argv.optimizeTs; + +const tsconfig = useOptimizedTsConfig + ? resolve(root, 'x-pack/tsconfig.json') + : resolve(root, 'x-pack/plugins/apm/tsconfig.json'); + +console.log( + resolve( + __dirname, + useOptimizedTsConfig ? './optimize-tsonfig.js' : './unoptimize-tsconfig.js' + ) +); const tasks = new Listr( [ @@ -37,9 +51,27 @@ const tasks = new Listr( title: 'Typescript', task: () => execa( - require.resolve('typescript/bin/tsc'), - ['--project', resolve(__dirname, '../tsconfig.json'), '--pretty'], + 'node', + [ + resolve( + __dirname, + useOptimizedTsConfig + ? './optimize-tsconfig.js' + : './unoptimize-tsconfig.js' + ), + ], execaOpts + ).then(() => + execa( + require.resolve('typescript/bin/tsc'), + [ + '--project', + tsconfig, + '--pretty', + ...(useOptimizedTsConfig ? ['--noEmit'] : []), + ], + execaOpts + ) ), }, { @@ -47,7 +79,7 @@ const tasks = new Listr( task: () => execa('node', [resolve(__dirname, 'eslint.js')], execaOpts), }, ], - { exitOnError: false, concurrent: true } + { exitOnError: true, concurrent: true } ); tasks.run().catch((error) => { diff --git a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts index e487684909633e..3457207eeee3c4 100644 --- a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts +++ b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts @@ -13,10 +13,9 @@ import { TRANSACTION_TYPE, } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; -import { rangeFilter } from '../../../../common/utils/range_filter'; +import { environmentQuery, rangeQuery } from '../../../../common/utils/queries'; import { AlertParams } from '../../../routes/alerts/chart_preview'; import { withApmSpan } from '../../../utils/with_apm_span'; -import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; @@ -39,13 +38,13 @@ export function getTransactionDurationChartPreview({ const query = { bool: { filter: [ - { range: rangeFilter(start, end) }, { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, ...(serviceName ? [{ term: { [SERVICE_NAME]: serviceName } }] : []), ...(transactionType ? [{ term: { [TRANSACTION_TYPE]: transactionType } }] : []), - ...getEnvironmentUiFilterES(environment), + ...rangeQuery(start, end), + ...environmentQuery(environment), ], }, }; diff --git a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts index 05ad743af0997b..aa85c44284d9d4 100644 --- a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts +++ b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts @@ -7,10 +7,9 @@ import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; -import { rangeFilter } from '../../../../common/utils/range_filter'; import { AlertParams } from '../../../routes/alerts/chart_preview'; +import { environmentQuery, rangeQuery } from '../../../../common/utils/queries'; import { withApmSpan } from '../../../utils/with_apm_span'; -import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; @@ -28,9 +27,9 @@ export function getTransactionErrorCountChartPreview({ const query = { bool: { filter: [ - { range: rangeFilter(start, end) }, ...(serviceName ? [{ term: { [SERVICE_NAME]: serviceName } }] : []), - ...getEnvironmentUiFilterES(environment), + ...rangeQuery(start, end), + ...environmentQuery(environment), ], }, }; diff --git a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts index fa01773c780703..88e249a71a81f0 100644 --- a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts @@ -11,9 +11,8 @@ import { TRANSACTION_TYPE, } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; -import { rangeFilter } from '../../../../common/utils/range_filter'; import { AlertParams } from '../../../routes/alerts/chart_preview'; -import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { environmentQuery, rangeQuery } from '../../../../common/utils/queries'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { @@ -34,13 +33,13 @@ export async function getTransactionErrorRateChartPreview({ const query = { bool: { filter: [ - { range: rangeFilter(start, end) }, { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, ...(serviceName ? [{ term: { [SERVICE_NAME]: serviceName } }] : []), ...(transactionType ? [{ term: { [TRANSACTION_TYPE]: transactionType } }] : []), - ...getEnvironmentUiFilterES(environment), + ...rangeQuery(start, end), + ...environmentQuery(environment), ], }, }; diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts index e9e2e078ec344e..c7861eaa819aed 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts @@ -27,7 +27,7 @@ import { SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; -import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { environmentQuery } from '../../../common/utils/queries'; import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from './action_variables'; import { alertingEsClient } from './alerting_es_client'; @@ -104,7 +104,7 @@ export function registerErrorCountAlertType({ ...(alertParams.serviceName ? [{ term: { [SERVICE_NAME]: alertParams.serviceName } }] : []), - ...getEnvironmentUiFilterES(alertParams.environment), + ...environmentQuery(alertParams.environment), ], }, }, diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts index 480efd8d4c7ad0..704aee932a604f 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts @@ -20,7 +20,7 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; import { getDurationFormatter } from '../../../common/utils/formatters'; -import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { environmentQuery } from '../../../common/utils/queries'; import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from './action_variables'; import { alertingEsClient } from './alerting_es_client'; @@ -96,7 +96,7 @@ export function registerTransactionDurationAlertType({ { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { term: { [SERVICE_NAME]: alertParams.serviceName } }, { term: { [TRANSACTION_TYPE]: alertParams.transactionType } }, - ...getEnvironmentUiFilterES(alertParams.environment), + ...environmentQuery(alertParams.environment), ], }, }, diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts index 882bde87927615..6f58b7714d8324 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts @@ -22,7 +22,7 @@ import { import { EventOutcome } from '../../../common/event_outcome'; import { ProcessorEvent } from '../../../common/processor_event'; import { asDecimalOrInteger } from '../../../common/utils/formatters'; -import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { environmentQuery } from '../../../common/utils/queries'; import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from './action_variables'; import { alertingEsClient } from './alerting_es_client'; @@ -103,7 +103,7 @@ export function registerTransactionErrorRateAlertType({ }, ] : []), - ...getEnvironmentUiFilterES(alertParams.environment), + ...environmentQuery(alertParams.environment), ], }, }, diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts index a560f011b186bb..d70e19bf4a5f52 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts @@ -9,15 +9,15 @@ import { Logger } from 'kibana/server'; import uuid from 'uuid/v4'; import { snakeCase } from 'lodash'; import Boom from '@hapi/boom'; -import { ProcessorEvent } from '../../../common/processor_event'; import { ML_ERRORS } from '../../../common/anomaly_detection'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { environmentQuery } from '../../../common/utils/queries'; import { Setup } from '../helpers/setup_request'; import { TRANSACTION_DURATION, PROCESSOR_EVENT, } from '../../../common/elasticsearch_fieldnames'; import { APM_ML_JOB_GROUP, ML_MODULE_ID_APM_TRANSACTION } from './constants'; -import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; import { withApmSpan } from '../../utils/with_apm_span'; export async function createAnomalyDetectionJobs( @@ -86,7 +86,7 @@ async function createAnomalyDetectionJob({ filter: [ { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { exists: { field: TRANSACTION_DURATION } }, - ...getEnvironmentUiFilterES(environment), + ...environmentQuery(environment), ], }, }, diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts index ecebf5b5715a18..ecefdfc2b3d9b2 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts +++ b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_failed_transactions/index.ts @@ -13,12 +13,13 @@ import { } from '../process_significant_term_aggs'; import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch/aggregations'; import { ESFilter } from '../../../../../../typings/elasticsearch'; -import { rangeFilter } from '../../../../common/utils/range_filter'; +import { environmentQuery, rangeQuery } from '../../../../common/utils/queries'; import { EVENT_OUTCOME, SERVICE_NAME, TRANSACTION_NAME, TRANSACTION_TYPE, + PROCESSOR_EVENT, } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; @@ -30,12 +31,14 @@ import { import { withApmSpan } from '../../../utils/with_apm_span'; export async function getCorrelationsForFailedTransactions({ + environment, serviceName, transactionType, transactionName, fieldNames, setup, }: { + environment?: string; serviceName: string | undefined; transactionType: string | undefined; transactionName: string | undefined; @@ -46,8 +49,10 @@ export async function getCorrelationsForFailedTransactions({ const { start, end, esFilter, apmEventClient } = setup; const backgroundFilters: ESFilter[] = [ + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), ...esFilter, - { range: rangeFilter(start, end) }, ]; if (serviceName) { @@ -82,7 +87,14 @@ export async function getCorrelationsForFailedTransactions({ significant_terms: { size: 10, field: fieldName, - background_filter: { bool: { filter: backgroundFilters } }, + background_filter: { + bool: { + filter: backgroundFilters, + must_not: { + term: { [EVENT_OUTCOME]: EventOutcome.failure }, + }, + }, + }, }, }, }; @@ -97,19 +109,12 @@ export async function getCorrelationsForFailedTransactions({ return {}; } - const failedTransactionCount = - response.aggregations?.failed_transactions.doc_count; - const totalTransactionCount = response.hits.total.value; - const avgErrorRate = (failedTransactionCount / totalTransactionCount) * 100; const sigTermAggs = omit( response.aggregations?.failed_transactions, 'doc_count' ); - const topSigTerms = processSignificantTermAggs({ - sigTermAggs, - thresholdPercentage: avgErrorRate, - }); + const topSigTerms = processSignificantTermAggs({ sigTermAggs }); return getErrorRateTimeSeries({ setup, backgroundFilters, topSigTerms }); }); } @@ -125,7 +130,7 @@ export async function getErrorRateTimeSeries({ }) { return withApmSpan('get_error_rate_timeseries', async () => { const { start, end, apmEventClient } = setup; - const { intervalString } = getBucketSize({ start, end, numBuckets: 30 }); + const { intervalString } = getBucketSize({ start, end, numBuckets: 15 }); if (isEmpty(topSigTerms)) { return {}; diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_latency_distribution.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_latency_distribution.ts index 6f7bd9537aa73b..eab09e814c18db 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_latency_distribution.ts +++ b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/get_latency_distribution.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { isEmpty } from 'lodash'; +import { isEmpty, dropRightWhile } from 'lodash'; import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch/aggregations'; import { ESFilter } from '../../../../../../typings/elasticsearch'; import { TRANSACTION_DURATION } from '../../../../common/elasticsearch_fieldnames'; @@ -41,8 +41,8 @@ export async function getLatencyDistribution({ return {}; } - const intervalBuckets = 20; - const distributionInterval = roundtoTenth(maxLatency / intervalBuckets); + const intervalBuckets = 15; + const distributionInterval = Math.floor(maxLatency / intervalBuckets); const distributionAgg = { // filter out outliers not included in the significant term docs @@ -111,7 +111,14 @@ export async function getLatencyDistribution({ function formatDistribution(distribution: Agg['distribution']) { const total = distribution.doc_count; - return distribution.dist_filtered_by_latency.buckets.map((bucket) => ({ + + // remove trailing buckets that are empty and out of bounds of the desired number of buckets + const buckets = dropRightWhile( + distribution.dist_filtered_by_latency.buckets, + (bucket, index) => bucket.doc_count === 0 && index > intervalBuckets - 1 + ); + + return buckets.map((bucket) => ({ x: bucket.key, y: (bucket.doc_count / total) * 100, })); @@ -134,7 +141,3 @@ export async function getLatencyDistribution({ }; }); } - -function roundtoTenth(v: number) { - return Math.pow(10, Math.round(Math.log10(v))); -} diff --git a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts index 0088b6ae7bb7bc..832b89a18d1028 100644 --- a/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts +++ b/x-pack/plugins/apm/server/lib/correlations/get_correlations_for_slow_transactions/index.ts @@ -7,12 +7,13 @@ import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch/aggregations'; import { ESFilter } from '../../../../../../typings/elasticsearch'; -import { rangeFilter } from '../../../../common/utils/range_filter'; +import { environmentQuery, rangeQuery } from '../../../../common/utils/queries'; import { SERVICE_NAME, TRANSACTION_DURATION, TRANSACTION_NAME, TRANSACTION_TYPE, + PROCESSOR_EVENT, } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; @@ -22,6 +23,7 @@ import { getLatencyDistribution } from './get_latency_distribution'; import { withApmSpan } from '../../../utils/with_apm_span'; export async function getCorrelationsForSlowTransactions({ + environment, serviceName, transactionType, transactionName, @@ -29,6 +31,7 @@ export async function getCorrelationsForSlowTransactions({ fieldNames, setup, }: { + environment?: string; serviceName: string | undefined; transactionType: string | undefined; transactionName: string | undefined; @@ -40,8 +43,10 @@ export async function getCorrelationsForSlowTransactions({ const { start, end, esFilter, apmEventClient } = setup; const backgroundFilters: ESFilter[] = [ + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), ...esFilter, - { range: rangeFilter(start, end) }, ]; if (serviceName) { @@ -70,14 +75,21 @@ export async function getCorrelationsForSlowTransactions({ query: { bool: { // foreground filters - filter: [ - ...backgroundFilters, - { - range: { - [TRANSACTION_DURATION]: { gte: durationForPercentile }, + filter: backgroundFilters, + must: { + function_score: { + query: { + range: { + [TRANSACTION_DURATION]: { gte: durationForPercentile }, + }, + }, + script_score: { + script: { + source: `Math.log(2 + doc['${TRANSACTION_DURATION}'].value)`, + }, }, }, - ], + }, }, }, aggs: fieldNames.reduce((acc, fieldName) => { @@ -87,7 +99,20 @@ export async function getCorrelationsForSlowTransactions({ significant_terms: { size: 10, field: fieldName, - background_filter: { bool: { filter: backgroundFilters } }, + background_filter: { + bool: { + filter: [ + ...backgroundFilters, + { + range: { + [TRANSACTION_DURATION]: { + lt: durationForPercentile, + }, + }, + }, + ], + }, + }, }, }, }; @@ -102,7 +127,6 @@ export async function getCorrelationsForSlowTransactions({ const topSigTerms = processSignificantTermAggs({ sigTermAggs: response.aggregations, - thresholdPercentage: 100 - durationPercentile, }); return getLatencyDistribution({ diff --git a/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts b/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts index 6bfc2ab2890b8a..1fe50c869f5bf3 100644 --- a/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts +++ b/x-pack/plugins/apm/server/lib/correlations/process_significant_term_aggs.ts @@ -12,11 +12,12 @@ import { } from '../../../../../typings/elasticsearch/aggregations'; export interface TopSigTerm { - bgCount: number; - fgCount: number; fieldName: string; fieldValue: string | number; score: number; + impact: number; + fieldCount: number; + valueCount: number; } type SigTermAgg = AggregationResultOf< @@ -24,31 +25,52 @@ type SigTermAgg = AggregationResultOf< {} >; +function getMaxImpactScore(scores: number[]) { + if (scores.length === 0) { + return 0; + } + + const sortedScores = scores.sort((a, b) => b - a); + const maxScore = sortedScores[0]; + + // calculate median + const halfSize = scores.length / 2; + const medianIndex = Math.floor(halfSize); + const medianScore = + medianIndex < halfSize + ? sortedScores[medianIndex] + : (sortedScores[medianIndex - 1] + sortedScores[medianIndex]) / 2; + + return Math.max(maxScore, medianScore * 2); +} + export function processSignificantTermAggs({ sigTermAggs, - thresholdPercentage, }: { sigTermAggs: Record; - thresholdPercentage: number; }) { const significantTerms = Object.entries(sigTermAggs).flatMap( ([fieldName, agg]) => { return agg.buckets.map((bucket) => ({ fieldName, fieldValue: bucket.key, - bgCount: bucket.bg_count, - fgCount: bucket.doc_count, + fieldCount: agg.doc_count, + valueCount: bucket.doc_count, score: bucket.score, })); } ); + const maxImpactScore = getMaxImpactScore( + significantTerms.map(({ score }) => score) + ); + // get top 10 terms ordered by score const topSigTerms = orderBy(significantTerms, 'score', 'desc') - .filter(({ bgCount, fgCount }) => { - // only include results that are above the threshold - return Math.floor((fgCount / bgCount) * 100) > thresholdPercentage; - }) + .map((significantTerm) => ({ + ...significantTerm, + impact: significantTerm.score / maxImpactScore, + })) .slice(0, 10); return topSigTerms; } diff --git a/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_environments.test.ts.snap similarity index 92% rename from x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap rename to x-pack/plugins/apm/server/lib/environments/__snapshots__/get_environments.test.ts.snap index 3baaefe203ce75..a244eee3d0544c 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_environments.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ui filter queries fetches environments 1`] = ` +exports[`getEnvironments fetches environments 1`] = ` Object { "apm": Object { "events": Array [ @@ -44,7 +44,7 @@ Object { } `; -exports[`ui filter queries fetches environments without a service name 1`] = ` +exports[`getEnvironments fetches environments without a service name 1`] = ` Object { "apm": Object { "events": Array [ diff --git a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts index 112700d0b65833..1bf01c24776fb0 100644 --- a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts @@ -15,6 +15,10 @@ import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_valu import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; import { withApmSpan } from '../../utils/with_apm_span'; +/** + * This is used for getting *all* environments, and does not filter by range. + * It's used in places where we get the list of all possible environments. + */ export async function getAllEnvironments({ serviceName, setup, @@ -27,8 +31,8 @@ export async function getAllEnvironments({ includeMissing?: boolean; }) { const spanName = serviceName - ? 'get_all_environments_for_all_services' - : 'get_all_environments_for_service'; + ? 'get_all_environments_for_service' + : 'get_all_environments_for_all_services'; return withApmSpan(spanName, async () => { const { apmEventClient, config } = setup; const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments']; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/queries.test.ts b/x-pack/plugins/apm/server/lib/environments/get_environments.test.ts similarity index 96% rename from x-pack/plugins/apm/server/lib/ui_filters/queries.test.ts rename to x-pack/plugins/apm/server/lib/environments/get_environments.test.ts index 4a7d2029463e51..53292cabfc3389 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_environments.test.ts @@ -11,7 +11,7 @@ import { inspectSearchParams, } from '../../utils/test_helpers'; -describe('ui filter queries', () => { +describe('getEnvironments', () => { let mock: SearchParamsMock; afterEach(() => { diff --git a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts b/x-pack/plugins/apm/server/lib/environments/get_environments.ts similarity index 91% rename from x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts rename to x-pack/plugins/apm/server/lib/environments/get_environments.ts index dc7def06259331..af88493c148ce1 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/get_environments.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_environments.ts @@ -5,18 +5,21 @@ * 2.0. */ -import { ESFilter } from '../../../../../typings/elasticsearch'; import { SERVICE_ENVIRONMENT, SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values'; import { ProcessorEvent } from '../../../common/processor_event'; -import { rangeFilter } from '../../../common/utils/range_filter'; +import { rangeQuery } from '../../../common/utils/queries'; import { withApmSpan } from '../../utils/with_apm_span'; import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; +/** + * This is used for getting the list of environments for the environments selector, + * filtered by range. + */ export async function getEnvironments({ setup, serviceName, @@ -33,7 +36,7 @@ export async function getEnvironments({ return withApmSpan(spanName, async () => { const { start, end, apmEventClient, config } = setup; - const filter: ESFilter[] = [{ range: rangeFilter(start, end) }]; + const filter = rangeQuery(start, end); if (serviceName) { filter.push({ diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts index db8414864f5771..1712699162b73c 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts @@ -25,6 +25,7 @@ describe('get buckets', () => { }); await getBuckets({ + environment: 'prod', serviceName: 'myServiceName', bucketSize: 10, setup: { @@ -42,14 +43,8 @@ describe('get buckets', () => { get: () => 'myIndex', } ) as APMConfig, - uiFilters: { - environment: 'prod', - }, - esFilter: [ - { - term: { 'service.environment': 'prod' }, - }, - ], + uiFilters: {}, + esFilter: [], indices: { /* eslint-disable @typescript-eslint/naming-convention */ 'apm_oss.sourcemapIndices': 'apm-*', diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts index 383fcbb2f5ce72..fbe406d8d1a9d2 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts @@ -11,16 +11,18 @@ import { SERVICE_NAME, } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; -import { rangeFilter } from '../../../../common/utils/range_filter'; +import { environmentQuery, rangeQuery } from '../../../../common/utils/queries'; import { withApmSpan } from '../../../utils/with_apm_span'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; export async function getBuckets({ + environment, serviceName, groupId, bucketSize, setup, }: { + environment?: string; serviceName: string; groupId?: string; bucketSize: number; @@ -30,7 +32,8 @@ export async function getBuckets({ const { start, end, esFilter, apmEventClient } = setup; const filter: ESFilter[] = [ { term: { [SERVICE_NAME]: serviceName } }, - { range: rangeFilter(start, end) }, + ...rangeQuery(start, end), + ...environmentQuery(environment), ...esFilter, ]; diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts index be3a29780a5b65..1fb0cbad4a5f09 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_distribution.ts @@ -14,16 +14,19 @@ function getBucketSize({ start, end }: SetupTimeRange) { } export async function getErrorDistribution({ + environment, serviceName, groupId, setup, }: { + environment?: string; serviceName: string; groupId?: string; setup: Setup & SetupTimeRange; }) { const bucketSize = getBucketSize({ start: setup.start, end: setup.end }); const { buckets, noHits } = await getBuckets({ + environment, serviceName, groupId, bucketSize, diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_group_sample.ts b/x-pack/plugins/apm/server/lib/errors/get_error_group_sample.ts index 121b9b3d0c46fb..0ab26f3c6e969d 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_group_sample.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_group_sample.ts @@ -11,16 +11,18 @@ import { TRANSACTION_SAMPLED, } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; -import { rangeFilter } from '../../../common/utils/range_filter'; +import { environmentQuery, rangeQuery } from '../../../common/utils/queries'; import { withApmSpan } from '../../utils/with_apm_span'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { getTransaction } from '../transactions/get_transaction'; export function getErrorGroupSample({ + environment, serviceName, groupId, setup, }: { + environment?: string; serviceName: string; groupId: string; setup: Setup & SetupTimeRange; @@ -39,7 +41,8 @@ export function getErrorGroupSample({ filter: [ { term: { [SERVICE_NAME]: serviceName } }, { term: { [ERROR_GROUP_ID]: groupId } }, - { range: rangeFilter(start, end) }, + ...rangeQuery(start, end), + ...environmentQuery(environment), ...esFilter, ], should: [{ term: { [TRANSACTION_SAMPLED]: true } }], diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts index 6e91f8fe7cdd28..28d89eb0574709 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts +++ b/x-pack/plugins/apm/server/lib/errors/get_error_groups.ts @@ -21,11 +21,13 @@ import { getErrorName } from '../helpers/get_error_name'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; export function getErrorGroups({ + environment, serviceName, sortField, sortDirection = 'desc', setup, }: { + environment?: string; serviceName: string; sortField?: string; sortDirection?: 'asc' | 'desc'; @@ -37,7 +39,11 @@ export function getErrorGroups({ // sort buckets by last occurrence of error const sortByLatestOccurrence = sortField === 'latestOccurrenceAt'; - const projection = getErrorGroupsProjection({ setup, serviceName }); + const projection = getErrorGroupsProjection({ + environment, + setup, + serviceName, + }); const order: SortOptions = sortByLatestOccurrence ? { diff --git a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts index 424c85d55a36e1..71744c3e590929 100644 --- a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts @@ -6,7 +6,7 @@ */ import { SearchAggregatedTransactionSetting } from '../../../../common/aggregated_transactions'; -import { rangeFilter } from '../../../../common/utils/range_filter'; +import { rangeQuery } from '../../../../common/utils/queries'; import { ProcessorEvent } from '../../../../common/processor_event'; import { TRANSACTION_DURATION, @@ -35,7 +35,7 @@ export async function getHasAggregatedTransactions({ bool: { filter: [ { exists: { field: TRANSACTION_DURATION_HISTOGRAM } }, - ...(start && end ? [{ range: rangeFilter(start, end) }] : []), + ...(start && end ? rangeQuery(start, end) : []), ], }, }, diff --git a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.test.ts b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.test.ts deleted file mode 100644 index 57bf511f459428..00000000000000 --- a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.test.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getEnvironmentUiFilterES } from './get_environment_ui_filter_es'; -import { ENVIRONMENT_NOT_DEFINED } from '../../../../common/environment_filter_values'; -import { SERVICE_ENVIRONMENT } from '../../../../common/elasticsearch_fieldnames'; - -describe('getEnvironmentUiFilterES', () => { - it('should return empty array, when environment is undefined', () => { - const uiFilterES = getEnvironmentUiFilterES(); - expect(uiFilterES).toHaveLength(0); - }); - - it('should create a filter for a service environment', () => { - const uiFilterES = getEnvironmentUiFilterES('test'); - expect(uiFilterES).toHaveLength(1); - expect(uiFilterES[0]).toHaveProperty(['term', SERVICE_ENVIRONMENT], 'test'); - }); - - it('should create a filter for missing service environments', () => { - const uiFilterES = getEnvironmentUiFilterES(ENVIRONMENT_NOT_DEFINED.value); - expect(uiFilterES).toHaveLength(1); - expect(uiFilterES[0]).toHaveProperty( - ['bool', 'must_not', 'exists', 'field'], - SERVICE_ENVIRONMENT - ); - }); -}); diff --git a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_es_filter.ts b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_es_filter.ts index 7d8bc59e61124a..e91c9b52deecf1 100644 --- a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_es_filter.ts +++ b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_es_filter.ts @@ -7,11 +7,10 @@ import { ESFilter } from '../../../../../../typings/elasticsearch'; import { UIFilters } from '../../../../typings/ui_filters'; -import { getEnvironmentUiFilterES } from './get_environment_ui_filter_es'; import { localUIFilters, localUIFilterNames, -} from '../../ui_filters/local_ui_filters/config'; +} from '../../rum_client/ui_filters/local_ui_filters/config'; import { esKuery } from '../../../../../../../src/plugins/data/server'; export function getEsFilter(uiFilters: UIFilters) { @@ -28,10 +27,7 @@ export function getEsFilter(uiFilters: UIFilters) { }; }) as ESFilter[]; - const esFilters = [ - ...getKueryUiFilterES(uiFilters.kuery), - ...getEnvironmentUiFilterES(uiFilters.environment), - ].concat(mappedFilters) as ESFilter[]; + const esFilters = [...getKueryUiFilterES(uiFilters.kuery), ...mappedFilters]; return esFilters; } diff --git a/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap index 961a1eee61d1d5..4eed09f3e5c28b 100644 --- a/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap @@ -71,6 +71,11 @@ Object { "service.name": "foo", }, }, + Object { + "term": Object { + "service.node.name": "bar", + }, + }, Object { "range": Object { "@timestamp": Object { @@ -80,11 +85,6 @@ Object { }, }, }, - Object { - "term": Object { - "service.node.name": "bar", - }, - }, Object { "term": Object { "service.environment": "test", @@ -159,6 +159,11 @@ Object { "service.name": "foo", }, }, + Object { + "term": Object { + "service.node.name": "bar", + }, + }, Object { "range": Object { "@timestamp": Object { @@ -168,11 +173,6 @@ Object { }, }, }, - Object { - "term": Object { - "service.node.name": "bar", - }, - }, Object { "term": Object { "service.environment": "test", @@ -322,6 +322,11 @@ Object { "service.name": "foo", }, }, + Object { + "term": Object { + "service.node.name": "bar", + }, + }, Object { "range": Object { "@timestamp": Object { @@ -331,11 +336,6 @@ Object { }, }, }, - Object { - "term": Object { - "service.node.name": "bar", - }, - }, Object { "term": Object { "service.environment": "test", @@ -415,6 +415,11 @@ Object { "service.name": "foo", }, }, + Object { + "term": Object { + "service.node.name": "bar", + }, + }, Object { "range": Object { "@timestamp": Object { @@ -424,11 +429,6 @@ Object { }, }, }, - Object { - "term": Object { - "service.node.name": "bar", - }, - }, Object { "term": Object { "service.environment": "test", @@ -498,6 +498,11 @@ Object { "service.name": "foo", }, }, + Object { + "term": Object { + "service.node.name": "bar", + }, + }, Object { "range": Object { "@timestamp": Object { @@ -507,11 +512,6 @@ Object { }, }, }, - Object { - "term": Object { - "service.node.name": "bar", - }, - }, Object { "term": Object { "service.environment": "test", @@ -601,15 +601,6 @@ Object { "service.name": "foo", }, }, - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, - }, - }, - }, Object { "bool": Object { "must_not": Array [ @@ -621,6 +612,15 @@ Object { ], }, }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, Object { "term": Object { "service.environment": "test", @@ -695,15 +695,6 @@ Object { "service.name": "foo", }, }, - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, - }, - }, - }, Object { "bool": Object { "must_not": Array [ @@ -715,6 +706,15 @@ Object { ], }, }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, Object { "term": Object { "service.environment": "test", @@ -864,15 +864,6 @@ Object { "service.name": "foo", }, }, - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, - }, - }, - }, Object { "bool": Object { "must_not": Array [ @@ -884,6 +875,15 @@ Object { ], }, }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, Object { "term": Object { "service.environment": "test", @@ -963,15 +963,6 @@ Object { "service.name": "foo", }, }, - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, - }, - }, - }, Object { "bool": Object { "must_not": Array [ @@ -983,6 +974,15 @@ Object { ], }, }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, Object { "term": Object { "service.environment": "test", @@ -1052,15 +1052,6 @@ Object { "service.name": "foo", }, }, - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, - }, - }, - }, Object { "bool": Object { "must_not": Array [ @@ -1072,6 +1063,15 @@ Object { ], }, }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, Object { "term": Object { "service.environment": "test", diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/default.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/default.ts index e03be2391597d4..c5e80600b69d43 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/default.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/default.ts @@ -9,13 +9,18 @@ import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { getCPUChartData } from './shared/cpu'; import { getMemoryChartData } from './shared/memory'; -export async function getDefaultMetricsCharts( - setup: Setup & SetupTimeRange, - serviceName: string -) { +export async function getDefaultMetricsCharts({ + environment, + serviceName, + setup, +}: { + environment?: string; + serviceName: string; + setup: Setup & SetupTimeRange; +}) { const charts = await Promise.all([ - getCPUChartData({ setup, serviceName }), - getMemoryChartData({ setup, serviceName }), + getCPUChartData({ environment, setup, serviceName }), + getMemoryChartData({ environment, setup, serviceName }), ]); return { charts }; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts index e76ad10b535b09..d7c9294c8ec7a1 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts @@ -22,12 +22,14 @@ import { getBucketSize } from '../../../../helpers/get_bucket_size'; import { getVizColorForIndex } from '../../../../../../common/viz_colors'; export async function fetchAndTransformGcMetrics({ + environment, setup, serviceName, serviceNodeName, chartBase, fieldName, }: { + environment?: string; setup: Setup & SetupTimeRange; serviceName: string; serviceNodeName?: string; @@ -39,6 +41,7 @@ export async function fetchAndTransformGcMetrics({ const { bucketSize } = getBucketSize({ start, end }); const projection = getMetricsProjection({ + environment, setup, serviceName, serviceNodeName, diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_rate_chart.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_rate_chart.ts index 7989f57046ae78..8c5b9fb3db9221 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_rate_chart.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_rate_chart.ts @@ -33,16 +33,19 @@ const chartBase: ChartBase = { }; function getGcRateChart({ + environment, setup, serviceName, serviceNodeName, }: { + environment?: string; setup: Setup & SetupTimeRange; serviceName: string; serviceNodeName?: string; }) { return withApmSpan('get_gc_rate_charts', () => fetchAndTransformGcMetrics({ + environment, setup, serviceName, serviceNodeName, diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_time_chart.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_time_chart.ts index 446894f82b75e3..98f31f06c1b644 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_time_chart.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_time_chart.ts @@ -33,16 +33,19 @@ const chartBase: ChartBase = { }; function getGcTimeChart({ + environment, setup, serviceName, serviceNodeName, }: { + environment?: string; setup: Setup & SetupTimeRange; serviceName: string; serviceNodeName?: string; }) { return withApmSpan('get_gc_time_charts', () => fetchAndTransformGcMetrics({ + environment, setup, serviceName, serviceNodeName, diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts index 2b7bb9ea8da6ed..d6cbc4a07e8f9b 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts @@ -53,16 +53,19 @@ const chartBase: ChartBase = { }; export function getHeapMemoryChart({ + environment, setup, serviceName, serviceNodeName, }: { + environment?: string; setup: Setup & SetupTimeRange; serviceName: string; serviceNodeName?: string; }) { return withApmSpan('get_heap_memory_charts', () => fetchAndTransformMetrics({ + environment, setup, serviceName, serviceNodeName, diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/index.ts index e137720000262a..970b4d3499b798 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/index.ts @@ -16,23 +16,30 @@ import { getGcRateChart } from './gc/get_gc_rate_chart'; import { getGcTimeChart } from './gc/get_gc_time_chart'; export function getJavaMetricsCharts({ + environment, setup, serviceName, serviceNodeName, }: { + environment?: string; setup: Setup & SetupTimeRange; serviceName: string; serviceNodeName?: string; }) { return withApmSpan('get_java_system_metric_charts', async () => { const charts = await Promise.all([ - getCPUChartData({ setup, serviceName, serviceNodeName }), - getMemoryChartData({ setup, serviceName, serviceNodeName }), - getHeapMemoryChart({ setup, serviceName, serviceNodeName }), - getNonHeapMemoryChart({ setup, serviceName, serviceNodeName }), - getThreadCountChart({ setup, serviceName, serviceNodeName }), - getGcRateChart({ setup, serviceName, serviceNodeName }), - getGcTimeChart({ setup, serviceName, serviceNodeName }), + getCPUChartData({ environment, setup, serviceName, serviceNodeName }), + getMemoryChartData({ environment, setup, serviceName, serviceNodeName }), + getHeapMemoryChart({ environment, setup, serviceName, serviceNodeName }), + getNonHeapMemoryChart({ + environment, + setup, + serviceName, + serviceNodeName, + }), + getThreadCountChart({ environment, setup, serviceName, serviceNodeName }), + getGcRateChart({ environment, setup, serviceName, serviceNodeName }), + getGcTimeChart({ environment, setup, serviceName, serviceNodeName }), ]); return { charts }; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts index a3e253d2c81d67..25abd2c34c83a9 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts @@ -50,16 +50,19 @@ const chartBase: ChartBase = { }; export async function getNonHeapMemoryChart({ + environment, setup, serviceName, serviceNodeName, }: { + environment?: string; setup: Setup & SetupTimeRange; serviceName: string; serviceNodeName?: string; }) { return withApmSpan('get_non_heap_memory_charts', () => fetchAndTransformMetrics({ + environment, setup, serviceName, serviceNodeName, diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts index e176c156ad05a5..c8a209fee701a1 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts @@ -42,16 +42,19 @@ const chartBase: ChartBase = { }; export async function getThreadCountChart({ + environment, setup, serviceName, serviceNodeName, }: { + environment?: string; setup: Setup & SetupTimeRange; serviceName: string; serviceNodeName?: string; }) { return withApmSpan('get_thread_count_charts', () => fetchAndTransformMetrics({ + environment, setup, serviceName, serviceNodeName, diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts index e7f576b73c5aef..ebfe504e5269b7 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts @@ -54,16 +54,19 @@ const chartBase: ChartBase = { }; export function getCPUChartData({ + environment, setup, serviceName, serviceNodeName, }: { + environment?: string; setup: Setup & SetupTimeRange; serviceName: string; serviceNodeName?: string; }) { return withApmSpan('get_cpu_metric_charts', () => fetchAndTransformMetrics({ + environment, setup, serviceName, serviceNodeName, diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts index 0f7954d86d3e2c..55b3328bcd2a9b 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts @@ -71,10 +71,12 @@ export const percentCgroupMemoryUsedScript = { }; export async function getMemoryChartData({ + environment, setup, serviceName, serviceNodeName, }: { + environment?: string; setup: Setup & SetupTimeRange; serviceName: string; serviceNodeName?: string; @@ -84,6 +86,7 @@ export async function getMemoryChartData({ 'get_cgroup_memory_metrics_charts', () => fetchAndTransformMetrics({ + environment, setup, serviceName, serviceNodeName, @@ -101,6 +104,7 @@ export async function getMemoryChartData({ if (cgroupResponse.noHits) { return await withApmSpan('get_system_memory_metrics_charts', () => fetchAndTransformMetrics({ + environment, setup, serviceName, serviceNodeName, diff --git a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts index 7a52806601e0e8..17e9aef29ba82d 100644 --- a/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts @@ -48,6 +48,7 @@ interface Filter { } export async function fetchAndTransformMetrics({ + environment, setup, serviceName, serviceNodeName, @@ -55,6 +56,7 @@ export async function fetchAndTransformMetrics({ aggs, additionalFilters = [], }: { + environment?: string; setup: Setup & SetupTimeRange; serviceName: string; serviceNodeName?: string; @@ -65,6 +67,7 @@ export async function fetchAndTransformMetrics({ const { start, end, apmEventClient, config } = setup; const projection = getMetricsProjection({ + environment, setup, serviceName, serviceNodeName, diff --git a/x-pack/plugins/apm/server/lib/metrics/get_metrics_chart_data_by_agent.ts b/x-pack/plugins/apm/server/lib/metrics/get_metrics_chart_data_by_agent.ts index 5083982f1cb9cb..eda71ef380ee9c 100644 --- a/x-pack/plugins/apm/server/lib/metrics/get_metrics_chart_data_by_agent.ts +++ b/x-pack/plugins/apm/server/lib/metrics/get_metrics_chart_data_by_agent.ts @@ -15,11 +15,13 @@ export interface MetricsChartsByAgentAPIResponse { } export async function getMetricsChartDataByAgent({ + environment, setup, serviceName, serviceNodeName, agentName, }: { + environment?: string; setup: Setup & SetupTimeRange; serviceName: string; serviceNodeName?: string; @@ -27,11 +29,16 @@ export async function getMetricsChartDataByAgent({ }): Promise { switch (agentName) { case 'java': { - return getJavaMetricsCharts({ setup, serviceName, serviceNodeName }); + return getJavaMetricsCharts({ + environment, + setup, + serviceName, + serviceNodeName, + }); } default: { - return getDefaultMetricsCharts(setup, serviceName); + return getDefaultMetricsCharts({ environment, setup, serviceName }); } } } diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts index 4af57a685bf839..c7ac678899b58c 100644 --- a/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts +++ b/x-pack/plugins/apm/server/lib/observability_overview/get_service_count.ts @@ -6,7 +6,7 @@ */ import { ProcessorEvent } from '../../../common/processor_event'; -import { rangeFilter } from '../../../common/utils/range_filter'; +import { rangeQuery } from '../../../common/utils/queries'; import { SERVICE_NAME } from '../../../common/elasticsearch_fieldnames'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; @@ -36,7 +36,7 @@ export function getServiceCount({ size: 0, query: { bool: { - filter: [{ range: rangeFilter(start, end) }], + filter: rangeQuery(start, end), }, }, aggs: { serviceCount: { cardinality: { field: SERVICE_NAME } } }, diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts index 87394567afc50b..2da4b0f8de3632 100644 --- a/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts +++ b/x-pack/plugins/apm/server/lib/observability_overview/get_transaction_coordinates.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { rangeFilter } from '../../../common/utils/range_filter'; +import { rangeQuery } from '../../../common/utils/queries'; import { Coordinates } from '../../../../observability/typings/common'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; @@ -38,7 +38,7 @@ export function getTransactionCoordinates({ size: 0, query: { bool: { - filter: [{ range: rangeFilter(start, end) }], + filter: rangeQuery(start, end), }, }, aggs: { diff --git a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts index 368c5ec5463592..9626019347e5bb 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts @@ -11,7 +11,7 @@ import { TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; -import { rangeFilter } from '../../../common/utils/range_filter'; +import { rangeQuery } from '../../../common/utils/queries'; import { TRANSACTION_PAGE_LOAD } from '../../../common/transaction_types'; export async function hasRumData({ setup }: { setup: Setup & SetupTimeRange }) { @@ -31,9 +31,7 @@ export async function hasRumData({ setup }: { setup: Setup & SetupTimeRange }) { }, aggs: { services: { - filter: { - range: rangeFilter(start, end), - }, + filter: rangeQuery(start, end)[0], aggs: { mostTraffic: { terms: { diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/__snapshots__/index.test.ts.snap similarity index 93% rename from x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap rename to x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/__snapshots__/index.test.ts.snap index e7ca65eb740b6a..40504cec36a633 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/__snapshots__/index.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`local ui filter queries fetches local ui filter aggregations 1`] = ` +exports[`getLocalUIFilters fetches local ui filter aggregations 1`] = ` Object { "apm": Object { "events": Array [ diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/config.ts similarity index 89% rename from x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts rename to x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/config.ts index 27287ce80ca3ed..dfe3efe2aadfe3 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/config.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/config.ts @@ -5,7 +5,10 @@ * 2.0. */ -import { filtersByName, LocalUIFilterName } from '../../../../common/ui_filter'; +import { + filtersByName, + LocalUIFilterName, +} from '../../../../../common/ui_filter'; export interface LocalUIFilter { name: LocalUIFilterName; diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/get_local_filter_query.ts similarity index 79% rename from x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts rename to x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/get_local_filter_query.ts index 14b6eeb78c943f..8ea635467d0a15 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/get_local_filter_query.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/get_local_filter_query.ts @@ -6,12 +6,12 @@ */ import { omit } from 'lodash'; -import { mergeProjection } from '../../../projections/util/merge_projection'; -import { Projection } from '../../../projections/typings'; -import { UIFilters } from '../../../../typings/ui_filters'; -import { getEsFilter } from '../../helpers/convert_ui_filters/get_es_filter'; +import { mergeProjection } from '../../../../projections/util/merge_projection'; +import { Projection } from '../../../../projections/typings'; +import { UIFilters } from '../../../../../typings/ui_filters'; +import { getEsFilter } from '../../../helpers/convert_ui_filters/get_es_filter'; import { localUIFilters } from './config'; -import { LocalUIFilterName } from '../../../../common/ui_filter'; +import { LocalUIFilterName } from '../../../../../common/ui_filter'; export const getLocalFilterQuery = ({ uiFilters, diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/index.test.ts similarity index 77% rename from x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts rename to x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/index.test.ts index 4452a9a80d0389..7254bb25cc5fe1 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/index.test.ts @@ -5,18 +5,18 @@ * 2.0. */ -import { getLocalUIFilters } from './'; +import { getLocalUIFilters } from '.'; import { SearchParamsMock, inspectSearchParams, -} from '../../../utils/test_helpers'; -import { getServicesProjection } from '../../../projections/services'; +} from '../../../../utils/test_helpers'; +import { getServicesProjection } from '../../../../projections/services'; -describe('local ui filter queries', () => { +describe('getLocalUIFilters', () => { let mock: SearchParamsMock; beforeEach(() => { - jest.mock('../../helpers/convert_ui_filters/get_es_filter', () => { + jest.mock('../../../helpers/convert_ui_filters/get_es_filter', () => { return []; }); }); diff --git a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/index.ts similarity index 82% rename from x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts rename to x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/index.ts index 966f44158a7bbd..8fdeb77171862b 100644 --- a/x-pack/plugins/apm/server/lib/ui_filters/local_ui_filters/index.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/ui_filters/local_ui_filters/index.ts @@ -6,14 +6,14 @@ */ import { cloneDeep, orderBy } from 'lodash'; -import { UIFilters } from '../../../../typings/ui_filters'; -import { Projection } from '../../../projections/typings'; -import { PromiseReturnType } from '../../../../../observability/typings/common'; +import { UIFilters } from '../../../../../typings/ui_filters'; +import { Projection } from '../../../../projections/typings'; +import { PromiseReturnType } from '../../../../../../observability/typings/common'; import { getLocalFilterQuery } from './get_local_filter_query'; -import { Setup } from '../../helpers/setup_request'; +import { Setup } from '../../../helpers/setup_request'; import { localUIFilters } from './config'; -import { LocalUIFilterName } from '../../../../common/ui_filter'; -import { withApmSpan } from '../../../utils/with_apm_span'; +import { LocalUIFilterName } from '../../../../../common/ui_filter'; +import { withApmSpan } from '../../../../utils/with_apm_span'; export type LocalUIFiltersAPIResponse = PromiseReturnType< typeof getLocalUIFilters diff --git a/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts b/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts index 939ebbb1f79419..259a0e6daea6fc 100644 --- a/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/fetch_service_paths_from_trace_ids.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { rangeFilter } from '../../../common/utils/range_filter'; +import { rangeQuery } from '../../../common/utils/queries'; import { ProcessorEvent } from '../../../common/processor_event'; import { TRACE_ID } from '../../../common/elasticsearch_fieldnames'; import { @@ -42,7 +42,7 @@ export async function fetchServicePathsFromTraceIds( [TRACE_ID]: traceIds, }, }, - { range: rangeFilter(start, end) }, + ...rangeQuery(start, end), ], }, }, diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts index 2c64678eb082ec..ab221e30ea4897 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -17,6 +17,7 @@ import { TRANSACTION_PAGE_LOAD, TRANSACTION_REQUEST, } from '../../../common/transaction_types'; +import { rangeQuery } from '../../../common/utils/queries'; import { withApmSpan } from '../../utils/with_apm_span'; import { getMlJobsWithAPMGroup } from '../anomaly_detection/get_ml_jobs_with_apm_group'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; @@ -51,16 +52,11 @@ export async function getServiceAnomalies({ bool: { filter: [ { terms: { result_type: ['model_plot', 'record'] } }, - { - range: { - timestamp: { - // fetch data for at least 30 minutes - gte: Math.min(end - 30 * 60 * 1000, start), - lte: end, - format: 'epoch_millis', - }, - }, - }, + ...rangeQuery( + Math.min(end - 30 * 60 * 1000, start), + end, + 'timestamp' + ), { terms: { // Only retrieving anomalies for transaction types "request" and "page-load" diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index 951484308db195..1aee1bb5b242af 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -15,8 +15,8 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { getServicesProjection } from '../../projections/services'; import { mergeProjection } from '../../projections/util/merge_projection'; +import { environmentQuery } from '../../../common/utils/queries'; import { withApmSpan } from '../../utils/with_apm_span'; -import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { DEFAULT_ANOMALIES, @@ -88,14 +88,17 @@ async function getConnectionData({ async function getServicesData(options: IEnvOptions) { return withApmSpan('get_service_stats_for_service_map', async () => { - const { setup, searchAggregatedTransactions } = options; + const { environment, setup, searchAggregatedTransactions } = options; const projection = getServicesProjection({ setup: { ...setup, esFilter: [] }, searchAggregatedTransactions, }); - let { filter } = projection.body.query.bool; + let filter = [ + ...projection.body.query.bool.filter, + ...environmentQuery(environment), + ]; if (options.serviceName) { filter = filter.concat({ @@ -105,10 +108,6 @@ async function getServicesData(options: IEnvOptions) { }); } - if (options.environment) { - filter = filter.concat(getEnvironmentUiFilterES(options.environment)); - } - const params = mergeProjection(projection, { body: { size: 0, diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts index ef4faf94063461..6e9225041b199c 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_from_trace_ids.ts @@ -6,7 +6,10 @@ */ import { find, uniqBy } from 'lodash'; -import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values'; +import { + ENVIRONMENT_ALL, + ENVIRONMENT_NOT_DEFINED, +} from '../../../common/environment_filter_values'; import { SERVICE_ENVIRONMENT, SERVICE_NAME, @@ -27,8 +30,10 @@ export function getConnections({ if (!paths) { return []; } + const isEnvironmentSelected = + environment && environment !== ENVIRONMENT_ALL.value; - if (serviceName || environment) { + if (serviceName || isEnvironmentSelected) { paths = paths.filter((path) => { return ( path diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts index 63f28abab8f3aa..b161345e729d35 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts @@ -19,11 +19,13 @@ describe('getServiceMapServiceNodeInfo', () => { hits: { total: { value: 0 } }, }), }, + esFilter: [], indices: {}, - uiFilters: { environment: 'test environment' }, + uiFilters: {}, } as unknown) as Setup & SetupTimeRange; const serviceName = 'test service name'; const result = await getServiceMapServiceNodeInfo({ + environment: 'test environment', setup, serviceName, searchAggregatedTransactions: false, diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts index 213702bf06f4ce..e384b15685dad4 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -19,14 +19,13 @@ import { TRANSACTION_PAGE_LOAD, TRANSACTION_REQUEST, } from '../../../common/transaction_types'; -import { rangeFilter } from '../../../common/utils/range_filter'; +import { environmentQuery, rangeQuery } from '../../../common/utils/queries'; import { withApmSpan } from '../../utils/with_apm_span'; import { getDocumentTypeFilterForAggregatedTransactions, getProcessorEventForAggregatedTransactions, getTransactionDurationFieldForAggregatedTransactions, } from '../helpers/aggregated_transactions'; -import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { percentCgroupMemoryUsedScript, @@ -51,6 +50,7 @@ interface TaskParameters { } export function getServiceMapServiceNodeInfo({ + environment, serviceName, setup, searchAggregatedTransactions, @@ -59,9 +59,9 @@ export function getServiceMapServiceNodeInfo({ const { start, end, uiFilters } = setup; const filter: ESFilter[] = [ - { range: rangeFilter(start, end) }, { term: { [SERVICE_NAME]: serviceName } }, - ...getEnvironmentUiFilterES(uiFilters.environment), + ...rangeQuery(start, end), + ...environmentQuery(environment), ]; const minutes = Math.abs((end - start) / (1000 * 60)); @@ -106,16 +106,13 @@ async function getErrorStats({ searchAggregatedTransactions: boolean; }) { return withApmSpan('get_error_rate_for_service_map_node', async () => { - const setupWithBlankUiFilters = { - ...setup, - uiFilters: { environment }, - esFilter: getEnvironmentUiFilterES(environment), - }; const { noHits, average } = await getErrorRate({ - setup: setupWithBlankUiFilters, + environment, + setup, serviceName, searchAggregatedTransactions, }); + return { avgErrorRate: noHits ? null : average }; }); } diff --git a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts index deb9104a839051..e8dcb28baa9a38 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts @@ -16,9 +16,8 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; import { SERVICE_MAP_TIMEOUT_ERROR } from '../../../common/service_map'; -import { rangeFilter } from '../../../common/utils/range_filter'; +import { environmentQuery, rangeQuery } from '../../../common/utils/queries'; import { withApmSpan } from '../../utils/with_apm_span'; -import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; const MAX_TRACES_TO_INSPECT = 1000; @@ -35,8 +34,6 @@ export function getTraceSampleIds({ return withApmSpan('get_trace_sample_ids', async () => { const { start, end, apmEventClient, config } = setup; - const rangeQuery = { range: rangeFilter(start, end) }; - const query = { bool: { filter: [ @@ -45,7 +42,7 @@ export function getTraceSampleIds({ field: SPAN_DESTINATION_SERVICE_RESOURCE, }, }, - rangeQuery, + ...rangeQuery(start, end), ] as ESFilter[], }, } as { bool: { filter: ESFilter[]; must_not?: ESFilter[] | ESFilter } }; @@ -54,7 +51,7 @@ export function getTraceSampleIds({ query.bool.filter.push({ term: { [SERVICE_NAME]: serviceName } }); } - query.bool.filter.push(...getEnvironmentUiFilterES(environment)); + query.bool.filter.push(...environmentQuery(environment)); const fingerprintBucketSize = serviceName ? config['xpack.apm.serviceMapFingerprintBucketSize'] diff --git a/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap index d83e558775be40..e6d702cc03c0b1 100644 --- a/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap @@ -35,6 +35,11 @@ Object { "service.name": "foo", }, }, + Object { + "term": Object { + "service.node.name": "bar", + }, + }, Object { "range": Object { "@timestamp": Object { @@ -44,11 +49,6 @@ Object { }, }, }, - Object { - "term": Object { - "service.node.name": "bar", - }, - }, Object { "term": Object { "service.environment": "test", @@ -97,15 +97,6 @@ Object { "service.name": "foo", }, }, - Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, - }, - }, - }, Object { "bool": Object { "must_not": Array [ @@ -117,6 +108,15 @@ Object { ], }, }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, Object { "term": Object { "service.environment": "test", diff --git a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap index 606ce870351564..3e68831ee7cba7 100644 --- a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -101,14 +101,6 @@ Array [ "aggs": Object { "transactionType": Object { "aggs": Object { - "agentName": Object { - "top_hits": Object { - "docvalue_fields": Array [ - "agent.name", - ], - "size": 1, - }, - }, "avg_duration": Object { "avg": Object { "field": "transaction.duration.us", @@ -129,6 +121,16 @@ Array [ ], }, }, + "sample": Object { + "top_metrics": Object { + "metrics": Object { + "field": "agent.name", + }, + "sort": Object { + "@timestamp": "desc", + }, + }, + }, "timeseries": Object { "aggs": Object { "avg_duration": Object { diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts index 7c746aac29af92..67aa9d7fcd8707 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_derived_service_annotations.ts @@ -12,13 +12,12 @@ import { SERVICE_NAME, SERVICE_VERSION, } from '../../../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../../../../common/utils/range_filter'; +import { environmentQuery, rangeQuery } from '../../../../common/utils/queries'; import { withApmSpan } from '../../../utils/with_apm_span'; import { getDocumentTypeFilterForAggregatedTransactions, getProcessorEventForAggregatedTransactions, } from '../../helpers/aggregated_transactions'; -import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; export async function getDerivedServiceAnnotations({ @@ -40,7 +39,7 @@ export async function getDerivedServiceAnnotations({ ...getDocumentTypeFilterForAggregatedTransactions( searchAggregatedTransactions ), - ...getEnvironmentUiFilterES(environment), + ...environmentQuery(environment), ]; const versions = @@ -57,7 +56,7 @@ export async function getDerivedServiceAnnotations({ size: 0, query: { bool: { - filter: [...filter, { range: rangeFilter(start, end) }], + filter: [...filter, ...rangeQuery(start, end)], }, }, aggs: { diff --git a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts index 20629a4b4f5538..6c7cbc26ea6536 100644 --- a/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts +++ b/x-pack/plugins/apm/server/lib/services/annotations/get_stored_annotations.ts @@ -5,15 +5,18 @@ * 2.0. */ +import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import { ElasticsearchClient, Logger } from 'kibana/server'; -import { unwrapEsResponse } from '../../../../../observability/server'; -import { rangeFilter } from '../../../../common/utils/range_filter'; +import { environmentQuery, rangeQuery } from '../../../../common/utils/queries'; +import { + unwrapEsResponse, + WrappedElasticsearchClientError, +} from '../../../../../observability/server'; import { ESSearchResponse } from '../../../../../../typings/elasticsearch'; import { Annotation as ESAnnotation } from '../../../../../observability/common/annotations'; import { ScopedAnnotationsClient } from '../../../../../observability/server'; import { Annotation, AnnotationType } from '../../../../common/annotations'; import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; -import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { withApmSpan } from '../../../utils/with_apm_span'; @@ -33,18 +36,18 @@ export function getStoredAnnotations({ logger: Logger; }): Promise { return withApmSpan('get_stored_annotations', async () => { + const { start, end } = setup; + const body = { size: 50, query: { bool: { filter: [ - { - range: rangeFilter(setup.start, setup.end), - }, { term: { 'annotation.type': 'deployment' } }, { term: { tags: 'apm' } }, { term: { [SERVICE_NAME]: serviceName } }, - ...getEnvironmentUiFilterES(environment), + ...rangeQuery(start, end), + ...environmentQuery(environment), ], }, }, @@ -72,15 +75,22 @@ export function getStoredAnnotations({ } catch (error) { // index is only created when an annotation has been indexed, // so we should handle this error gracefully - if (error.body?.error?.type === 'index_not_found_exception') { - return []; - } + if ( + error instanceof WrappedElasticsearchClientError && + error.originalError instanceof ResponseError + ) { + const type = error.originalError.body.error.type; + + if (type === 'index_not_found_exception') { + return []; + } - if (error.body?.error?.type === 'security_exception') { - logger.warn( - `Unable to get stored annotations due to a security exception. Please make sure that the user has 'indices:data/read/search' permissions for ${annotationsClient.index}` - ); - return []; + if (type === 'security_exception') { + logger.warn( + `Unable to get stored annotations due to a security exception. Please make sure that the user has 'indices:data/read/search' permissions for ${annotationsClient.index}` + ); + return []; + } } throw error; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts b/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts index 29c77da6e4075f..3683a069342a9b 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts @@ -10,7 +10,7 @@ import { AGENT_NAME, SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../../../common/utils/range_filter'; +import { rangeQuery } from '../../../common/utils/queries'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; import { withApmSpan } from '../../utils/with_apm_span'; @@ -44,7 +44,7 @@ export function getServiceAgentName({ bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { range: rangeFilter(start, end) }, + ...rangeQuery(start, end), ], }, }, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.ts index ef2b50cbdbedfe..558d6ae22f00f5 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_destination_map.ts @@ -19,9 +19,8 @@ import { SPAN_SUBTYPE, SPAN_TYPE, } from '../../../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../../../../common/utils/range_filter'; import { ProcessorEvent } from '../../../../common/processor_event'; -import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { environmentQuery, rangeQuery } from '../../../../common/utils/queries'; import { joinByKey } from '../../../../common/utils/join_by_key'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { withApmSpan } from '../../../utils/with_apm_span'; @@ -33,7 +32,7 @@ export const getDestinationMap = ({ }: { setup: Setup & SetupTimeRange; serviceName: string; - environment: string; + environment?: string; }) => { return withApmSpan('get_service_destination_map', async () => { const { start, end, apmEventClient } = setup; @@ -50,8 +49,8 @@ export const getDestinationMap = ({ filter: [ { term: { [SERVICE_NAME]: serviceName } }, { exists: { field: SPAN_DESTINATION_SERVICE_RESOURCE } }, - { range: rangeFilter(start, end) }, - ...getEnvironmentUiFilterES(environment), + ...rangeQuery(start, end), + ...environmentQuery(environment), ], }, }, @@ -71,14 +70,13 @@ export const getDestinationMap = ({ ], }, aggs: { - docs: { - top_hits: { - docvalue_fields: [ - SPAN_TYPE, - SPAN_SUBTYPE, - SPAN_ID, + sample: { + top_metrics: { + metrics: [ + { field: SPAN_TYPE }, + { field: SPAN_SUBTYPE }, + { field: SPAN_ID }, ] as const, - _source: false, sort: { '@timestamp': 'desc', }, @@ -93,15 +91,15 @@ export const getDestinationMap = ({ const outgoingConnections = response.aggregations?.connections.buckets.map((bucket) => { - const doc = bucket.docs.hits.hits[0]; + const fieldValues = bucket.sample.top[0].metrics; return { [SPAN_DESTINATION_SERVICE_RESOURCE]: String( bucket.key[SPAN_DESTINATION_SERVICE_RESOURCE] ), - [SPAN_ID]: String(doc.fields[SPAN_ID]?.[0]), - [SPAN_TYPE]: String(doc.fields[SPAN_TYPE]?.[0] ?? ''), - [SPAN_SUBTYPE]: String(doc.fields[SPAN_SUBTYPE]?.[0] ?? ''), + [SPAN_ID]: (fieldValues[SPAN_ID] ?? '') as string, + [SPAN_TYPE]: (fieldValues[SPAN_TYPE] ?? '') as string, + [SPAN_SUBTYPE]: (fieldValues[SPAN_SUBTYPE] ?? '') as string, }; }) ?? []; @@ -123,7 +121,7 @@ export const getDestinationMap = ({ ), }, }, - { range: rangeFilter(start, end) }, + ...rangeQuery(start, end), ], }, }, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_metrics.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_metrics.ts index 9a020daa7e095d..dfbdfb3f504e8c 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_metrics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/get_metrics.ts @@ -13,9 +13,8 @@ import { SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM, } from '../../../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../../../../common/utils/range_filter'; import { ProcessorEvent } from '../../../../common/processor_event'; -import { getEnvironmentUiFilterES } from '../../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { environmentQuery, rangeQuery } from '../../../../common/utils/queries'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { EventOutcome } from '../../../../common/event_outcome'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; @@ -29,7 +28,7 @@ export const getMetrics = ({ }: { setup: Setup & SetupTimeRange; serviceName: string; - environment: string; + environment?: string; numBuckets: number; }) => { return withApmSpan('get_service_destination_metrics', async () => { @@ -49,8 +48,8 @@ export const getMetrics = ({ { exists: { field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT }, }, - { range: rangeFilter(start, end) }, - ...getEnvironmentUiFilterES(environment), + ...rangeQuery(start, end), + ...environmentQuery(environment), ], }, }, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/index.ts b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/index.ts index 19f306f5cb8035..724b5278d7edfc 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_dependencies/index.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_dependencies/index.ts @@ -51,7 +51,7 @@ export function getServiceDependencies({ }: { serviceName: string; setup: Setup & SetupTimeRange; - environment: string; + environment?: string; numBuckets: number; }): Promise { return withApmSpan('get_service_dependencies', async () => { diff --git a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts index f5aa01e1dfa58a..a17fb6da2007fa 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_error_groups/index.ts @@ -9,7 +9,7 @@ import { ValuesType } from 'utility-types'; import { orderBy } from 'lodash'; import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; import { PromiseReturnType } from '../../../../../observability/typings/common'; -import { rangeFilter } from '../../../../common/utils/range_filter'; +import { environmentQuery, rangeQuery } from '../../../../common/utils/queries'; import { ProcessorEvent } from '../../../../common/processor_event'; import { ERROR_EXC_MESSAGE, @@ -28,6 +28,7 @@ export type ServiceErrorGroupItem = ValuesType< >; export async function getServiceErrorGroups({ + environment, serviceName, setup, size, @@ -37,6 +38,7 @@ export async function getServiceErrorGroups({ sortField, transactionType, }: { + environment?: string; serviceName: string; setup: Setup & SetupTimeRange; size: number; @@ -63,7 +65,8 @@ export async function getServiceErrorGroups({ filter: [ { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: transactionType } }, - { range: rangeFilter(start, end) }, + ...rangeQuery(start, end), + ...environmentQuery(environment), ...esFilter, ], }, @@ -145,7 +148,8 @@ export async function getServiceErrorGroups({ { terms: { [ERROR_GROUP_ID]: sortedErrorGroupIds } }, { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: transactionType } }, - { range: rangeFilter(start, end) }, + ...rangeQuery(start, end), + ...environmentQuery(environment), ...esFilter, ], }, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_system_metric_stats.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_system_metric_stats.ts index 4f8088352d0ae2..ef90e5197229b9 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_system_metric_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_system_metric_stats.ts @@ -6,7 +6,7 @@ */ import { AggregationOptionsByType } from '../../../../../../typings/elasticsearch'; -import { rangeFilter } from '../../../../common/utils/range_filter'; +import { environmentQuery, rangeQuery } from '../../../../common/utils/queries'; import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; import { METRIC_CGROUP_MEMORY_USAGE_BYTES, @@ -26,6 +26,7 @@ import { import { withApmSpan } from '../../../utils/with_apm_span'; export async function getServiceInstanceSystemMetricStats({ + environment, setup, serviceName, size, @@ -95,8 +96,9 @@ export async function getServiceInstanceSystemMetricStats({ query: { bool: { filter: [ - { range: rangeFilter(start, end) }, { term: { [SERVICE_NAME]: serviceName } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), ...esFilter, ], should: [cgroupMemoryFilter, systemMemoryFilter, cpuUsageFilter], diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_transaction_stats.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_transaction_stats.ts index 2cbe5a42206d18..620fd9828bd37f 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_transaction_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/get_service_instance_transaction_stats.ts @@ -6,7 +6,7 @@ */ import { EventOutcome } from '../../../../common/event_outcome'; -import { rangeFilter } from '../../../../common/utils/range_filter'; +import { environmentQuery, rangeQuery } from '../../../../common/utils/queries'; import { SERVICE_NODE_NAME_MISSING } from '../../../../common/service_nodes'; import { EVENT_OUTCOME, @@ -22,8 +22,14 @@ import { } from '../../helpers/aggregated_transactions'; import { calculateThroughput } from '../../helpers/calculate_throughput'; import { withApmSpan } from '../../../utils/with_apm_span'; +import { + getLatencyAggregation, + getLatencyValue, +} from '../../helpers/latency_aggregation_type'; export async function getServiceInstanceTransactionStats({ + environment, + latencyAggregationType, setup, transactionType, serviceName, @@ -45,11 +51,7 @@ export async function getServiceInstanceTransactionStats({ ); const subAggs = { - avg_transaction_duration: { - avg: { - field, - }, - }, + ...getLatencyAggregation(latencyAggregationType, field), failures: { filter: { term: { @@ -72,9 +74,10 @@ export async function getServiceInstanceTransactionStats({ query: { bool: { filter: [ - { range: rangeFilter(start, end) }, { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: transactionType } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), ...esFilter, ], }, @@ -115,7 +118,7 @@ export async function getServiceInstanceTransactionStats({ (serviceNodeBucket) => { const { doc_count: count, - avg_transaction_duration: avgTransactionDuration, + latency, key, failures, timeseries, @@ -138,10 +141,16 @@ export async function getServiceInstanceTransactionStats({ })), }, latency: { - value: avgTransactionDuration.value, + value: getLatencyValue({ + aggregation: latency, + latencyAggregationType, + }), timeseries: timeseries.buckets.map((dateBucket) => ({ x: dateBucket.key, - y: dateBucket.avg_transaction_duration.value, + y: getLatencyValue({ + aggregation: dateBucket.latency, + latencyAggregationType, + }), })), }, }; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_instances/index.ts b/x-pack/plugins/apm/server/lib/services/get_service_instances/index.ts index 021774f9522c1b..7c0124f4ce0040 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_instances/index.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_instances/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; import { joinByKey } from '../../../../common/utils/join_by_key'; import { withApmSpan } from '../../../utils/with_apm_span'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; @@ -12,6 +13,8 @@ import { getServiceInstanceSystemMetricStats } from './get_service_instance_syst import { getServiceInstanceTransactionStats } from './get_service_instance_transaction_stats'; export interface ServiceInstanceParams { + environment?: string; + latencyAggregationType: LatencyAggregationType; setup: Setup & SetupTimeRange; serviceName: string; transactionType: string; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_metadata_details.ts b/x-pack/plugins/apm/server/lib/services/get_service_metadata_details.ts index f1198a4d858fda..5c43191cf588c2 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_metadata_details.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_metadata_details.ts @@ -21,7 +21,7 @@ import { SERVICE_VERSION, } from '../../../common/elasticsearch_fieldnames'; import { ContainerType } from '../../../common/service_metadata'; -import { rangeFilter } from '../../../common/utils/range_filter'; +import { rangeQuery } from '../../../common/utils/queries'; import { TransactionRaw } from '../../../typings/es_schemas/raw/transaction_raw'; import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; @@ -74,7 +74,7 @@ export function getServiceMetadataDetails({ const filter = [ { term: { [SERVICE_NAME]: serviceName } }, - { range: rangeFilter(start, end) }, + ...rangeQuery(start, end), ]; const params = { diff --git a/x-pack/plugins/apm/server/lib/services/get_service_metadata_icons.ts b/x-pack/plugins/apm/server/lib/services/get_service_metadata_icons.ts index 0ea95a08abaa98..b342ffea02464e 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_metadata_icons.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_metadata_icons.ts @@ -16,7 +16,7 @@ import { HOST_OS_PLATFORM, } from '../../../common/elasticsearch_fieldnames'; import { ContainerType } from '../../../common/service_metadata'; -import { rangeFilter } from '../../../common/utils/range_filter'; +import { rangeQuery } from '../../../common/utils/queries'; import { TransactionRaw } from '../../../typings/es_schemas/raw/transaction_raw'; import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; @@ -55,7 +55,7 @@ export function getServiceMetadataIcons({ const filter = [ { term: { [SERVICE_NAME]: serviceName } }, - { range: rangeFilter(start, end) }, + ...rangeQuery(start, end), ]; const params = { diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_comparison_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_comparison_statistics.ts index 54cf89d6125b6e..ce36db3e82babe 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_comparison_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_group_comparison_statistics.ts @@ -14,7 +14,7 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { EventOutcome } from '../../../common/event_outcome'; import { LatencyAggregationType } from '../../../common/latency_aggregation_types'; -import { rangeFilter } from '../../../common/utils/range_filter'; +import { environmentQuery, rangeQuery } from '../../../common/utils/queries'; import { Coordinate } from '../../../typings/timeseries'; import { withApmSpan } from '../../utils/with_apm_span'; import { @@ -31,6 +31,7 @@ import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { calculateTransactionErrorPercentage } from '../helpers/transaction_error_rate'; export async function getServiceTransactionGroupComparisonStatistics({ + environment, serviceName, transactionNames, setup, @@ -39,6 +40,7 @@ export async function getServiceTransactionGroupComparisonStatistics({ transactionType, latencyAggregationType, }: { + environment?: string; serviceName: string; transactionNames: string[]; setup: Setup & SetupTimeRange; @@ -82,10 +84,11 @@ export async function getServiceTransactionGroupComparisonStatistics({ filter: [ { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: transactionType } }, - { range: rangeFilter(start, end) }, ...getDocumentTypeFilterForAggregatedTransactions( searchAggregatedTransactions ), + ...rangeQuery(start, end), + ...environmentQuery(environment), ...esFilter, ], }, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts index 168eed8e38374c..ddbfd617faf65b 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups.ts @@ -13,7 +13,7 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { EventOutcome } from '../../../common/event_outcome'; import { LatencyAggregationType } from '../../../common/latency_aggregation_types'; -import { rangeFilter } from '../../../common/utils/range_filter'; +import { environmentQuery, rangeQuery } from '../../../common/utils/queries'; import { withApmSpan } from '../../utils/with_apm_span'; import { getDocumentTypeFilterForAggregatedTransactions, @@ -36,12 +36,14 @@ export type ServiceOverviewTransactionGroupSortField = | 'impact'; export async function getServiceTransactionGroups({ + environment, serviceName, setup, searchAggregatedTransactions, transactionType, latencyAggregationType, }: { + environment?: string; serviceName: string; setup: Setup & SetupTimeRange; searchAggregatedTransactions: boolean; @@ -70,10 +72,11 @@ export async function getServiceTransactionGroups({ filter: [ { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: transactionType } }, - { range: rangeFilter(start, end) }, ...getDocumentTypeFilterForAggregatedTransactions( searchAggregatedTransactions ), + ...rangeQuery(start, end), + ...environmentQuery(environment), ...esFilter, ], }, diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts index bc4660e2c01a5e..3d77bf5bd6baf3 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_types.ts @@ -9,7 +9,7 @@ import { SERVICE_NAME, TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../../../common/utils/range_filter'; +import { rangeQuery } from '../../../common/utils/queries'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { getDocumentTypeFilterForAggregatedTransactions, @@ -46,7 +46,7 @@ export function getServiceTransactionTypes({ searchAggregatedTransactions ), { term: { [SERVICE_NAME]: serviceName } }, - { range: rangeFilter(start, end) }, + ...rangeQuery(start, end), ], }, }, diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_health_statuses.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_health_statuses.ts index e88fafb061912d..6fc868b0f0a4ec 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_health_statuses.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_health_statuses.ts @@ -8,28 +8,25 @@ import { getSeverity } from '../../../../common/anomaly_detection'; import { getServiceHealthStatus } from '../../../../common/service_health_status'; import { getServiceAnomalies } from '../../service_map/get_service_anomalies'; -import { - ServicesItemsProjection, - ServicesItemsSetup, -} from './get_services_items'; +import { ServicesItemsSetup } from './get_services_items'; interface AggregationParams { + environment?: string; setup: ServicesItemsSetup; - projection: ServicesItemsProjection; searchAggregatedTransactions: boolean; } -export const getHealthStatuses = async ( - { setup }: AggregationParams, - mlAnomaliesEnvironment?: string -) => { +export const getHealthStatuses = async ({ + environment, + setup, +}: AggregationParams) => { if (!setup.ml) { return []; } const anomalies = await getServiceAnomalies({ setup, - environment: mlAnomaliesEnvironment, + environment, }); return anomalies.serviceAnomalies.map((anomalyStats) => { diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts index 36540b01a07cc9..e1f8bca83829cc 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_service_transaction_stats.ts @@ -15,7 +15,7 @@ import { TRANSACTION_PAGE_LOAD, TRANSACTION_REQUEST, } from '../../../../common/transaction_types'; -import { rangeFilter } from '../../../../common/utils/range_filter'; +import { environmentQuery, rangeQuery } from '../../../../common/utils/queries'; import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; import { getDocumentTypeFilterForAggregatedTransactions, @@ -32,6 +32,7 @@ import { ServicesItemsSetup } from './get_services_items'; import { withApmSpan } from '../../../utils/with_apm_span'; interface AggregationParams { + environment?: string; setup: ServicesItemsSetup; searchAggregatedTransactions: boolean; } @@ -39,6 +40,7 @@ interface AggregationParams { const MAX_NUMBER_OF_SERVICES = 500; export async function getServiceTransactionStats({ + environment, setup, searchAggregatedTransactions, }: AggregationParams) { @@ -71,11 +73,12 @@ export async function getServiceTransactionStats({ query: { bool: { filter: [ - { range: rangeFilter(start, end) }, - ...esFilter, ...getDocumentTypeFilterForAggregatedTransactions( searchAggregatedTransactions ), + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...esFilter, ], }, }, @@ -98,10 +101,12 @@ export async function getServiceTransactionStats({ missing: '', }, }, - agentName: { - top_hits: { - docvalue_fields: [AGENT_NAME] as const, - size: 1, + sample: { + top_metrics: { + metrics: { field: AGENT_NAME } as const, + sort: { + '@timestamp': 'desc', + }, }, }, timeseries: { @@ -139,9 +144,9 @@ export async function getServiceTransactionStats({ environments: topTransactionTypeBucket.environments.buckets .map((environmentBucket) => environmentBucket.key as string) .filter(Boolean), - agentName: topTransactionTypeBucket.agentName.hits.hits[0].fields[ - 'agent.name' - ]?.[0] as AgentName, + agentName: topTransactionTypeBucket.sample.top[0].metrics[ + AGENT_NAME + ] as AgentName, avgResponseTime: { value: topTransactionTypeBucket.avg_duration.value, timeseries: topTransactionTypeBucket.timeseries.buckets.map( diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index 1ba9aaf980201f..c2677af038486b 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -14,19 +14,21 @@ import { getHealthStatuses } from './get_health_statuses'; import { getServiceTransactionStats } from './get_service_transaction_stats'; export type ServicesItemsSetup = Setup & SetupTimeRange; -export type ServicesItemsProjection = ReturnType; export async function getServicesItems({ + environment, setup, searchAggregatedTransactions, logger, }: { + environment?: string; setup: ServicesItemsSetup; searchAggregatedTransactions: boolean; logger: Logger; }) { return withApmSpan('get_services_items', async () => { const params = { + environment, projection: getServicesProjection({ setup, searchAggregatedTransactions, @@ -37,7 +39,7 @@ export async function getServicesItems({ const [transactionStats, healthStatuses] = await Promise.all([ getServiceTransactionStats(params), - getHealthStatuses(params, setup.uiFilters.environment).catch((err) => { + getHealthStatuses(params).catch((err) => { logger.error(err); return []; }), diff --git a/x-pack/plugins/apm/server/lib/services/get_services/index.ts b/x-pack/plugins/apm/server/lib/services/get_services/index.ts index 45efd80c45a980..1a0ddeda11651b 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/index.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/index.ts @@ -14,10 +14,12 @@ import { getServicesItems } from './get_services_items'; import { hasHistoricalAgentData } from './has_historical_agent_data'; export async function getServices({ + environment, setup, searchAggregatedTransactions, logger, }: { + environment?: string; setup: Setup & SetupTimeRange; searchAggregatedTransactions: boolean; logger: Logger; @@ -25,6 +27,7 @@ export async function getServices({ return withApmSpan('get_services', async () => { const [items, hasLegacyData] = await Promise.all([ getServicesItems({ + environment, setup, searchAggregatedTransactions, logger, diff --git a/x-pack/plugins/apm/server/lib/services/get_throughput.ts b/x-pack/plugins/apm/server/lib/services/get_throughput.ts index 33268e9b3332d7..f7cd23b0e37a7b 100644 --- a/x-pack/plugins/apm/server/lib/services/get_throughput.ts +++ b/x-pack/plugins/apm/server/lib/services/get_throughput.ts @@ -10,7 +10,7 @@ import { SERVICE_NAME, TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../../../common/utils/range_filter'; +import { environmentQuery, rangeQuery } from '../../../common/utils/queries'; import { getDocumentTypeFilterForAggregatedTransactions, getProcessorEventForAggregatedTransactions, @@ -20,6 +20,7 @@ import { Setup } from '../helpers/setup_request'; import { withApmSpan } from '../../utils/with_apm_span'; interface Options { + environment?: string; searchAggregatedTransactions: boolean; serviceName: string; setup: Setup; @@ -29,6 +30,7 @@ interface Options { } function fetcher({ + environment, searchAggregatedTransactions, serviceName, setup, @@ -36,16 +38,17 @@ function fetcher({ start, end, }: Options) { - const { apmEventClient } = setup; + const { esFilter, apmEventClient } = setup; const { intervalString } = getBucketSize({ start, end }); const filter: ESFilter[] = [ { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: transactionType } }, - { range: rangeFilter(start, end) }, ...getDocumentTypeFilterForAggregatedTransactions( searchAggregatedTransactions ), - ...setup.esFilter, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...esFilter, ]; const params = { diff --git a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts index bd3ecf1e0f862d..f631657f872761 100644 --- a/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts +++ b/x-pack/plugins/apm/server/lib/traces/get_trace_items.ts @@ -15,7 +15,7 @@ import { ERROR_LOG_LEVEL, } from '../../../common/elasticsearch_fieldnames'; import { APMError } from '../../../typings/es_schemas/ui/apm_error'; -import { rangeFilter } from '../../../common/utils/range_filter'; +import { rangeQuery } from '../../../common/utils/queries'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { PromiseValueType } from '../../../typings/common'; import { withApmSpan } from '../../utils/with_apm_span'; @@ -44,7 +44,7 @@ export async function getTraceItems( bool: { filter: [ { term: { [TRACE_ID]: traceId } }, - { range: rangeFilter(start, end) }, + ...rangeQuery(start, end), ], must_not: { terms: { [ERROR_LOG_LEVEL]: excludedLogLevels } }, }, @@ -74,7 +74,7 @@ export async function getTraceItems( bool: { filter: [ { term: { [TRACE_ID]: traceId } }, - { range: rangeFilter(start, end) }, + ...rangeQuery(start, end), ], should: { exists: { field: PARENT_ID }, diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap index 89069d74bacf8b..7fb2bb2fcbeeb1 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/transaction_groups/__snapshots__/queries.test.ts.snap @@ -13,11 +13,13 @@ Array [ "transaction_groups": Object { "aggs": Object { "transaction_type": Object { - "top_hits": Object { - "_source": Array [ - "transaction.type", - ], - "size": 1, + "top_metrics": Object { + "metrics": Object { + "field": "transaction.type", + }, + "sort": Object { + "@timestamp": "desc", + }, }, }, }, @@ -222,11 +224,13 @@ Array [ "transaction_groups": Object { "aggs": Object { "transaction_type": Object { - "top_hits": Object { - "_source": Array [ - "transaction.type", - ], - "size": 1, + "top_metrics": Object { + "metrics": Object { + "field": "transaction.type", + }, + "sort": Object { + "@timestamp": "desc", + }, }, }, }, @@ -240,12 +244,8 @@ Array [ "bool": Object { "filter": Array [ Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, - }, + "term": Object { + "service.name": "foo", }, }, Object { @@ -254,8 +254,12 @@ Array [ }, }, Object { - "term": Object { - "service.name": "foo", + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, }, }, Object { @@ -295,12 +299,8 @@ Array [ "bool": Object { "filter": Array [ Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, - }, + "term": Object { + "service.name": "foo", }, }, Object { @@ -309,8 +309,12 @@ Array [ }, }, Object { - "term": Object { - "service.name": "foo", + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, }, }, Object { @@ -350,12 +354,8 @@ Array [ "bool": Object { "filter": Array [ Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, - }, + "term": Object { + "service.name": "foo", }, }, Object { @@ -364,8 +364,12 @@ Array [ }, }, Object { - "term": Object { - "service.name": "foo", + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, }, }, Object { @@ -411,12 +415,8 @@ Array [ "bool": Object { "filter": Array [ Object { - "range": Object { - "@timestamp": Object { - "format": "epoch_millis", - "gte": 1528113600000, - "lte": 1528977600000, - }, + "term": Object { + "service.name": "foo", }, }, Object { @@ -425,8 +425,12 @@ Array [ }, }, Object { - "term": Object { - "service.name": "foo", + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, }, }, Object { diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index 0fad948edde19b..09e5e358a1b7c1 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -27,6 +27,7 @@ import { } from './get_transaction_group_stats'; interface TopTransactionOptions { + environment?: string; type: 'top_transactions'; serviceName: string; transactionType: string; @@ -35,6 +36,7 @@ interface TopTransactionOptions { } interface TopTraceOptions { + environment?: string; type: 'top_traces'; transactionName?: string; searchAggregatedTransactions: boolean; diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts index 839efc9009c389..d1a056002db078 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts @@ -14,7 +14,7 @@ import { TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; import { EventOutcome } from '../../../common/event_outcome'; -import { rangeFilter } from '../../../common/utils/range_filter'; +import { environmentQuery, rangeQuery } from '../../../common/utils/queries'; import { getDocumentTypeFilterForAggregatedTransactions, getProcessorEventForAggregatedTransactions, @@ -29,12 +29,14 @@ import { import { withApmSpan } from '../../utils/with_apm_span'; export async function getErrorRate({ + environment, serviceName, transactionType, transactionName, setup, searchAggregatedTransactions, }: { + environment?: string; serviceName: string; transactionType?: string; transactionName?: string; @@ -57,17 +59,18 @@ export async function getErrorRate({ const filter = [ { term: { [SERVICE_NAME]: serviceName } }, - { range: rangeFilter(start, end) }, { terms: { [EVENT_OUTCOME]: [EventOutcome.failure, EventOutcome.success], }, }, + ...transactionNamefilter, + ...transactionTypefilter, ...getDocumentTypeFilterForAggregatedTransactions( searchAggregatedTransactions ), - ...transactionNamefilter, - ...transactionTypefilter, + ...rangeQuery(start, end), + ...environmentQuery(environment), ...esFilter, ]; diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts index ee19a6a8d15918..5ee46bf1a5918a 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_transaction_group_stats.ts @@ -69,9 +69,13 @@ export function getCounts({ request, setup }: MetricParams) { return withApmSpan('get_transaction_group_transaction_count', async () => { const params = mergeRequestWithAggs(request, { transaction_type: { - top_hits: { - size: 1, - _source: [TRANSACTION_TYPE], + top_metrics: { + sort: { + '@timestamp': 'desc', + }, + metrics: { + field: TRANSACTION_TYPE, + } as const, }, }, }); @@ -81,14 +85,12 @@ export function getCounts({ request, setup }: MetricParams) { return arrayUnionToCallable( response.aggregations?.transaction_groups.buckets ?? [] ).map((bucket) => { - // type is Transaction | APMBaseDoc because it could be a metric document - const source = (bucket.transaction_type.hits.hits[0] - ._source as unknown) as { transaction: { type: string } }; - return { key: bucket.key as BucketKey, count: bucket.doc_count, - transactionType: source.transaction.type, + transactionType: bucket.transaction_type.top[0].metrics[ + TRANSACTION_TYPE + ] as string, }; }); }); diff --git a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts index 8a2579b4a2b875..c3741184c807db 100644 --- a/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/breakdown/index.ts @@ -18,18 +18,20 @@ import { TRANSACTION_BREAKDOWN_COUNT, } from '../../../../common/elasticsearch_fieldnames'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; -import { rangeFilter } from '../../../../common/utils/range_filter'; +import { environmentQuery, rangeQuery } from '../../../../common/utils/queries'; import { getMetricsDateHistogramParams } from '../../helpers/metrics'; import { MAX_KPIS } from './constants'; import { getVizColorForIndex } from '../../../../common/viz_colors'; import { withApmSpan } from '../../../utils/with_apm_span'; export function getTransactionBreakdown({ + environment, setup, serviceName, transactionName, transactionType, }: { + environment?: string; setup: Setup & SetupTimeRange; serviceName: string; transactionName?: string; @@ -82,7 +84,8 @@ export function getTransactionBreakdown({ const filters = [ { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: transactionType } }, - { range: rangeFilter(start, end) }, + ...rangeQuery(start, end), + ...environmentQuery(environment), ...esFilter, ]; diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts index 22a34ded4c20d5..7ed016cd4b4c61 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_buckets/index.ts @@ -17,7 +17,10 @@ import { } from '../../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../../common/processor_event'; import { joinByKey } from '../../../../../common/utils/join_by_key'; -import { rangeFilter } from '../../../../../common/utils/range_filter'; +import { + environmentQuery, + rangeQuery, +} from '../../../../../common/utils/queries'; import { getDocumentTypeFilterForAggregatedTransactions, getProcessorEventForAggregatedTransactions, @@ -46,6 +49,7 @@ function getHistogramAggOptions({ } export async function getBuckets({ + environment, serviceName, transactionName, transactionType, @@ -56,6 +60,7 @@ export async function getBuckets({ setup, searchAggregatedTransactions, }: { + environment?: string; serviceName: string; transactionName: string; transactionType: string; @@ -75,7 +80,8 @@ export async function getBuckets({ { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: transactionType } }, { term: { [TRANSACTION_NAME]: transactionName } }, - { range: rangeFilter(start, end) }, + ...rangeQuery(start, end), + ...environmentQuery(environment), ...esFilter, ]; @@ -109,8 +115,11 @@ export async function getBuckets({ }), aggs: { samples: { - top_hits: { - _source: [TRANSACTION_ID, TRACE_ID], + top_metrics: { + metrics: [ + { field: TRANSACTION_ID }, + { field: TRACE_ID }, + ] as const, size: 10, sort: { _score: 'desc', @@ -128,9 +137,9 @@ export async function getBuckets({ response.aggregations?.distribution.buckets.map((bucket) => { return { key: bucket.key, - samples: bucket.samples.hits.hits.map((hit) => ({ - traceId: hit._source.trace.id, - transactionId: hit._source.transaction.id, + samples: bucket.samples.top.map((sample) => ({ + traceId: sample.metrics[TRACE_ID] as string, + transactionId: sample.metrics[TRANSACTION_ID] as string, })), }; }) ?? [] diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts index ed54dae32704e3..f8061ea9894698 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution_max.ts @@ -15,15 +15,18 @@ import { getProcessorEventForAggregatedTransactions, getTransactionDurationFieldForAggregatedTransactions, } from '../../helpers/aggregated_transactions'; +import { environmentQuery, rangeQuery } from '../../../../common/utils/queries'; import { withApmSpan } from '../../../utils/with_apm_span'; export async function getDistributionMax({ + environment, serviceName, transactionName, transactionType, setup, searchAggregatedTransactions, }: { + environment?: string; serviceName: string; transactionName: string; transactionType: string; @@ -49,15 +52,8 @@ export async function getDistributionMax({ { term: { [SERVICE_NAME]: serviceName } }, { term: { [TRANSACTION_TYPE]: transactionType } }, { term: { [TRANSACTION_NAME]: transactionName } }, - { - range: { - '@timestamp': { - gte: start, - lte: end, - format: 'epoch_millis', - }, - }, - }, + ...rangeQuery(start, end), + ...environmentQuery(environment), ...esFilter, ], }, diff --git a/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts b/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts index 22436cac40183d..92d1d96b4a8e3a 100644 --- a/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/distribution/index.ts @@ -20,6 +20,7 @@ function getBucketSize(max: number) { } export async function getTransactionDistribution({ + environment, serviceName, transactionName, transactionType, @@ -28,6 +29,7 @@ export async function getTransactionDistribution({ setup, searchAggregatedTransactions, }: { + environment?: string; serviceName: string; transactionName: string; transactionType: string; @@ -38,6 +40,7 @@ export async function getTransactionDistribution({ }) { return withApmSpan('get_transaction_latency_distribution', async () => { const distributionMax = await getDistributionMax({ + environment, serviceName, transactionName, transactionType, @@ -52,6 +55,7 @@ export async function getTransactionDistribution({ const bucketSize = getBucketSize(distributionMax); const { buckets, noHits } = await getBuckets({ + environment, serviceName, transactionName, transactionType, diff --git a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts index 002ddd1ec35f04..d566f3a169e78a 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts @@ -7,6 +7,7 @@ import { ESSearchResponse } from '../../../../../../typings/elasticsearch'; import { PromiseReturnType } from '../../../../../observability/typings/common'; +import { rangeQuery } from '../../../../common/utils/queries'; import { withApmSpan } from '../../../utils/with_apm_span'; import { Setup } from '../../helpers/setup_request'; @@ -40,15 +41,7 @@ export function anomalySeriesFetcher({ { terms: { result_type: ['model_plot', 'record'] } }, { term: { partition_field_value: serviceName } }, { term: { by_field_value: transactionType } }, - { - range: { - timestamp: { - gte: start, - lte: end, - format: 'epoch_millis', - }, - }, - }, + ...rangeQuery(start, end, 'timestamp'), ], }, }, diff --git a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/index.ts index 29dd562330cc15..a03b1ac82e90a6 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/index.ts @@ -18,20 +18,21 @@ import { ANOMALY_THRESHOLD } from '../../../../../ml/common'; import { withApmSpan } from '../../../utils/with_apm_span'; export async function getAnomalySeries({ + environment, serviceName, transactionType, transactionName, setup, logger, }: { + environment?: string; serviceName: string; transactionType: string; transactionName?: string; setup: Setup & SetupTimeRange; logger: Logger; }) { - const { uiFilters, start, end, ml } = setup; - const { environment } = uiFilters; + const { start, end, ml } = setup; // don't fetch anomalies if the ML plugin is not setup if (!ml) { @@ -45,18 +46,17 @@ export async function getAnomalySeries({ } // don't fetch anomalies when no specific environment is selected - if (environment === ENVIRONMENT_ALL.value) { + if (!environment || environment === ENVIRONMENT_ALL.value) { return undefined; } - // don't fetch anomalies if unknown uiFilters are applied - const knownFilters = ['environment', 'serviceName']; - const hasUnknownFiltersApplied = Object.entries(setup.uiFilters) - .filter(([key, value]) => !!value) - .map(([key]) => key) - .some((uiFilterName) => !knownFilters.includes(uiFilterName)); + // Don't fetch anomalies if uiFilters are applied. This filters out anything + // with empty values so `kuery: ''` returns false but `kuery: 'x:y'` returns true. + const hasUiFiltersApplied = + Object.entries(setup.uiFilters).filter(([_key, value]) => !!value).length > + 0; - if (hasUnknownFiltersApplied) { + if (hasUiFiltersApplied) { return undefined; } diff --git a/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts index ee27d00fdc0d4d..e1d3921d298c77 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts @@ -13,7 +13,7 @@ import { TRANSACTION_TYPE, } from '../../../../common/elasticsearch_fieldnames'; import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; -import { rangeFilter } from '../../../../common/utils/range_filter'; +import { environmentQuery, rangeQuery } from '../../../../common/utils/queries'; import { getDocumentTypeFilterForAggregatedTransactions, getProcessorEventForAggregatedTransactions, @@ -31,6 +31,7 @@ export type LatencyChartsSearchResponse = PromiseReturnType< >; function searchLatency({ + environment, serviceName, transactionType, transactionName, @@ -38,6 +39,7 @@ function searchLatency({ searchAggregatedTransactions, latencyAggregationType, }: { + environment?: string; serviceName: string; transactionType: string | undefined; transactionName: string | undefined; @@ -45,16 +47,17 @@ function searchLatency({ searchAggregatedTransactions: boolean; latencyAggregationType: LatencyAggregationType; }) { - const { start, end, apmEventClient } = setup; + const { esFilter, start, end, apmEventClient } = setup; const { intervalString } = getBucketSize({ start, end }); const filter: ESFilter[] = [ { term: { [SERVICE_NAME]: serviceName } }, - { range: rangeFilter(start, end) }, ...getDocumentTypeFilterForAggregatedTransactions( searchAggregatedTransactions ), - ...setup.esFilter, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...esFilter, ]; if (transactionName) { @@ -102,6 +105,7 @@ function searchLatency({ } export function getLatencyTimeseries({ + environment, serviceName, transactionType, transactionName, @@ -109,6 +113,7 @@ export function getLatencyTimeseries({ searchAggregatedTransactions, latencyAggregationType, }: { + environment?: string; serviceName: string; transactionType: string | undefined; transactionName: string | undefined; @@ -118,6 +123,7 @@ export function getLatencyTimeseries({ }) { return withApmSpan('get_latency_charts', async () => { const response = await searchLatency({ + environment, serviceName, transactionType, transactionName, diff --git a/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts index 7c1fa9c3a2368e..ec5dbf0eab3e9e 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_throughput_charts/index.ts @@ -13,7 +13,7 @@ import { TRANSACTION_RESULT, TRANSACTION_TYPE, } from '../../../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../../../../common/utils/range_filter'; +import { environmentQuery, rangeQuery } from '../../../../common/utils/queries'; import { getDocumentTypeFilterForAggregatedTransactions, getProcessorEventForAggregatedTransactions, @@ -28,6 +28,7 @@ export type ThroughputChartsResponse = PromiseReturnType< >; function searchThroughput({ + environment, serviceName, transactionType, transactionName, @@ -35,6 +36,7 @@ function searchThroughput({ searchAggregatedTransactions, intervalString, }: { + environment?: string; serviceName: string; transactionType: string; transactionName: string | undefined; @@ -42,16 +44,17 @@ function searchThroughput({ searchAggregatedTransactions: boolean; intervalString: string; }) { - const { start, end, apmEventClient } = setup; + const { esFilter, start, end, apmEventClient } = setup; const filter: ESFilter[] = [ { term: { [SERVICE_NAME]: serviceName } }, - { range: rangeFilter(start, end) }, + { term: { [TRANSACTION_TYPE]: transactionType } }, ...getDocumentTypeFilterForAggregatedTransactions( searchAggregatedTransactions ), - { term: { [TRANSACTION_TYPE]: transactionType } }, - ...setup.esFilter, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...esFilter, ]; if (transactionName) { @@ -91,12 +94,14 @@ function searchThroughput({ } export async function getThroughputCharts({ + environment, serviceName, transactionType, transactionName, setup, searchAggregatedTransactions, }: { + environment?: string; serviceName: string; transactionType: string; transactionName: string | undefined; @@ -107,6 +112,7 @@ export async function getThroughputCharts({ const { bucketSize, intervalString } = getBucketSize(setup); const response = await searchThroughput({ + environment, serviceName, transactionType, transactionName, diff --git a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts index 2fdb8e25fd9962..38d6b593dc72dc 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts @@ -9,7 +9,7 @@ import { TRACE_ID, TRANSACTION_ID, } from '../../../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../../../../common/utils/range_filter'; +import { rangeQuery } from '../../../../common/utils/queries'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { ProcessorEvent } from '../../../../common/processor_event'; import { withApmSpan } from '../../../utils/with_apm_span'; @@ -37,7 +37,7 @@ export function getTransaction({ filter: [ { term: { [TRANSACTION_ID]: transactionId } }, { term: { [TRACE_ID]: traceId } }, - { range: rangeFilter(start, end) }, + ...rangeQuery(start, end), ], }, }, diff --git a/x-pack/plugins/apm/server/projections/errors.ts b/x-pack/plugins/apm/server/projections/errors.ts index 082fd53a0ca93d..342d78608efbf6 100644 --- a/x-pack/plugins/apm/server/projections/errors.ts +++ b/x-pack/plugins/apm/server/projections/errors.ts @@ -10,13 +10,15 @@ import { SERVICE_NAME, ERROR_GROUP_ID, } from '../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../../common/utils/range_filter'; +import { environmentQuery, rangeQuery } from '../../common/utils/queries'; import { ProcessorEvent } from '../../common/processor_event'; export function getErrorGroupsProjection({ + environment, setup, serviceName, }: { + environment?: string; setup: Setup & SetupTimeRange; serviceName: string; }) { @@ -31,7 +33,8 @@ export function getErrorGroupsProjection({ bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, - { range: rangeFilter(start, end) }, + ...rangeQuery(start, end), + ...environmentQuery(environment), ...esFilter, ], }, diff --git a/x-pack/plugins/apm/server/projections/metrics.ts b/x-pack/plugins/apm/server/projections/metrics.ts index f6c3f85ed48075..a32c2ae46c870b 100644 --- a/x-pack/plugins/apm/server/projections/metrics.ts +++ b/x-pack/plugins/apm/server/projections/metrics.ts @@ -10,7 +10,7 @@ import { SERVICE_NAME, SERVICE_NODE_NAME, } from '../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../../common/utils/range_filter'; +import { environmentQuery, rangeQuery } from '../../common/utils/queries'; import { SERVICE_NODE_NAME_MISSING } from '../../common/service_nodes'; import { ProcessorEvent } from '../../common/processor_event'; @@ -27,10 +27,12 @@ function getServiceNodeNameFilters(serviceNodeName?: string) { } export function getMetricsProjection({ + environment, setup, serviceName, serviceNodeName, }: { + environment?: string; setup: Setup & SetupTimeRange; serviceName: string; serviceNodeName?: string; @@ -39,8 +41,9 @@ export function getMetricsProjection({ const filter = [ { term: { [SERVICE_NAME]: serviceName } }, - { range: rangeFilter(start, end) }, ...getServiceNodeNameFilters(serviceNodeName), + ...rangeQuery(start, end), + ...environmentQuery(environment), ...esFilter, ]; diff --git a/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts b/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts index ff8d868bc4abe5..1d5f7316b69ad6 100644 --- a/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts +++ b/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts @@ -11,7 +11,7 @@ import { TRANSACTION_TYPE, SERVICE_LANGUAGE_NAME, } from '../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../../common/utils/range_filter'; +import { rangeQuery } from '../../common/utils/queries'; import { ProcessorEvent } from '../../common/processor_event'; import { TRANSACTION_PAGE_LOAD } from '../../common/transaction_types'; @@ -28,7 +28,7 @@ export function getRumPageLoadTransactionsProjection({ const bool = { filter: [ - { range: rangeFilter(start, end) }, + ...rangeQuery(start, end), { term: { [TRANSACTION_TYPE]: TRANSACTION_PAGE_LOAD } }, ...(checkFetchStartFieldExists ? [ @@ -79,7 +79,7 @@ export function getRumErrorsProjection({ const bool = { filter: [ - { range: rangeFilter(start, end) }, + ...rangeQuery(start, end), { term: { [AGENT_NAME]: 'rum-js' } }, { term: { diff --git a/x-pack/plugins/apm/server/projections/services.ts b/x-pack/plugins/apm/server/projections/services.ts index 33ffb45a006378..a9f5a7efd0e67e 100644 --- a/x-pack/plugins/apm/server/projections/services.ts +++ b/x-pack/plugins/apm/server/projections/services.ts @@ -7,7 +7,7 @@ import { Setup, SetupTimeRange } from '../../server/lib/helpers/setup_request'; import { SERVICE_NAME } from '../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../../common/utils/range_filter'; +import { rangeQuery } from '../../common/utils/queries'; import { ProcessorEvent } from '../../common/processor_event'; import { getProcessorEventForAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; @@ -34,7 +34,7 @@ export function getServicesProjection({ size: 0, query: { bool: { - filter: [{ range: rangeFilter(start, end) }, ...esFilter], + filter: [...rangeQuery(start, end), ...esFilter], }, }, aggs: { diff --git a/x-pack/plugins/apm/server/projections/transactions.ts b/x-pack/plugins/apm/server/projections/transactions.ts index 76f2fc164e3fd2..45ed5d2865a67e 100644 --- a/x-pack/plugins/apm/server/projections/transactions.ts +++ b/x-pack/plugins/apm/server/projections/transactions.ts @@ -11,19 +11,21 @@ import { TRANSACTION_TYPE, TRANSACTION_NAME, } from '../../common/elasticsearch_fieldnames'; -import { rangeFilter } from '../../common/utils/range_filter'; +import { environmentQuery, rangeQuery } from '../../common/utils/queries'; import { getProcessorEventForAggregatedTransactions, getDocumentTypeFilterForAggregatedTransactions, } from '../lib/helpers/aggregated_transactions'; export function getTransactionsProjection({ + environment, setup, serviceName, transactionName, transactionType, searchAggregatedTransactions, }: { + environment?: string; setup: Setup & SetupTimeRange; serviceName?: string; transactionName?: string; @@ -44,14 +46,15 @@ export function getTransactionsProjection({ const bool = { filter: [ - { range: rangeFilter(start, end) }, + ...serviceNameFilter, ...transactionNameFilter, ...transactionTypeFilter, - ...serviceNameFilter, - ...esFilter, ...getDocumentTypeFilterForAggregatedTransactions( searchAggregatedTransactions ), + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...esFilter, ], }; diff --git a/x-pack/plugins/apm/server/routes/correlations.ts b/x-pack/plugins/apm/server/routes/correlations.ts index 3c2ff00153ce79..d4a0db3c0d6c7b 100644 --- a/x-pack/plugins/apm/server/routes/correlations.ts +++ b/x-pack/plugins/apm/server/routes/correlations.ts @@ -13,7 +13,7 @@ import { getCorrelationsForFailedTransactions } from '../lib/correlations/get_co import { getCorrelationsForSlowTransactions } from '../lib/correlations/get_correlations_for_slow_transactions'; import { setupRequest } from '../lib/helpers/setup_request'; import { createRoute } from './create_route'; -import { rangeRt } from './default_api_types'; +import { environmentRt, rangeRt } from './default_api_types'; const INVALID_LICENSE = i18n.translate( 'xpack.apm.significanTerms.license.text', @@ -37,6 +37,7 @@ export const correlationsForSlowTransactionsRoute = createRoute({ fieldNames: t.string, }), t.partial({ uiFilters: t.string }), + environmentRt, rangeRt, ]), }), @@ -47,6 +48,7 @@ export const correlationsForSlowTransactionsRoute = createRoute({ } const setup = await setupRequest(context, request); const { + environment, serviceName, transactionType, transactionName, @@ -55,6 +57,7 @@ export const correlationsForSlowTransactionsRoute = createRoute({ } = context.params.query; return getCorrelationsForSlowTransactions({ + environment, serviceName, transactionType, transactionName, @@ -78,6 +81,7 @@ export const correlationsForFailedTransactionsRoute = createRoute({ fieldNames: t.string, }), t.partial({ uiFilters: t.string }), + environmentRt, rangeRt, ]), }), @@ -88,14 +92,15 @@ export const correlationsForFailedTransactionsRoute = createRoute({ } const setup = await setupRequest(context, request); const { + environment, serviceName, transactionType, transactionName, - fieldNames, } = context.params.query; return getCorrelationsForFailedTransactions({ + environment, serviceName, transactionType, transactionName, diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index d22bcb1c501e0a..822a45fca269fd 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -11,6 +11,7 @@ import { apmIndexPatternTitleRoute, } from './index_pattern'; import { createApi } from './create_api'; +import { environmentsRoute } from './environments'; import { errorDistributionRoute, errorGroupsRoute, @@ -62,14 +63,10 @@ import { transactionChartsErrorRateRoute, transactionGroupsRoute, transactionGroupsPrimaryStatisticsRoute, - transactionLatencyChatsRoute, - transactionThroughputChatsRoute, + transactionLatencyChartsRoute, + transactionThroughputChartsRoute, transactionGroupsComparisonStatisticsRoute, } from './transactions'; -import { - rumOverviewLocalFiltersRoute, - uiFiltersEnvironmentsRoute, -} from './ui_filters'; import { serviceMapRoute, serviceMapServiceNodeRoute } from './service_map'; import { createCustomLinkRoute, @@ -92,6 +89,7 @@ import { rumClientMetricsRoute, rumJSErrors, rumLongTaskMetrics, + rumOverviewLocalFiltersRoute, rumPageLoadDistBreakdownRoute, rumPageLoadDistributionRoute, rumPageViewsTrendRoute, @@ -113,6 +111,9 @@ const createApmApi = () => { .add(dynamicIndexPatternRoute) .add(apmIndexPatternTitleRoute) + // Environments + .add(environmentsRoute) + // Errors .add(errorDistributionRoute) .add(errorGroupsRoute) @@ -166,13 +167,10 @@ const createApmApi = () => { .add(transactionChartsErrorRateRoute) .add(transactionGroupsRoute) .add(transactionGroupsPrimaryStatisticsRoute) - .add(transactionLatencyChatsRoute) - .add(transactionThroughputChatsRoute) + .add(transactionLatencyChartsRoute) + .add(transactionThroughputChartsRoute) .add(transactionGroupsComparisonStatisticsRoute) - // UI filters - .add(uiFiltersEnvironmentsRoute) - // Service map .add(serviceMapRoute) .add(serviceMapServiceNodeRoute) diff --git a/x-pack/plugins/apm/server/routes/default_api_types.ts b/x-pack/plugins/apm/server/routes/default_api_types.ts index fdc1e8ebe5a55f..990b462a520d23 100644 --- a/x-pack/plugins/apm/server/routes/default_api_types.ts +++ b/x-pack/plugins/apm/server/routes/default_api_types.ts @@ -18,4 +18,6 @@ export const comparisonRangeRt = t.partial({ comparisonEnd: isoToEpochRt, }); +export const environmentRt = t.partial({ environment: t.string }); + export const uiFiltersRt = t.type({ uiFilters: t.string }); diff --git a/x-pack/plugins/apm/server/routes/environments.ts b/x-pack/plugins/apm/server/routes/environments.ts new file mode 100644 index 00000000000000..448591f7e143ff --- /dev/null +++ b/x-pack/plugins/apm/server/routes/environments.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as t from 'io-ts'; +import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; +import { setupRequest } from '../lib/helpers/setup_request'; +import { getEnvironments } from '../lib/environments/get_environments'; +import { createRoute } from './create_route'; +import { rangeRt } from './default_api_types'; + +export const environmentsRoute = createRoute({ + endpoint: 'GET /api/apm/environments', + params: t.type({ + query: t.intersection([ + t.partial({ + serviceName: t.string, + }), + rangeRt, + ]), + }), + options: { tags: ['access:apm'] }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { serviceName } = context.params.query; + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + + return getEnvironments({ + setup, + serviceName, + searchAggregatedTransactions, + }); + }, +}); diff --git a/x-pack/plugins/apm/server/routes/errors.ts b/x-pack/plugins/apm/server/routes/errors.ts index cc9db2e6a48551..073a91bfe15487 100644 --- a/x-pack/plugins/apm/server/routes/errors.ts +++ b/x-pack/plugins/apm/server/routes/errors.ts @@ -11,7 +11,7 @@ import { getErrorDistribution } from '../lib/errors/distribution/get_distributio import { getErrorGroupSample } from '../lib/errors/get_error_group_sample'; import { getErrorGroups } from '../lib/errors/get_error_groups'; import { setupRequest } from '../lib/helpers/setup_request'; -import { uiFiltersRt, rangeRt } from './default_api_types'; +import { environmentRt, uiFiltersRt, rangeRt } from './default_api_types'; export const errorsRoute = createRoute({ endpoint: 'GET /api/apm/services/{serviceName}/errors', @@ -24,6 +24,7 @@ export const errorsRoute = createRoute({ sortField: t.string, sortDirection: t.union([t.literal('asc'), t.literal('desc')]), }), + environmentRt, uiFiltersRt, rangeRt, ]), @@ -33,9 +34,10 @@ export const errorsRoute = createRoute({ const setup = await setupRequest(context, request); const { params } = context; const { serviceName } = params.path; - const { sortField, sortDirection } = params.query; + const { environment, sortField, sortDirection } = params.query; return getErrorGroups({ + environment, serviceName, sortField, sortDirection, @@ -51,13 +53,15 @@ export const errorGroupsRoute = createRoute({ serviceName: t.string, groupId: t.string, }), - query: t.intersection([uiFiltersRt, rangeRt]), + query: t.intersection([environmentRt, uiFiltersRt, rangeRt]), }), options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName, groupId } = context.params.path; - return getErrorGroupSample({ serviceName, groupId, setup }); + const { environment } = context.params.query; + + return getErrorGroupSample({ environment, serviceName, groupId, setup }); }, }); @@ -71,6 +75,7 @@ export const errorDistributionRoute = createRoute({ t.partial({ groupId: t.string, }), + environmentRt, uiFiltersRt, rangeRt, ]), @@ -80,7 +85,7 @@ export const errorDistributionRoute = createRoute({ const setup = await setupRequest(context, request); const { params } = context; const { serviceName } = params.path; - const { groupId } = params.query; - return getErrorDistribution({ serviceName, groupId, setup }); + const { environment, groupId } = params.query; + return getErrorDistribution({ environment, serviceName, groupId, setup }); }, }); diff --git a/x-pack/plugins/apm/server/routes/metrics.ts b/x-pack/plugins/apm/server/routes/metrics.ts index d07504a1046ee5..08376ed0e37ffb 100644 --- a/x-pack/plugins/apm/server/routes/metrics.ts +++ b/x-pack/plugins/apm/server/routes/metrics.ts @@ -9,7 +9,7 @@ import * as t from 'io-ts'; import { setupRequest } from '../lib/helpers/setup_request'; import { getMetricsChartDataByAgent } from '../lib/metrics/get_metrics_chart_data_by_agent'; import { createRoute } from './create_route'; -import { uiFiltersRt, rangeRt } from './default_api_types'; +import { environmentRt, uiFiltersRt, rangeRt } from './default_api_types'; export const metricsChartsRoute = createRoute({ endpoint: `GET /api/apm/services/{serviceName}/metrics/charts`, @@ -24,6 +24,7 @@ export const metricsChartsRoute = createRoute({ t.partial({ serviceNodeName: t.string, }), + environmentRt, uiFiltersRt, rangeRt, ]), @@ -33,8 +34,9 @@ export const metricsChartsRoute = createRoute({ const setup = await setupRequest(context, request); const { params } = context; const { serviceName } = params.path; - const { agentName, serviceNodeName } = params.query; + const { agentName, environment, serviceNodeName } = params.query; return await getMetricsChartDataByAgent({ + environment, setup, serviceName, agentName, diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts index 69e169e96af78a..c9fa4253bb58e2 100644 --- a/x-pack/plugins/apm/server/routes/rum_client.ts +++ b/x-pack/plugins/apm/server/routes/rum_client.ts @@ -6,20 +6,33 @@ */ import * as t from 'io-ts'; -import { createRoute } from './create_route'; -import { setupRequest } from '../lib/helpers/setup_request'; +import { omit } from 'lodash'; +import { jsonRt } from '../../common/runtime_types/json_rt'; +import { LocalUIFilterName } from '../../common/ui_filter'; +import { getEsFilter } from '../lib/helpers/convert_ui_filters/get_es_filter'; +import { + Setup, + setupRequest, + SetupTimeRange, +} from '../lib/helpers/setup_request'; import { getClientMetrics } from '../lib/rum_client/get_client_metrics'; -import { rangeRt, uiFiltersRt } from './default_api_types'; -import { getPageViewTrends } from '../lib/rum_client/get_page_view_trends'; +import { getJSErrors } from '../lib/rum_client/get_js_errors'; +import { getLongTaskMetrics } from '../lib/rum_client/get_long_task_metrics'; import { getPageLoadDistribution } from '../lib/rum_client/get_page_load_distribution'; +import { getPageViewTrends } from '../lib/rum_client/get_page_view_trends'; import { getPageLoadDistBreakdown } from '../lib/rum_client/get_pl_dist_breakdown'; import { getRumServices } from '../lib/rum_client/get_rum_services'; +import { getUrlSearch } from '../lib/rum_client/get_url_search'; import { getVisitorBreakdown } from '../lib/rum_client/get_visitor_breakdown'; import { getWebCoreVitals } from '../lib/rum_client/get_web_core_vitals'; -import { getJSErrors } from '../lib/rum_client/get_js_errors'; -import { getLongTaskMetrics } from '../lib/rum_client/get_long_task_metrics'; -import { getUrlSearch } from '../lib/rum_client/get_url_search'; import { hasRumData } from '../lib/rum_client/has_rum_data'; +import { getLocalUIFilters } from '../lib/rum_client/ui_filters/local_ui_filters'; +import { localUIFilterNames } from '../lib/rum_client/ui_filters/local_ui_filters/config'; +import { getRumPageLoadTransactionsProjection } from '../projections/rum_page_load_transactions'; +import { Projection } from '../projections/typings'; +import { createRoute } from './create_route'; +import { rangeRt, uiFiltersRt } from './default_api_types'; +import { APMRequestHandlerContext } from './typings'; export const percentileRangeRt = t.partial({ minPercentile: t.string, @@ -253,3 +266,96 @@ export const rumHasDataRoute = createRoute({ return await hasRumData({ setup }); }, }); + +// Everything below here was originally in ui_filters.ts but now is here, since +// UX is the only part of APM using UI filters now. + +const filterNamesRt = t.type({ + filterNames: jsonRt.pipe( + t.array( + t.keyof( + Object.fromEntries( + localUIFilterNames.map((filterName) => [filterName, null]) + ) as Record + ) + ) + ), +}); + +const localUiBaseQueryRt = t.intersection([ + filterNamesRt, + uiFiltersRt, + rangeRt, +]); + +function createLocalFiltersRoute< + TEndpoint extends string, + TProjection extends Projection, + TQueryRT extends t.HasProps +>({ + endpoint, + getProjection, + queryRt, +}: { + endpoint: TEndpoint; + getProjection: GetProjection< + TProjection, + t.IntersectionC<[TQueryRT, BaseQueryType]> + >; + queryRt: TQueryRT; +}) { + return createRoute({ + endpoint, + params: t.type({ + query: t.intersection([localUiBaseQueryRt, queryRt]), + }), + options: { tags: ['access:apm'] }, + handler: async ({ context, request }) => { + const setup = await setupRequest(context, request); + const { uiFilters } = setup; + const { query } = context.params; + + const { filterNames } = query; + const projection = await getProjection({ + query, + context, + setup: { + ...setup, + esFilter: getEsFilter(omit(uiFilters, filterNames)), + }, + }); + + return getLocalUIFilters({ + projection, + setup, + uiFilters, + localFilterNames: filterNames, + }); + }, + }); +} + +export const rumOverviewLocalFiltersRoute = createLocalFiltersRoute({ + endpoint: 'GET /api/apm/rum/local_filters', + getProjection: async ({ setup }) => { + return getRumPageLoadTransactionsProjection({ + setup, + }); + }, + queryRt: t.type({}), +}); + +type BaseQueryType = typeof localUiBaseQueryRt; + +type GetProjection< + TProjection extends Projection, + TQueryRT extends t.HasProps +> = ({ + query, + setup, + context, +}: { + query: t.TypeOf; + setup: Setup & SetupTimeRange; + context: APMRequestHandlerContext; +}) => Promise | TProjection; diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts index 7cca6cd0a19430..65c7b245958f32 100644 --- a/x-pack/plugins/apm/server/routes/service_map.ts +++ b/x-pack/plugins/apm/server/routes/service_map.ts @@ -12,7 +12,7 @@ import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceMap } from '../lib/service_map/get_service_map'; import { getServiceMapServiceNodeInfo } from '../lib/service_map/get_service_map_service_node_info'; import { createRoute } from './create_route'; -import { rangeRt, uiFiltersRt } from './default_api_types'; +import { environmentRt, rangeRt, uiFiltersRt } from './default_api_types'; import { notifyFeatureUsage } from '../feature'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { isActivePlatinumLicense } from '../../common/license_check'; @@ -22,9 +22,9 @@ export const serviceMapRoute = createRoute({ params: t.type({ query: t.intersection([ t.partial({ - environment: t.string, serviceName: t.string, }), + environmentRt, rangeRt, ]), }), diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index ff064e0571d138..24c7c6e3e23d7e 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -25,23 +25,33 @@ import { getServiceTransactionTypes } from '../lib/services/get_service_transact import { getThroughput } from '../lib/services/get_throughput'; import { offsetPreviousPeriodCoordinates } from '../utils/offset_previous_period_coordinate'; import { createRoute } from './create_route'; -import { comparisonRangeRt, rangeRt, uiFiltersRt } from './default_api_types'; +import { + comparisonRangeRt, + environmentRt, + rangeRt, + uiFiltersRt, +} from './default_api_types'; import { withApmSpan } from '../utils/with_apm_span'; +import { + latencyAggregationTypeRt, + LatencyAggregationType, +} from '../../common/latency_aggregation_types'; export const servicesRoute = createRoute({ endpoint: 'GET /api/apm/services', params: t.type({ - query: t.intersection([uiFiltersRt, rangeRt]), + query: t.intersection([environmentRt, uiFiltersRt, rangeRt]), }), options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - + const { environment } = context.params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup ); const services = await getServices({ + environment, setup, searchAggregatedTransactions, logger: context.logger, @@ -273,6 +283,7 @@ export const serviceErrorGroupsRoute = createRoute({ serviceName: t.string, }), query: t.intersection([ + environmentRt, rangeRt, uiFiltersRt, t.type({ @@ -296,6 +307,7 @@ export const serviceErrorGroupsRoute = createRoute({ const { path: { serviceName }, query: { + environment, numBuckets, pageIndex, size, @@ -304,7 +316,9 @@ export const serviceErrorGroupsRoute = createRoute({ transactionType, }, } = context.params; + return getServiceErrorGroups({ + environment, serviceName, setup, size, @@ -325,6 +339,7 @@ export const serviceThroughputRoute = createRoute({ }), query: t.intersection([ t.type({ transactionType: t.string }), + environmentRt, uiFiltersRt, rangeRt, comparisonRangeRt, @@ -335,6 +350,7 @@ export const serviceThroughputRoute = createRoute({ const setup = await setupRequest(context, request); const { serviceName } = context.params.path; const { + environment, transactionType, comparisonStart, comparisonEnd, @@ -355,12 +371,14 @@ export const serviceThroughputRoute = createRoute({ const [currentPeriod, previousPeriod] = await Promise.all([ getThroughput({ ...commonProps, + environment, start, end, }), comparisonStart && comparisonEnd ? getThroughput({ ...commonProps, + environment, start: comparisonStart, end: comparisonEnd, }).then((coordinates) => @@ -387,7 +405,12 @@ export const serviceInstancesRoute = createRoute({ serviceName: t.string, }), query: t.intersection([ - t.type({ transactionType: t.string, numBuckets: toNumberRt }), + t.type({ + latencyAggregationType: latencyAggregationTypeRt, + transactionType: t.string, + numBuckets: toNumberRt, + }), + environmentRt, uiFiltersRt, rangeRt, ]), @@ -396,13 +419,17 @@ export const serviceInstancesRoute = createRoute({ handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.path; - const { transactionType, numBuckets } = context.params.query; + const { environment, transactionType, numBuckets } = context.params.query; + const latencyAggregationType = (context.params.query + .latencyAggregationType as unknown) as LatencyAggregationType; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup ); return getServiceInstances({ + environment, + latencyAggregationType, serviceName, setup, transactionType, @@ -420,9 +447,9 @@ export const serviceDependenciesRoute = createRoute({ }), query: t.intersection([ t.type({ - environment: t.string, numBuckets: toNumberRt, }), + environmentRt, rangeRt, ]), }), diff --git a/x-pack/plugins/apm/server/routes/traces.ts b/x-pack/plugins/apm/server/routes/traces.ts index 722675906487ce..5d3f99be7af344 100644 --- a/x-pack/plugins/apm/server/routes/traces.ts +++ b/x-pack/plugins/apm/server/routes/traces.ts @@ -10,23 +10,25 @@ import { setupRequest } from '../lib/helpers/setup_request'; import { getTrace } from '../lib/traces/get_trace'; import { getTransactionGroupList } from '../lib/transaction_groups'; import { createRoute } from './create_route'; -import { rangeRt, uiFiltersRt } from './default_api_types'; +import { environmentRt, rangeRt, uiFiltersRt } from './default_api_types'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { getRootTransactionByTraceId } from '../lib/transactions/get_transaction_by_trace'; export const tracesRoute = createRoute({ endpoint: 'GET /api/apm/traces', params: t.type({ - query: t.intersection([rangeRt, uiFiltersRt]), + query: t.intersection([environmentRt, rangeRt, uiFiltersRt]), }), options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); + const { environment } = context.params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup ); + return getTransactionGroupList( - { type: 'top_traces', searchAggregatedTransactions }, + { environment, type: 'top_traces', searchAggregatedTransactions }, setup ); }, diff --git a/x-pack/plugins/apm/server/routes/transactions.ts b/x-pack/plugins/apm/server/routes/transactions.ts index bef96cb7f0767e..5a4be216a817c2 100644 --- a/x-pack/plugins/apm/server/routes/transactions.ts +++ b/x-pack/plugins/apm/server/routes/transactions.ts @@ -5,7 +5,6 @@ * 2.0. */ -import Boom from '@hapi/boom'; import * as t from 'io-ts'; import { LatencyAggregationType, @@ -25,7 +24,7 @@ import { getThroughputCharts } from '../lib/transactions/get_throughput_charts'; import { getTransactionGroupList } from '../lib/transaction_groups'; import { getErrorRate } from '../lib/transaction_groups/get_error_rate'; import { createRoute } from './create_route'; -import { rangeRt, uiFiltersRt } from './default_api_types'; +import { environmentRt, rangeRt, uiFiltersRt } from './default_api_types'; /** * Returns a list of transactions grouped by name @@ -39,6 +38,7 @@ export const transactionGroupsRoute = createRoute({ }), query: t.intersection([ t.type({ transactionType: t.string }), + environmentRt, uiFiltersRt, rangeRt, ]), @@ -47,7 +47,7 @@ export const transactionGroupsRoute = createRoute({ handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.path; - const { transactionType } = context.params.query; + const { environment, transactionType } = context.params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup @@ -55,6 +55,7 @@ export const transactionGroupsRoute = createRoute({ return getTransactionGroupList( { + environment, type: 'top_transactions', serviceName, transactionType, @@ -71,6 +72,7 @@ export const transactionGroupsPrimaryStatisticsRoute = createRoute({ params: t.type({ path: t.type({ serviceName: t.string }), query: t.intersection([ + environmentRt, rangeRt, uiFiltersRt, t.type({ @@ -91,10 +93,11 @@ export const transactionGroupsPrimaryStatisticsRoute = createRoute({ const { path: { serviceName }, - query: { latencyAggregationType, transactionType }, + query: { environment, latencyAggregationType, transactionType }, } = context.params; return getServiceTransactionGroups({ + environment, setup, serviceName, searchAggregatedTransactions, @@ -110,6 +113,7 @@ export const transactionGroupsComparisonStatisticsRoute = createRoute({ params: t.type({ path: t.type({ serviceName: t.string }), query: t.intersection([ + environmentRt, rangeRt, uiFiltersRt, t.type({ @@ -133,6 +137,7 @@ export const transactionGroupsComparisonStatisticsRoute = createRoute({ const { path: { serviceName }, query: { + environment, transactionNames, latencyAggregationType, numBuckets, @@ -141,6 +146,7 @@ export const transactionGroupsComparisonStatisticsRoute = createRoute({ } = context.params; return getServiceTransactionGroupComparisonStatistics({ + environment, setup, serviceName, transactionNames, @@ -152,7 +158,7 @@ export const transactionGroupsComparisonStatisticsRoute = createRoute({ }, }); -export const transactionLatencyChatsRoute = createRoute({ +export const transactionLatencyChartsRoute = createRoute({ endpoint: 'GET /api/apm/services/{serviceName}/transactions/charts/latency', params: t.type({ path: t.type({ @@ -166,6 +172,7 @@ export const transactionLatencyChatsRoute = createRoute({ transactionType: t.string, latencyAggregationType: latencyAggregationTypeRt, }), + environmentRt, uiFiltersRt, rangeRt, ]), @@ -176,22 +183,18 @@ export const transactionLatencyChatsRoute = createRoute({ const logger = context.logger; const { serviceName } = context.params.path; const { + environment, transactionType, transactionName, latencyAggregationType, } = context.params.query; - if (!setup.uiFilters.environment) { - throw Boom.badRequest( - `environment is a required property of the ?uiFilters JSON for transaction_groups/charts.` - ); - } - const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup ); const options = { + environment, serviceName, transactionType, transactionName, @@ -222,7 +225,7 @@ export const transactionLatencyChatsRoute = createRoute({ }, }); -export const transactionThroughputChatsRoute = createRoute({ +export const transactionThroughputChartsRoute = createRoute({ endpoint: 'GET /api/apm/services/{serviceName}/transactions/charts/throughput', params: t.type({ @@ -234,25 +237,25 @@ export const transactionThroughputChatsRoute = createRoute({ t.partial({ transactionName: t.string }), uiFiltersRt, rangeRt, + environmentRt, ]), }), options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.path; - const { transactionType, transactionName } = context.params.query; - - if (!setup.uiFilters.environment) { - throw Boom.badRequest( - `environment is a required property of the ?uiFilters JSON for transaction_groups/charts.` - ); - } + const { + environment, + transactionType, + transactionName, + } = context.params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup ); return await getThroughputCharts({ + environment, serviceName, transactionType, transactionName, @@ -278,6 +281,7 @@ export const transactionChartsDistributionRoute = createRoute({ transactionId: t.string, traceId: t.string, }), + environmentRt, uiFiltersRt, rangeRt, ]), @@ -287,6 +291,7 @@ export const transactionChartsDistributionRoute = createRoute({ const setup = await setupRequest(context, request); const { serviceName } = context.params.path; const { + environment, transactionType, transactionName, transactionId = '', @@ -298,6 +303,7 @@ export const transactionChartsDistributionRoute = createRoute({ ); return getTransactionDistribution({ + environment, serviceName, transactionType, transactionName, @@ -318,6 +324,7 @@ export const transactionChartsBreakdownRoute = createRoute({ query: t.intersection([ t.type({ transactionType: t.string }), t.partial({ transactionName: t.string }), + environmentRt, uiFiltersRt, rangeRt, ]), @@ -326,9 +333,14 @@ export const transactionChartsBreakdownRoute = createRoute({ handler: async ({ context, request }) => { const setup = await setupRequest(context, request); const { serviceName } = context.params.path; - const { transactionName, transactionType } = context.params.query; + const { + environment, + transactionName, + transactionType, + } = context.params.query; return getTransactionBreakdown({ + environment, serviceName, transactionName, transactionType, @@ -345,6 +357,7 @@ export const transactionChartsErrorRateRoute = createRoute({ serviceName: t.string, }), query: t.intersection([ + environmentRt, uiFiltersRt, rangeRt, t.type({ transactionType: t.string }), @@ -356,13 +369,14 @@ export const transactionChartsErrorRateRoute = createRoute({ const setup = await setupRequest(context, request); const { params } = context; const { serviceName } = params.path; - const { transactionType, transactionName } = params.query; + const { environment, transactionType, transactionName } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions( setup ); return getErrorRate({ + environment, serviceName, transactionType, transactionName, diff --git a/x-pack/plugins/apm/server/routes/ui_filters.ts b/x-pack/plugins/apm/server/routes/ui_filters.ts deleted file mode 100644 index b14a47e302caa9..00000000000000 --- a/x-pack/plugins/apm/server/routes/ui_filters.ts +++ /dev/null @@ -1,142 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { omit } from 'lodash'; -import { jsonRt } from '../../common/runtime_types/json_rt'; -import { LocalUIFilterName } from '../../common/ui_filter'; -import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; -import { getEsFilter } from '../lib/helpers/convert_ui_filters/get_es_filter'; -import { - Setup, - setupRequest, - SetupTimeRange, -} from '../lib/helpers/setup_request'; -import { getEnvironments } from '../lib/ui_filters/get_environments'; -import { getLocalUIFilters } from '../lib/ui_filters/local_ui_filters'; -import { localUIFilterNames } from '../lib/ui_filters/local_ui_filters/config'; -import { getRumPageLoadTransactionsProjection } from '../projections/rum_page_load_transactions'; -import { Projection } from '../projections/typings'; -import { createRoute } from './create_route'; -import { rangeRt, uiFiltersRt } from './default_api_types'; -import { APMRequestHandlerContext } from './typings'; - -export const uiFiltersEnvironmentsRoute = createRoute({ - endpoint: 'GET /api/apm/ui_filters/environments', - params: t.type({ - query: t.intersection([ - t.partial({ - serviceName: t.string, - }), - rangeRt, - ]), - }), - options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { serviceName } = context.params.query; - const searchAggregatedTransactions = await getSearchAggregatedTransactions( - setup - ); - - return getEnvironments({ - setup, - serviceName, - searchAggregatedTransactions, - }); - }, -}); - -const filterNamesRt = t.type({ - filterNames: jsonRt.pipe( - t.array( - t.keyof( - Object.fromEntries( - localUIFilterNames.map((filterName) => [filterName, null]) - ) as Record - ) - ) - ), -}); - -const localUiBaseQueryRt = t.intersection([ - filterNamesRt, - uiFiltersRt, - rangeRt, -]); - -function createLocalFiltersRoute< - TEndpoint extends string, - TProjection extends Projection, - TQueryRT extends t.HasProps ->({ - endpoint, - getProjection, - queryRt, -}: { - endpoint: TEndpoint; - getProjection: GetProjection< - TProjection, - t.IntersectionC<[TQueryRT, BaseQueryType]> - >; - queryRt: TQueryRT; -}) { - return createRoute({ - endpoint, - params: t.type({ - query: t.intersection([localUiBaseQueryRt, queryRt]), - }), - options: { tags: ['access:apm'] }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { uiFilters } = setup; - const { query } = context.params; - - const { filterNames } = query; - const projection = await getProjection({ - query, - context, - setup: { - ...setup, - esFilter: getEsFilter(omit(uiFilters, filterNames)), - }, - }); - - return getLocalUIFilters({ - projection, - setup, - uiFilters, - localFilterNames: filterNames, - }); - }, - }); -} - -export const rumOverviewLocalFiltersRoute = createLocalFiltersRoute({ - endpoint: 'GET /api/apm/ui_filters/local_filters/rumOverview', - getProjection: async ({ setup }) => { - return getRumPageLoadTransactionsProjection({ - setup, - }); - }, - queryRt: t.type({}), -}); - -type BaseQueryType = typeof localUiBaseQueryRt; - -type GetProjection< - TProjection extends Projection, - TQueryRT extends t.HasProps -> = ({ - query, - setup, - context, -}: { - query: t.TypeOf; - setup: Setup & SetupTimeRange; - context: APMRequestHandlerContext; -}) => Promise | TProjection; diff --git a/x-pack/plugins/apm/server/ui_settings.ts b/x-pack/plugins/apm/server/ui_settings.ts index a52cdbcc4f0795..5952cdb7022951 100644 --- a/x-pack/plugins/apm/server/ui_settings.ts +++ b/x-pack/plugins/apm/server/ui_settings.ts @@ -8,29 +8,12 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { UiSettingsParams } from '../../../../src/core/types'; -import { - enableCorrelations, - enableServiceOverview, -} from '../common/ui_settings_keys'; +import { enableServiceOverview } from '../common/ui_settings_keys'; /** * uiSettings definitions for APM. */ export const uiSettings: Record> = { - [enableCorrelations]: { - category: ['observability'], - name: i18n.translate('xpack.apm.enableCorrelationsExperimentName', { - defaultMessage: 'APM correlations (Platinum required)', - }), - value: false, - description: i18n.translate( - 'xpack.apm.enableCorrelationsExperimentDescription', - { - defaultMessage: 'Enable the experimental correlations feature in APM', - } - ), - schema: schema.boolean(), - }, [enableServiceOverview]: { category: ['observability'], name: i18n.translate('xpack.apm.enableServiceOverviewExperimentName', { diff --git a/x-pack/plugins/apm/server/utils/test_helpers.tsx b/x-pack/plugins/apm/server/utils/test_helpers.tsx index 19ac562121d9dc..4df638cc2c5df2 100644 --- a/x-pack/plugins/apm/server/utils/test_helpers.tsx +++ b/x-pack/plugins/apm/server/utils/test_helpers.tsx @@ -88,7 +88,7 @@ export async function inspectSearchParams( }, } ) as APMConfig, - uiFilters: { environment: 'test' }, + uiFilters: {}, esFilter: [{ term: { 'service.environment': 'test' } }], indices: { /* eslint-disable @typescript-eslint/naming-convention */ diff --git a/x-pack/plugins/beats_management/public/components/table/controls/action_control.tsx b/x-pack/plugins/beats_management/public/components/table/controls/action_control.tsx index c498f9e06e1d36..5badef9a71fe17 100644 --- a/x-pack/plugins/beats_management/public/components/table/controls/action_control.tsx +++ b/x-pack/plugins/beats_management/public/components/table/controls/action_control.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiButton, EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiButton, EuiConfirmModal } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; import { AssignmentActionType } from '../table'; @@ -58,40 +58,38 @@ export class ActionControl extends React.PureComponent {this.state.showModal && ( - - + } + confirmButtonText={ + + } + onConfirm={() => { + actionHandler(action); + this.setState({ showModal: false }); + }} + onCancel={() => this.setState({ showModal: false })} + title={ + warningHeading ? ( + warningHeading + ) : ( - } - confirmButtonText={ - - } - onConfirm={() => { - actionHandler(action); - this.setState({ showModal: false }); - }} - onCancel={() => this.setState({ showModal: false })} - title={ - warningHeading ? ( - warningHeading - ) : ( - - ) - } - > - {warningMessage} - - + ) + } + > + {warningMessage} + )}
    ); diff --git a/x-pack/plugins/beats_management/public/pages/overview/enrolled_beats.tsx b/x-pack/plugins/beats_management/public/pages/overview/enrolled_beats.tsx index f09d34eaa6e614..0ab02430e90e61 100644 --- a/x-pack/plugins/beats_management/public/pages/overview/enrolled_beats.tsx +++ b/x-pack/plugins/beats_management/public/pages/overview/enrolled_beats.tsx @@ -13,7 +13,6 @@ import { EuiModalBody, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; @@ -104,58 +103,56 @@ class BeatsPageComponent extends React.PureComponent { {this.props.location.pathname === '/overview/enrolled_beats/enroll' && ( - - { - this.props.setUrlState({ - enrollmentToken: '', - }); - this.props.goTo(`/overview/enrolled_beats`); - }} - style={{ width: '640px' }} - > - - - - - - - { - const enrollmentTokens = await this.props.libs.tokens.createEnrollmentTokens(); - this.props.setUrlState({ - enrollmentToken: enrollmentTokens[0], - }); - }} - onBeatEnrolled={() => { - this.props.setUrlState({ - enrollmentToken: '', - }); - }} + { + this.props.setUrlState({ + enrollmentToken: '', + }); + this.props.goTo(`/overview/enrolled_beats`); + }} + style={{ width: '640px' }} + > + + + - {!this.props.urlState.enrollmentToken && ( - - { - this.props.goTo('/overview/enrolled_beats'); - }} - > - Done - - - )} - - - + + + + { + const enrollmentTokens = await this.props.libs.tokens.createEnrollmentTokens(); + this.props.setUrlState({ + enrollmentToken: enrollmentTokens[0], + }); + }} + onBeatEnrolled={() => { + this.props.setUrlState({ + enrollmentToken: '', + }); + }} + /> + {!this.props.urlState.enrollmentToken && ( + + { + this.props.goTo('/overview/enrolled_beats'); + }} + > + Done + + + )} + + )} ); diff --git a/x-pack/plugins/beats_management/server/routes/tokens/create.ts b/x-pack/plugins/beats_management/server/routes/tokens/create.ts index d61e96900e04b8..c44f9c2dd4e7dc 100644 --- a/x-pack/plugins/beats_management/server/routes/tokens/create.ts +++ b/x-pack/plugins/beats_management/server/routes/tokens/create.ts @@ -50,11 +50,7 @@ export const registerCreateTokenRoute = (router: BeatsManagementRouter) => { }); } catch (err) { beatsManagement.framework.log(err.message); - return response.internalError({ - body: { - message: 'An error occurred, please check your Kibana logs', - }, - }); + throw new Error('An error occurred, please check your Kibana logs'); } } ) diff --git a/x-pack/plugins/canvas/i18n/components.ts b/x-pack/plugins/canvas/i18n/components.ts index 88ea8700bd464a..afd3d1408e1f1a 100644 --- a/x-pack/plugins/canvas/i18n/components.ts +++ b/x-pack/plugins/canvas/i18n/components.ts @@ -1752,6 +1752,12 @@ export const ComponentStrings = { description: 'This column in the table contains the date/time the workpad was last updated.', }), + getTableActionsColumnTitle: () => + i18n.translate('xpack.canvas.workpadLoader.table.actionsColumnTitle', { + defaultMessage: 'Actions', + description: + 'This column in the table contains the actions that can be taken on a workpad.', + }), }, WorkpadManager: { getModalTitle: () => diff --git a/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx b/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx index f06c305e47beaa..7795aa9671b83d 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx +++ b/x-pack/plugins/canvas/public/components/asset_manager/asset_manager.component.tsx @@ -19,7 +19,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiPanel, EuiProgress, EuiSpacer, @@ -75,71 +74,69 @@ export const AssetManager: FC = (props) => { }; return ( - - onClose()} - className="canvasAssetManager canvasModal--fixedSize" - maxWidth="1000px" - > - - - {strings.getModalTitle()} - - - - {isLoading ? ( - - ) : ( - - )} - - - - - -

    {strings.getDescription()}

    -
    - - {assets.length ? ( - - {assets.map((asset) => ( - - ))} - - ) : ( - emptyAssets - )} -
    - - - - onClose()} + className="canvasAssetManager canvasModal--fixedSize" + maxWidth="1000px" + > + + + {strings.getModalTitle()} + + + + {isLoading ? ( + + ) : ( + - - - - {strings.getSpaceUsedText(percentageUsed)} - - - - onClose()}> - {strings.getModalCloseButtonLabel()} - - -
    -
    + )} +
    +
    + + + +

    {strings.getDescription()}

    +
    + + {assets.length ? ( + + {assets.map((asset) => ( + + ))} + + ) : ( + emptyAssets + )} +
    + + + + + + + + {strings.getSpaceUsedText(percentageUsed)} + + + + onClose()}> + {strings.getModalCloseButtonLabel()} + + + ); }; diff --git a/x-pack/plugins/canvas/public/components/confirm_modal/confirm_modal.tsx b/x-pack/plugins/canvas/public/components/confirm_modal/confirm_modal.tsx index 38be3b8559af2c..3156b14f209f1a 100644 --- a/x-pack/plugins/canvas/public/components/confirm_modal/confirm_modal.tsx +++ b/x-pack/plugins/canvas/public/components/confirm_modal/confirm_modal.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import PropTypes from 'prop-types'; import React, { FunctionComponent } from 'react'; @@ -39,21 +39,20 @@ export const ConfirmModal: FunctionComponent = (props) => { } return ( - - - {message} - - + + {message} + ); }; diff --git a/x-pack/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js b/x-pack/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js index 4a5861b41d06cc..a55f73a0874676 100644 --- a/x-pack/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js +++ b/x-pack/plugins/canvas/public/components/datasource/datasource_preview/datasource_preview.js @@ -8,7 +8,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import { - EuiOverlayMask, EuiModal, EuiModalBody, EuiModalHeader, @@ -27,48 +26,46 @@ const { DatasourceDatasourcePreview: strings } = ComponentStrings; const { DatasourceDatasourceComponent: datasourceStrings } = ComponentStrings; export const DatasourcePreview = ({ done, datatable }) => ( - - - - {strings.getModalTitle()} - - - -

    - {datasourceStrings.getSaveButtonLabel()}, - }} + + + {strings.getModalTitle()} + + + +

    + {datasourceStrings.getSaveButtonLabel()}, + }} + /> +

    +
    + + {datatable.type === 'error' ? ( + + ) : ( + + {datatable.rows.length > 0 ? ( + + ) : ( + {strings.getEmptyTitle()}} + titleSize="s" + body={ +

    + {strings.getEmptyFirstLineDescription()} +
    + {strings.getEmptySecondLineDescription()} +

    + } /> -

    - - - {datatable.type === 'error' ? ( - - ) : ( - - {datatable.rows.length > 0 ? ( - - ) : ( - {strings.getEmptyTitle()}} - titleSize="s" - body={ -

    - {strings.getEmptyFirstLineDescription()} -
    - {strings.getEmptySecondLineDescription()} -

    - } - /> - )} -
    - )} -
    -
    -
    + )} + + )} + + ); DatasourcePreview.propTypes = { diff --git a/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot b/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot index aea9626d7b57ad..a28986c0418a29 100644 --- a/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/keyboard_shortcuts_doc/__stories__/__snapshots__/keyboard_shortcuts_doc.stories.storyshot @@ -5,7 +5,7 @@ exports[`Storyshots components/KeyboardShortcutsDoc default 1`] = ` data-eui="EuiFocusTrap" >
    diff --git a/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx index e99cc60dfcaa4b..bc0039245f4322 100644 --- a/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx +++ b/x-pack/plugins/canvas/public/components/saved_elements_modal/saved_elements_modal.component.tsx @@ -23,7 +23,6 @@ import { EuiEmptyPrompt, EuiFieldSearch, EuiSpacer, - EuiOverlayMask, EuiButton, } from '@elastic/eui'; import { sortBy } from 'lodash'; @@ -117,16 +116,14 @@ export const SavedElementsModal: FunctionComponent = ({ } return ( - - - + ); }; @@ -176,40 +173,34 @@ export const SavedElementsModal: FunctionComponent = ({ return ( - - - - - {strings.getModalTitle()} - - - - - - - {customElementContent} - - - - {strings.getSavedElementsModalCloseButtonLabel()} - - - - + + + + {strings.getModalTitle()} + + + + + + + {customElementContent} + + + + {strings.getSavedElementsModalCloseButtonLabel()} + + + {renderDeleteModal()} {renderEditModal()} diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx index edf7d33eff79c4..6e5c936a113bf8 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx +++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx @@ -12,7 +12,6 @@ import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, - EuiOverlayMask, EuiModal, EuiModalFooter, EuiButton, @@ -93,16 +92,14 @@ export const Toolbar: FC = ({ const openWorkpadManager = () => setShowWorkpadManager(true); const workpadManager = ( - - - - - - {strings.getWorkpadManagerCloseButtonLabel()} - - - - + + + + + {strings.getWorkpadManagerCloseButtonLabel()} + + + ); const trays = { diff --git a/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/var_config.stories.storyshot b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/var_config.stories.storyshot index bf5629596d6b67..6277c599032c1e 100644 --- a/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/var_config.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/var_config/__stories__/__snapshots__/var_config.stories.storyshot @@ -77,8 +77,11 @@ exports[`Storyshots components/Variables/VarConfig default 1`] = `
    = ({ )} {isModalVisible ? ( - - - + ) : null} ); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/pdf_panel.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/pdf_panel.stories.storyshot index 4e84a9d5a0d211..010037bee4a0fd 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/pdf_panel.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/__snapshots__/pdf_panel.stories.storyshot @@ -150,8 +150,11 @@ exports[`Storyshots components/WorkpadHeader/ShareMenu/PDFPanel default 1`] = `
    diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js index 5a33b25399f77e..25c17fabe9fad8 100644 --- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js +++ b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js @@ -213,7 +213,7 @@ export class WorkpadLoader extends React.PureComponent { width: '20%', render: (date) => this.props.formatDate(date), }, - { name: '', actions, width: '5%' }, + { name: strings.getTableActionsColumnTitle(), actions, width: '100px' }, ]; const sorting = { @@ -310,6 +310,7 @@ export class WorkpadLoader extends React.PureComponent { onClick={this.openRemoveConfirm} disabled={!canUserWrite} aria-label={strings.getDeleteButtonAriaLabel(selectedWorkpads.length)} + data-test-subj="deleteWorkpadButton" > {strings.getDeleteButtonLabel(selectedWorkpads.length)} @@ -331,6 +332,7 @@ export class WorkpadLoader extends React.PureComponent { display="default" compressed className="canvasWorkpad__upload--compressed" + aria-label={strings.getFilePickerPlaceholder()} initialPromptText={strings.getFilePickerPlaceholder()} onChange={([file]) => uploadWorkpad(file, this.onUpload, this.props.notify)} accept="application/json" diff --git a/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js b/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js index 3f128a1758c3f9..8055be32ac481a 100644 --- a/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js +++ b/x-pack/plugins/canvas/public/components/workpad_manager/workpad_manager.js @@ -37,6 +37,7 @@ export const WorkpadManager = ({ onClose }) => { { id: 'workpadTemplates', name: strings.getWorkpadTemplatesTabLabel(), + 'data-test-subj': 'workpadTemplates', content: ( @@ -50,7 +51,9 @@ export const WorkpadManager = ({ onClose }) => { - {strings.getModalTitle()} + +

    {strings.getModalTitle()}

    +
    diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot index 489827246e998a..e984aae3356366 100644 --- a/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot @@ -95,6 +95,7 @@ exports[`Storyshots components/WorkpadTemplates default 1`] = ` />
    {rows.length > 0 && ( diff --git a/x-pack/plugins/canvas/server/lib/normalize_type.ts b/x-pack/plugins/canvas/server/lib/normalize_type.ts index 68313d8f39ae38..76290ca0b497cf 100644 --- a/x-pack/plugins/canvas/server/lib/normalize_type.ts +++ b/x-pack/plugins/canvas/server/lib/normalize_type.ts @@ -7,7 +7,7 @@ export function normalizeType(type: string) { const normalTypes: Record = { - string: ['string', 'text', 'keyword', '_type', '_id', '_index', 'geo_point'], + string: ['string', 'text', 'keyword', '_type', '_id', '_index', 'geo_point', 'ip'], number: [ 'float', 'half_float', diff --git a/x-pack/plugins/canvas/server/routes/catch_error_handler.ts b/x-pack/plugins/canvas/server/routes/catch_error_handler.ts index 2abdda1932535a..b1fe4bc798f6bc 100644 --- a/x-pack/plugins/canvas/server/routes/catch_error_handler.ts +++ b/x-pack/plugins/canvas/server/routes/catch_error_handler.ts @@ -20,7 +20,7 @@ export const catchErrorHandler: ( statusCode: error.output.statusCode, }); } - return response.internalError({ body: error }); + throw error; } }; }; diff --git a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts index 618625cd6cdd43..1e95ee809e7679 100644 --- a/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts +++ b/x-pack/plugins/canvas/server/routes/es_fields/es_fields.test.ts @@ -147,8 +147,8 @@ describe('Retrieve ES Fields', () => { callAsCurrentUserMock.mockRejectedValueOnce(new Error('Index not found')); - const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); - - expect(response.status).toBe(500); + await expect( + routeHandler(mockRouteContext, request, kibanaResponseFactory) + ).rejects.toThrowError('Index not found'); }); }); diff --git a/x-pack/plugins/canvas/storybook/preview-head.html b/x-pack/plugins/canvas/storybook/preview-head.html index bef08a5120a36b..f8a7de6ddbaf1a 100644 --- a/x-pack/plugins/canvas/storybook/preview-head.html +++ b/x-pack/plugins/canvas/storybook/preview-head.html @@ -2,5 +2,5 @@ This file is looked for by Storybook and included in the HEAD element if it exists. This is how we load the DLL content into the Storybook UI. --> - - + + diff --git a/x-pack/plugins/case/common/api/cases/case.ts b/x-pack/plugins/case/common/api/cases/case.ts index 49643ca1f4d0c7..33a93952b0e2d7 100644 --- a/x-pack/plugins/case/common/api/cases/case.ts +++ b/x-pack/plugins/case/common/api/cases/case.ts @@ -123,6 +123,7 @@ export const CaseResponseRt = rt.intersection([ version: rt.string, }), rt.partial({ + subCaseIds: rt.array(rt.string), subCases: rt.array(SubCaseResponseRt), comments: rt.array(CommentResponseRt), }), diff --git a/x-pack/plugins/case/common/api/cases/comment.ts b/x-pack/plugins/case/common/api/cases/comment.ts index cfc6099fa4bb57..41ad0e87f14d2f 100644 --- a/x-pack/plugins/case/common/api/cases/comment.ts +++ b/x-pack/plugins/case/common/api/cases/comment.ts @@ -52,7 +52,11 @@ export const ContextTypeUserRt = rt.type({ export const AlertCommentRequestRt = rt.type({ type: rt.union([rt.literal(CommentType.generatedAlert), rt.literal(CommentType.alert)]), alertId: rt.union([rt.array(rt.string), rt.string]), - index: rt.string, + index: rt.union([rt.array(rt.string), rt.string]), + rule: rt.type({ + id: rt.union([rt.string, rt.null]), + name: rt.union([rt.string, rt.null]), + }), }); const AttributesTypeUserRt = rt.intersection([ContextTypeUserRt, CommentAttributesBasicRt]); @@ -108,6 +112,7 @@ export const CommentsResponseRt = rt.type({ export const AllCommentsResponseRt = rt.array(CommentResponseRt); +export type AttributesTypeAlerts = rt.TypeOf; export type CommentAttributes = rt.TypeOf; export type CommentRequest = rt.TypeOf; export type CommentResponse = rt.TypeOf; diff --git a/x-pack/plugins/case/common/api/cases/user_actions.ts b/x-pack/plugins/case/common/api/cases/user_actions.ts index de9e88993df9a2..6c8e0de80903df 100644 --- a/x-pack/plugins/case/common/api/cases/user_actions.ts +++ b/x-pack/plugins/case/common/api/cases/user_actions.ts @@ -49,6 +49,7 @@ const CaseUserActionResponseRT = rt.intersection([ case_id: rt.string, comment_id: rt.union([rt.string, rt.null]), }), + rt.partial({ sub_case_id: rt.string }), ]); export const CaseUserActionAttributesRt = CaseUserActionBasicRT; diff --git a/x-pack/plugins/case/common/api/helpers.ts b/x-pack/plugins/case/common/api/helpers.ts index 9c290c0a4d6128..00c8ff402c802e 100644 --- a/x-pack/plugins/case/common/api/helpers.ts +++ b/x-pack/plugins/case/common/api/helpers.ts @@ -13,6 +13,7 @@ import { SUB_CASE_DETAILS_URL, SUB_CASES_URL, CASE_PUSH_URL, + SUB_CASE_USER_ACTIONS_URL, } from '../constants'; export const getCaseDetailsUrl = (id: string): string => { @@ -38,6 +39,11 @@ export const getCaseCommentDetailsUrl = (caseId: string, commentId: string): str export const getCaseUserActionUrl = (id: string): string => { return CASE_USER_ACTIONS_URL.replace('{case_id}', id); }; + +export const getSubCaseUserActionUrl = (caseID: string, subCaseID: string): string => { + return SUB_CASE_USER_ACTIONS_URL.replace('{case_id}', caseID).replace('{sub_case_id}', subCaseID); +}; + export const getCasePushUrl = (caseId: string, connectorId: string): string => { return CASE_PUSH_URL.replace('{case_id}', caseId).replace('{connector_id}', connectorId); }; diff --git a/x-pack/plugins/case/common/constants.ts b/x-pack/plugins/case/common/constants.ts index 5d34ed120ff6f8..cc69c7ecc29094 100644 --- a/x-pack/plugins/case/common/constants.ts +++ b/x-pack/plugins/case/common/constants.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { DEFAULT_MAX_SIGNALS } from '../../security_solution/common/constants'; + export const APP_ID = 'case'; /** @@ -19,6 +21,7 @@ export const CASE_CONFIGURE_CONNECTORS_URL = `${CASE_CONFIGURE_URL}/connectors`; export const SUB_CASES_PATCH_DEL_URL = `${CASES_URL}/sub_cases`; export const SUB_CASES_URL = `${CASE_DETAILS_URL}/sub_cases`; export const SUB_CASE_DETAILS_URL = `${CASE_DETAILS_URL}/sub_cases/{sub_case_id}`; +export const SUB_CASE_USER_ACTIONS_URL = `${SUB_CASE_DETAILS_URL}/user_actions`; export const CASE_COMMENTS_URL = `${CASE_DETAILS_URL}/comments`; export const CASE_COMMENT_DETAILS_URL = `${CASE_DETAILS_URL}/comments/{comment_id}`; @@ -45,3 +48,10 @@ export const SUPPORTED_CONNECTORS = [ JIRA_ACTION_TYPE_ID, RESILIENT_ACTION_TYPE_ID, ]; + +/** + * Alerts + */ + +export const MAX_ALERTS_PER_SUB_CASE = 5000; +export const MAX_GENERATED_ALERTS_PER_SUB_CASE = MAX_ALERTS_PER_SUB_CASE / DEFAULT_MAX_SIGNALS; diff --git a/x-pack/plugins/case/server/client/cases/create.test.ts b/x-pack/plugins/case/server/client/cases/create.test.ts index 065825472954b8..3016a57f218757 100644 --- a/x-pack/plugins/case/server/client/cases/create.test.ts +++ b/x-pack/plugins/case/server/client/cases/create.test.ts @@ -76,6 +76,7 @@ describe('create', () => { "syncAlerts": true, }, "status": "open", + "subCaseIds": undefined, "subCases": undefined, "tags": Array [ "defacement", @@ -174,6 +175,7 @@ describe('create', () => { "syncAlerts": true, }, "status": "open", + "subCaseIds": undefined, "subCases": undefined, "tags": Array [ "defacement", @@ -239,6 +241,7 @@ describe('create', () => { "syncAlerts": true, }, "status": "open", + "subCaseIds": undefined, "subCases": undefined, "tags": Array [ "defacement", diff --git a/x-pack/plugins/case/server/client/cases/get.ts b/x-pack/plugins/case/server/client/cases/get.ts index eab43a0c4d4536..ab0b97abbcb76a 100644 --- a/x-pack/plugins/case/server/client/cases/get.ts +++ b/x-pack/plugins/case/server/client/cases/get.ts @@ -26,19 +26,24 @@ export const get = async ({ includeComments = false, includeSubCaseComments = false, }: GetParams): Promise => { - const theCase = await caseService.getCase({ - client: savedObjectsClient, - id, - }); + const [theCase, subCasesForCaseId] = await Promise.all([ + caseService.getCase({ + client: savedObjectsClient, + id, + }), + caseService.findSubCasesByCaseId({ client: savedObjectsClient, ids: [id] }), + ]); + + const subCaseIds = subCasesForCaseId.saved_objects.map((so) => so.id); if (!includeComments) { return CaseResponseRt.encode( flattenCaseSavedObject({ savedObject: theCase, + subCaseIds, }) ); } - const theComments = await caseService.getAllCaseComments({ client: savedObjectsClient, id, @@ -53,6 +58,7 @@ export const get = async ({ flattenCaseSavedObject({ savedObject: theCase, comments: theComments.saved_objects, + subCaseIds, totalComment: theComments.total, totalAlerts: countAlertsForID({ comments: theComments, id }), }) diff --git a/x-pack/plugins/case/server/client/cases/mock.ts b/x-pack/plugins/case/server/client/cases/mock.ts index 2be9f410598312..809c4ad1ea1bd7 100644 --- a/x-pack/plugins/case/server/client/cases/mock.ts +++ b/x-pack/plugins/case/server/client/cases/mock.ts @@ -54,6 +54,10 @@ export const commentAlert: CommentResponse = { id: 'mock-comment-1', alertId: 'alert-id-1', index: 'alert-index-1', + rule: { + id: 'rule-id-1', + name: 'rule-name-1', + }, type: CommentType.alert as const, created_at: '2019-11-25T21:55:00.177Z', created_by: { diff --git a/x-pack/plugins/case/server/client/cases/update.test.ts b/x-pack/plugins/case/server/client/cases/update.test.ts index 53e233c74deb4e..7a3e4458f25c54 100644 --- a/x-pack/plugins/case/server/client/cases/update.test.ts +++ b/x-pack/plugins/case/server/client/cases/update.test.ts @@ -71,6 +71,7 @@ describe('update', () => { "syncAlerts": true, }, "status": "closed", + "subCaseIds": undefined, "subCases": undefined, "tags": Array [ "defacement", @@ -166,6 +167,7 @@ describe('update', () => { "syncAlerts": true, }, "status": "open", + "subCaseIds": undefined, "subCases": undefined, "tags": Array [ "defacement", @@ -233,6 +235,7 @@ describe('update', () => { "syncAlerts": true, }, "status": "in-progress", + "subCaseIds": undefined, "subCases": undefined, "tags": Array [ "LOLBins", @@ -300,6 +303,7 @@ describe('update', () => { "syncAlerts": true, }, "status": "closed", + "subCaseIds": undefined, "subCases": undefined, "tags": Array [ "defacement", @@ -371,6 +375,7 @@ describe('update', () => { "syncAlerts": true, }, "status": "open", + "subCaseIds": undefined, "subCases": undefined, "tags": Array [ "LOLBins", diff --git a/x-pack/plugins/case/server/client/cases/utils.ts b/x-pack/plugins/case/server/client/cases/utils.ts index 78bdc6d282c698..fda4142bf77c7b 100644 --- a/x-pack/plugins/case/server/client/cases/utils.ts +++ b/x-pack/plugins/case/server/client/cases/utils.ts @@ -314,6 +314,7 @@ export const getCommentContextFromAttributes = ( type: attributes.type, alertId: attributes.alertId, index: attributes.index, + rule: attributes.rule, }; default: return { diff --git a/x-pack/plugins/case/server/client/comments/add.test.ts b/x-pack/plugins/case/server/client/comments/add.test.ts index 315203a1f5e1d9..c9b1e4fd132722 100644 --- a/x-pack/plugins/case/server/client/comments/add.test.ts +++ b/x-pack/plugins/case/server/client/comments/add.test.ts @@ -75,6 +75,10 @@ describe('addComment', () => { type: CommentType.alert, alertId: 'test-id', index: 'test-index', + rule: { + id: 'test-rule1', + name: 'test-rule', + }, }, }); @@ -94,6 +98,10 @@ describe('addComment', () => { "index": "test-index", "pushed_at": null, "pushed_by": null, + "rule": Object { + "id": "test-rule1", + "name": "test-rule", + }, "type": "alert", "updated_at": null, "updated_by": null, @@ -231,6 +239,10 @@ describe('addComment', () => { type: CommentType.alert, alertId: 'test-alert', index: 'test-index', + rule: { + id: 'test-rule1', + name: 'test-rule', + }, }, }); @@ -265,6 +277,10 @@ describe('addComment', () => { type: CommentType.alert, alertId: 'test-alert', index: 'test-index', + rule: { + id: 'test-rule1', + name: 'test-rule', + }, }, }); @@ -406,6 +422,10 @@ describe('addComment', () => { type: CommentType.alert, index: 'test-index', alertId: 'test-id', + rule: { + id: 'test-rule1', + name: 'test-rule', + }, }, }) .catch((e) => { @@ -478,6 +498,10 @@ describe('addComment', () => { type: CommentType.alert, alertId: 'test-alert', index: 'test-index', + rule: { + id: 'test-rule1', + name: 'test-rule', + }, }, }) .catch((e) => { diff --git a/x-pack/plugins/case/server/client/comments/add.ts b/x-pack/plugins/case/server/client/comments/add.ts index 7dd1b4a8f6c5cd..0a86c1825fedce 100644 --- a/x-pack/plugins/case/server/client/comments/add.ts +++ b/x-pack/plugins/case/server/client/comments/add.ts @@ -38,6 +38,8 @@ import { import { CaseServiceSetup, CaseUserActionServiceSetup } from '../../services'; import { CommentableCase } from '../../common'; import { CaseClientHandler } from '..'; +import { CASE_COMMENT_SAVED_OBJECT } from '../../saved_object_types'; +import { MAX_GENERATED_ALERTS_PER_SUB_CASE } from '../../../common/constants'; async function getSubCase({ caseService, @@ -56,7 +58,20 @@ async function getSubCase({ }): Promise> { const mostRecentSubCase = await caseService.getMostRecentSubCase(savedObjectsClient, caseId); if (mostRecentSubCase && mostRecentSubCase.attributes.status !== CaseStatuses.closed) { - return mostRecentSubCase; + const subCaseAlertsAttachement = await caseService.getAllSubCaseComments({ + client: savedObjectsClient, + id: mostRecentSubCase.id, + options: { + fields: [], + filter: `${CASE_COMMENT_SAVED_OBJECT}.attributes.type: ${CommentType.generatedAlert}`, + page: 1, + perPage: 1, + }, + }); + + if (subCaseAlertsAttachement.total <= MAX_GENERATED_ALERTS_PER_SUB_CASE) { + return mostRecentSubCase; + } } const newSubCase = await caseService.createSubCase({ @@ -160,7 +175,11 @@ const addGeneratedAlerts = async ({ await caseClient.updateAlertsStatus({ ids, status: subCase.attributes.status, - indices: new Set([newComment.attributes.index]), + indices: new Set([ + ...(Array.isArray(newComment.attributes.index) + ? newComment.attributes.index + : [newComment.attributes.index]), + ]), }); } @@ -282,7 +301,11 @@ export const addComment = async ({ await caseClient.updateAlertsStatus({ ids, status: updatedCase.status, - indices: new Set([newComment.attributes.index]), + indices: new Set([ + ...(Array.isArray(newComment.attributes.index) + ? newComment.attributes.index + : [newComment.attributes.index]), + ]), }); } diff --git a/x-pack/plugins/case/server/client/types.ts b/x-pack/plugins/case/server/client/types.ts index a8f64227daf83f..ba5677426c2228 100644 --- a/x-pack/plugins/case/server/client/types.ts +++ b/x-pack/plugins/case/server/client/types.ts @@ -59,6 +59,7 @@ export interface CaseClientGetAlerts { export interface CaseClientGetUserActions { caseId: string; + subCaseId?: string; } export interface MappingsClient { diff --git a/x-pack/plugins/case/server/client/user_actions/get.ts b/x-pack/plugins/case/server/client/user_actions/get.ts index 8a4e45f71b9ca3..f6371b8e8b1e7b 100644 --- a/x-pack/plugins/case/server/client/user_actions/get.ts +++ b/x-pack/plugins/case/server/client/user_actions/get.ts @@ -6,7 +6,11 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; -import { CASE_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT } from '../../saved_object_types'; +import { + CASE_SAVED_OBJECT, + CASE_COMMENT_SAVED_OBJECT, + SUB_CASE_SAVED_OBJECT, +} from '../../saved_object_types'; import { CaseUserActionsResponseRt, CaseUserActionsResponse } from '../../../common/api'; import { CaseUserActionServiceSetup } from '../../services'; @@ -14,24 +18,36 @@ interface GetParams { savedObjectsClient: SavedObjectsClientContract; userActionService: CaseUserActionServiceSetup; caseId: string; + subCaseId?: string; } export const get = async ({ savedObjectsClient, userActionService, caseId, + subCaseId, }: GetParams): Promise => { const userActions = await userActionService.getUserActions({ client: savedObjectsClient, caseId, + subCaseId, }); return CaseUserActionsResponseRt.encode( - userActions.saved_objects.map((ua) => ({ - ...ua.attributes, - action_id: ua.id, - case_id: ua.references.find((r) => r.type === CASE_SAVED_OBJECT)?.id ?? '', - comment_id: ua.references.find((r) => r.type === CASE_COMMENT_SAVED_OBJECT)?.id ?? null, - })) + userActions.saved_objects.reduce((acc, ua) => { + if (subCaseId == null && ua.references.some((uar) => uar.type === SUB_CASE_SAVED_OBJECT)) { + return acc; + } + return [ + ...acc, + { + ...ua.attributes, + action_id: ua.id, + case_id: ua.references.find((r) => r.type === CASE_SAVED_OBJECT)?.id ?? '', + comment_id: ua.references.find((r) => r.type === CASE_COMMENT_SAVED_OBJECT)?.id ?? null, + sub_case_id: ua.references.find((r) => r.type === SUB_CASE_SAVED_OBJECT)?.id ?? '', + }, + ]; + }, []) ); }; diff --git a/x-pack/plugins/case/server/common/utils.test.ts b/x-pack/plugins/case/server/common/utils.test.ts index d89feb009f8069..5e6a86358de256 100644 --- a/x-pack/plugins/case/server/common/utils.test.ts +++ b/x-pack/plugins/case/server/common/utils.test.ts @@ -99,6 +99,10 @@ describe('common utils', () => { alertId: ['a', 'b', 'c'], index: '', type: CommentType.generatedAlert, + rule: { + id: 'rule-id-1', + name: 'rule-name-1', + }, }, ], }, @@ -118,6 +122,10 @@ describe('common utils', () => { alertId: ['a', 'b', 'c'], index: '', type: CommentType.alert, + rule: { + id: 'rule-id-1', + name: 'rule-name-1', + }, }, ], }, @@ -139,6 +147,10 @@ describe('common utils', () => { alertId: ['a', 'b'], index: '', type: CommentType.alert, + rule: { + id: 'rule-id-1', + name: 'rule-name-1', + }, }, { comment: '', @@ -164,6 +176,10 @@ describe('common utils', () => { alertId: ['a', 'b'], index: '', type: CommentType.alert, + rule: { + id: 'rule-id-1', + name: 'rule-name-1', + }, }, ], }, @@ -197,6 +213,10 @@ describe('common utils', () => { alertId: ['a', 'b'], index: '', type: CommentType.alert, + rule: { + id: 'rule-id-1', + name: 'rule-name-1', + }, }, ], }, @@ -224,6 +244,10 @@ describe('common utils', () => { alertId: ['a', 'b'], index: '', type: CommentType.alert, + rule: { + id: 'rule-id-1', + name: 'rule-name-1', + }, }, ], }, diff --git a/x-pack/plugins/case/server/connectors/case/index.test.ts b/x-pack/plugins/case/server/connectors/case/index.test.ts index 6b7e395bae4dc1..4be519858db18c 100644 --- a/x-pack/plugins/case/server/connectors/case/index.test.ts +++ b/x-pack/plugins/case/server/connectors/case/index.test.ts @@ -717,6 +717,10 @@ describe('case connector', () => { type: CommentType.alert, alertId: 'test-id', index: 'test-index', + rule: { + id: null, + name: null, + }, }, }, }; diff --git a/x-pack/plugins/case/server/connectors/case/index.ts b/x-pack/plugins/case/server/connectors/case/index.ts index 34b407616cfe48..a64cba567ce46d 100644 --- a/x-pack/plugins/case/server/connectors/case/index.ts +++ b/x-pack/plugins/case/server/connectors/case/index.ts @@ -122,23 +122,48 @@ async function executor( /** * This converts a connector style generated alert ({_id: string} | {_id: string}[]) to the expected format of addComment. */ +interface AttachmentAlerts { + ids: string[]; + indices: string[]; + rule: { id: string | null; name: string | null }; +} export const transformConnectorComment = (comment: CommentSchemaType): CommentRequest => { if (isCommentGeneratedAlert(comment)) { - const alertId: string[] = []; - if (Array.isArray(comment.alerts)) { - alertId.push( - ...comment.alerts.map((alert: { _id: string }) => { - return alert._id; - }) + try { + const genAlerts: Array<{ + _id: string; + _index: string; + ruleId: string | undefined; + ruleName: string | undefined; + }> = JSON.parse( + `${comment.alerts.substring(0, comment.alerts.lastIndexOf('__SEPARATOR__'))}]`.replace( + /__SEPARATOR__/gi, + ',' + ) + ); + + const { ids, indices, rule } = genAlerts.reduce( + (acc, { _id, _index, ruleId, ruleName }) => { + // Mutation is faster than destructing. + // Mutation usually leads to side effects but for this scenario it's ok to do it. + acc.ids.push(_id); + acc.indices.push(_index); + // We assume one rule per batch of alerts + acc.rule = { id: ruleId ?? null, name: ruleName ?? null }; + return acc; + }, + { ids: [], indices: [], rule: { id: null, name: null } } ); - } else { - alertId.push(comment.alerts._id); + + return { + type: CommentType.generatedAlert, + alertId: ids, + index: indices, + rule, + }; + } catch (e) { + throw new Error(`Error parsing generated alert in case connector -> ${e.message}`); } - return { - type: CommentType.generatedAlert, - alertId, - index: comment.index, - }; } else { return comment; } diff --git a/x-pack/plugins/case/server/connectors/case/schema.ts b/x-pack/plugins/case/server/connectors/case/schema.ts index cdeb00209f846b..ac34ad40cfa13a 100644 --- a/x-pack/plugins/case/server/connectors/case/schema.ts +++ b/x-pack/plugins/case/server/connectors/case/schema.ts @@ -17,17 +17,9 @@ const ContextTypeUserSchema = schema.object({ comment: schema.string(), }); -const AlertIDSchema = schema.object( - { - _id: schema.string(), - }, - { unknowns: 'ignore' } -); - const ContextTypeAlertGroupSchema = schema.object({ type: schema.literal(CommentType.generatedAlert), - alerts: schema.oneOf([schema.arrayOf(AlertIDSchema), AlertIDSchema]), - index: schema.string(), + alerts: schema.string(), }); export type ContextTypeGeneratedAlertType = typeof ContextTypeAlertGroupSchema.type; @@ -37,6 +29,10 @@ const ContextTypeAlertSchema = schema.object({ // allowing either an array or a single value to preserve the previous API of attaching a single alert ID alertId: schema.oneOf([schema.arrayOf(schema.string()), schema.string()]), index: schema.string(), + rule: schema.object({ + id: schema.nullable(schema.string()), + name: schema.nullable(schema.string()), + }), }); export type ContextTypeAlertSchemaType = typeof ContextTypeAlertSchema.type; diff --git a/x-pack/plugins/case/server/connectors/index.ts b/x-pack/plugins/case/server/connectors/index.ts index 056ccff2733a76..898d61301a1402 100644 --- a/x-pack/plugins/case/server/connectors/index.ts +++ b/x-pack/plugins/case/server/connectors/index.ts @@ -65,3 +65,27 @@ export const isCommentAlert = ( ): comment is ContextTypeAlertSchemaType => { return comment.type === CommentType.alert; }; + +/** + * Separator field for the case connector alerts string parser. + */ +const separator = '__SEPARATOR__'; + +interface AlertIDIndex { + _id: string; + _index: string; + ruleId: string; + ruleName: string; +} + +/** + * Creates the format that the connector's parser is expecting, it should result in something like this: + * [{"_id":"1","_index":"index1"}__SEPARATOR__{"_id":"id2","_index":"index2"}__SEPARATOR__] + * + * This should only be used for testing purposes. + */ +export function createAlertsString(alerts: AlertIDIndex[]) { + return `[${alerts.reduce((acc, alert) => { + return `${acc}${JSON.stringify(alert)}${separator}`; + }, '')}]`; +} diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts index 2fe0be3e08ede6..e67a6f6dd3344b 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -346,6 +346,10 @@ export const mockCaseComments: Array> = [ }, pushed_at: null, pushed_by: null, + rule: { + id: 'rule-id-1', + name: 'rule-name-1', + }, updated_at: '2019-11-25T22:32:30.608Z', updated_by: { full_name: 'elastic', @@ -379,6 +383,10 @@ export const mockCaseComments: Array> = [ }, pushed_at: null, pushed_by: null, + rule: { + id: 'rule-id-2', + name: 'rule-name-2', + }, updated_at: '2019-11-25T22:32:30.608Z', updated_by: { full_name: 'elastic', diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts index 9dec910f9fc46f..1ebd336c83af75 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts @@ -69,6 +69,10 @@ describe('PATCH comment', () => { type: CommentType.alert, alertId: 'new-id', index: 'test-index', + rule: { + id: 'rule-id', + name: 'rule', + }, id: commentID, version: 'WzYsMV0=', }, @@ -218,6 +222,10 @@ describe('PATCH comment', () => { type: CommentType.alert, alertId: 'test-id', index: 'test-index', + rule: { + id: 'rule-id', + name: 'rule', + }, id: 'mock-comment-1', version: 'WzEsMV0=', }, diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts index fb51b8f76d0ef4..807ec0d089a524 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts @@ -68,6 +68,10 @@ describe('POST comment', () => { type: CommentType.alert, alertId: 'test-id', index: 'test-index', + rule: { + id: 'rule-id', + name: 'rule-name', + }, }, }); diff --git a/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts index e50d14e5c66c4f..b3f87211c95475 100644 --- a/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts @@ -81,6 +81,7 @@ describe('PATCH cases', () => { "syncAlerts": true, }, "status": "closed", + "subCaseIds": undefined, "subCases": undefined, "tags": Array [ "defacement", @@ -154,6 +155,7 @@ describe('PATCH cases', () => { "syncAlerts": true, }, "status": "open", + "subCaseIds": undefined, "subCases": undefined, "tags": Array [ "LOLBins", @@ -222,6 +224,7 @@ describe('PATCH cases', () => { "syncAlerts": true, }, "status": "in-progress", + "subCaseIds": undefined, "subCases": undefined, "tags": Array [ "defacement", diff --git a/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts index 53829157c5b049..e1669203d3dedb 100644 --- a/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts @@ -213,6 +213,7 @@ describe('POST cases', () => { "syncAlerts": true, }, "status": "open", + "subCaseIds": undefined, "subCases": undefined, "tags": Array [ "defacement", diff --git a/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts b/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts index 06e929cc40e6bb..488f32a795811f 100644 --- a/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts @@ -9,9 +9,9 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; -import { CASE_USER_ACTIONS_URL } from '../../../../../common/constants'; +import { CASE_USER_ACTIONS_URL, SUB_CASE_USER_ACTIONS_URL } from '../../../../../common/constants'; -export function initGetAllUserActionsApi({ router }: RouteDeps) { +export function initGetAllCaseUserActionsApi({ router }: RouteDeps) { router.get( { path: CASE_USER_ACTIONS_URL, @@ -39,3 +39,34 @@ export function initGetAllUserActionsApi({ router }: RouteDeps) { } ); } + +export function initGetAllSubCaseUserActionsApi({ router }: RouteDeps) { + router.get( + { + path: SUB_CASE_USER_ACTIONS_URL, + validate: { + params: schema.object({ + case_id: schema.string(), + sub_case_id: schema.string(), + }), + }, + }, + async (context, request, response) => { + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } + + const caseClient = context.case.getCaseClient(); + const caseId = request.params.case_id; + const subCaseId = request.params.sub_case_id; + + try { + return response.ok({ + body: await caseClient.getUserActions({ caseId, subCaseId }), + }); + } catch (error) { + return response.customError(wrapError(error)); + } + } + ); +} diff --git a/x-pack/plugins/case/server/routes/api/index.ts b/x-pack/plugins/case/server/routes/api/index.ts index f2fd986dd8a3ae..12d1da36077c79 100644 --- a/x-pack/plugins/case/server/routes/api/index.ts +++ b/x-pack/plugins/case/server/routes/api/index.ts @@ -14,7 +14,10 @@ import { initPushCaseApi } from './cases/push_case'; import { initGetReportersApi } from './cases/reporters/get_reporters'; import { initGetCasesStatusApi } from './cases/status/get_status'; import { initGetTagsApi } from './cases/tags/get_tags'; -import { initGetAllUserActionsApi } from './cases/user_actions/get_all_user_actions'; +import { + initGetAllCaseUserActionsApi, + initGetAllSubCaseUserActionsApi, +} from './cases/user_actions/get_all_user_actions'; import { initDeleteCommentApi } from './cases/comments/delete_comment'; import { initDeleteAllCommentsApi } from './cases/comments/delete_all_comments'; @@ -52,7 +55,8 @@ export function initCaseApi(deps: RouteDeps) { initPatchCasesApi(deps); initPostCaseApi(deps); initPushCaseApi(deps); - initGetAllUserActionsApi(deps); + initGetAllCaseUserActionsApi(deps); + initGetAllSubCaseUserActionsApi(deps); // Sub cases initGetSubCaseApi(deps); initPatchSubCasesApi(deps); diff --git a/x-pack/plugins/case/server/routes/api/utils.test.ts b/x-pack/plugins/case/server/routes/api/utils.test.ts index 1efec927efb62f..f6bc1e4f718971 100644 --- a/x-pack/plugins/case/server/routes/api/utils.test.ts +++ b/x-pack/plugins/case/server/routes/api/utils.test.ts @@ -401,6 +401,7 @@ describe('Utils', () => { "syncAlerts": true, }, "status": "open", + "subCaseIds": undefined, "subCases": undefined, "tags": Array [ "defacement", @@ -440,6 +441,7 @@ describe('Utils', () => { "syncAlerts": true, }, "status": "open", + "subCaseIds": undefined, "subCases": undefined, "tags": Array [ "Data Destruction", @@ -483,6 +485,7 @@ describe('Utils', () => { "syncAlerts": true, }, "status": "open", + "subCaseIds": undefined, "subCases": undefined, "tags": Array [ "LOLBins", @@ -530,6 +533,7 @@ describe('Utils', () => { "syncAlerts": true, }, "status": "closed", + "subCaseIds": undefined, "subCases": undefined, "tags": Array [ "LOLBins", @@ -594,6 +598,7 @@ describe('Utils', () => { "syncAlerts": true, }, "status": "open", + "subCaseIds": undefined, "subCases": undefined, "tags": Array [ "LOLBins", @@ -649,6 +654,7 @@ describe('Utils', () => { "syncAlerts": true, }, "status": "open", + "subCaseIds": undefined, "subCases": undefined, "tags": Array [ "LOLBins", @@ -727,6 +733,7 @@ describe('Utils', () => { "syncAlerts": true, }, "status": "open", + "subCaseIds": undefined, "subCases": undefined, "tags": Array [ "LOLBins", @@ -781,6 +788,7 @@ describe('Utils', () => { "syncAlerts": true, }, "status": "open", + "subCaseIds": undefined, "subCases": undefined, "tags": Array [ "defacement", diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts index bc82f656f477b3..084b1a17a1434a 100644 --- a/x-pack/plugins/case/server/routes/api/utils.ts +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { isEmpty } from 'lodash'; import { badRequest, boomify, isBoom } from '@hapi/boom'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; @@ -120,7 +121,8 @@ export interface AlertInfo { const accumulateIndicesAndIDs = (comment: CommentAttributes, acc: AlertInfo): AlertInfo => { if (isCommentRequestTypeAlertOrGenAlert(comment)) { acc.ids.push(...getAlertIds(comment)); - acc.indices.add(comment.index); + const indices = Array.isArray(comment.index) ? comment.index : [comment.index]; + indices.forEach((index) => acc.indices.add(index)); } return acc; }; @@ -249,12 +251,14 @@ export const flattenCaseSavedObject = ({ totalComment = comments.length, totalAlerts = 0, subCases, + subCaseIds, }: { savedObject: SavedObject; comments?: Array>; totalComment?: number; totalAlerts?: number; subCases?: SubCaseResponse[]; + subCaseIds?: string[]; }): CaseResponse => ({ id: savedObject.id, version: savedObject.version ?? '0', @@ -264,6 +268,7 @@ export const flattenCaseSavedObject = ({ ...savedObject.attributes, connector: transformESConnectorToCaseConnector(savedObject.attributes.connector), subCases, + subCaseIds: !isEmpty(subCaseIds) ? subCaseIds : undefined, }); export const flattenSubCaseSavedObject = ({ diff --git a/x-pack/plugins/case/server/saved_object_types/comments.ts b/x-pack/plugins/case/server/saved_object_types/comments.ts index 9eabf744f2e132..a4fdc24b6e4eec 100644 --- a/x-pack/plugins/case/server/saved_object_types/comments.ts +++ b/x-pack/plugins/case/server/saved_object_types/comments.ts @@ -63,6 +63,16 @@ export const caseCommentSavedObjectType: SavedObjectsType = { }, }, }, + rule: { + properties: { + id: { + type: 'keyword', + }, + name: { + type: 'keyword', + }, + }, + }, updated_at: { type: 'date', }, diff --git a/x-pack/plugins/case/server/saved_object_types/migrations.ts b/x-pack/plugins/case/server/saved_object_types/migrations.ts index a0b22c49d0bc6d..21ef27de1ec851 100644 --- a/x-pack/plugins/case/server/saved_object_types/migrations.ts +++ b/x-pack/plugins/case/server/saved_object_types/migrations.ts @@ -173,8 +173,9 @@ interface SanitizedComment { type: CommentType; } -interface SanitizedCommentAssociationType { +interface SanitizedCommentFoSubCases { associationType: AssociationType; + rule: { id: string | null; name: string | null }; } export const commentsMigrations = { @@ -192,11 +193,12 @@ export const commentsMigrations = { }, '7.12.0': ( doc: SavedObjectUnsanitizedDoc - ): SavedObjectSanitizedDoc => { + ): SavedObjectSanitizedDoc => { return { ...doc, attributes: { ...doc.attributes, + rule: { id: null, name: null }, associationType: AssociationType.case, }, references: doc.references || [], diff --git a/x-pack/plugins/case/server/scripts/sub_cases/index.ts b/x-pack/plugins/case/server/scripts/sub_cases/index.ts index 2ea9718d18487e..9dd577c40c74e6 100644 --- a/x-pack/plugins/case/server/scripts/sub_cases/index.ts +++ b/x-pack/plugins/case/server/scripts/sub_cases/index.ts @@ -16,6 +16,7 @@ import { import { CommentType } from '../../../common/api/cases/comment'; import { CASES_URL } from '../../../common/constants'; import { ActionResult, ActionTypeExecutorResult } from '../../../../actions/common'; +import { ContextTypeGeneratedAlertType, createAlertsString } from '../../connectors'; main(); @@ -105,6 +106,18 @@ async function handleGenGroupAlerts(argv: any) { } console.log('Case id: ', caseID); + const comment: ContextTypeGeneratedAlertType = { + type: CommentType.generatedAlert, + alerts: createAlertsString( + argv.ids.map((id: string) => ({ + _id: id, + _index: argv.signalsIndex, + ruleId: argv.ruleID, + ruleName: argv.ruleName, + })) + ), + }; + const executeResp = await client.request< ActionTypeExecutorResult >({ @@ -115,11 +128,7 @@ async function handleGenGroupAlerts(argv: any) { subAction: 'addComment', subActionParams: { caseId: caseID, - comment: { - type: CommentType.generatedAlert, - alerts: argv.ids.map((id: string) => ({ _id: id })), - index: argv.signalsIndex, - }, + comment, }, }, }, @@ -175,6 +184,18 @@ async function main() { type: 'string', default: '.siem-signals-default', }, + ruleID: { + alias: 'ri', + describe: 'siem signals rule id', + type: 'string', + default: 'rule-id', + }, + ruleName: { + alias: 'rn', + describe: 'siem signals rule name', + type: 'string', + default: 'rule-name', + }, }) .demandOption(['ids']); }, diff --git a/x-pack/plugins/case/server/services/alerts/index.ts b/x-pack/plugins/case/server/services/alerts/index.ts index 320d32ac0d7887..a19e533418bc9a 100644 --- a/x-pack/plugins/case/server/services/alerts/index.ts +++ b/x-pack/plugins/case/server/services/alerts/index.ts @@ -11,6 +11,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { ElasticsearchClient } from 'kibana/server'; import { CaseStatuses } from '../../../common/api'; +import { MAX_ALERTS_PER_SUB_CASE } from '../../../common/constants'; export type AlertServiceContract = PublicMethodsOf; @@ -95,14 +96,14 @@ export class AlertService { query: { bool: { filter: { - bool: { - should: ids.map((_id) => ({ match: { _id } })), - minimum_should_match: 1, + ids: { + values: ids, }, }, }, }, }, + size: MAX_ALERTS_PER_SUB_CASE, ignore_unavailable: true, }); diff --git a/x-pack/plugins/case/server/services/user_actions/index.ts b/x-pack/plugins/case/server/services/user_actions/index.ts index 091775827c6a61..d05ada0dba30cd 100644 --- a/x-pack/plugins/case/server/services/user_actions/index.ts +++ b/x-pack/plugins/case/server/services/user_actions/index.ts @@ -13,11 +13,16 @@ import { } from 'kibana/server'; import { CaseUserActionAttributes } from '../../../common/api'; -import { CASE_USER_ACTION_SAVED_OBJECT, CASE_SAVED_OBJECT } from '../../saved_object_types'; +import { + CASE_USER_ACTION_SAVED_OBJECT, + CASE_SAVED_OBJECT, + SUB_CASE_SAVED_OBJECT, +} from '../../saved_object_types'; import { ClientArgs } from '..'; interface GetCaseUserActionArgs extends ClientArgs { caseId: string; + subCaseId?: string; } export interface UserActionItem { @@ -41,18 +46,20 @@ export interface CaseUserActionServiceSetup { export class CaseUserActionService { constructor(private readonly log: Logger) {} public setup = async (): Promise => ({ - getUserActions: async ({ client, caseId }: GetCaseUserActionArgs) => { + getUserActions: async ({ client, caseId, subCaseId }: GetCaseUserActionArgs) => { try { + const id = subCaseId ?? caseId; + const type = subCaseId ? SUB_CASE_SAVED_OBJECT : CASE_SAVED_OBJECT; const caseUserActionInfo = await client.find({ type: CASE_USER_ACTION_SAVED_OBJECT, fields: [], - hasReference: { type: CASE_SAVED_OBJECT, id: caseId }, + hasReference: { type, id }, page: 1, perPage: 1, }); return await client.find({ type: CASE_USER_ACTION_SAVED_OBJECT, - hasReference: { type: CASE_SAVED_OBJECT, id: caseId }, + hasReference: { type, id }, page: 1, perPage: caseUserActionInfo.total, sortField: 'action_at', diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.js index b07583837636ed..034e08b5c6ab84 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.js @@ -9,7 +9,7 @@ import React, { PureComponent, Fragment } from 'react'; import { connect } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { deleteAutoFollowPattern } from '../store/actions'; import { arrify } from '../../../common/services/utils'; @@ -61,45 +61,43 @@ class AutoFollowPatternDeleteProviderUi extends PureComponent { ); return ( - - {/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */} - - {!isSingle && ( - -

    - -

    -
      - {ids.map((id) => ( -
    • {id}
    • - ))} -
    -
    - )} -
    -
    + // eslint-disable-next-line jsx-a11y/mouse-events-have-key-events + + {!isSingle && ( + +

    + +

    +
      + {ids.map((id) => ( +
    • {id}
    • + ))} +
    +
    + )} +
    ); }; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_pause_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_pause_provider.js index e84816d0d71af9..34697a80121ccf 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_pause_provider.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_pause_provider.js @@ -10,7 +10,7 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { pauseFollowerIndex } from '../../store/actions'; import { arrify } from '../../../../common/services/utils'; @@ -69,64 +69,62 @@ class FollowerIndexPauseProviderUi extends PureComponent { const hasCustomSettings = indices.some((index) => !areAllSettingsDefault(index)); return ( - - {/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */} - - {hasCustomSettings && ( -

    - {isSingle ? ( - + {hasCustomSettings && ( +

    + {isSingle ? ( + - ) : ( - - )} + /> + )} +

    + )} + + {!isSingle && ( + +

    +

    - )} - - {!isSingle && ( - -

    - -

    - -
      - {indices.map((index) => ( -
    • {index.name}
    • - ))} -
    -
    - )} -
    -
    + +
      + {indices.map((index) => ( +
    • {index.name}
    • + ))} +
    + + )} + ); }; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_resume_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_resume_provider.js index 0517f841599122..91c6cb6e243acd 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_resume_provider.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_resume_provider.js @@ -10,7 +10,7 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiConfirmModal, EuiLink, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal, EuiLink } from '@elastic/eui'; import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; import { routing } from '../../services/routing'; import { resumeFollowerIndex } from '../../store/actions'; @@ -68,77 +68,75 @@ class FollowerIndexResumeProviderUi extends PureComponent { ); return ( - - {/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */} - - {isSingle ? ( + // eslint-disable-next-line jsx-a11y/mouse-events-have-key-events + + {isSingle ? ( +

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

    + ) : ( +

    - - - ), - }} + id="xpack.crossClusterReplication.resumeFollowerIndex.confirmModal.multipleResumeDescriptionWithSettingWarning" + defaultMessage="Replication resumes using the default advanced settings." />

    - ) : ( - -

    - -

    -

    - -

    +

    + +

    -
      - {ids.map((id) => ( -
    • {id}
    • - ))} -
    -
    - )} -
    -
    +
      + {ids.map((id) => ( +
    • {id}
    • + ))} +
    + + )} + ); }; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_unfollow_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_unfollow_provider.js index 9b0f0ad3111e03..72d262bcf7af32 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_unfollow_provider.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/components/follower_index_actions_providers/follower_index_unfollow_provider.js @@ -10,7 +10,7 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { unfollowLeaderIndex } from '../../store/actions'; import { arrify } from '../../../../common/services/utils'; @@ -67,58 +67,56 @@ class FollowerIndexUnfollowProviderUi extends PureComponent { ); return ( - - {/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */} - - {isSingle ? ( - -

    - + {isSingle ? ( + +

    + -

    -
    - ) : ( - -

    - -

    -
      - {ids.map((id) => ( -
    • {id}
    • - ))} -
    -
    - )} -
    -
    + /> +

    +
      + {ids.map((id) => ( +
    • {id}
    • + ))} +
    + + )} + ); }; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js index 8fb4fb27006cb9..8d6e47d4004b6e 100644 --- a/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js @@ -15,7 +15,6 @@ import { EuiConfirmModal, EuiFlexGroup, EuiFlexItem, - EuiOverlayMask, EuiPageContent, EuiSpacer, } from '@elastic/eui'; @@ -182,47 +181,45 @@ export class FollowerIndexEdit extends PureComponent { ); return ( - - - ) : ( - - ) + -

    - {isPaused ? ( - - ) : ( - + ) : ( + + ) + } + > +

    + {isPaused ? ( + + ) : ( + - )} -

    -
    -
    + /> + )} +

    + ); }; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts index 10cb83877f57f3..608e369828de6f 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts @@ -55,7 +55,7 @@ export const registerCreateRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } } @@ -71,7 +71,7 @@ export const registerCreateRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts index bc18d5c4b10f81..868e847bd6bf48 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts @@ -41,7 +41,7 @@ export const registerDeleteRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + return response.customError({ statusCode: 500, body: err }); }; await Promise.all( diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts index 0e6ebc5270986f..632fdb03dd5887 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts @@ -37,7 +37,7 @@ export const registerFetchRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts index cd1cb227a3eba7..3529fe313dbb16 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts @@ -48,7 +48,7 @@ export const registerGetRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts index 3bdcd4824a9d45..a9aa94cdf4f299 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts @@ -40,7 +40,7 @@ export const registerPauseRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + return response.customError({ statusCode: 500, body: err }); }; await Promise.all( diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts index 594ef79eaedc96..1c6396d0b35022 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts @@ -40,7 +40,7 @@ export const registerResumeRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + return response.customError({ statusCode: 500, body: err }); }; await Promise.all( diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts index e4d0b7d9d65149..a3e7c3544ca370 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts @@ -54,7 +54,7 @@ export const registerUpdateRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts index ac956a45b87026..130adb2e0b9892 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts @@ -64,7 +64,7 @@ export const registerPermissionsRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts index f37dafca86c985..6636f6b1c5accc 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts @@ -36,7 +36,7 @@ export const registerStatsRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts index b254606af8a867..f44e5a749baad7 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts @@ -59,7 +59,7 @@ export const registerCreateRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts index ffb560ebd5f2bf..c72706cf5d10dc 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts @@ -56,7 +56,7 @@ export const registerFetchRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts index 36feb2e69a3f5c..cdcfff97e645db 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts @@ -72,7 +72,7 @@ export const registerGetRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts index dd76114d30215a..2e4e71278df0e9 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts @@ -38,7 +38,7 @@ export const registerPauseRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + return response.customError({ statusCode: 500, body: err }); }; await Promise.all( diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts index 22206c70fdbbf8..34f204f3b64b96 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts @@ -38,7 +38,7 @@ export const registerResumeRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + return response.customError({ statusCode: 500, body: err }); }; await Promise.all( diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts index 05bf99e2d8c67f..848408e14662fd 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts @@ -39,7 +39,7 @@ export const registerUnfollowRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + return response.customError({ statusCode: 500, body: err }); }; await Promise.all( diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts index 0b61db42cc7635..933d13a0a223c0 100644 --- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts @@ -87,7 +87,7 @@ export const registerUpdateRoute = ({ return response.customError(formatEsError(err)); } // Case: default - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/delete_button.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/delete_button.tsx index d505752ec3fad9..6a952d2f8d9d74 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/delete_button.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/delete_button.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useState } from 'react'; @@ -44,24 +44,22 @@ const DeleteConfirm = ({ }); return ( - - { - setIsLoading(true); - await api.sendCancel(id); - onActionComplete(); - }} - confirmButtonText={confirm} - confirmButtonDisabled={isLoading} - cancelButtonText={cancel} - defaultFocusedButton="confirm" - buttonColor="danger" - > - {message} - - + { + setIsLoading(true); + await api.sendCancel(id); + onActionComplete(); + }} + confirmButtonText={confirm} + confirmButtonDisabled={isLoading} + cancelButtonText={cancel} + defaultFocusedButton="confirm" + buttonColor="danger" + > + {message} + ); }; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx index 381c44b1bf7bef..856e7c8d434835 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useState } from 'react'; @@ -52,26 +52,24 @@ const ExtendConfirm = ({ }); return ( - - { - setIsLoading(true); - await api.sendExtend(id, `${newExpiration.toISOString()}`); - setIsLoading(false); - onConfirmDismiss(); - onActionComplete(); - }} - confirmButtonText={confirm} - confirmButtonDisabled={isLoading} - cancelButtonText={extend} - defaultFocusedButton="confirm" - buttonColor="primary" - > - {message} - - + { + setIsLoading(true); + await api.sendExtend(id, `${newExpiration.toISOString()}`); + setIsLoading(false); + onConfirmDismiss(); + onActionComplete(); + }} + confirmButtonText={confirm} + confirmButtonDisabled={isLoading} + cancelButtonText={extend} + defaultFocusedButton="confirm" + buttonColor="primary" + > + {message} + ); }; diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/context_variables.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/context_variables.ts index c0ff776c94ca10..65c665a182e18a 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/context_variables.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/variables/context_variables.ts @@ -50,7 +50,7 @@ interface PanelValues extends EmbeddableInput { savedObjectId?: string; } -interface ContextValues { +export interface ContextValues { panel: PanelValues; } diff --git a/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json b/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json new file mode 100644 index 00000000000000..50fe41c49b0c88 --- /dev/null +++ b/x-pack/plugins/drilldowns/url_drilldown/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["public/**/*"], + "references": [ + { "path": "../../../../src/core/tsconfig.json" }, + { "path": "../../ui_actions_enhanced/tsconfig.json" }, + { "path": "../../../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../../../src/plugins/expressions/tsconfig.json" }, + { "path": "../../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../../../src/plugins/ui_actions/tsconfig.json" } + ] +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.tsx index e05fc10053ff1c..9bc838c01f636e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.tsx @@ -20,7 +20,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -62,97 +61,94 @@ export const CustomizationModal: React.FC = ({ ); return ( - - - - - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.documents.search.customizationModal.title', + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.documents.search.customizationModal.title', + { + defaultMessage: 'Customize document search', + } + )} + + + + + - - - - - - - - - - - - - - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.documents.search.customizationModal.cancel', + fullWidth + helpText={i18n.translate( + 'xpack.enterpriseSearch.appSearch.documents.search.customizationModal.filterFields', { - defaultMessage: 'Cancel', + defaultMessage: + 'Faceted values rendered as filters and available as query refinement', } )} - - { - onSave({ - filterFields: selectedFilterFields.map(comboBoxOptionToFieldName), - sortFields: selectedSortFields.map(comboBoxOptionToFieldName), - }); - }} > - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.documents.search.customizationModal.save', + + + - - - + > + + + + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.documents.search.customizationModal.cancel', + { + defaultMessage: 'Cancel', + } + )} + + { + onSave({ + filterFields: selectedFilterFields.map(comboBoxOptionToFieldName), + sortFields: selectedSortFields.map(comboBoxOptionToFieldName), + }); + }} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.documents.search.customizationModal.save', + { + defaultMessage: 'Save', + } + )} + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boost_icon.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boost_icon.test.tsx new file mode 100644 index 00000000000000..fd567f52ada249 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boost_icon.test.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiToken } from '@elastic/eui'; + +import { BoostIcon } from './boost_icon'; +import { BoostType } from './types'; + +describe('BoostIcon', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders a token according to the provided type', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiToken).prop('iconType')).toBe('tokenNumber'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boost_icon.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boost_icon.tsx new file mode 100644 index 00000000000000..2570a29274d069 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/boost_icon.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiToken } from '@elastic/eui'; + +import { BOOST_TYPE_TO_ICON_MAP } from './constants'; +import { BoostType } from './types'; + +interface Props { + type: BoostType; +} + +export const BoostIcon: React.FC = ({ type }) => { + return ( + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts index 211995b2a7d188..9fdbb8e979b316 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/constants.ts @@ -7,33 +7,66 @@ import { i18n } from '@kbn/i18n'; +import { BoostType } from './types'; + +export const FIELD_FILTER_CUTOFF = 10; + export const RELEVANCE_TUNING_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.title', { defaultMessage: 'Relevance Tuning' } ); export const UPDATE_SUCCESS_MESSAGE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.relevanceTuning.messages.updateSuccess', + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.messages.updateSuccess', { defaultMessage: 'Relevance successfully tuned. The changes will impact your results shortly.', } ); export const DELETE_SUCCESS_MESSAGE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.relevanceTuning.messages.deleteSuccess', + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.messages.deleteSuccess', { defaultMessage: 'Relevance has been reset to default values. The change will impact your results shortly.', } ); export const RESET_CONFIRMATION_MESSAGE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.relevanceTuning.messages.resetConfirmation', + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.messages.resetConfirmation', { defaultMessage: 'Are you sure you want to restore relevance defaults?', } ); export const DELETE_CONFIRMATION_MESSAGE = i18n.translate( - 'xpack.enterpriseSearch.appSearch.relevanceTuning.messages.deleteConfirmation', + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.messages.deleteConfirmation', { defaultMessage: 'Are you sure you want to delete this boost?', } ); +export const PROXIMITY_DISPLAY = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.proximityDropDownOptionLabel', + { + defaultMessage: 'Proximity', + } +); +export const FUNCTIONAL_DISPLAY = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.functionalDropDownOptionLabel', + { + defaultMessage: 'Functional', + } +); +export const VALUE_DISPLAY = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.valueDropDownOptionLabel', + { + defaultMessage: 'Value', + } +); +export const BOOST_TYPE_TO_DISPLAY_MAP = { + [BoostType.Proximity]: PROXIMITY_DISPLAY, + [BoostType.Functional]: FUNCTIONAL_DISPLAY, + [BoostType.Value]: VALUE_DISPLAY, +}; + +export const BOOST_TYPE_TO_ICON_MAP = { + [BoostType.Value]: 'tokenNumber', + [BoostType.Functional]: 'tokenFunction', + [BoostType.Proximity]: 'tokenGeo', +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx index ea8d43d183ef0f..85cf3dd8a68c97 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.test.tsx @@ -4,20 +4,34 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import '../../../__mocks__/shallow_useeffect.mock'; +import { setMockActions } from '../../../__mocks__/kea.mock'; import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, ShallowWrapper } from 'enzyme'; import { RelevanceTuning } from './relevance_tuning'; +import { RelevanceTuningForm } from './relevance_tuning_form'; describe('RelevanceTuning', () => { + let wrapper: ShallowWrapper; + + const actions = { + initializeRelevanceTuning: jest.fn(), + }; + beforeEach(() => { jest.clearAllMocks(); + setMockActions(actions); + wrapper = shallow(); }); it('renders', () => { - const wrapper = shallow(); - expect(wrapper.isEmptyRender()).toBe(false); + expect(wrapper.find(RelevanceTuningForm).exists()).toBe(true); + }); + + it('initializes relevance tuning data', () => { + expect(actions.initializeRelevanceTuning).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx index 83e83c0f9ea43e..f65a86b1e02f0a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx @@ -5,26 +5,41 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; + +import { useActions } from 'kea'; import { EuiPageHeader, EuiPageHeaderSection, EuiTitle, - EuiPageContentBody, - EuiPageContent, + EuiText, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiTextColor, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import { FlashMessages } from '../../../shared/flash_messages'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { RELEVANCE_TUNING_TITLE } from './constants'; +import { RelevanceTuningForm } from './relevance_tuning_form'; +import { RelevanceTuningLogic } from './relevance_tuning_logic'; interface Props { engineBreadcrumb: string[]; } export const RelevanceTuning: React.FC = ({ engineBreadcrumb }) => { + const { initializeRelevanceTuning } = useActions(RelevanceTuningLogic); + + useEffect(() => { + initializeRelevanceTuning(); + }, []); + return ( <> @@ -33,13 +48,26 @@ export const RelevanceTuning: React.FC = ({ engineBreadcrumb }) => {

    {RELEVANCE_TUNING_TITLE}

    + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.description', + { + defaultMessage: 'Set field weights and boosts', + } + )} + + - - - - - + + + + + + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/index.ts new file mode 100644 index 00000000000000..89e344860b2eb8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { RelevanceTuningForm } from './relevance_tuning_form'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.scss new file mode 100644 index 00000000000000..749fca6f798110 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.scss @@ -0,0 +1,20 @@ +.relevanceTuningForm { + &__item { + width: 100%; + margin-left: $euiSizeS; + } + + &__panel + &__panel { + margin-top: $euiSizeM; + } + + &__panel .euiAccordion__button { + &:hover, + &:focus { + text-decoration: none; + h3 { + text-decoration: underline; + } + } + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.test.tsx new file mode 100644 index 00000000000000..3965e9e81d1ba3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.test.tsx @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; + +import React from 'react'; + +import { shallow, mount, ReactWrapper, ShallowWrapper } from 'enzyme'; + +import { EuiFieldSearch } from '@elastic/eui'; + +import { BoostType } from '../types'; + +import { RelevanceTuningForm } from './relevance_tuning_form'; +import { RelevanceTuningItem } from './relevance_tuning_item'; + +describe('RelevanceTuningForm', () => { + const values = { + filterInputValue: '', + schemaFields: ['foo', 'bar', 'baz'], + filteredSchemaFields: ['foo', 'bar'], + schema: { + foo: 'text', + bar: 'number', + }, + searchSettings: { + boosts: { + foo: [ + { + factor: 2, + type: BoostType.Value, + }, + ], + }, + search_fields: { + bar: { + weight: 1, + }, + }, + }, + }; + const actions = { + setFilterValue: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(actions); + }); + + describe('fields', () => { + let wrapper: ReactWrapper; + let relevantTuningItems: any; + + beforeAll(() => { + setMockValues(values); + + wrapper = mount(); + relevantTuningItems = wrapper.find(RelevanceTuningItem); + }); + + it('renders a list of fields that may or may not have been filterd by user input', () => { + // The length is 2 because we're only pulling values from `filteredSchemaFields`, which + // is the list of schema fields that has been filtered by user input down to 2 + expect(relevantTuningItems.length).toBe(2); + }); + + it('will pass the schema field name in the "name" prop of each list item', () => { + expect(relevantTuningItems.at(0).prop('name')).toBe('foo'); + expect(relevantTuningItems.at(1).prop('name')).toBe('bar'); + }); + + it('will pass the schema type of the field in the "type" prop of each list item', () => { + expect(relevantTuningItems.at(0).prop('type')).toBe('text'); + expect(relevantTuningItems.at(1).prop('type')).toBe('number'); + }); + + it('will pass a list of boosts in the "boosts" field of each list item, or undefined if none exist', () => { + expect(relevantTuningItems.at(0).prop('boosts')).toEqual([ + { + factor: 2, + type: BoostType.Value, + }, + ]); + expect(relevantTuningItems.at(1).prop('boosts')).toBeUndefined(); + }); + + it('will pass the search_field configuration for the field in the "field" prop of each list item, or undefined if none exists', () => { + expect(relevantTuningItems.at(0).prop('field')).toBeUndefined(); + expect(relevantTuningItems.at(1).prop('field')).toEqual({ + weight: 1, + }); + }); + }); + + describe('field filtering', () => { + let searchField: ShallowWrapper; + + beforeEach(() => { + setMockValues({ + ...values, + filterInputValue: 'test', + schemaFields: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'], + }); + const wrapper = shallow(); + searchField = wrapper.find(EuiFieldSearch); + }); + + it('renders an input box for filtering the field list in case there is a large quantity of fields', () => { + expect(searchField.exists()).toBe(true); + }); + + it('initializes the input box with the user input value stored in state', () => { + expect(searchField.prop('value')).toBe('test'); + }); + + it('updates the user input value stored in state whenever the input box value changes', () => { + searchField.simulate('change', { + target: { + value: 'new value', + }, + }); + + expect(actions.setFilterValue).toHaveBeenCalledWith('new value'); + }); + + it('will not render a field filter if there are 10 or less fields', () => { + setMockValues({ + ...values, + schemaFields: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'], + }); + const wrapper = shallow(); + expect(wrapper.find(EuiFieldSearch).exists()).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.tsx new file mode 100644 index 00000000000000..e39c93fd5de3cb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_form.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiPageHeader, + EuiPageHeaderSection, + EuiTitle, + EuiFieldSearch, + EuiSpacer, + EuiAccordion, + EuiPanel, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { FIELD_FILTER_CUTOFF } from '../constants'; +import { RelevanceTuningLogic } from '../relevance_tuning_logic'; + +import { RelevanceTuningItem } from './relevance_tuning_item'; +import { RelevanceTuningItemContent } from './relevance_tuning_item_content'; + +import './relevance_tuning_form.scss'; + +export const RelevanceTuningForm: React.FC = () => { + const { + filterInputValue, + schemaFields, + filteredSchemaFields, + schema, + searchSettings, + } = useValues(RelevanceTuningLogic); + const { setFilterValue } = useActions(RelevanceTuningLogic); + + return ( +
    +
    + {/* TODO SchemaConflictCallout */} + + + + +

    + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.manageFields.title', + { + defaultMessage: 'Manage fields', + } + )} +

    +
    +
    +
    + {schemaFields.length > FIELD_FILTER_CUTOFF && ( + setFilterValue(e.target.value)} + placeholder={i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.manageFields.filterPlaceholder', + { + defaultMessage: 'Filter {schemaFieldsLength} fields...', + values: { + schemaFieldsLength: schemaFields.length, + }, + } + )} + fullWidth + /> + )} + + {filteredSchemaFields.map((fieldName) => ( + + + } + paddingSize="s" + > + + + + ))} + +
    + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.test.tsx new file mode 100644 index 00000000000000..6043e7ae65b268 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.test.tsx @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { SchemaTypes } from '../../../../shared/types'; + +import { BoostIcon } from '../boost_icon'; +import { Boost, BoostType, SearchField } from '../types'; + +import { RelevanceTuningItem } from './relevance_tuning_item'; +import { ValueBadge } from './value_badge'; + +describe('RelevanceTuningItem', () => { + const props = { + name: 'foo', + type: 'text' as SchemaTypes, + boosts: [ + { + factor: 2, + type: BoostType.Value, + }, + ], + field: { + weight: 1, + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('boosts prop', () => { + const renderComponentWithBoostsConfig = (boosts?: Boost[]) => { + return shallow( + + ); + }; + + describe('when there are boosts for this field', () => { + it('renders an icon for each boost that is applied', () => { + const wrapper = renderComponentWithBoostsConfig([ + { + factor: 2, + type: BoostType.Value, + }, + { + factor: 3, + type: BoostType.Proximity, + }, + ]); + expect(wrapper.find(BoostIcon).length).toBe(2); + expect(wrapper.find(BoostIcon).map((euiToken) => euiToken.prop('type'))).toEqual([ + BoostType.Value, + BoostType.Proximity, + ]); + }); + }); + + describe('when there are no boosts for this field', () => { + const wrapper = renderComponentWithBoostsConfig(); + + it('renders an icon for each boost that is applied', () => { + expect(wrapper.find(BoostIcon).length).toBe(0); + }); + }); + }); + + describe('field prop', () => { + const renderComponentWithFieldConfig = (field?: SearchField) => { + return shallow( + + ); + }; + + describe('when weight is set to any positive number', () => { + const wrapper = renderComponentWithFieldConfig({ + weight: 1, + }); + + it('will show the weight with an "enabled" style', () => { + const valueBadge = wrapper.find(ValueBadge); + expect(valueBadge.dive().text()).toContain('1'); + expect(valueBadge.prop('disabled')).toBe(false); + }); + }); + + describe('when weight set to "0", which means this field will not be searched', () => { + const wrapper = renderComponentWithFieldConfig({ + weight: 0, + }); + + it('will show 0 with a "disabled" style', () => { + const valueBadge = wrapper.find(ValueBadge); + expect(valueBadge.dive().text()).toContain('0'); + expect(valueBadge.prop('disabled')).toBe(true); + }); + }); + + describe('when there is no weight set, which means this field will not be searched', () => { + const wrapper = renderComponentWithFieldConfig(); + + it('will show "0" with a "disabled" style', () => { + const valueBadge = wrapper.find(ValueBadge); + expect(valueBadge.dive().text()).toContain('0'); + expect(valueBadge.prop('disabled')).toBe(true); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.tsx new file mode 100644 index 00000000000000..38cec4825cfe7f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiText, EuiFlexGroup, EuiFlexItem, EuiTitle, EuiTextColor, EuiIcon } from '@elastic/eui'; + +import { SchemaTypes } from '../../../../shared/types'; + +import { BoostIcon } from '../boost_icon'; +import { Boost, SearchField } from '../types'; + +import { ValueBadge } from './value_badge'; + +interface Props { + name: string; + type: SchemaTypes; + boosts?: Boost[]; + field?: SearchField; +} + +export const RelevanceTuningItem: React.FC = ({ name, type, boosts = [], field }) => { + return ( + + + +

    {name}

    +
    + + {type} + +
    + + + {boosts.map((boost, index) => ( + + + + ))} + + + + {!!field ? field.weight : 0} + + + + +
    + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boost_item.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boost_item.tsx new file mode 100644 index 00000000000000..e5a76bc586b80c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boost_item.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; + +import { EuiFlexItem, EuiAccordion, EuiFlexGroup, EuiHideFor } from '@elastic/eui'; + +import { BoostIcon } from '../../../boost_icon'; +import { BOOST_TYPE_TO_DISPLAY_MAP } from '../../../constants'; +import { Boost } from '../../../types'; +import { ValueBadge } from '../../value_badge'; + +import { getBoostSummary } from './get_boost_summary'; + +interface Props { + boost: Boost; + id: string; +} + +export const BoostItem: React.FC = ({ id, boost }) => { + const summary = useMemo(() => getBoostSummary(boost), [boost]); + + return ( + + + + + + + {BOOST_TYPE_TO_DISPLAY_MAP[boost.type]} + + {summary} + + + + + {boost.factor} + + + } + paddingSize="s" + /> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.scss new file mode 100644 index 00000000000000..53b3c233301b08 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.scss @@ -0,0 +1,28 @@ +.boosts { + &__select { + min-width: $euiSizeXXL * 4; + } + + &__itemContent { + width: 100%; + } + + &__item { + margin-top: $euiSize; + + & + & { + margin-top: $euiSizeS; + } + } +} + +.boostSelectOption { + .euiContextMenuItem__text { + display: flex; + align-items: center; + + .euiToken { + margin-right: $euiSizeS; + } + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.test.tsx new file mode 100644 index 00000000000000..b313e16c0bda11 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.test.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockActions } from '../../../../../../__mocks__/kea.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiSuperSelect } from '@elastic/eui'; + +import { SchemaTypes } from '../../../../../../shared/types'; + +import { Boosts } from './boosts'; + +describe('Boosts', () => { + const actions = { + addBoost: jest.fn(), + }; + + beforeAll(() => { + setMockActions(actions); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const props = { + name: 'foo', + type: 'number' as SchemaTypes, + }; + + it('renders a select box that allows users to create boosts of various types', () => { + const wrapper = shallow(); + + const select = wrapper.find(EuiSuperSelect); + expect(select.prop('options').map((o: any) => o.value)).toEqual([ + 'add-boost', + 'functional', + 'proximity', + 'value', + ]); + }); + + it('will not render functional or proximity options if "type" prop is "text"', () => { + const wrapper = shallow( + + ); + + const select = wrapper.find(EuiSuperSelect); + expect(select.prop('options').map((o: any) => o.value)).toEqual(['add-boost', 'value']); + }); + + it('will add a boost of the selected type when a selection is made', () => { + const wrapper = shallow(); + + wrapper.find(EuiSuperSelect).simulate('change', 'functional'); + + expect(actions.addBoost).toHaveBeenCalledWith('foo', 'functional'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.tsx new file mode 100644 index 00000000000000..1ad27346d2630e --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/boosts.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; + +import { useActions } from 'kea'; + +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiTitle, EuiSuperSelect } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { TEXT } from '../../../../../../shared/constants/field_types'; +import { SchemaTypes } from '../../../../../../shared/types'; + +import { BoostIcon } from '../../../boost_icon'; +import { FUNCTIONAL_DISPLAY, PROXIMITY_DISPLAY, VALUE_DISPLAY } from '../../../constants'; +import { RelevanceTuningLogic } from '../../../relevance_tuning_logic'; +import { Boost, BoostType } from '../../../types'; + +import { BoostItem } from './boost_item'; + +import './boosts.scss'; + +const BASE_OPTIONS = [ + { + value: 'add-boost', + inputDisplay: i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.addBoostDropDownOptionLabel', + { + defaultMessage: 'Add boost', + } + ), + disabled: true, + }, + { + value: BoostType.Functional, + inputDisplay: ( + <> + + {FUNCTIONAL_DISPLAY} + + ), + }, + { + value: BoostType.Proximity, + inputDisplay: ( + <> + + {PROXIMITY_DISPLAY} + + ), + }, + { + value: BoostType.Value, + inputDisplay: ( + <> + + {VALUE_DISPLAY} + + ), + }, +]; + +const filterInvalidOptions = (value: BoostType, type: SchemaTypes) => { + // Proximity and Functional boost types are not valid for text fields + if (type === TEXT && [BoostType.Proximity, BoostType.Functional].includes(value)) return false; + return true; +}; + +interface Props { + name: string; + type: SchemaTypes; + boosts?: Boost[]; +} + +export const Boosts: React.FC = ({ name, type, boosts = [] }) => { + const { addBoost } = useActions(RelevanceTuningLogic); + + const selectOptions = useMemo( + () => BASE_OPTIONS.filter((option) => filterInvalidOptions(option.value as BoostType, type)), + [type] + ); + + return ( + + + + +

    + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.relevanceTuning.boosts.title', + { + defaultMessage: 'Boosts', + } + )} +

    +
    +
    + + addBoost(name, value as BoostType)} + /> + +
    + {boosts.map((boost, index) => ( + + ))} +
    + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/get_boost_summary.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/get_boost_summary.test.ts new file mode 100644 index 00000000000000..f6852569213a68 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/get_boost_summary.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Boost, BoostFunction, BoostType, BoostOperation } from '../../../types'; + +import { getBoostSummary } from './get_boost_summary'; + +describe('getBoostSummary', () => { + describe('when the boost type is "value"', () => { + const boost: Boost = { + type: BoostType.Value, + value: ['1', '2'], + factor: 5, + }; + + it('creates a summary that is the joined values', () => { + expect(getBoostSummary(boost)).toEqual('1,2'); + }); + + it('creates an empty summary if there is no value', () => { + expect( + getBoostSummary({ + ...boost, + value: undefined, + }) + ).toEqual(''); + }); + }); + + describe('when the boost type is "proximity"', () => { + const boost: Boost = { + type: BoostType.Proximity, + function: 'gaussian' as BoostFunction, + factor: 5, + }; + + it('creates a summary that is just the name of the function', () => { + expect(getBoostSummary(boost)).toEqual('gaussian'); + }); + + it('creates an empty summary if there is no function', () => { + expect( + getBoostSummary({ + ...boost, + function: undefined, + }) + ).toEqual(''); + }); + }); + + describe('when the boost type is "functional"', () => { + const boost: Boost = { + type: BoostType.Functional, + function: BoostFunction.Gaussian, + operation: BoostOperation.Add, + factor: 5, + }; + + it('creates a summary that is name of the function and operation', () => { + expect(getBoostSummary(boost)).toEqual('gaussian add'); + }); + + it('prints empty if function or operation is missing', () => { + expect(getBoostSummary({ ...boost, function: undefined })).toEqual(BoostOperation.Add); + expect(getBoostSummary({ ...boost, operation: undefined })).toEqual(BoostFunction.Gaussian); + expect(getBoostSummary({ ...boost, function: undefined, operation: undefined })).toEqual(''); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/get_boost_summary.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/get_boost_summary.ts new file mode 100644 index 00000000000000..f3922ebb0fffe9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/get_boost_summary.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Boost, BoostType } from '../../../types'; + +export const getBoostSummary = (boost: Boost): string => { + if (boost.type === BoostType.Value) { + return !boost.value ? '' : boost.value.join(','); + } else if (boost.type === BoostType.Proximity) { + return boost.function || ''; + } else { + return [boost.function || '', boost.operation || ''].join(' ').trim(); + } +}; diff --git a/x-pack/plugins/security_solution/public/cases/components/property_actions/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/index.ts similarity index 86% rename from x-pack/plugins/security_solution/public/cases/components/property_actions/constants.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/index.ts index e23bae6f2403e6..dc269132769b81 100644 --- a/x-pack/plugins/security_solution/public/cases/components/property_actions/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/boosts/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export const SET_STATE = 'SET_STATE'; +export { Boosts } from './boosts'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/index.ts new file mode 100644 index 00000000000000..dc5b95320d4db6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { RelevanceTuningItemContent } from './relevance_tuning_item_content'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.scss new file mode 100644 index 00000000000000..63718a95551fa5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.scss @@ -0,0 +1,6 @@ +.relevanceTuningForm { + &__itemContent { + border: none; + border-top: $euiBorderThin; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.test.tsx new file mode 100644 index 00000000000000..18a75766cd67be --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { SchemaTypes } from '../../../../../shared/types'; +import { BoostType } from '../../types'; + +import { RelevanceTuningItemContent } from './relevance_tuning_item_content'; +import { TextSearchToggle } from './text_search_toggle'; +import { WeightSlider } from './weight_slider'; + +describe('RelevanceTuningItemContent', () => { + const props = { + name: 'foo', + type: 'text' as SchemaTypes, + boosts: [ + { + factor: 2, + type: BoostType.Value, + }, + ], + field: { + weight: 1, + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders', () => { + const wrapper = shallow(); + + const textSearchToggle = wrapper.find(TextSearchToggle); + expect(textSearchToggle.exists()).toBe(true); + expect(textSearchToggle.prop('name')).toBe(props.name); + expect(textSearchToggle.prop('type')).toBe(props.type); + expect(textSearchToggle.prop('field')).toBe(props.field); + + const weightSlider = wrapper.find(WeightSlider); + expect(weightSlider.exists()).toBe(true); + expect(weightSlider.prop('name')).toBe(props.name); + expect(weightSlider.prop('field')).toBe(props.field); + }); + + it('will not render a WeightSlider if the field prop is empty', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(WeightSlider).exists()).toBe(false); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.tsx new file mode 100644 index 00000000000000..29ab559485d776 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/relevance_tuning_item_content.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiPanel } from '@elastic/eui'; + +import { SchemaTypes } from '../../../../../shared/types'; + +import { Boost, SearchField } from '../../types'; + +import { Boosts } from './boosts'; +import { TextSearchToggle } from './text_search_toggle'; +import { WeightSlider } from './weight_slider'; + +import './relevance_tuning_item_content.scss'; + +interface Props { + name: string; + type: SchemaTypes; + boosts?: Boost[]; + field?: SearchField; +} + +export const RelevanceTuningItemContent: React.FC = ({ name, type, boosts, field }) => { + return ( + <> + + + {field && } + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/text_search_toggle.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/text_search_toggle.test.tsx new file mode 100644 index 00000000000000..7225fce5daa611 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/text_search_toggle.test.tsx @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockActions } from '../../../../../__mocks__/kea.mock'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiSwitch } from '@elastic/eui'; + +import { SchemaTypes } from '../../../../../shared/types'; + +import { TextSearchToggle } from './text_search_toggle'; + +describe('TextSearchToggle', () => { + const actions = { + toggleSearchField: jest.fn(), + }; + + beforeAll(() => { + setMockActions(actions); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('typical render', () => { + let wrapper: ShallowWrapper; + + const props = { + name: 'foo', + type: 'text' as SchemaTypes, + field: { + weight: 1, + }, + }; + + beforeAll(() => { + wrapper = shallow(); + }); + + it('renders a toggle button', () => { + expect(wrapper.find(EuiSwitch).exists()).toBe(true); + }); + + it('shows the toggle button as checked if any value was passed in the "field" prop', () => { + expect(wrapper.find(EuiSwitch).prop('checked')).toBe(true); + }); + + it('shows the toggle as enabled if "text" was passed in the "type" prop', () => { + expect(wrapper.find(EuiSwitch).prop('disabled')).toBe(false); + }); + + it('shows a relevant label if "text" was passed in the "type" prop', () => { + expect(wrapper.find(EuiSwitch).prop('label')).toBe('Search this field'); + }); + + it('will update toggled state when clicked', () => { + wrapper.find(EuiSwitch).simulate('change'); + expect(actions.toggleSearchField).toHaveBeenCalledWith('foo', true); + }); + }); + + describe('when a non-"text" type is passed in the "type" prop', () => { + let wrapper: ShallowWrapper; + + const props = { + name: 'foo', + type: 'number' as SchemaTypes, + field: { + weight: 1, + }, + }; + + beforeAll(() => { + wrapper = shallow(); + }); + + it('shows the toggle button as disabled', () => { + expect(wrapper.find(EuiSwitch).prop('checked')).toBe(true); + }); + + it('shows a relevant label', () => { + expect(wrapper.find(EuiSwitch).prop('label')).toBe( + 'Search can only be enabled on text fields' + ); + }); + + it('will not update state when the clicked', () => { + wrapper.find(EuiSwitch).simulate('change'); + expect(actions.toggleSearchField).not.toHaveBeenCalled(); + }); + }); + + describe('when no field prop is passed', () => { + let wrapper: ShallowWrapper; + + const props = { + name: 'foo', + type: 'text' as SchemaTypes, + }; + + beforeAll(() => { + wrapper = shallow(); + }); + + it('shows the toggle button as unchecked', () => { + expect(wrapper.find(EuiSwitch).prop('checked')).toBe(false); + }); + + it('will update toggled state when clicked', () => { + wrapper.find(EuiSwitch).simulate('change'); + expect(actions.toggleSearchField).toHaveBeenCalledWith('foo', false); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/text_search_toggle.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/text_search_toggle.tsx new file mode 100644 index 00000000000000..607ddd9c6b0782 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/text_search_toggle.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions } from 'kea'; + +import { EuiFormRow, EuiSwitch } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { TEXT } from '../../../../../shared/constants/field_types'; +import { SchemaTypes } from '../../../../../shared/types'; + +import { RelevanceTuningLogic } from '../../relevance_tuning_logic'; +import { SearchField } from '../../types'; + +interface Props { + name: string; + type: SchemaTypes; + field?: SearchField; +} + +export const TextSearchToggle: React.FC = ({ name, type, field }) => { + const { toggleSearchField } = useActions(RelevanceTuningLogic); + + return ( + + type === TEXT && toggleSearchField(name, !!field)} + checked={!!field} + disabled={type !== TEXT} + /> + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/weight_slider.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/weight_slider.test.tsx new file mode 100644 index 00000000000000..21a112a4ea9889 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/weight_slider.test.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockActions } from '../../../../../__mocks__/kea.mock'; + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +import { EuiRange } from '@elastic/eui'; + +import { WeightSlider } from './weight_slider'; + +describe('WeightSlider', () => { + let wrapper: ShallowWrapper; + + const actions = { + updateFieldWeight: jest.fn(), + }; + + beforeAll(() => { + setMockActions(actions); + wrapper = shallow( + + ); + }); + + it('renders with an initial value set', () => { + expect(wrapper.find(EuiRange).exists()).toBe(true); + expect(wrapper.find(EuiRange).prop('value')).toBe(2.2); + }); + + it('updates field weight in state when the value changes', () => { + wrapper.find(EuiRange).simulate('change', { + target: { + value: '1.3', + }, + }); + expect(actions.updateFieldWeight).toHaveBeenCalledWith('foo', 1.3); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/weight_slider.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/weight_slider.tsx new file mode 100644 index 00000000000000..02e83b81b2cb13 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/relevance_tuning_item_content/weight_slider.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions } from 'kea'; + +import { EuiFormRow, EuiRange } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; + +import { RelevanceTuningLogic } from '../../relevance_tuning_logic'; +import { SearchField } from '../../types'; + +interface Props { + name: string; + field: SearchField; +} + +export const WeightSlider: React.FC = ({ name, field }) => { + const { updateFieldWeight } = useActions(RelevanceTuningLogic); + + return ( + + + updateFieldWeight( + name, + parseFloat((e as React.ChangeEvent).target.value) + ) + } + showInput + compressed + fullWidth + /> + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/value_badge.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/value_badge.scss new file mode 100644 index 00000000000000..853edd3fed0a38 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/value_badge.scss @@ -0,0 +1,23 @@ +.relevanceTuningForm { + .valueBadge { + display: inline-flex; + align-items: center; + height: $euiSizeL; + border: 1px solid $euiColorLightShade; + border-radius: $euiSizeXS; + // To match the background of EuiToken, for which there is no direct variable to + // reference + background: tintOrShade($euiColorVis1, 90%, 70%); + color: $euiColorVis1; + padding: 0 $euiSizeS; + + .euiIcon { + margin-right: $euiSizeXS; + } + } + + .valueBadge--disabled { + background: transparent; + color: $euiColorMediumShade; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/value_badge.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/value_badge.tsx new file mode 100644 index 00000000000000..8397087ca69b46 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_form/value_badge.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import classNames from 'classnames'; + +import './value_badge.scss'; + +export const ValueBadge: React.FC<{ children: React.ReactNode; disabled?: boolean }> = ({ + children, + disabled = false, +}) => { + const className = classNames('valueBadge', { + 'valueBadge--disabled': disabled, + }); + return {children}; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts index 194848bcfc86c0..a7ee6f9755fc4a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts @@ -9,7 +9,7 @@ import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../ import { nextTick } from '@kbn/test/jest'; -import { Boost, BoostType } from './types'; +import { Boost, BoostFunction, BoostOperation, BoostType } from './types'; import { RelevanceTuningLogic } from './'; @@ -24,7 +24,7 @@ describe('RelevanceTuningLogic', () => { boosts: { foo: [ { - type: 'value' as BoostType, + type: BoostType.Value, factor: 5, }, ], @@ -97,6 +97,18 @@ describe('RelevanceTuningLogic', () => { schemaConflicts, }); }); + + it('should default schemaConflicts if it is not passed', () => { + mount({ + dataLoading: true, + }); + RelevanceTuningLogic.actions.onInitializeRelevanceTuning({ + searchSettings, + schema, + }); + + expect(RelevanceTuningLogic.values.schemaConflicts).toEqual({}); + }); }); describe('setSearchSettings', () => { @@ -237,7 +249,7 @@ describe('RelevanceTuningLogic', () => { foo: [ { factor: 1, - type: 'functional', + type: BoostType.Functional, }, boost, ], @@ -265,7 +277,7 @@ describe('RelevanceTuningLogic', () => { boosts: { foo: [ { - type: 'value' as BoostType, + type: BoostType.Value, factor: 5, value: 5, }, @@ -289,7 +301,7 @@ describe('RelevanceTuningLogic', () => { boosts: { foo: [ { - type: 'value' as BoostType, + type: BoostType.Value, factor: 5, value: ['5'], }, @@ -324,19 +336,25 @@ describe('RelevanceTuningLogic', () => { boosts: { foo: [ { - type: 'value' as BoostType, + type: BoostType.Value, factor: 5, newBoost: true, // This should be deleted before sent to the server }, ], }, + search_fields: { + bar: { + weight: 1, + }, + }, }; const searchSettingsWithoutNewBoostProp = { + ...searchSettingsWithNewBoostProp, boosts: { foo: [ { - type: 'value' as BoostType, + type: BoostType.Value, factor: 5, }, ], @@ -475,7 +493,7 @@ describe('RelevanceTuningLogic', () => { boosts: { foo: [ { - type: 'value' as BoostType, + type: BoostType.Value, factor: 5, newBoost: true, // This should be deleted before sent to the server }, @@ -487,7 +505,7 @@ describe('RelevanceTuningLogic', () => { boosts: { foo: [ { - type: 'value' as BoostType, + type: BoostType.Value, factor: 5, }, ], @@ -672,7 +690,7 @@ describe('RelevanceTuningLogic', () => { foo: [ { factor: 2, - type: 'value', + type: BoostType.Value, }, ], }, @@ -680,7 +698,7 @@ describe('RelevanceTuningLogic', () => { }); jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); - RelevanceTuningLogic.actions.addBoost('foo', 'functional'); + RelevanceTuningLogic.actions.addBoost('foo', BoostType.Functional); expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith({ ...searchSettings, @@ -688,12 +706,12 @@ describe('RelevanceTuningLogic', () => { foo: [ { factor: 2, - type: 'value', + type: BoostType.Value, }, { factor: 1, newBoost: true, - type: 'functional', + type: BoostType.Functional, }, ], }, @@ -709,7 +727,7 @@ describe('RelevanceTuningLogic', () => { }); jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); - RelevanceTuningLogic.actions.addBoost('foo', 'functional'); + RelevanceTuningLogic.actions.addBoost('foo', BoostType.Functional); expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith({ ...searchSettings, @@ -718,7 +736,7 @@ describe('RelevanceTuningLogic', () => { { factor: 1, newBoost: true, - type: 'functional', + type: BoostType.Functional, }, ], }, @@ -735,11 +753,11 @@ describe('RelevanceTuningLogic', () => { foo: [ { factor: 1, - type: 'functional', + type: BoostType.Functional, }, { factor: 2, - type: 'value', + type: BoostType.Value, }, ], }, @@ -756,7 +774,7 @@ describe('RelevanceTuningLogic', () => { foo: [ { factor: 1, - type: 'functional', + type: BoostType.Functional, }, ], }, @@ -771,7 +789,7 @@ describe('RelevanceTuningLogic', () => { foo: [ { factor: 1, - type: 'functional', + type: BoostType.Functional, }, ], }, @@ -796,7 +814,7 @@ describe('RelevanceTuningLogic', () => { foo: [ { factor: 1, - type: 'functional', + type: BoostType.Functional, }, ], }, @@ -816,7 +834,7 @@ describe('RelevanceTuningLogic', () => { mount({ searchSettings: searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, }), }); jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); @@ -826,7 +844,7 @@ describe('RelevanceTuningLogic', () => { expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( searchSettingsWithBoost({ factor: 5, - type: 'functional', + type: BoostType.Functional, }) ); }); @@ -835,7 +853,7 @@ describe('RelevanceTuningLogic', () => { mount({ searchSettings: searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, }), }); jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); @@ -845,7 +863,7 @@ describe('RelevanceTuningLogic', () => { expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( searchSettingsWithBoost({ factor: 5.3, - type: 'functional', + type: BoostType.Functional, }) ); }); @@ -856,7 +874,7 @@ describe('RelevanceTuningLogic', () => { mount({ searchSettings: searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, value: ['a', 'b', 'c'], }), }); @@ -867,7 +885,7 @@ describe('RelevanceTuningLogic', () => { expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, value: ['a', 'a', 'c'], }) ); @@ -877,7 +895,7 @@ describe('RelevanceTuningLogic', () => { mount({ searchSettings: searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, }), }); jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); @@ -887,7 +905,7 @@ describe('RelevanceTuningLogic', () => { expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, value: ['a'], }) ); @@ -902,7 +920,7 @@ describe('RelevanceTuningLogic', () => { }, searchSettings: searchSettingsWithBoost({ factor: 1, - type: 'proximity', + type: BoostType.Proximity, center: 1, }), }); @@ -913,7 +931,7 @@ describe('RelevanceTuningLogic', () => { expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( searchSettingsWithBoost({ factor: 1, - type: 'proximity', + type: BoostType.Proximity, center: 4, }) ); @@ -925,7 +943,7 @@ describe('RelevanceTuningLogic', () => { mount({ searchSettings: searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, value: ['a'], }), }); @@ -936,7 +954,7 @@ describe('RelevanceTuningLogic', () => { expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, value: ['a', ''], }) ); @@ -946,7 +964,7 @@ describe('RelevanceTuningLogic', () => { mount({ searchSettings: searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, }), }); jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); @@ -956,7 +974,7 @@ describe('RelevanceTuningLogic', () => { expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, value: ['', ''], }) ); @@ -966,7 +984,7 @@ describe('RelevanceTuningLogic', () => { mount({ searchSettings: searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, value: ['a', ''], }), }); @@ -977,7 +995,7 @@ describe('RelevanceTuningLogic', () => { expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, value: ['a', ''], }) ); @@ -989,7 +1007,7 @@ describe('RelevanceTuningLogic', () => { mount({ searchSettings: searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, value: ['a', 'b', 'c'], }), }); @@ -1000,7 +1018,7 @@ describe('RelevanceTuningLogic', () => { expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, value: ['a', 'c'], }) ); @@ -1010,7 +1028,7 @@ describe('RelevanceTuningLogic', () => { mount({ searchSettings: searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, }), }); jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); @@ -1026,18 +1044,23 @@ describe('RelevanceTuningLogic', () => { mount({ searchSettings: searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, }), }); jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); - RelevanceTuningLogic.actions.updateBoostSelectOption('foo', 1, 'function', 'exponential'); + RelevanceTuningLogic.actions.updateBoostSelectOption( + 'foo', + 1, + 'function', + BoostFunction.Exponential + ); expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( searchSettingsWithBoost({ factor: 1, - type: 'functional', - function: 'exponential', + type: BoostType.Functional, + function: BoostFunction.Exponential, }) ); }); @@ -1046,18 +1069,23 @@ describe('RelevanceTuningLogic', () => { mount({ searchSettings: searchSettingsWithBoost({ factor: 1, - type: 'functional', + type: BoostType.Functional, }), }); jest.spyOn(RelevanceTuningLogic.actions, 'setSearchSettings'); - RelevanceTuningLogic.actions.updateBoostSelectOption('foo', 1, 'operation', 'add'); + RelevanceTuningLogic.actions.updateBoostSelectOption( + 'foo', + 1, + 'operation', + BoostOperation.Add + ); expect(RelevanceTuningLogic.actions.setSearchSettings).toHaveBeenCalledWith( searchSettingsWithBoost({ factor: 1, - type: 'functional', - operation: 'add', + type: BoostType.Functional, + operation: BoostOperation.Add, }) ); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts index cd3d8b5686cc05..d567afee9d0627 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.ts @@ -21,7 +21,14 @@ import { DELETE_SUCCESS_MESSAGE, DELETE_CONFIRMATION_MESSAGE, } from './constants'; -import { BaseBoost, Boost, BoostType, SearchSettings } from './types'; +import { + BaseBoost, + Boost, + BoostFunction, + BoostOperation, + BoostType, + SearchSettings, +} from './types'; import { filterIfTerm, parseBoostCenter, @@ -32,7 +39,7 @@ import { interface RelevanceTuningProps { searchSettings: SearchSettings; schema: Schema; - schemaConflicts: SchemaConflicts; + schemaConflicts?: SchemaConflicts; } interface RelevanceTuningActions { @@ -82,7 +89,7 @@ interface RelevanceTuningActions { name: string, boostIndex: number, optionType: keyof BaseBoost, - value: string + value: BoostOperation | BoostFunction ): { name: string; boostIndex: number; @@ -176,7 +183,7 @@ export const RelevanceTuningLogic = kea< schemaConflicts: [ {}, { - onInitializeRelevanceTuning: (_, { schemaConflicts }) => schemaConflicts, + onInitializeRelevanceTuning: (_, { schemaConflicts }) => schemaConflicts || {}, }, ], showSchemaConflictCallout: [ @@ -497,7 +504,11 @@ export const RelevanceTuningLogic = kea< const { searchSettings } = values; const { boosts } = searchSettings; const updatedBoosts = cloneDeep(boosts[name]); - updatedBoosts[boostIndex][optionType] = value; + if (optionType === 'operation') { + updatedBoosts[boostIndex][optionType] = value as BoostOperation; + } else { + updatedBoosts[boostIndex][optionType] = value as BoostFunction; + } actions.setSearchSettings({ ...searchSettings, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts index a1ed9797b9f5a5..95bd33aac5b9ff 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/types.ts @@ -5,11 +5,26 @@ * 2.0. */ -export type BoostType = 'value' | 'functional' | 'proximity'; +export enum BoostType { + Value = 'value', + Functional = 'functional', + Proximity = 'proximity', +} + +export enum BoostFunction { + Gaussian = 'gaussian', + Exponential = 'exponential', + Linear = 'linear', +} + +export enum BoostOperation { + Add = 'add', + Multiple = 'multiply', +} export interface BaseBoost { - operation?: string; - function?: string; + operation?: BoostOperation; + function?: BoostFunction; } // A boost that comes from the server, before we normalize it has a much looser schema @@ -25,13 +40,13 @@ export interface RawBoost extends BaseBoost { export interface Boost extends RawBoost { value?: string[]; } + +export interface SearchField { + weight: number; +} + export interface SearchSettings { boosts: Record; - search_fields: Record< - string, - { - weight: number; - } - >; + search_fields: Record; result_fields?: object; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.test.ts index a6598bf991c13e..1694015ed68615 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/utils.test.ts @@ -39,7 +39,7 @@ describe('removeBoostStateProps', () => { boosts: { foo: [ { - type: 'value' as BoostType, + type: BoostType.Value, factor: 5, newBoost: true, }, @@ -56,7 +56,7 @@ describe('removeBoostStateProps', () => { boosts: { foo: [ { - type: 'value' as BoostType, + type: BoostType.Value, factor: 5, }, ], @@ -66,39 +66,46 @@ describe('removeBoostStateProps', () => { }); describe('parseBoostCenter', () => { - it('should parse a boost center', () => { - expect(parseBoostCenter('text', 5)).toEqual(5); - expect(parseBoostCenter('text', '4')).toEqual('4'); + it('should parse the value to a number when the type is number', () => { expect(parseBoostCenter('number', 5)).toEqual(5); expect(parseBoostCenter('number', '5')).toEqual(5); }); + + it('should not try to parse the value when the type is text', () => { + expect(parseBoostCenter('text', 5)).toEqual(5); + expect(parseBoostCenter('text', '4')).toEqual('4'); + }); + + it('should leave text invalid numbers alone', () => { + expect(parseBoostCenter('number', 'foo')).toEqual('foo'); + }); }); describe('normalizeBoostValues', () => { const boosts = { foo: [ { - type: 'value' as BoostType, + type: BoostType.Value, factor: 9.5, value: 1, }, { - type: 'value' as BoostType, + type: BoostType.Value, factor: 9.5, value: '1', }, { - type: 'value' as BoostType, + type: BoostType.Value, factor: 9.5, value: [1], }, { - type: 'value' as BoostType, + type: BoostType.Value, factor: 9.5, value: ['1'], }, { - type: 'value' as BoostType, + type: BoostType.Value, factor: 9.5, value: [ '1', @@ -115,13 +122,13 @@ describe('normalizeBoostValues', () => { ], bar: [ { - type: 'proximity' as BoostType, + type: BoostType.Proximity, factor: 9.5, }, ], sp_def: [ { - type: 'functional' as BoostType, + type: BoostType.Functional, factor: 5, }, ], @@ -129,19 +136,19 @@ describe('normalizeBoostValues', () => { it('converts all value types to string for consistency', () => { expect(normalizeBoostValues(boosts)).toEqual({ - bar: [{ factor: 9.5, type: 'proximity' }], + bar: [{ factor: 9.5, type: BoostType.Proximity }], foo: [ - { factor: 9.5, type: 'value', value: ['1'] }, - { factor: 9.5, type: 'value', value: ['1'] }, - { factor: 9.5, type: 'value', value: ['1'] }, - { factor: 9.5, type: 'value', value: ['1'] }, + { factor: 9.5, type: BoostType.Value, value: ['1'] }, + { factor: 9.5, type: BoostType.Value, value: ['1'] }, + { factor: 9.5, type: BoostType.Value, value: ['1'] }, + { factor: 9.5, type: BoostType.Value, value: ['1'] }, { factor: 9.5, - type: 'value', + type: BoostType.Value, value: ['1', '1', '2', '2', 'true', '[object Object]', '[object Object]'], }, ], - sp_def: [{ type: 'functional', factor: 5 }], + sp_def: [{ type: BoostType.Functional, factor: 5 }], }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx index ca1fa9a8d0737b..ba79d62cfe615a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { useActions, useValues } from 'kea'; -import { EuiTextColor, EuiOverlayMask } from '@elastic/eui'; +import { EuiTextColor } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { LogRetentionLogic, LogRetentionOptions } from '../../log_retention'; @@ -40,7 +40,7 @@ export const LogRetentionConfirmationModal: React.FC = () => { } return ( - + <> {openedModal === LogRetentionOptions.Analytics && ( { onSave={() => saveLogRetention(LogRetentionOptions.API, false)} /> )} - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.tsx index bbde6c5d3b55de..bd9b6b51a43b1b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.tsx @@ -20,7 +20,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiSelect, EuiSpacer, } from '@elastic/eui'; @@ -79,71 +78,69 @@ export const SchemaAddFieldModal: React.FC = ({ ); return ( - +
    - - - {FIELD_NAME_MODAL_TITLE} - - -

    {FIELD_NAME_MODAL_DESCRIPTION}

    - - - - - + {FIELD_NAME_MODAL_TITLE} + + +

    {FIELD_NAME_MODAL_DESCRIPTION}

    + + + + + + - - - - - - updateNewFieldType(e.target.value)} - data-test-subj="SchemaSelect" - /> - - - - -
    - - {FIELD_NAME_MODAL_CANCEL} - - {FIELD_NAME_MODAL_ADD_FIELD} - - -
    + autoFocus + isLoading={loading} + data-test-subj="SchemaAddFieldNameField" + /> + + + + + updateNewFieldType(e.target.value)} + data-test-subj="SchemaSelect" + /> + + + + + + + {FIELD_NAME_MODAL_CANCEL} + + {FIELD_NAME_MODAL_ADD_FIELD} + + -
    + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/box.svg b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/box.svg new file mode 100644 index 00000000000000..827f8cf0a55ec8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/box.svg @@ -0,0 +1 @@ + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/index.ts index 1c66b7cf1758c6..d9a0975abef7c8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/sources_full_bleed/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import box from './box.svg'; import confluence from './confluence.svg'; import custom from './custom.svg'; import dropbox from './dropbox.svg'; @@ -21,6 +22,7 @@ import slack from './slack.svg'; import zendesk from './zendesk.svg'; export const imagesFull = { + box, confluence, confluenceCloud: confluence, confluenceServer: confluence, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts index 4fd4d480108def..cdfd07b07de912 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts @@ -679,6 +679,10 @@ export const DESCRIPTION_LABEL = i18n.translate( } ); +export const AND = i18n.translate('xpack.enterpriseSearch.workplaceSearch.and', { + defaultMessage: 'and', +}); + export const UPDATE_LABEL = i18n.translate('xpack.enterpriseSearch.workplaceSearch.update.label', { defaultMessage: 'Update', }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts new file mode 100644 index 00000000000000..37228cf9e7025d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { toSentenceSerial } from './to_sentence_serial'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/to_sentence_serial.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/to_sentence_serial.test.ts new file mode 100644 index 00000000000000..fb6501d3cb943a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/to_sentence_serial.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { toSentenceSerial } from './to_sentence_serial'; + +describe('toSentenceSerial', () => { + it('works correctly for 1 word', () => { + expect(toSentenceSerial(['One'])).toEqual('One'); + }); + + it('works correctly for 2 words', () => { + expect(toSentenceSerial(['One', 'Two'])).toEqual('One and Two'); + }); + + it('works correctly for 3+ words', () => { + expect(toSentenceSerial(['One', 'Two', 'Three'])).toEqual('One, Two, and Three'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/to_sentence_serial.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/to_sentence_serial.ts new file mode 100644 index 00000000000000..ad6383a76adc22 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/to_sentence_serial.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AND } from '../constants'; + +export const toSentenceSerial = (array: string[]) => + array.length === 1 + ? array[0] + : `${array.slice(0, array.length - 1).join(', ')}${ + array.length === 2 ? '' : ',' + } ${AND} ${array.slice(-1)}`; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.scss b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.scss new file mode 100644 index 00000000000000..fbc10b5e8ed0f1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.scss @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// -------------------------------------------------- +// View: Adding a Source flow +// -------------------------------------------------- + +.adding-a-source { + ul { + padding: 0; + } + + li + li { + margin-top: auto !important; + } + + &__acl-tooltip { + cursor: not-allowed; + + .euiSwitch__label { + cursor: not-allowed; + } + } + + &__icon { + width: 3.5em; + height: 3.5em; + } + + &__category { + text-transform: capitalize; + + &:after { + content: ', '; + } + + &:last-child:after { + content: ''; + } + } + + &__outer-box { + border: 1px solid #DBE2EB; + padding-right: 16px; + border-radius: 6px; + overflow: hidden; + background-color: #FFFFFF; + box-shadow: 0 2px 2px -1px rgba(152, 162, 179, .3), + 0 1px 5px -2px rgba(152, 162, 179, .3); + } + + &__intro-image { + background-color: #22272E; + display: flex; + justify-content: center; + align-items: center; + height: 100%; + } + + &__intro-image img { + width: auto; + height: auto; + } + + &__intro-step { + width: 80px; + height: 100%; + display: flex; + background: transparent; + border-radius: 0; + border-right: 2px solid #DBE2EB; + padding: 0 18px 0 0; + text-align: center; + justify-content: center; + align-items: center; + } + + &__config-steps { + p { + margin: 0; + } + } + + &__button-row { + .euiFlexItem:first-child { + align-self: flex-end; + } + .euiFlexItem:last-child { + align-self: flex-start; + } + } + + &__connect-an-instance { + flex-basis: auto; + } + + &__features-list { + flex-basis: 100%; + } + + &__feature-image { + width: 2.5em; + margin: 0; + background: transparent; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx index b00f9807f0acdf..64431a800487f7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx @@ -28,6 +28,8 @@ import { ReAuthenticate } from './re_authenticate'; import { SaveConfig } from './save_config'; import { SaveCustom } from './save_custom'; +import './add_source.scss'; + export const AddSource: React.FC = (props) => { const { initializeAddSource, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.tsx index f12c24feb8e1a9..bf472240d3c89d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.tsx @@ -52,7 +52,6 @@ export const AddSourceHeader: React.FC = ({ - ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx index 914eee74dfc4e5..917886d69bd19d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx @@ -45,6 +45,7 @@ export const ConfigurationIntro: React.FC = ({ }) => (
    {header} + = ({ return (
    {header} +
    diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx index b795b0af09944d..d6b427630e48e1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx @@ -12,9 +12,8 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiBadge, EuiCallOut, EuiSwitch } from '@elastic/eui'; +import { EuiSwitch } from '@elastic/eui'; -import { FeatureIds } from '../../../../types'; import { staticSourceData } from '../../source_data'; import { ConnectInstance } from './connect_instance'; @@ -46,7 +45,6 @@ describe('ConnectInstance', () => { const credentialsSourceData = staticSourceData[13]; const oauthSourceData = staticSourceData[0]; const subdomainSourceData = staticSourceData[16]; - const privateSourceData = staticSourceData[6]; const props = { ...credentialsSourceData, @@ -128,22 +126,6 @@ describe('ConnectInstance', () => { expect(setSourceSubdomainValue).toHaveBeenCalledWith(TEXT); }); - it('shows correct feature badges', () => { - setMockValues({ ...values, isOrganization: false }); - const wrapper = shallow(); - - expect(wrapper.find(EuiBadge)).toHaveLength(2); - }); - - it('shows no feature badges', () => { - setMockValues({ ...values, isOrganization: false }); - const features = { ...privateSourceData.features }; - features.platinumPrivateContext = [FeatureIds.SyncFrequency]; - const wrapper = shallow(); - - expect(wrapper.find(EuiBadge)).toHaveLength(0); - }); - it('calls handler on click', () => { const wrapper = shallow(); wrapper.find(EuiSwitch).simulate('change', { target: { checked: true } }); @@ -163,16 +145,23 @@ describe('ConnectInstance', () => { expect(mockReplace).toHaveBeenCalled(); }); - it('renders permissions link', () => { - const wrapper = shallow(); + it('renders doc-level permissions message when not available', () => { + const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="NeedsPermissionsMessage"]')).toHaveLength(1); + expect(wrapper.find('FormattedMessage')).toHaveLength(1); }); - it('shows permissions callout', () => { + it('renders callout when not synced', () => { setMockValues({ ...values, indexPermissionsValue: false }); - const wrapper = shallow(); + const wrapper = shallow(); + + expect(wrapper.find('EuiCallOut')).toHaveLength(1); + }); + + it('renders documentLevelPermissionsCallout', () => { + setMockValues({ ...values, hasPlatinumLicense: false }); + const wrapper = shallow(); - expect(wrapper.find(EuiCallOut)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="DocumentLevelPermissionsCallout"]')).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx index 08b29075f3d0d1..2290a659127974 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx @@ -12,38 +12,39 @@ import { useActions, useValues } from 'kea'; import { EuiButton, EuiCallOut, + EuiFieldText, EuiFlexGroup, EuiFlexItem, - EuiFieldText, EuiFormRow, + EuiHorizontalRule, + EuiIcon, EuiLink, + EuiPanel, EuiSpacer, EuiSwitch, EuiText, EuiTitle, - EuiTextColor, - EuiBadge, - EuiBadgeGroup, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { LicensingLogic } from '../../../../../shared/licensing'; import { AppLogic } from '../../../../app_logic'; -import { DOCUMENT_PERMISSIONS_DOCS_URL } from '../../../../routes'; +import { DOCUMENT_PERMISSIONS_DOCS_URL, ENT_SEARCH_LICENSE_MANAGEMENT } from '../../../../routes'; import { FeatureIds, Configuration, Features } from '../../../../types'; import { LEARN_MORE_LINK } from '../../constants'; import { AddSourceLogic } from './add_source_logic'; import { - CONNECT_REMOTE, - CONNECT_PRIVATE, CONNECT_WHICH_OPTION_LINK, CONNECT_DOC_PERMISSIONS_LABEL, CONNECT_DOC_PERMISSIONS_TITLE, CONNECT_NEEDS_PERMISSIONS, CONNECT_NOT_SYNCED_TITLE, CONNECT_NOT_SYNCED_TEXT, + SOURCE_FEATURES_DOCUMENT_LEVEL_PERMISSIONS_FEATURE, + SOURCE_FEATURES_DOCUMENT_LEVEL_PERMISSIONS_TITLE, + SOURCE_FEATURES_EXPLORE_BUTTON, } from './constants'; import { SourceFeatures } from './source_features'; @@ -110,6 +111,10 @@ export const ConnectInstance: React.FC = ({ onSubmit(); }; + const permissionsExcluded = features?.basicOrgContextExcludedFeatures?.includes( + FeatureIds.DocumentLevelPermissions + ); + const credentialsFields = ( <> @@ -147,35 +152,6 @@ export const ConnectInstance: React.FC = ({ ); - const featureBadgeGroup = () => { - if (isOrganization) { - return null; - } - - const isRemote = features?.platinumPrivateContext.includes(FeatureIds.Remote); - const isPrivate = features?.platinumPrivateContext.includes(FeatureIds.Private); - - if (isRemote || isPrivate) { - return ( - <> - - {isRemote && {CONNECT_REMOTE}} - {isPrivate && {CONNECT_PRIVATE}} - - - - ); - } - }; - - const descriptionBlock = ( - - {sourceDescription &&

    {sourceDescription}

    } - {connectStepDescription &&

    {connectStepDescription}

    } - -
    - ); - const whichDocsLink = ( {CONNECT_WHICH_OPTION_LINK} @@ -184,54 +160,87 @@ export const ConnectInstance: React.FC = ({ const permissionField = ( <> - - - {CONNECT_DOC_PERMISSIONS_TITLE} - - - - {CONNECT_DOC_PERMISSIONS_LABEL}} - name="index_permissions" - onChange={(e) => setSourceIndexPermissionsValue(e.target.checked)} - checked={indexPermissionsValue} - disabled={!needsPermissions} - /> - - - {!needsPermissions && ( - - - {LEARN_MORE_LINK} - - ), - }} - /> - - )} - {needsPermissions && indexPermissionsValue && ( - - {CONNECT_NEEDS_PERMISSIONS} -
    - {whichDocsLink} -
    + + +

    + {CONNECT_DOC_PERMISSIONS_TITLE} +

    +
    + + + {!needsPermissions && ( + + + {LEARN_MORE_LINK} + + ), + }} + /> + + )} + {needsPermissions && indexPermissionsValue && ( + + {CONNECT_NEEDS_PERMISSIONS} + + {whichDocsLink} + + )} + + + {!indexPermissionsValue && ( + <> + + +

    + {CONNECT_NOT_SYNCED_TEXT} + {needsPermissions && whichDocsLink} +

    +
    + )} -
    - - {!indexPermissionsValue && ( - -

    - {CONNECT_NOT_SYNCED_TEXT} - {needsPermissions && whichDocsLink} -

    -
    - )} - + + {CONNECT_DOC_PERMISSIONS_LABEL}} + name="index_permissions" + onChange={(e) => setSourceIndexPermissionsValue(e.target.checked)} + checked={indexPermissionsValue} + disabled={!needsPermissions} + /> + + + + ); + + const documentLevelPermissionsCallout = ( + <> + + + + + + + + {SOURCE_FEATURES_DOCUMENT_LEVEL_PERMISSIONS_TITLE} + + + + + +

    {SOURCE_FEATURES_DOCUMENT_LEVEL_PERMISSIONS_FEATURE}

    +
    + + + + {SOURCE_FEATURES_EXPLORE_BUTTON} + + +
    + ); @@ -240,6 +249,7 @@ export const ConnectInstance: React.FC = ({ {isOrganization && hasPlatinumLicense && permissionField} {!hasOauthRedirect && credentialsFields} {needsSubdomain && subdomainField} + {permissionsExcluded && !hasPlatinumLicense && documentLevelPermissionsCallout} @@ -262,15 +272,19 @@ export const ConnectInstance: React.FC = ({ gutterSize="xl" responsive={false} > - - {header} - {featureBadgeGroup()} - {descriptionBlock} + + + + {header} + + + + + + + {formFields} - - -
    diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts index dcede11fbdd3ac..dd756a51fded37 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts @@ -338,6 +338,13 @@ export const SOURCE_FEATURES_GLOBAL_ACCESS_PERMISSIONS_FEATURE = i18n.translate( } ); +export const SOURCE_FEATURES_DOCUMENT_LEVEL_PERMISSIONS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.contentSource.sourceFeatures.documentLevelPermissions.title', + { + defaultMessage: 'Document-level permissions available with Platinum license', + } +); + export const SOURCE_FEATURES_DOCUMENT_LEVEL_PERMISSIONS_FEATURE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.contentSource.sourceFeatures.documentLevelPermissions.text', { @@ -352,28 +359,6 @@ export const SOURCE_FEATURES_EXPLORE_BUTTON = i18n.translate( defaultMessage: 'Explore Platinum features', } ); - -export const SOURCE_FEATURES_INCLUDED_FEATURES_TITLE = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSource.sourceFeatures.included.title', - { - defaultMessage: 'Included features', - } -); - -export const CONNECT_REMOTE = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSource.connect.remote.text', - { - defaultMessage: 'Remote', - } -); - -export const CONNECT_PRIVATE = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSource.connect.private.text', - { - defaultMessage: 'Private', - } -); - export const CONNECT_WHICH_OPTION_LINK = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.contentSource.connect.whichOption.link', { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx index eb6736d84a197e..61682dbb87d587 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx @@ -51,6 +51,7 @@ export const ReAuthenticate: React.FC = ({ name, header }) return (
    {header} +
    ; + export const SourceFeatures: React.FC = ({ features, objTypes, name }) => { const { hasPlatinumLicense } = useValues(LicensingLogic); const { isOrganization } = useValues(AppLogic); - const Feature = ({ title, children }: { title: string; children: React.ReactElement }) => ( - <> - - - {title} - - - {children} - - ); + const Feature = ({ + icon, + title, + children, + }: { + icon: string; + title: string; + children: React.ReactElement; + }) => { + return ( + <> + + {icon && ( + <> + + + + + )} + + + {title} + + + + + {children} + + ); + }; const SyncFrequencyFeature = ( - +

    = ({ features, objTy ); const SyncedItemsFeature = ( - + <>

    {SOURCE_FEATURES_SEARCHABLE}

    @@ -93,7 +110,7 @@ export const SourceFeatures: React.FC = ({ features, objTy ); const SearchableContentFeature = ( - +

    {SOURCE_FEATURES_SEARCHABLE}

    @@ -109,7 +126,7 @@ export const SourceFeatures: React.FC = ({ features, objTy ); const RemoteFeature = ( - +

    {SOURCE_FEATURES_REMOTE_FEATURE}

    @@ -117,7 +134,7 @@ export const SourceFeatures: React.FC = ({ features, objTy ); const PrivateFeature = ( - +

    {SOURCE_FEATURES_PRIVATE_FEATURE}

    @@ -125,25 +142,14 @@ export const SourceFeatures: React.FC = ({ features, objTy ); const GlobalAccessPermissionsFeature = ( - +

    {SOURCE_FEATURES_GLOBAL_ACCESS_PERMISSIONS_FEATURE}

    ); - const DocumentLevelPermissionsFeature = ( - - -

    {SOURCE_FEATURES_DOCUMENT_LEVEL_PERMISSIONS_FEATURE}

    - - {SOURCE_FEATURES_EXPLORE_BUTTON} - -
    -
    - ); - - const FeaturesRouter = ({ featureId }: { featureId: FeatureIds }) => + const FeaturesRouter = ({ featureId }: { featureId: IncludedFeatureIds }) => ({ [FeatureIds.SyncFrequency]: SyncFrequencyFeature, [FeatureIds.SearchableContent]: SearchableContentFeature, @@ -151,10 +157,9 @@ export const SourceFeatures: React.FC = ({ features, objTy [FeatureIds.Remote]: RemoteFeature, [FeatureIds.Private]: PrivateFeature, [FeatureIds.GlobalAccessPermissions]: GlobalAccessPermissionsFeature, - [FeatureIds.DocumentLevelPermissions]: DocumentLevelPermissionsFeature, }[featureId]); - const IncludedFeatures = () => { + const IncludedFeatureIds = () => { let includedFeatures: FeatureIds[] | undefined; if (!hasPlatinumLicense && isOrganization) { @@ -172,55 +177,40 @@ export const SourceFeatures: React.FC = ({ features, objTy } return ( - + <> -

    {SOURCE_FEATURES_INCLUDED_FEATURES_TITLE}

    +

    + Included features +

    - {includedFeatures.map((featureId, i) => ( - - ))} -
    - ); - }; - - const ExcludedFeatures = () => { - let excludedFeatures: FeatureIds[] | undefined; - - if (!hasPlatinumLicense && isOrganization) { - excludedFeatures = features?.basicOrgContextExcludedFeatures; - } - - if (!excludedFeatures?.length) { - return null; - } - - return ( - - - {excludedFeatures.map((featureId, i) => ( - - ))} - + + + {includedFeatures.map((featureId, i) => ( + + + + + + ))} + + ); }; return ( - - - - - + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx index 9a6af035c1c8d8..717eebf5cf8733 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.tsx @@ -20,7 +20,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiSelect, } from '@elastic/eui'; @@ -59,48 +58,46 @@ export const FieldEditorModal: React.FC = () => { const ACTION_LABEL = isEditing ? UPDATE_LABEL : ADD_LABEL; return ( - + - - - - {ACTION_LABEL} {FIELD_LABEL} - - - - - - setName(e.target.value)} - /> - - - setLabel(e.target.value)} - /> - - - - - {CANCEL_BUTTON} - - {ACTION_LABEL} {FIELD_LABEL} - - - + + + {ACTION_LABEL} {FIELD_LABEL} + + + + + + setName(e.target.value)} + /> + + + setLabel(e.target.value)} + /> + + + + + {CANCEL_BUTTON} + + {ACTION_LABEL} {FIELD_LABEL} + + - + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx index 75a1779a1fda8e..d99f9a4cb1a463 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx @@ -15,7 +15,6 @@ import { EuiButton, EuiButtonEmpty, EuiConfirmModal, - EuiOverlayMask, EuiFieldText, EuiFlexGroup, EuiFlexItem, @@ -101,26 +100,24 @@ export const SourceSettings: React.FC = () => { }; const confirmModal = ( - - - , - }} - /> - - + + , + }} + /> + ); return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts index 8efa61c1ed524f..3e1290292704e3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts @@ -413,10 +413,6 @@ export const SHARED_EMPTY_DESCRIPTION = i18n.translate( } ); -export const AND = i18n.translate('xpack.enterpriseSearch.workplaceSearch.and', { - defaultMessage: 'and', -}); - export const LICENSE_CALLOUT_TITLE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.sources.licenseCallout.title', { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.test.tsx new file mode 100644 index 00000000000000..e6f19ff13b3ccf --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.test.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import '../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues, setMockActions } from '../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiCallOut, EuiEmptyPrompt } from '@elastic/eui'; + +import { Loading } from '../../../shared/loading'; +import { ContentSection } from '../../components/shared/content_section'; +import { SourcesTable } from '../../components/shared/sources_table'; + +import { PrivateSources } from './private_sources'; +import { SourcesView } from './sources_view'; + +describe('PrivateSources', () => { + const mockValues = { + account: { canCreatePersonalSources: false, groups: [] }, + dataLoading: false, + contentSources: [], + privateContentSources: [], + serviceTypes: [], + hasPlatinumLicense: true, + }; + + beforeEach(() => { + setMockActions({ initializeSources: jest.fn() }); + setMockValues({ ...mockValues }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(SourcesView)).toHaveLength(1); + }); + + it('renders Loading when loading', () => { + setMockValues({ ...mockValues, dataLoading: true }); + const wrapper = shallow(); + + expect(wrapper.find(Loading)).toHaveLength(1); + }); + + it('renders only shared sources section when canCreatePersonalSources is false', () => { + setMockValues({ ...mockValues }); + const wrapper = shallow(); + + expect(wrapper.find(ContentSection)).toHaveLength(1); + }); + + it('renders both shared and private sources sections when canCreatePersonalSources is true', () => { + setMockValues({ ...mockValues, account: { canCreatePersonalSources: true, groups: [] } }); + const wrapper = shallow(); + + expect(wrapper.find(ContentSection)).toHaveLength(2); + }); + + it('renders license callout when has private sources with non-Platinum license', () => { + setMockValues({ + ...mockValues, + privateContentSources: ['source1', 'source2'], + hasPlatinumLicense: false, + account: { canCreatePersonalSources: true, groups: [] }, + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiCallOut)).toHaveLength(1); + }); + + it('renders an action button when user can add private sources', () => { + setMockValues({ + ...mockValues, + account: { canCreatePersonalSources: true, groups: [] }, + serviceTypes: [{ configured: true }], + }); + const wrapper = shallow(); + + expect(wrapper.find(ContentSection).first().prop('action')).toBeTruthy(); + }); + + it('renders empty prompts if no sources are available', () => { + setMockValues({ + ...mockValues, + account: { canCreatePersonalSources: true, groups: [] }, + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(2); + }); + + it('renders SourcesTable if sources are available', () => { + setMockValues({ + ...mockValues, + account: { canCreatePersonalSources: true, groups: [] }, + contentSources: ['1', '2'], + privateContentSources: ['1', '2'], + }); + const wrapper = shallow(); + + expect(wrapper.find(SourcesTable)).toHaveLength(2); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx index 3ff14602b979d6..114df3cf41e399 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx @@ -20,9 +20,9 @@ import noSharedSourcesIcon from '../../assets/share_circle.svg'; import { ContentSection } from '../../components/shared/content_section'; import { SourcesTable } from '../../components/shared/sources_table'; import { ADD_SOURCE_PATH, getSourcesPath } from '../../routes'; +import { toSentenceSerial } from '../../utils'; import { - AND, PRIVATE_LINK_TITLE, PRIVATE_HEADER_TITLE, PRIVATE_HEADER_DESCRIPTION, @@ -122,13 +122,6 @@ export const PrivateSources: React.FC = () => { ); - const groupsSentence = - groups.length === 1 - ? `${groups}` - : `${groups.slice(0, groups.length - 1).join(', ')}${ - groups.length === 2 ? '' : ',' - } ${AND} ${groups.slice(-1)}`; - const sharedSourcesSection = ( { }} + values={{ + groups: groups.length, + groupsSentence: toSentenceSerial(groups), + newline:
    , + }} /> ) } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx new file mode 100644 index 00000000000000..488eb4b49853bd --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources_layout.test.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import '../../../__mocks__/shallow_useeffect.mock'; + +import { setMockValues } from '../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiCallOut } from '@elastic/eui'; + +import { ViewContentHeader } from '../../components/shared/view_content_header'; + +import { + PRIVATE_CAN_CREATE_PAGE_TITLE, + PRIVATE_VIEW_ONLY_PAGE_TITLE, + PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION, + PRIVATE_CAN_CREATE_PAGE_DESCRIPTION, +} from './constants'; +import { PrivateSourcesLayout } from './private_sources_layout'; + +describe('PrivateSourcesLayout', () => { + const mockValues = { + account: { canCreatePersonalSources: true }, + }; + + const children =

    test

    ; + + beforeEach(() => { + setMockValues({ ...mockValues }); + }); + + it('renders', () => { + const wrapper = shallow({children}); + + expect(wrapper.find('[data-test-subj="TestChildren"]')).toHaveLength(1); + }); + + it('uses correct title and description when private sources are enabled', () => { + const wrapper = shallow({children}); + + expect(wrapper.find(ViewContentHeader).prop('title')).toEqual(PRIVATE_CAN_CREATE_PAGE_TITLE); + expect(wrapper.find(ViewContentHeader).prop('description')).toEqual( + PRIVATE_CAN_CREATE_PAGE_DESCRIPTION + ); + }); + + it('uses correct title and description when private sources are disabled', () => { + setMockValues({ account: { canCreatePersonalSources: false } }); + const wrapper = shallow({children}); + + expect(wrapper.find(ViewContentHeader).prop('title')).toEqual(PRIVATE_VIEW_ONLY_PAGE_TITLE); + expect(wrapper.find(ViewContentHeader).prop('description')).toEqual( + PRIVATE_VIEW_ONLY_PAGE_DESCRIPTION + ); + }); + + it('renders callout when in read-only mode', () => { + const wrapper = shallow({children}); + + expect(wrapper.find(EuiCallOut)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx index c62f0b00258d65..247df5556ada01 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx @@ -19,7 +19,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -53,70 +52,65 @@ export const SourcesView: React.FC = ({ children }) => { addedSourceName: string; serviceType: string; }) => ( - - - - - - - - - - {i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.sourcesView.modal.heading', - { - defaultMessage: '{addedSourceName} requires additional configuration', - values: { addedSourceName }, - } - )} - - - - - - -

    - - {EXTERNAL_IDENTITIES_LINK} - - ), - }} - /> -

    + + + + + + + + + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.sourcesView.modal.heading', { + defaultMessage: '{addedSourceName} requires additional configuration', + values: { addedSourceName }, + })} + + + + + + +

    + + {EXTERNAL_IDENTITIES_LINK} + + ), + }} + /> +

    -

    - - {DOCUMENT_PERMISSIONS_LINK} - - ), - }} - /> -

    -
    -
    - - - {UNDERSTAND_BUTTON} - - -
    -
    +

    + + {DOCUMENT_PERMISSIONS_LINK} + + ), + }} + /> +

    +
    + + + + {UNDERSTAND_BUTTON} + + + ); return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.test.tsx index 26ac5e484f0d77..784544b0001fa0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.test.tsx @@ -11,7 +11,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiModal } from '@elastic/eui'; import { AddGroupModal } from './add_group_modal'; @@ -36,7 +36,6 @@ describe('AddGroupModal', () => { const wrapper = shallow(); expect(wrapper.find(EuiModal)).toHaveLength(1); - expect(wrapper.find(EuiOverlayMask)).toHaveLength(1); }); it('updates the input value', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx index fb82e9393f2a22..2c5732b4b71573 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx @@ -19,7 +19,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -49,37 +48,35 @@ export const AddGroupModal: React.FC<{}> = () => { }; return ( - - -
    - - {ADD_GROUP_HEADER} - + + + + {ADD_GROUP_HEADER} + - - - setNewGroupName(e.target.value)} - /> - - + + + setNewGroupName(e.target.value)} + /> + + - - {CANCEL_BUTTON} - - {ADD_GROUP_SUBMIT} - - - - -
    + + {CANCEL_BUTTON} + + {ADD_GROUP_SUBMIT} + + + + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.test.tsx index 949ae9d502e73a..7c39414f158eff 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.test.tsx @@ -13,7 +13,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiOverlayMask, EuiModal, EuiEmptyPrompt } from '@elastic/eui'; +import { EuiModal, EuiEmptyPrompt } from '@elastic/eui'; import { GroupManagerModal } from './group_manager_modal'; @@ -46,7 +46,6 @@ describe('GroupManagerModal', () => { const wrapper = shallow(); expect(wrapper.find(EuiModal)).toHaveLength(1); - expect(wrapper.find(EuiOverlayMask)).toHaveLength(1); }); it('renders empty state', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx index b4317ed9bd417c..1b051394dcdcf6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx @@ -21,7 +21,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -161,14 +160,12 @@ export const GroupManagerModal: React.FC = ({ ); return ( - - - {showEmptyState ? emptyState : modalContent} - - + + {showEmptyState ? emptyState : modalContent} + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx index df9c0b5db9b7d1..375ac7476f9b69 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx @@ -12,7 +12,6 @@ import { useActions, useValues } from 'kea'; import { EuiButton, EuiConfirmModal, - EuiOverlayMask, EuiFieldText, EuiFlexGroup, EuiFlexItem, @@ -226,18 +225,16 @@ export const GroupOverview: React.FC = () => { {confirmDeleteModalVisible && ( - - - {CONFIRM_REMOVE_DESCRIPTION} - - + + {CONFIRM_REMOVE_DESCRIPTION} + )} { ); const confirmModal = ( - - - {PRIVATE_SOURCES_UPDATE_CONFIRMATION_TEXT} - - + + {PRIVATE_SOURCES_UPDATE_CONFIRMATION_TEXT} + ); return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx index 28e7e2a33eaa18..3f2e55d23722c2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx @@ -18,7 +18,6 @@ import { EuiSwitch, EuiCode, EuiSpacer, - EuiOverlayMask, EuiLink, EuiModal, EuiModalBody, @@ -93,25 +92,28 @@ export const OauthApplication: React.FC = () => { }; const licenseModal = ( - - - - - - - -

    {LICENSE_MODAL_TITLE}

    -
    - - {LICENSE_MODAL_DESCRIPTION} - - - {LICENSE_MODAL_LINK} - - -
    -
    -
    + + + + + + +

    {LICENSE_MODAL_TITLE}

    +
    + + {LICENSE_MODAL_DESCRIPTION} + + + {LICENSE_MODAL_LINK} + + +
    +
    ); return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx index 4ed223931d6a46..47a24e7912c3c9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx @@ -9,7 +9,7 @@ import React, { useEffect, useState } from 'react'; import { useActions, useValues } from 'kea'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Loading } from '../../../../shared/loading'; @@ -56,22 +56,19 @@ export const SourceConfig: React.FC = ({ sourceIndex }) => { header={header} /> {confirmModalVisible && ( - - deleteSourceConfig(serviceType, name)} - onCancel={hideConfirmModal} - buttonColor="danger" - > - {i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.settings.confirmRemoveConfig.message', - { - defaultMessage: - 'Are you sure you want to remove the OAuth configuration for {name}?', - values: { name }, - } - )} - - + deleteSourceConfig(serviceType, name)} + onCancel={hideConfirmModal} + buttonColor="danger" + > + {i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.settings.confirmRemoveConfig.message', + { + defaultMessage: 'Are you sure you want to remove the OAuth configuration for {name}?', + values: { name }, + } + )} + )} ); diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts index 62f68748fcea19..53ddc21cba3994 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts @@ -7,7 +7,7 @@ import { MockRouter, mockLogger, mockDependencies } from '../../__mocks__'; -import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; +import { savedObjectsServiceMock } from 'src/core/server/mocks'; jest.mock('../../collectors/lib/telemetry', () => ({ incrementUICounter: jest.fn(), @@ -84,17 +84,17 @@ describe('Enterprise Search Telemetry API', () => { it('throws an error when incrementing fails', async () => { (incrementUICounter as jest.Mock).mockImplementation(jest.fn(() => Promise.reject('Failed'))); - await mockRouter.callRoute({ - body: { - product: 'enterprise_search', - action: 'error', - metric: 'error', - }, - }); + await expect( + mockRouter.callRoute({ + body: { + product: 'enterprise_search', + action: 'error', + metric: 'error', + }, + }) + ).rejects.toEqual('Failed'); expect(incrementUICounter).toHaveBeenCalled(); - expect(mockLogger.error).toHaveBeenCalled(); - expect(mockRouter.response.internalError).toHaveBeenCalled(); }); it('throws an error if the Saved Objects service is unavailable', async () => { @@ -104,16 +104,9 @@ describe('Enterprise Search Telemetry API', () => { getSavedObjectsService: null, log: mockLogger, } as any); - await mockRouter.callRoute({}); + await expect(mockRouter.callRoute({})).rejects.toThrow(); expect(incrementUICounter).not.toHaveBeenCalled(); - expect(mockLogger.error).toHaveBeenCalled(); - expect(mockRouter.response.internalError).toHaveBeenCalled(); - expect(loggingSystemMock.collect(mockLogger).error[0][0]).toEqual( - expect.stringContaining( - 'Enterprise Search UI telemetry error: Error: Could not find Saved Objects service' - ) - ); }); describe('validates', () => { diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts index 90afba414c0447..e15be8fcd0d8b8 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts @@ -20,7 +20,7 @@ const productToTelemetryMap = { workplace_search: WS_TELEMETRY_NAME, }; -export function registerTelemetryRoute({ router, getSavedObjectsService, log }: RouteDependencies) { +export function registerTelemetryRoute({ router, getSavedObjectsService }: RouteDependencies) { router.put( { path: '/api/enterprise_search/stats', @@ -43,23 +43,16 @@ export function registerTelemetryRoute({ router, getSavedObjectsService, log }: async (ctx, request, response) => { const { product, action, metric } = request.body; - try { - if (!getSavedObjectsService) throw new Error('Could not find Saved Objects service'); + if (!getSavedObjectsService) throw new Error('Could not find Saved Objects service'); - return response.ok({ - body: await incrementUICounter({ - id: productToTelemetryMap[product], - savedObjects: getSavedObjectsService(), - uiAction: `ui_${action}`, - metric, - }), - }); - } catch (e) { - log.error( - `Enterprise Search UI telemetry error: ${e instanceof Error ? e.stack : e.toString()}` - ); - return response.internalError({ body: 'Enterprise Search UI telemetry failed' }); - } + return response.ok({ + body: await incrementUICounter({ + id: productToTelemetryMap[product], + savedObjects: getSavedObjectsService(), + uiAction: `ui_${action}`, + metric, + }), + }); } ); } diff --git a/x-pack/plugins/fleet/common/types/rest_spec/common.ts b/x-pack/plugins/fleet/common/types/rest_spec/common.ts index d03129efd8fad0..de5e87d2e59a5d 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/common.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/common.ts @@ -14,3 +14,10 @@ export interface ListWithKuery extends HttpFetchQuery { sortOrder?: 'desc' | 'asc'; kuery?: string; } + +export interface ListResult { + items: T[]; + total: number; + page: number; + perPage: number; +} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_copy_provider.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_copy_provider.tsx index d161dfcc5894c1..2b7ecc75195b0c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_copy_provider.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_copy_provider.tsx @@ -6,7 +6,7 @@ */ import React, { Fragment, useRef, useState } from 'react'; -import { EuiConfirmModal, EuiOverlayMask, EuiFormRow, EuiFieldText } from '@elastic/eui'; +import { EuiConfirmModal, EuiFormRow, EuiFieldText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { AgentPolicy } from '../../../types'; @@ -92,75 +92,71 @@ export const AgentPolicyCopyProvider: React.FunctionComponent = ({ childr } return ( - - - - - } - onCancel={closeModal} - onConfirm={copyAgentPolicy} - cancelButtonText={ + - } - confirmButtonText={ + + } + onCancel={closeModal} + onConfirm={copyAgentPolicy} + cancelButtonText={ + + } + confirmButtonText={ + + } + confirmButtonDisabled={isLoading || !newAgentPolicy.name.trim()} + > +

    + +

    + } - confirmButtonDisabled={isLoading || !newAgentPolicy.name.trim()} + fullWidth > -

    - -

    - - } + - setNewAgentPolicy({ ...newAgentPolicy, name: e.target.value })} + value={newAgentPolicy.name} + onChange={(e) => setNewAgentPolicy({ ...newAgentPolicy, name: e.target.value })} + /> + + - - - } + } + fullWidth + > + - - setNewAgentPolicy({ ...newAgentPolicy, description: e.target.value }) - } - /> - -
    -
    + value={newAgentPolicy.description} + onChange={(e) => setNewAgentPolicy({ ...newAgentPolicy, description: e.target.value })} + /> + + ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx index b03d70a78c51a3..014af7f54d020b 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_delete_provider.tsx @@ -6,7 +6,7 @@ */ import React, { Fragment, useRef, useState } from 'react'; -import { EuiConfirmModal, EuiOverlayMask, EuiCallOut } from '@elastic/eui'; +import { EuiConfirmModal, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { AGENT_SAVED_OBJECT_TYPE } from '../../../constants'; @@ -110,69 +110,67 @@ export const AgentPolicyDeleteProvider: React.FunctionComponent = ({ chil } return ( - - - } - onCancel={closeModal} - onConfirm={deleteAgentPolicy} - cancelButtonText={ + + } + onCancel={closeModal} + onConfirm={deleteAgentPolicy} + cancelButtonText={ + + } + confirmButtonText={ + isLoading || isLoadingAgentsCount ? ( - } - confirmButtonText={ - isLoading || isLoadingAgentsCount ? ( - - ) : ( - - ) - } - buttonColor="danger" - confirmButtonDisabled={isLoading || isLoadingAgentsCount || !!agentsCount} - > - {isLoadingAgentsCount ? ( + ) : ( - ) : agentsCount ? ( - - - - ) : ( + ) + } + buttonColor="danger" + confirmButtonDisabled={isLoading || isLoadingAgentsCount || !!agentsCount} + > + {isLoadingAgentsCount ? ( + + ) : agentsCount ? ( + - )} - - + + ) : ( + + )} + ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_yaml_flyout.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_yaml_flyout.tsx index 63fb1f5b4b6385..9ed4bb6ff6ff4f 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_yaml_flyout.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_yaml_flyout.tsx @@ -52,8 +52,6 @@ export const AgentPolicyYamlFlyout = memo<{ policyId: string; onClose: () => voi {error.message} ) : ( - // Property 'whiteSpace' does not exist on type 'IntrinsicAttributes & CommonProps & OwnProps & HTMLAttributes & { children?: ReactNode; }'. - // @ts-expect-error linter complains whiteSpace isn't available but docs show it on EuiCodeBlockImpl {fullAgentPolicyToYaml(yamlData!.item)} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/confirm_deploy_modal.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/confirm_deploy_modal.tsx index 02d7a6423edc84..f3d01e6b528cae 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/confirm_deploy_modal.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/confirm_deploy_modal.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiCallOut, EuiOverlayMask, EuiConfirmModal, EuiSpacer } from '@elastic/eui'; +import { EuiCallOut, EuiConfirmModal, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { AgentPolicy } from '../../../types'; @@ -18,58 +18,56 @@ export const ConfirmDeployAgentPolicyModal: React.FunctionComponent<{ agentPolicy: AgentPolicy; }> = ({ onConfirm, onCancel, agentCount, agentPolicy }) => { return ( - - - } - onCancel={onCancel} - onConfirm={onConfirm} - cancelButtonText={ - - } - confirmButtonText={ - - } - buttonColor="primary" + + } + onCancel={onCancel} + onConfirm={onConfirm} + cancelButtonText={ + + } + confirmButtonText={ + + } + buttonColor="primary" + > + - -
    - + {agentPolicy.name}, - }} - /> -
    -
    - - -
    -
    + values={{ + policyName: {agentPolicy.name}, + }} + /> +
    + + + + ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/package_policy_delete_provider.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/package_policy_delete_provider.tsx index 2ea94e88ed8c61..80952fee05bb4d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/package_policy_delete_provider.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/package_policy_delete_provider.tsx @@ -6,7 +6,7 @@ */ import React, { Fragment, useMemo, useRef, useState } from 'react'; -import { EuiCallOut, EuiConfirmModal, EuiOverlayMask, EuiSpacer } from '@elastic/eui'; +import { EuiCallOut, EuiConfirmModal, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useStartServices, sendRequest, sendDeletePackagePolicy, useConfig } from '../../../hooks'; @@ -142,78 +142,76 @@ export const PackagePolicyDeleteProvider: React.FunctionComponent = ({ } return ( - - + } + onCancel={closeModal} + onConfirm={deletePackagePolicies} + cancelButtonText={ + + } + confirmButtonText={ + isLoading || isLoadingAgentsCount ? ( - } - onCancel={closeModal} - onConfirm={deletePackagePolicies} - cancelButtonText={ + ) : ( - } - confirmButtonText={ - isLoading || isLoadingAgentsCount ? ( - - ) : ( + ) + } + buttonColor="danger" + confirmButtonDisabled={isLoading || isLoadingAgentsCount} + > + {isLoadingAgentsCount ? ( + + ) : agentsCount ? ( + <> + + } + > {agentPolicy.name}, }} /> - ) - } - buttonColor="danger" - confirmButtonDisabled={isLoading || isLoadingAgentsCount} - > - {isLoadingAgentsCount ? ( - - ) : agentsCount ? ( - <> - - } - > - {agentPolicy.name}, - }} - /> - - - - ) : null} - {!isLoadingAgentsCount && ( - - )} - - + + + + ) : null} + {!isLoadingAgentsCount && ( + + )} + ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_unenroll_modal/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_unenroll_modal/index.tsx index 69bff78d604132..a50cc18d46f550 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_unenroll_modal/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_unenroll_modal/index.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiConfirmModal, EuiOverlayMask, EuiFormFieldset, EuiCheckbox } from '@elastic/eui'; +import { EuiConfirmModal, EuiFormFieldset, EuiCheckbox } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { Agent } from '../../../../types'; import { @@ -81,90 +81,88 @@ export const AgentUnenrollAgentModal: React.FunctionComponent = ({ } return ( - - - ) : ( - - ) - } - onCancel={onClose} - onConfirm={onSubmit} - cancelButtonText={ + - } - confirmButtonDisabled={isSubmitting} - confirmButtonText={ - isSingleAgent ? ( - - ) : ( + ) : ( + + ) + } + onCancel={onClose} + onConfirm={onSubmit} + cancelButtonText={ + + } + confirmButtonDisabled={isSubmitting} + confirmButtonText={ + isSingleAgent ? ( + + ) : ( + + ) + } + buttonColor="danger" + > +

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

    + - ) - } - buttonColor="danger" + ), + }} > -

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

    - - ), - }} - > - - } - checked={forceUnenroll} - onChange={(e) => setForceUnenroll(e.target.checked)} - disabled={useForceUnenroll} - /> - -
    -
    + values={{ count: agentCount }} + /> + } + checked={forceUnenroll} + onChange={(e) => setForceUnenroll(e.target.checked)} + disabled={useForceUnenroll} + /> + + ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx index a836e3ec3149bc..57f4007a002740 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx @@ -7,13 +7,7 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { - EuiConfirmModal, - EuiOverlayMask, - EuiBetaBadge, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; +import { EuiConfirmModal, EuiBetaBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { Agent } from '../../../../types'; import { @@ -74,85 +68,83 @@ export const AgentUpgradeAgentModal: React.FunctionComponent = ({ } return ( - - - - {isSingleAgent ? ( + + + {isSingleAgent ? ( + + ) : ( + + )} + + + - ) : ( + } + tooltipContent={ - )} - - - - } - tooltipContent={ - - } - /> - -
    - } - onCancel={onClose} - onConfirm={onSubmit} - cancelButtonText={ + } + /> + + + } + onCancel={onClose} + onConfirm={onSubmit} + cancelButtonText={ + + } + confirmButtonDisabled={isSubmitting} + confirmButtonText={ + isSingleAgent ? ( - } - confirmButtonDisabled={isSubmitting} - confirmButtonText={ - isSingleAgent ? ( - - ) : ( - - ) - } - > -

    - {isSingleAgent ? ( - - ) : ( - - )} -

    - - + ) : ( + + ) + } + > +

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

    + ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/confirm_delete_modal.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/confirm_delete_modal.tsx index 22e2e68e6d83e9..565657c70e17f6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/confirm_delete_modal.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/enrollment_token_list_page/components/confirm_delete_modal.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiConfirmModal, EuiCallOut, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal, EuiCallOut } from '@elastic/eui'; import { EnrollmentAPIKey } from '../../../../types'; interface Props { @@ -19,33 +19,31 @@ interface Props { export const ConfirmEnrollmentTokenDelete = (props: Props) => { const { onCancel, onConfirm, enrollmentKey } = props; return ( - - + - - - + color="danger" + /> + ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/confirm_package_install.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/confirm_package_install.tsx index ef3ca3ce664c11..5144b2a6487862 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/confirm_package_install.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/confirm_package_install.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiCallOut, EuiConfirmModal, EuiOverlayMask, EuiSpacer } from '@elastic/eui'; +import { EuiCallOut, EuiConfirmModal, EuiSpacer } from '@elastic/eui'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -18,50 +18,48 @@ interface ConfirmPackageInstallProps { export const ConfirmPackageInstall = (props: ConfirmPackageInstallProps) => { const { onCancel, onConfirm, packageName, numOfAssets } = props; return ( - - + } + onCancel={onCancel} + onConfirm={onConfirm} + cancelButtonText={ + + } + confirmButtonText={ + + } + defaultFocusedButton="confirm" + > + - } - onCancel={onCancel} - onConfirm={onConfirm} - cancelButtonText={ - } - confirmButtonText={ - - } - defaultFocusedButton="confirm" - > - - } + /> + +

    + - -

    - -

    -
    -
    +

    + ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/confirm_package_uninstall.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/confirm_package_uninstall.tsx index 7688c0269d3588..2def57b0409447 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/confirm_package_uninstall.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/settings/confirm_package_uninstall.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiCallOut, EuiConfirmModal, EuiOverlayMask, EuiSpacer } from '@elastic/eui'; +import { EuiCallOut, EuiConfirmModal, EuiSpacer } from '@elastic/eui'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -18,58 +18,56 @@ interface ConfirmPackageUninstallProps { export const ConfirmPackageUninstall = (props: ConfirmPackageUninstallProps) => { const { onCancel, onConfirm, packageName, numOfAssets } = props; return ( - - + } + onCancel={onCancel} + onConfirm={onConfirm} + cancelButtonText={ + + } + confirmButtonText={ + + } + defaultFocusedButton="confirm" + buttonColor="danger" + > + } - onCancel={onCancel} - onConfirm={onConfirm} - cancelButtonText={ - - } - confirmButtonText={ - - } - defaultFocusedButton="confirm" - buttonColor="danger" > - - } - > -

    - -

    -
    -

    -
    -
    + + +

    + +

    + ); }; diff --git a/x-pack/plugins/fleet/server/mocks.ts b/x-pack/plugins/fleet/server/mocks.ts index c650995c809cbb..430e38bd1bc3ef 100644 --- a/x-pack/plugins/fleet/server/mocks.ts +++ b/x-pack/plugins/fleet/server/mocks.ts @@ -53,6 +53,7 @@ export const createPackagePolicyServiceMock = () => { get: jest.fn(), getByIDs: jest.fn(), list: jest.fn(), + listIds: jest.fn(), update: jest.fn(), runExternalCallbacks: jest.fn(), } as jest.Mocked; diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts index 2b44975cc3b4de..813279f2a800fc 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts @@ -47,6 +47,7 @@ jest.mock('../../services/package_policy', (): { get: jest.fn(), getByIDs: jest.fn(), list: jest.fn(), + listIds: jest.fn(), update: jest.fn(), runExternalCallbacks: jest.fn((callbackType, newPackagePolicy, context, request) => Promise.resolve(newPackagePolicy) diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index a882ceb0037f24..335cd7c956faf9 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -20,6 +20,7 @@ import { PackagePolicyInputStream, PackageInfo, ListWithKuery, + ListResult, packageToPackagePolicy, isPackageLimited, doesAgentPolicyAlreadyIncludePackage, @@ -248,7 +249,7 @@ class PackagePolicyService { public async list( soClient: SavedObjectsClientContract, options: ListWithKuery - ): Promise<{ items: PackagePolicy[]; total: number; page: number; perPage: number }> { + ): Promise> { const { page = 1, perPage = 20, sortField = 'updated_at', sortOrder = 'desc', kuery } = options; const packagePolicies = await soClient.find({ @@ -272,6 +273,30 @@ class PackagePolicyService { }; } + public async listIds( + soClient: SavedObjectsClientContract, + options: ListWithKuery + ): Promise> { + const { page = 1, perPage = 20, sortField = 'updated_at', sortOrder = 'desc', kuery } = options; + + const packagePolicies = await soClient.find<{}>({ + type: SAVED_OBJECT_TYPE, + sortField, + sortOrder, + page, + perPage, + fields: [], + filter: kuery ? normalizeKuery(SAVED_OBJECT_TYPE, kuery) : undefined, + }); + + return { + items: packagePolicies.saved_objects.map((packagePolicySO) => packagePolicySO.id), + total: packagePolicies.total, + page, + perPage, + }; + } + public async update( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, diff --git a/x-pack/plugins/graph/server/routes/explore.ts b/x-pack/plugins/graph/server/routes/explore.ts index 95be71812d06c8..9a9a267c40f32a 100644 --- a/x-pack/plugins/graph/server/routes/explore.ts +++ b/x-pack/plugins/graph/server/routes/explore.ts @@ -76,11 +76,7 @@ export function registerExploreRoute({ } } - return response.internalError({ - body: { - message: error.message, - }, - }); + throw error; } } ) diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts index f1a15d805faf8a..859b4adce50285 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts @@ -479,6 +479,29 @@ describe('', () => { component.update(); }); + test('serialization', async () => { + httpRequestsMockHelpers.setLoadPolicies([DEFAULT_POLICY]); + await act(async () => { + testBed = await setup(); + }); + const { component, actions } = testBed; + component.update(); + await actions.delete.enablePhase(); + await actions.setWaitForSnapshotPolicy('test'); + await actions.savePolicy(); + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + expect(entirePolicy.phases.delete).toEqual({ + min_age: '365d', + actions: { + delete: {}, + wait_for_snapshot: { + policy: 'test', + }, + }, + }); + }); + test('wait for snapshot policy field should correctly display snapshot policy name', () => { expect(testBed.find('snapshotPolicyCombobox').prop('data-currentvalue')).toEqual([ { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts index 600a660657863c..65fc82b7ccc685 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts @@ -354,7 +354,7 @@ export const schema: FormSchema = { }, delete: { min_age: { - defaultValue: '0', + defaultValue: '365', validations: [ { validator: minAgeValidator, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/add_policy_to_template_confirm_modal.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/add_policy_to_template_confirm_modal.tsx index f20ea0f5d1bf46..8971f18ef8e5fe 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/add_policy_to_template_confirm_modal.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/add_policy_to_template_confirm_modal.tsx @@ -13,7 +13,6 @@ import { EuiComboBox, EuiForm, EuiFormRow, - EuiOverlayMask, EuiConfirmModal, EuiFieldText, EuiSpacer, @@ -257,46 +256,44 @@ export const AddPolicyToTemplateConfirmModal: React.FunctionComponent = ( ); return ( - - - -

    - {' '} - - } - /> -

    -
    - - {renderForm()} -
    -
    + />{' '} + + } + /> +

    + + + {renderForm()} + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/confirm_delete.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/confirm_delete.tsx index 80039a18ef17a9..e42aa97a10d4f1 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/confirm_delete.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/confirm_delete.tsx @@ -8,7 +8,7 @@ import React, { Component } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { PolicyFromES } from '../../../../../common/types'; import { toasts } from '../../../services/notification'; @@ -50,33 +50,31 @@ export class ConfirmDelete extends Component { values: { name: policyToDelete.name }, }); return ( - - - } - confirmButtonText={ - - } - buttonColor="danger" - > -
    - -
    -
    -
    + + } + confirmButtonText={ + + } + buttonColor="danger" + > +
    + +
    +
    ); } } diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx index 550cefb488f667..36df4d9527a5c8 100644 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/add_lifecycle_confirm_modal.tsx @@ -16,7 +16,6 @@ import { EuiSelect, EuiForm, EuiFormRow, - EuiOverlayMask, EuiConfirmModal, EuiModal, EuiModalBody, @@ -246,63 +245,59 @@ export class AddLifecyclePolicyConfirmModal extends Component { ); if (!policies.length) { return ( - - - - {title} - + + + {title} + - - + + } + color="warning" + > +

    + - } - color="warning" - > -

    - - - -

    -
    -
    -
    -
    + +

    + + + ); } return ( - - - } - confirmButtonText={ - - } - > - {this.renderForm()} - - + + } + confirmButtonText={ + + } + > + {this.renderForm()} + ); } } diff --git a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/remove_lifecycle_confirm_modal.tsx b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/remove_lifecycle_confirm_modal.tsx index 8ce4ac052fce23..2f22a0b347db95 100644 --- a/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/remove_lifecycle_confirm_modal.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/extend_index_management/components/remove_lifecycle_confirm_modal.tsx @@ -8,7 +8,7 @@ import React, { Component, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { removeLifecycleForIndex } from '../../application/services/api'; import { showApiError } from '../../application/services/api_errors'; @@ -57,54 +57,52 @@ export class RemoveLifecyclePolicyConfirmModal extends Component { const { closeModal, indexNames } = this.props; return ( - - + } + onCancel={closeModal} + onConfirm={this.removePolicy} + cancelButtonText={ + + } + buttonColor="danger" + confirmButtonText={ + + } + > + +

    - } - onCancel={closeModal} - onConfirm={this.removePolicy} - cancelButtonText={ - - } - buttonColor="danger" - confirmButtonText={ - - } - > - -

    - -

    +

    -
      - {indexNames.map((indexName) => ( -
    • {indexName}
    • - ))} -
    -
    -
    -
    +
      + {indexNames.map((indexName) => ( +
    • {indexName}
    • + ))} +
    + + ); } } diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/delete_modal.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/delete_modal.tsx index cac26d948b11aa..0b20bebf431436 100644 --- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/delete_modal.tsx +++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_list/delete_modal.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -81,49 +81,47 @@ export const ComponentTemplatesDeleteModal = ({ }; return ( - - + } + onCancel={handleOnCancel} + onConfirm={handleDeleteComponentTemplates} + cancelButtonText={ + + } + confirmButtonText={ + + } + > + <> +

    - } - onCancel={handleOnCancel} - onConfirm={handleDeleteComponentTemplates} - cancelButtonText={ - - } - confirmButtonText={ - - } - > - <> -

    - -

    +

    -
      - {componentTemplatesToDelete.map((name) => ( -
    • {name}
    • - ))} -
    - -
    -
    +
      + {componentTemplatesToDelete.map((name) => ( +
    • {name}
    • + ))} +
    + + ); }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/modal_confirmation_delete_fields.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/modal_confirmation_delete_fields.tsx index e6a7e42c089365..2a65906ea56b40 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/modal_confirmation_delete_fields.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/modal_confirmation_delete_fields.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiConfirmModal, EuiOverlayMask, EuiBadge, EuiCode } from '@elastic/eui'; +import { EuiConfirmModal, EuiBadge, EuiCode } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { NormalizedFields, NormalizedField } from '../../../types'; @@ -59,55 +59,53 @@ export const ModalConfirmationDeleteFields = ({ : null; return ( - - + <> + {fieldsTree && ( + <> +

    + {i18n.translate( + 'xpack.idxMgmt.mappingsEditor.confirmationModal.deleteFieldsDescription', + { + defaultMessage: 'This will also delete the following fields.', + } + )} +

    + + + )} + {aliases && ( + <> +

    + {i18n.translate( + 'xpack.idxMgmt.mappingsEditor.confirmationModal.deleteAliasesDescription', + { + defaultMessage: 'The following aliases will also be deleted.', + } + )} +

    +
      + {aliases.map((aliasPath) => ( +
    • + {aliasPath} +
    • + ))} +
    + )} - buttonColor="danger" - confirmButtonText={confirmButtonText} - > - <> - {fieldsTree && ( - <> -

    - {i18n.translate( - 'xpack.idxMgmt.mappingsEditor.confirmationModal.deleteFieldsDescription', - { - defaultMessage: 'This will also delete the following fields.', - } - )} -

    - - - )} - {aliases && ( - <> -

    - {i18n.translate( - 'xpack.idxMgmt.mappingsEditor.confirmationModal.deleteAliasesDescription', - { - defaultMessage: 'The following aliases will also be deleted.', - } - )} -

    -
      - {aliases.map((aliasPath) => ( -
    • - {aliasPath} -
    • - ))} -
    - - )} - -
    -
    + + ); }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.tsx index a4c88b1e61b8b4..8f023156456dcb 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/load_mappings/load_mappings_provider.tsx @@ -8,14 +8,7 @@ import React, { useState, useRef, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiConfirmModal, - EuiOverlayMask, - EuiCallOut, - EuiText, - EuiSpacer, - EuiButtonEmpty, -} from '@elastic/eui'; +import { EuiConfirmModal, EuiCallOut, EuiText, EuiSpacer, EuiButtonEmpty } from '@elastic/eui'; import { JsonEditor, OnJsonEditorUpdateHandler } from '../../shared_imports'; import { validateMappings, MappingsValidationError } from '../../lib'; @@ -220,61 +213,55 @@ export const LoadMappingsProvider = ({ onJson, children }: Props) => { {children(openModal)} {state.isModalOpen && ( - - - {view === 'json' ? ( - // The CSS override for the EuiCodeEditor requires a parent .application css class -
    - - mappings, - }} - /> - - - - - + {view === 'json' ? ( + // The CSS override for the EuiCodeEditor requires a parent .application css class +
    + + mappings, }} /> -
    - ) : ( - <> - - -

    {i18nTexts.validationErrors.description}

    -
    - -
      - {state.errors!.slice(0, totalErrorsToDisplay).map((error, i) => ( -
    1. {getErrorMessage(error)}
    2. - ))} -
    - {state.errors!.length > MAX_ERRORS_TO_DISPLAY && renderErrorsFilterButton()} -
    - - )} - - + + + + + +
    + ) : ( + <> + + +

    {i18nTexts.validationErrors.description}

    +
    + +
      + {state.errors!.slice(0, totalErrorsToDisplay).map((error, i) => ( +
    1. {getErrorMessage(error)}
    2. + ))} +
    + {state.errors!.length > MAX_ERRORS_TO_DISPLAY && renderErrorsFilterButton()} +
    + + )} +
    )} ); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/delete_field_provider.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/delete_field_provider.tsx index e48172c417d0ad..f9ecca1f8cb61b 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/delete_field_provider.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/runtime_fields/delete_field_provider.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { useDispatch } from '../../mappings_state_context'; import { NormalizedRuntimeField } from '../../types'; @@ -68,22 +68,20 @@ export const DeleteRuntimeFieldProvider = ({ children }: Props) => { {children(deleteField)} {state.isModalOpen && ( - - - + )} ); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx index 2f49e95a1bd62a..d7db98731427db 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx @@ -86,7 +86,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { id="xpack.idxMgmt.mappingsEditor.dataType.constantKeywordLongDescription" defaultMessage="Constant keyword fields are a special type of keyword fields for fields that contain the same keyword across all documents in the index. Supports the same queries and aggregations as {keyword} fields." values={{ - keyword: {'keyword'}, + keyword: {'keyword'}, }} />

    @@ -836,7 +836,7 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = { id="xpack.idxMgmt.mappingsEditor.dataType.pointLongDescription" defaultMessage="Point fields enable searching of {code} pairs that fall in a 2-dimensional planar coordinate system." values={{ - code: {'x,y'}, + code: {'x,y'}, }} />

    diff --git a/x-pack/plugins/index_management/public/application/components/template_delete_modal.tsx b/x-pack/plugins/index_management/public/application/components/template_delete_modal.tsx index 0dc2407d22c294..f22fa2a3b4f8a5 100644 --- a/x-pack/plugins/index_management/public/application/components/template_delete_modal.tsx +++ b/x-pack/plugins/index_management/public/application/components/template_delete_modal.tsx @@ -6,7 +6,7 @@ */ import React, { Fragment, useState } from 'react'; -import { EuiConfirmModal, EuiOverlayMask, EuiCallOut, EuiCheckbox, EuiBadge } from '@elastic/eui'; +import { EuiConfirmModal, EuiCallOut, EuiCheckbox, EuiBadge } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -81,95 +81,93 @@ export const TemplateDeleteModal = ({ }; return ( - - - } - onCancel={handleOnCancel} - onConfirm={handleDeleteTemplates} - cancelButtonText={ - - } - confirmButtonText={ + + } + onCancel={handleOnCancel} + onConfirm={handleDeleteTemplates} + cancelButtonText={ + + } + confirmButtonText={ + + } + confirmButtonDisabled={hasSystemTemplate ? !isDeleteConfirmed : false} + > + +

    - } - confirmButtonDisabled={hasSystemTemplate ? !isDeleteConfirmed : false} - > - -

    - -

    +

    -
      - {templatesToDelete.map(({ name }) => ( -
    • - {name} - {name.startsWith('.') ? ( - - {' '} - - - - - ) : null} -
    • - ))} -
    - {hasSystemTemplate && ( - + {templatesToDelete.map(({ name }) => ( +
  • + {name} + {name.startsWith('.') ? ( + + {' '} + + + + + ) : null} +
  • + ))} + + {hasSystemTemplate && ( + + } + color="danger" + iconType="alert" + data-test-subj="deleteSystemTemplateCallOut" + > +

    + +

    + } - color="danger" - iconType="alert" - data-test-subj="deleteSystemTemplateCallOut" - > -

    - -

    - - } - checked={isDeleteConfirmed} - onChange={(e) => setIsDeleteConfirmed(e.target.checked)} - /> -
    - )} -
    -
    -
    + checked={isDeleteConfirmed} + onChange={(e) => setIsDeleteConfirmed(e.target.checked)} + /> + + )} + +
    ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/delete_data_stream_confirmation_modal/delete_data_stream_confirmation_modal.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/delete_data_stream_confirmation_modal/delete_data_stream_confirmation_modal.tsx index 7475a87ca24d99..f555706a28cdd4 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/delete_data_stream_confirmation_modal/delete_data_stream_confirmation_modal.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/delete_data_stream_confirmation_modal/delete_data_stream_confirmation_modal.tsx @@ -6,7 +6,7 @@ */ import React, { Fragment } from 'react'; -import { EuiCallOut, EuiConfirmModal, EuiOverlayMask, EuiSpacer } from '@elastic/eui'; +import { EuiCallOut, EuiConfirmModal, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -82,69 +82,67 @@ export const DeleteDataStreamConfirmationModal: React.FunctionComponent = }; return ( - - - } - onCancel={() => onClose()} - onConfirm={handleDeleteDataStreams} - cancelButtonText={ - - } - confirmButtonText={ - - } - > - - - } - color="danger" - iconType="alert" - > -

    - -

    -
    - - - + + } + onCancel={() => onClose()} + onConfirm={handleDeleteDataStreams} + cancelButtonText={ + + } + confirmButtonText={ + + } + > + + + } + color="danger" + iconType="alert" + >

    +
    + + + +

    + +

    -
      - {dataStreams.map((name) => ( -
    • {name}
    • - ))} -
    -
    -
    -
    +
      + {dataStreams.map((name) => ( +
    • {name}
    • + ))} +
    + + ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js index 6282469b092669..20a4af59bab111 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js @@ -20,7 +20,6 @@ import { EuiPopover, EuiSpacer, EuiConfirmModal, - EuiOverlayMask, EuiCheckbox, } from '@elastic/eui'; @@ -301,102 +300,97 @@ export class IndexActionsContextMenu extends Component { const selectedIndexCount = indexNames.length; return ( - - { - if (!this.forcemergeSegmentsError()) { - this.closePopoverAndExecute(() => { - forcemergeIndices(this.state.forcemergeSegments); - this.setState({ - forcemergeSegments: null, - showForcemergeSegmentsModal: null, - }); + { + if (!this.forcemergeSegmentsError()) { + this.closePopoverAndExecute(() => { + forcemergeIndices(this.state.forcemergeSegments); + this.setState({ + forcemergeSegments: null, + showForcemergeSegmentsModal: null, }); - } - }} - cancelButtonText={i18n.translate( - 'xpack.idxMgmt.indexActionsMenu.forceMerge.confirmModal.cancelButtonText', - { - defaultMessage: 'Cancel', - } - )} - confirmButtonText={i18n.translate( - 'xpack.idxMgmt.indexActionsMenu.forceMerge.confirmModal.confirmButtonText', + }); + } + }} + cancelButtonText={i18n.translate( + 'xpack.idxMgmt.indexActionsMenu.forceMerge.confirmModal.cancelButtonText', + { + defaultMessage: 'Cancel', + } + )} + confirmButtonText={i18n.translate( + 'xpack.idxMgmt.indexActionsMenu.forceMerge.confirmModal.confirmButtonText', + { + defaultMessage: 'Force merge', + } + )} + > +

    + +

    + +
      + {indexNames.map((indexName) => ( +
    • {indexName}
    • + ))} +
    + +

    -

    - -
      - {indexNames.map((indexName) => ( -
    • {indexName}
    • - ))} -
    - - -

    - -

    -
    + /> +

    +
    - + - + - - { - this.setState({ forcemergeSegments: event.target.value }); - }} - min={1} - name="maxNumberSegments" - /> - - -
    -
    + { + this.setState({ forcemergeSegments: event.target.value }); + }} + min={1} + name="maxNumberSegments" + /> + + + ); }; @@ -494,39 +488,37 @@ export class IndexActionsContextMenu extends Component { ); return ( - - { - this.confirmAction(false); - this.closeConfirmModal(); - }} - onConfirm={() => this.closePopoverAndExecute(deleteIndices)} - buttonColor="danger" - confirmButtonDisabled={hasSystemIndex ? !isActionConfirmed : false} - cancelButtonText={i18n.translate( - 'xpack.idxMgmt.indexActionsMenu.deleteIndex.confirmModal.cancelButtonText', - { - defaultMessage: 'Cancel', - } - )} - confirmButtonText={i18n.translate( - 'xpack.idxMgmt.indexActionsMenu.deleteIndex.confirmModal.confirmButtonText', - { - defaultMessage: 'Delete {selectedIndexCount, plural, one {index} other {indices} }', - values: { selectedIndexCount }, - } - )} - > - {hasSystemIndex ? systemIndexModalBody : standardIndexModalBody} - - + { + this.confirmAction(false); + this.closeConfirmModal(); + }} + onConfirm={() => this.closePopoverAndExecute(deleteIndices)} + buttonColor="danger" + confirmButtonDisabled={hasSystemIndex ? !isActionConfirmed : false} + cancelButtonText={i18n.translate( + 'xpack.idxMgmt.indexActionsMenu.deleteIndex.confirmModal.cancelButtonText', + { + defaultMessage: 'Cancel', + } + )} + confirmButtonText={i18n.translate( + 'xpack.idxMgmt.indexActionsMenu.deleteIndex.confirmModal.confirmButtonText', + { + defaultMessage: 'Delete {selectedIndexCount, plural, one {index} other {indices} }', + values: { selectedIndexCount }, + } + )} + > + {hasSystemIndex ? systemIndexModalBody : standardIndexModalBody} + ); }; @@ -536,96 +528,91 @@ export class IndexActionsContextMenu extends Component { const selectedIndexCount = indexNames.length; return ( - - { + this.confirmAction(false); + this.closeConfirmModal(); + }} + onConfirm={() => this.closePopoverAndExecute(closeIndices)} + buttonColor="danger" + confirmButtonDisabled={!isActionConfirmed} + cancelButtonText={i18n.translate( + 'xpack.idxMgmt.indexActionsMenu.deleteIndex.confirmModal.cancelButtonText', + { + defaultMessage: 'Cancel', + } + )} + confirmButtonText={i18n.translate( + 'xpack.idxMgmt.indexActionsMenu.closeIndex.confirmModal.confirmButtonText', + { + defaultMessage: 'Close {selectedIndexCount, plural, one {index} other {indices} }', + values: { selectedIndexCount }, + } + )} + > +

    + +

    + +
      + {indexNames.map((indexName) => ( +
    • + {indexName} + {isSystemIndexByName[indexName] ? ( + + {' '} + + + + + ) : ( + '' + )} +
    • + ))} +
    + + { - this.confirmAction(false); - this.closeConfirmModal(); - }} - onConfirm={() => this.closePopoverAndExecute(closeIndices)} - buttonColor="danger" - confirmButtonDisabled={!isActionConfirmed} - cancelButtonText={i18n.translate( - 'xpack.idxMgmt.indexActionsMenu.deleteIndex.confirmModal.cancelButtonText', + 'xpack.idxMgmt.indexActionsMenu.closeIndex.proceedWithCautionCallOutTitle', { - defaultMessage: 'Cancel', - } - )} - confirmButtonText={i18n.translate( - 'xpack.idxMgmt.indexActionsMenu.closeIndex.confirmModal.confirmButtonText', - { - defaultMessage: 'Close {selectedIndexCount, plural, one {index} other {indices} }', - values: { selectedIndexCount }, + defaultMessage: 'Closing a system index can break Kibana', } )} + color="danger" + iconType="alert" >

    - -
      - {indexNames.map((indexName) => ( -
    • - {indexName} - {isSystemIndexByName[indexName] ? ( - - {' '} - - - - - ) : ( - '' - )} -
    • - ))} -
    - - -

    + -

    - - } - checked={isActionConfirmed} - onChange={(e) => this.confirmAction(e.target.checked)} - /> -
    -
    -
    + } + checked={isActionConfirmed} + onChange={(e) => this.confirmAction(e.target.checked)} + /> + + ); }; @@ -633,71 +620,69 @@ export class IndexActionsContextMenu extends Component { const { freezeIndices, indexNames } = this.props; return ( - - this.closePopoverAndExecute(freezeIndices)} + cancelButtonText={i18n.translate( + 'xpack.idxMgmt.indexActionsMenu.freezeEntity.confirmModal.cancelButtonText', + { + defaultMessage: 'Cancel', + } + )} + confirmButtonText={i18n.translate( + 'xpack.idxMgmt.indexActionsMenu.freezeEntity.confirmModal.confirmButtonText', + { + defaultMessage: 'Freeze {count, plural, one {index} other {indices}}', + values: { + count: indexNames.length, + }, + } + )} + > +

    + +

    + +
      + {indexNames.map((indexName) => ( +
    • {indexName}
    • + ))} +
    + + this.closePopoverAndExecute(freezeIndices)} - cancelButtonText={i18n.translate( - 'xpack.idxMgmt.indexActionsMenu.freezeEntity.confirmModal.cancelButtonText', - { - defaultMessage: 'Cancel', - } - )} - confirmButtonText={i18n.translate( - 'xpack.idxMgmt.indexActionsMenu.freezeEntity.confirmModal.confirmButtonText', + 'xpack.idxMgmt.indexActionsMenu.freezeEntity.proceedWithCautionCallOutTitle', { - defaultMessage: 'Freeze {count, plural, one {index} other {indices}}', - values: { - count: indexNames.length, - }, + defaultMessage: 'Proceed with caution', } )} + color="warning" + iconType="help" >

    -

    - -
      - {indexNames.map((indexName) => ( -
    • {indexName}
    • - ))} -
    - - -

    - -

    -
    -
    -
    + /> +

    + + ); }; diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts index d4d16933c518ce..a6c0592e035e79 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/create.ts @@ -71,7 +71,7 @@ export const registerCreateRoute = ({ }); } - return res.internalError({ body: error }); + throw error; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts index 28773b6233b552..552aa5a9a2888c 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/get.ts @@ -54,7 +54,7 @@ export function registerGetAllRoute({ router, license, lib: { isEsError } }: Rou }); } - return res.internalError({ body: error }); + throw error; } }) ); @@ -94,7 +94,7 @@ export function registerGetAllRoute({ router, license, lib: { isEsError } }: Rou }); } - return res.internalError({ body: error }); + throw error; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.ts index b7957737d3aae8..1ed6555eb38067 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/privileges.ts @@ -64,7 +64,7 @@ export const registerPrivilegesRoute = ({ license, router, config }: RouteDepend return res.ok({ body: privilegesResult }); } catch (e) { - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts index b0113e8566ae2e..42b53ab6ee25b3 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/update.ts @@ -55,7 +55,7 @@ export const registerUpdateRoute = ({ }); } - return res.internalError({ body: error }); + throw error; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_clear_cache_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_clear_cache_route.ts index cbb7add344a60e..2f5da4b1d8957a 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_clear_cache_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_clear_cache_route.ts @@ -38,7 +38,7 @@ export function registerClearCacheRoute({ router, license, lib }: RouteDependenc }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_close_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_close_route.ts index 5a0d692c743185..1a0babfc3a5b1f 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_close_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_close_route.ts @@ -38,7 +38,7 @@ export function registerCloseRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_delete_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_delete_route.ts index dfe571f352296f..9a022d4595d1c0 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_delete_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_delete_route.ts @@ -38,7 +38,7 @@ export function registerDeleteRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_flush_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_flush_route.ts index 8faba8a5d54cd0..b064f3520004a0 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_flush_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_flush_route.ts @@ -38,7 +38,7 @@ export function registerFlushRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_forcemerge_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_forcemerge_route.ts index d8a777196e7ef5..1c14f660b98c67 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_forcemerge_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_forcemerge_route.ts @@ -45,7 +45,7 @@ export function registerForcemergeRoute({ router, license, lib }: RouteDependenc }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_freeze_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_freeze_route.ts index 4d18650d928265..b669d78f2ba597 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_freeze_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_freeze_route.ts @@ -40,7 +40,7 @@ export function registerFreezeRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_list_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_list_route.ts index b940253635ad0e..0b253b9fe66c97 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_list_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_list_route.ts @@ -27,7 +27,7 @@ export function registerListRoute({ router, license, indexDataEnricher, lib }: R }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_open_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_open_route.ts index 322eac45d7bd1f..a35ddfcf4d91b2 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_open_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_open_route.ts @@ -38,7 +38,7 @@ export function registerOpenRoute({ router, license, lib }: RouteDependencies) { }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_refresh_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_refresh_route.ts index a070208d30d4cd..f69d2d90a5b8fc 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_refresh_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_refresh_route.ts @@ -38,7 +38,7 @@ export function registerRefreshRoute({ router, license, lib }: RouteDependencies }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_reload_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_reload_route.ts index b6b8d741c1202c..04b7d760fc1d62 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_reload_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_reload_route.ts @@ -43,7 +43,7 @@ export function registerReloadRoute({ }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/indices/register_unfreeze_route.ts b/x-pack/plugins/index_management/server/routes/api/indices/register_unfreeze_route.ts index 55281951790331..3cda4d6b5f1682 100644 --- a/x-pack/plugins/index_management/server/routes/api/indices/register_unfreeze_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/indices/register_unfreeze_route.ts @@ -35,7 +35,7 @@ export function registerUnfreezeRoute({ router, license, lib }: RouteDependencie }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/mapping/register_mapping_route.ts b/x-pack/plugins/index_management/server/routes/api/mapping/register_mapping_route.ts index 5cc0c92969ab09..f0b62bacdee426 100644 --- a/x-pack/plugins/index_management/server/routes/api/mapping/register_mapping_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/mapping/register_mapping_route.ts @@ -46,7 +46,7 @@ export function registerMappingRoute({ router, license, lib }: RouteDependencies }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/settings/register_load_route.ts b/x-pack/plugins/index_management/server/routes/api/settings/register_load_route.ts index 6d355ced5e9939..7a661a9e9e4f49 100644 --- a/x-pack/plugins/index_management/server/routes/api/settings/register_load_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/settings/register_load_route.ts @@ -48,7 +48,7 @@ export function registerLoadRoute({ router, license, lib }: RouteDependencies) { }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/settings/register_update_route.ts b/x-pack/plugins/index_management/server/routes/api/settings/register_update_route.ts index 1216a9c74e48c4..4c153d6293a79f 100644 --- a/x-pack/plugins/index_management/server/routes/api/settings/register_update_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/settings/register_update_route.ts @@ -46,7 +46,7 @@ export function registerUpdateRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/stats/register_stats_route.ts b/x-pack/plugins/index_management/server/routes/api/stats/register_stats_route.ts index 45bf114805a787..f8385711b55fe2 100644 --- a/x-pack/plugins/index_management/server/routes/api/stats/register_stats_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/stats/register_stats_route.ts @@ -47,7 +47,7 @@ export function registerStatsRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts index 81286b69f2ded8..97e3c380e13ecd 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_create_route.ts @@ -62,7 +62,7 @@ export function registerCreateRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts index 88aa8d3a793501..006532cfd4dbe1 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts @@ -104,7 +104,7 @@ export function registerGetOneRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts index d24837fa01cd50..f4554bd2fb1fa6 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_simulate_route.ts @@ -40,7 +40,7 @@ export function registerSimulateRoute({ router, license, lib }: RouteDependencie }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts index bf88d63572a823..f0070408768cbf 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_update_route.ts @@ -56,7 +56,7 @@ export function registerUpdateRoute({ router, license, lib }: RouteDependencies) }); } // Case: default - return res.internalError({ body: e }); + throw e; } }) ); diff --git a/x-pack/plugins/infra/public/apps/common_providers.tsx b/x-pack/plugins/infra/public/apps/common_providers.tsx index 972c6f8b14f8c4..64867c5743d0d4 100644 --- a/x-pack/plugins/infra/public/apps/common_providers.tsx +++ b/x-pack/plugins/infra/public/apps/common_providers.tsx @@ -7,18 +7,18 @@ import { AppMountParameters, CoreStart } from 'kibana/public'; import React, { useMemo } from 'react'; +import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; import { - useUiSetting$, KibanaContextProvider, + useUiSetting$, } from '../../../../../src/plugins/kibana_react/public'; -import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { TriggersAndActionsUIPublicPluginStart } from '../../../triggers_actions_ui/public'; import { createKibanaContextForPlugin } from '../hooks/use_kibana'; import { InfraClientStartDeps } from '../types'; import { HeaderActionMenuProvider } from '../utils/header_action_menu_provider'; import { NavigationWarningPromptProvider } from '../utils/navigation_warning_prompt'; import { TriggersActionsProvider } from '../utils/triggers_actions_context'; -import { Storage } from '../../../../../src/plugins/kibana_utils/public'; export const CommonInfraProviders: React.FC<{ appName: string; diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx b/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx index 766b8076bc93dc..e1427bc96e7e0b 100644 --- a/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx @@ -5,19 +5,18 @@ * 2.0. */ +import { CoreStart } from 'kibana/public'; import React from 'react'; import ReactDOM from 'react-dom'; -import { CoreStart } from 'kibana/public'; - -import { I18nProvider } from '@kbn/i18n/react'; -import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; -import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common'; import { Query, TimeRange } from '../../../../../../src/plugins/data/public'; import { Embeddable, EmbeddableInput, IContainer, } from '../../../../../../src/plugins/embeddable/public'; +import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common'; +import { CoreProviders } from '../../apps/common_providers'; +import { InfraClientStartDeps } from '../../types'; import { datemathToEpochMillis } from '../../utils/datemath'; import { LazyLogStreamWrapper } from './lazy_log_stream_wrapper'; @@ -33,7 +32,8 @@ export class LogStreamEmbeddable extends Embeddable { private node?: HTMLElement; constructor( - private services: CoreStart, + private core: CoreStart, + private pluginDeps: InfraClientStartDeps, initialInput: LogStreamEmbeddableInput, parent?: IContainer ) { @@ -73,20 +73,18 @@ export class LogStreamEmbeddable extends Embeddable { } ReactDOM.render( - + - -
    - -
    -
    +
    + +
    -
    , + , this.node ); } diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable_factory.ts b/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable_factory.ts index 609a29e5842fe2..d621cae3e628cb 100644 --- a/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable_factory.ts +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable_factory.ts @@ -5,31 +5,32 @@ * 2.0. */ -import { CoreStart } from 'kibana/public'; +import { StartServicesAccessor } from 'kibana/public'; import { EmbeddableFactoryDefinition, IContainer, } from '../../../../../../src/plugins/embeddable/public'; +import { InfraClientStartDeps } from '../../types'; import { LogStreamEmbeddable, - LOG_STREAM_EMBEDDABLE, LogStreamEmbeddableInput, + LOG_STREAM_EMBEDDABLE, } from './log_stream_embeddable'; export class LogStreamEmbeddableFactoryDefinition implements EmbeddableFactoryDefinition { public readonly type = LOG_STREAM_EMBEDDABLE; - constructor(private getCoreServices: () => Promise) {} + constructor(private getStartServices: StartServicesAccessor) {} public async isEditable() { - const { application } = await this.getCoreServices(); + const [{ application }] = await this.getStartServices(); return application.capabilities.logs.save as boolean; } public async create(initialInput: LogStreamEmbeddableInput, parent?: IContainer) { - const services = await this.getCoreServices(); - return new LogStreamEmbeddable(services, initialInput, parent); + const [core, plugins] = await this.getStartServices(); + return new LogStreamEmbeddable(core, plugins, initialInput, parent); } public getDisplayName() { diff --git a/x-pack/plugins/infra/public/components/saved_views/create_modal.tsx b/x-pack/plugins/infra/public/components/saved_views/create_modal.tsx index 985db9872ef3f6..654cba0721bb8a 100644 --- a/x-pack/plugins/infra/public/components/saved_views/create_modal.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/create_modal.tsx @@ -16,7 +16,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiFieldText, EuiSpacer, EuiSwitch, @@ -40,69 +39,67 @@ export const SavedViewCreateModal = ({ close, save, isInvalid }: Props) => { }, [includeTime, save, viewName]); return ( - - - - - - - - - - + + + - - - } - checked={includeTime} - onChange={onCheckChange} - /> - - - - - + + - - + + + + - - - - - - - + } + checked={includeTime} + onChange={onCheckChange} + /> + + + + + + + + + + + + + + + ); }; diff --git a/x-pack/plugins/infra/public/components/saved_views/update_modal.tsx b/x-pack/plugins/infra/public/components/saved_views/update_modal.tsx index 15d0d162604a4b..c6d87d9a8ca158 100644 --- a/x-pack/plugins/infra/public/components/saved_views/update_modal.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/update_modal.tsx @@ -16,7 +16,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiFieldText, EuiSpacer, EuiSwitch, @@ -46,69 +45,67 @@ export function SavedViewUpdateModal - - - - - - - - - + + + - - - } - checked={includeTime} - onChange={onCheckChange} - /> - - - - - + + - - + + + + - - - - - - -
    + } + checked={includeTime} + onChange={onCheckChange} + /> + + + + + + + + + + + + + + + ); } diff --git a/x-pack/plugins/infra/public/components/saved_views/view_list_modal.tsx b/x-pack/plugins/infra/public/components/saved_views/view_list_modal.tsx index 9ab742d720eb5e..aad50c4dcb45d0 100644 --- a/x-pack/plugins/infra/public/components/saved_views/view_list_modal.tsx +++ b/x-pack/plugins/infra/public/components/saved_views/view_list_modal.tsx @@ -9,13 +9,7 @@ import React, { useCallback, useState, useMemo } from 'react'; import { EuiButtonEmpty, EuiModalFooter, EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiOverlayMask, - EuiModal, - EuiModalHeader, - EuiModalHeaderTitle, - EuiModalBody, -} from '@elastic/eui'; +import { EuiModal, EuiModalHeader, EuiModalHeaderTitle, EuiModalBody } from '@elastic/eui'; import { EuiSelectable } from '@elastic/eui'; import { EuiSelectableOption } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -64,51 +58,49 @@ export function SavedViewListModal - - - - - - - - - {(list, search) => ( - <> - {search} -
    {list}
    - - )} -
    -
    - - - - - - - - -
    - + + + + + + + + + {(list, search) => ( + <> + {search} +
    {list}
    + + )} +
    +
    + + + + + + + + +
    ); } 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 index df21a66a5226f4..5537ef9541f892 100644 --- 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 @@ -9,7 +9,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiModal, - EuiOverlayMask, EuiText, EuiTextColor, EuiToolTip, @@ -51,33 +50,26 @@ export const PageViewLogInContext: React.FC = () => { } return ( - - - - - - - - - - - - - - + + + + + + + + + + + + ); }; diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index d4bb83e8668ba5..07afbfdb5d4ed6 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -52,11 +52,9 @@ export class Plugin implements InfraClientPluginClass { }); } - const getCoreServices = async () => (await core.getStartServices())[0]; - pluginsSetup.embeddable.registerEmbeddableFactory( LOG_STREAM_EMBEDDABLE, - new LogStreamEmbeddableFactoryDefinition(getCoreServices) + new LogStreamEmbeddableFactoryDefinition(core.getStartServices) ); core.application.register({ 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 7147f224d09d57..70d29773f76c5d 100644 --- a/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts +++ b/x-pack/plugins/infra/server/routes/inventory_metadata/index.ts @@ -33,33 +33,27 @@ export const initInventoryMetaRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const { sourceId, nodeType, currentTime } = pipe( - InventoryMetaRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - - const { configuration } = await libs.sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - sourceId - ); - - const awsMetadata = await getCloudMetadata( - framework, - requestContext, - configuration, - nodeType, - currentTime - ); - - return response.ok({ - body: InventoryMetaResponseRT.encode(awsMetadata), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + const { sourceId, nodeType, currentTime } = pipe( + InventoryMetaRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + + const { configuration } = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); + + const awsMetadata = await getCloudMetadata( + framework, + requestContext, + configuration, + nodeType, + currentTime + ); + + return response.ok({ + body: InventoryMetaResponseRT.encode(awsMetadata), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/log_analysis/validation/indices.ts b/x-pack/plugins/infra/server/routes/log_analysis/validation/indices.ts index c49d6034c95c41..463ac77891263c 100644 --- a/x-pack/plugins/infra/server/routes/log_analysis/validation/indices.ts +++ b/x-pack/plugins/infra/server/routes/log_analysis/validation/indices.ts @@ -31,65 +31,59 @@ export const initValidateLogAnalysisIndicesRoute = ({ framework }: InfraBackendL validate: { body: escapeHatch }, }, async (requestContext, request, response) => { - try { - const payload = pipe( - validationIndicesRequestPayloadRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + const payload = pipe( + validationIndicesRequestPayloadRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - const { fields, indices } = payload.data; - const errors: ValidationIndicesError[] = []; + const { fields, indices } = payload.data; + const errors: ValidationIndicesError[] = []; - // Query each pattern individually, to map correctly the errors - await Promise.all( - indices.map(async (index) => { - const fieldCaps = await framework.callWithRequest(requestContext, 'fieldCaps', { - allow_no_indices: true, - fields: fields.map((field) => field.name), - ignore_unavailable: true, + // Query each pattern individually, to map correctly the errors + await Promise.all( + indices.map(async (index) => { + const fieldCaps = await framework.callWithRequest(requestContext, 'fieldCaps', { + allow_no_indices: true, + fields: fields.map((field) => field.name), + ignore_unavailable: true, + index, + }); + + if (fieldCaps.indices.length === 0) { + errors.push({ + error: 'INDEX_NOT_FOUND', index, }); + return; + } + + fields.forEach(({ name: fieldName, validTypes }) => { + const fieldMetadata = fieldCaps.fields[fieldName]; - if (fieldCaps.indices.length === 0) { + if (fieldMetadata === undefined) { errors.push({ - error: 'INDEX_NOT_FOUND', + error: 'FIELD_NOT_FOUND', index, + field: fieldName, }); - return; - } - - fields.forEach(({ name: fieldName, validTypes }) => { - const fieldMetadata = fieldCaps.fields[fieldName]; + } else { + const fieldTypes = Object.keys(fieldMetadata); - if (fieldMetadata === undefined) { + if (!fieldTypes.every((fieldType) => validTypes.includes(fieldType))) { errors.push({ - error: 'FIELD_NOT_FOUND', + error: `FIELD_NOT_VALID`, index, field: fieldName, }); - } else { - const fieldTypes = Object.keys(fieldMetadata); - - if (!fieldTypes.every((fieldType) => validTypes.includes(fieldType))) { - errors.push({ - error: `FIELD_NOT_VALID`, - index, - field: fieldName, - }); - } } - }); - }) - ); + } + }); + }) + ); - return response.ok({ - body: validationIndicesResponsePayloadRT.encode({ data: { errors } }), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: validationIndicesResponsePayloadRT.encode({ data: { errors } }), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/log_entries/highlights.ts b/x-pack/plugins/infra/server/routes/log_entries/highlights.ts index c72590ca01a5ef..bb7c615358c0e1 100644 --- a/x-pack/plugins/infra/server/routes/log_entries/highlights.ts +++ b/x-pack/plugins/infra/server/routes/log_entries/highlights.ts @@ -33,75 +33,69 @@ export const initLogEntriesHighlightsRoute = ({ framework, logEntries }: InfraBa validate: { body: escapeHatch }, }, async (requestContext, request, response) => { - try { - const payload = pipe( - logEntriesHighlightsRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - - const { startTimestamp, endTimestamp, sourceId, query, size, highlightTerms } = payload; + const payload = pipe( + logEntriesHighlightsRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - let entriesPerHighlightTerm; + const { startTimestamp, endTimestamp, sourceId, query, size, highlightTerms } = payload; - if ('center' in payload) { - entriesPerHighlightTerm = await Promise.all( - highlightTerms.map((highlightTerm) => - logEntries.getLogEntriesAround(requestContext, sourceId, { - startTimestamp, - endTimestamp, - query: parseFilterQuery(query), - center: payload.center, - size, - highlightTerm, - }) - ) - ); - } else { - let cursor: LogEntriesParams['cursor']; - if ('before' in payload) { - cursor = { before: payload.before }; - } else if ('after' in payload) { - cursor = { after: payload.after }; - } + let entriesPerHighlightTerm; - entriesPerHighlightTerm = await Promise.all( - highlightTerms.map((highlightTerm) => - logEntries.getLogEntries(requestContext, sourceId, { - startTimestamp, - endTimestamp, - query: parseFilterQuery(query), - cursor, - size, - highlightTerm, - }) - ) - ); + if ('center' in payload) { + entriesPerHighlightTerm = await Promise.all( + highlightTerms.map((highlightTerm) => + logEntries.getLogEntriesAround(requestContext, sourceId, { + startTimestamp, + endTimestamp, + query: parseFilterQuery(query), + center: payload.center, + size, + highlightTerm, + }) + ) + ); + } else { + let cursor: LogEntriesParams['cursor']; + if ('before' in payload) { + cursor = { before: payload.before }; + } else if ('after' in payload) { + cursor = { after: payload.after }; } - return response.ok({ - body: logEntriesHighlightsResponseRT.encode({ - data: entriesPerHighlightTerm.map(({ entries }) => { - if (entries.length > 0) { - return { - entries, - topCursor: entries[0].cursor, - bottomCursor: entries[entries.length - 1].cursor, - }; - } else { - return { - entries, - topCursor: null, - bottomCursor: null, - }; - } - }), - }), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); + entriesPerHighlightTerm = await Promise.all( + highlightTerms.map((highlightTerm) => + logEntries.getLogEntries(requestContext, sourceId, { + startTimestamp, + endTimestamp, + query: parseFilterQuery(query), + cursor, + size, + highlightTerm, + }) + ) + ); } + + return response.ok({ + body: logEntriesHighlightsResponseRT.encode({ + data: entriesPerHighlightTerm.map(({ entries }) => { + if (entries.length > 0) { + return { + entries, + topCursor: entries[0].cursor, + bottomCursor: entries[entries.length - 1].cursor, + }; + } else { + return { + entries, + topCursor: null, + bottomCursor: null, + }; + } + }), + }), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/log_entries/summary.ts b/x-pack/plugins/infra/server/routes/log_entries/summary.ts index 4849b56b1d579d..3ff0ded8a7c244 100644 --- a/x-pack/plugins/infra/server/routes/log_entries/summary.ts +++ b/x-pack/plugins/infra/server/routes/log_entries/summary.ts @@ -33,38 +33,32 @@ export const initLogEntriesSummaryRoute = ({ framework, logEntries }: InfraBacke validate: { body: escapeHatch }, }, async (requestContext, request, response) => { - try { - const payload = pipe( - logEntriesSummaryRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - const { sourceId, startTimestamp, endTimestamp, bucketSize, query } = payload; + const payload = pipe( + logEntriesSummaryRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + const { sourceId, startTimestamp, endTimestamp, bucketSize, query } = payload; - const buckets = await logEntries.getLogSummaryBucketsBetween( - requestContext, - sourceId, - startTimestamp, - endTimestamp, - bucketSize, - parseFilterQuery(query) - ); + const buckets = await logEntries.getLogSummaryBucketsBetween( + requestContext, + sourceId, + startTimestamp, + endTimestamp, + bucketSize, + parseFilterQuery(query) + ); - UsageCollector.countLogs(); + UsageCollector.countLogs(); - return response.ok({ - body: logEntriesSummaryResponseRT.encode({ - data: { - start: startTimestamp, - end: endTimestamp, - buckets, - }, - }), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: logEntriesSummaryResponseRT.encode({ + data: { + start: startTimestamp, + end: endTimestamp, + buckets, + }, + }), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/log_entries/summary_highlights.ts b/x-pack/plugins/infra/server/routes/log_entries/summary_highlights.ts index 62a9d15c4e68bc..ca219cac41e2bf 100644 --- a/x-pack/plugins/infra/server/routes/log_entries/summary_highlights.ts +++ b/x-pack/plugins/infra/server/routes/log_entries/summary_highlights.ts @@ -35,44 +35,31 @@ export const initLogEntriesSummaryHighlightsRoute = ({ validate: { body: escapeHatch }, }, async (requestContext, request, response) => { - try { - const payload = pipe( - logEntriesSummaryHighlightsRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - const { - sourceId, - startTimestamp, - endTimestamp, - bucketSize, - query, - highlightTerms, - } = payload; + const payload = pipe( + logEntriesSummaryHighlightsRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + const { sourceId, startTimestamp, endTimestamp, bucketSize, query, highlightTerms } = payload; - const bucketsPerHighlightTerm = await logEntries.getLogSummaryHighlightBucketsBetween( - requestContext, - sourceId, - startTimestamp, - endTimestamp, - bucketSize, - highlightTerms, - parseFilterQuery(query) - ); + const bucketsPerHighlightTerm = await logEntries.getLogSummaryHighlightBucketsBetween( + requestContext, + sourceId, + startTimestamp, + endTimestamp, + bucketSize, + highlightTerms, + parseFilterQuery(query) + ); - return response.ok({ - body: logEntriesSummaryHighlightsResponseRT.encode({ - data: bucketsPerHighlightTerm.map((buckets) => ({ - start: startTimestamp, - end: endTimestamp, - buckets, - })), - }), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: logEntriesSummaryHighlightsResponseRT.encode({ + data: bucketsPerHighlightTerm.map((buckets) => ({ + start: startTimestamp, + end: endTimestamp, + buckets, + })), + }), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/metadata/index.ts b/x-pack/plugins/infra/server/routes/metadata/index.ts index b2abe1c35a3ff1..cc8888e9bd09d3 100644 --- a/x-pack/plugins/infra/server/routes/metadata/index.ts +++ b/x-pack/plugins/infra/server/routes/metadata/index.ts @@ -37,65 +37,57 @@ export const initMetadataRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const { nodeId, nodeType, sourceId, timeRange } = pipe( - InfraMetadataRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + const { nodeId, nodeType, sourceId, timeRange } = pipe( + InfraMetadataRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - const { configuration } = await libs.sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - sourceId - ); - const metricsMetadata = await getMetricMetadata( - framework, - requestContext, - configuration, - nodeId, - nodeType, - timeRange - ); - const metricFeatures = pickFeatureName(metricsMetadata.buckets).map( - nameToFeature('metrics') - ); + const { configuration } = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); + const metricsMetadata = await getMetricMetadata( + framework, + requestContext, + configuration, + nodeId, + nodeType, + timeRange + ); + const metricFeatures = pickFeatureName(metricsMetadata.buckets).map(nameToFeature('metrics')); - const info = await getNodeInfo( - framework, - requestContext, - configuration, - nodeId, - nodeType, - timeRange - ); - const cloudInstanceId = get(info, 'cloud.instance.id'); + const info = await getNodeInfo( + framework, + requestContext, + configuration, + nodeId, + nodeType, + timeRange + ); + const cloudInstanceId = get(info, 'cloud.instance.id'); - const cloudMetricsMetadata = cloudInstanceId - ? await getCloudMetricsMetadata( - framework, - requestContext, - configuration, - cloudInstanceId, - timeRange - ) - : { buckets: [] }; - const cloudMetricsFeatures = pickFeatureName(cloudMetricsMetadata.buckets).map( - nameToFeature('metrics') - ); - const id = metricsMetadata.id; - const name = metricsMetadata.name || id; - return response.ok({ - body: InfraMetadataRT.encode({ - id, - name, - features: [...metricFeatures, ...cloudMetricsFeatures], - info, - }), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + const cloudMetricsMetadata = cloudInstanceId + ? await getCloudMetricsMetadata( + framework, + requestContext, + configuration, + cloudInstanceId, + timeRange + ) + : { buckets: [] }; + const cloudMetricsFeatures = pickFeatureName(cloudMetricsMetadata.buckets).map( + nameToFeature('metrics') + ); + const id = metricsMetadata.id; + const name = metricsMetadata.name || id; + return response.ok({ + body: InfraMetadataRT.encode({ + id, + name, + features: [...metricFeatures, ...cloudMetricsFeatures], + info, + }), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/metrics_api/index.ts b/x-pack/plugins/infra/server/routes/metrics_api/index.ts index 7d616f5b9dfeae..5c0569d6e7a94f 100644 --- a/x-pack/plugins/infra/server/routes/metrics_api/index.ts +++ b/x-pack/plugins/infra/server/routes/metrics_api/index.ts @@ -29,23 +29,17 @@ export const initMetricsAPIRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const options = pipe( - MetricsAPIRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + const options = pipe( + MetricsAPIRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - const client = createSearchClient(requestContext, framework); - const metricsApiResponse = await query(client, options); + const client = createSearchClient(requestContext, framework); + const metricsApiResponse = await query(client, options); - return response.ok({ - body: MetricsAPIResponseRT.encode(metricsApiResponse), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: MetricsAPIResponseRT.encode(metricsApiResponse), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/index.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/index.ts index b8a48df43bc10f..d61dcfad974947 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/index.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/index.ts @@ -37,55 +37,49 @@ export const initMetricExplorerRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const options = pipe( - metricsExplorerRequestBodyRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + const options = pipe( + metricsExplorerRequestBodyRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - const client = createSearchClient(requestContext, framework); - const interval = await findIntervalForMetrics(client, options); + const client = createSearchClient(requestContext, framework); + const interval = await findIntervalForMetrics(client, options); - const optionsWithInterval = options.forceInterval - ? options - : { - ...options, - timerange: { - ...options.timerange, - interval: interval ? `>=${interval}s` : options.timerange.interval, - }, - }; + const optionsWithInterval = options.forceInterval + ? options + : { + ...options, + timerange: { + ...options.timerange, + interval: interval ? `>=${interval}s` : options.timerange.interval, + }, + }; - const metricsApiOptions = convertRequestToMetricsAPIOptions(optionsWithInterval); - const metricsApiResponse = await query(client, metricsApiOptions); - const totalGroupings = await queryTotalGroupings(client, metricsApiOptions); - const hasGroupBy = - Array.isArray(metricsApiOptions.groupBy) && metricsApiOptions.groupBy.length > 0; + const metricsApiOptions = convertRequestToMetricsAPIOptions(optionsWithInterval); + const metricsApiResponse = await query(client, metricsApiOptions); + const totalGroupings = await queryTotalGroupings(client, metricsApiOptions); + const hasGroupBy = + Array.isArray(metricsApiOptions.groupBy) && metricsApiOptions.groupBy.length > 0; - const pageInfo: MetricsExplorerPageInfo = { - total: totalGroupings, - afterKey: null, - }; + const pageInfo: MetricsExplorerPageInfo = { + total: totalGroupings, + afterKey: null, + }; - if (metricsApiResponse.info.afterKey) { - pageInfo.afterKey = metricsApiResponse.info.afterKey; - } + if (metricsApiResponse.info.afterKey) { + pageInfo.afterKey = metricsApiResponse.info.afterKey; + } - // If we have a groupBy but there are ZERO groupings returned then we need to - // return an empty array. Otherwise we transform the series to match the current schema. - const series = - hasGroupBy && totalGroupings === 0 - ? [] - : metricsApiResponse.series.map(transformSeries(hasGroupBy)); + // If we have a groupBy but there are ZERO groupings returned then we need to + // return an empty array. Otherwise we transform the series to match the current schema. + const series = + hasGroupBy && totalGroupings === 0 + ? [] + : metricsApiResponse.series.map(transformSeries(hasGroupBy)); - return response.ok({ - body: metricsExplorerResponseRT.encode({ series, pageInfo }), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: metricsExplorerResponseRT.encode({ series, pageInfo }), + }); } ); }; 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 d407a9f65f983e..8e305226112bd3 100644 --- a/x-pack/plugins/infra/server/routes/node_details/index.ts +++ b/x-pack/plugins/infra/server/routes/node_details/index.ts @@ -34,38 +34,32 @@ export const initNodeDetailsRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const { nodeId, cloudId, nodeType, metrics, timerange, sourceId } = pipe( - NodeDetailsRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - const source = await libs.sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - sourceId - ); + const { nodeId, cloudId, nodeType, metrics, timerange, sourceId } = pipe( + NodeDetailsRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); + const source = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); - UsageCollector.countNode(nodeType); + UsageCollector.countNode(nodeType); - const options: InfraMetricsRequestOptions = { - nodeIds: { - nodeId, - cloudId, - }, - nodeType, - sourceConfiguration: source.configuration, - metrics, - timerange, - }; - return response.ok({ - body: NodeDetailsMetricDataResponseRT.encode({ - metrics: await libs.metrics.getMetrics(requestContext, options, request), - }), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + const options: InfraMetricsRequestOptions = { + nodeIds: { + nodeId, + cloudId, + }, + nodeType, + sourceConfiguration: source.configuration, + metrics, + timerange, + }; + return response.ok({ + body: NodeDetailsMetricDataResponseRT.encode({ + metrics: await libs.metrics.getMetrics(requestContext, options, request), + }), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/overview/index.ts b/x-pack/plugins/infra/server/routes/overview/index.ts index 4102fd883e9127..fe988abcc2883d 100644 --- a/x-pack/plugins/infra/server/routes/overview/index.ts +++ b/x-pack/plugins/infra/server/routes/overview/index.ts @@ -36,77 +36,71 @@ export const initOverviewRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const overviewRequest = pipe( - OverviewRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + const overviewRequest = pipe( + OverviewRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - const client = createSearchClient(requestContext, framework); - const source = await libs.sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - overviewRequest.sourceId - ); + const client = createSearchClient(requestContext, framework); + const source = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + overviewRequest.sourceId + ); - const inventoryModelFields = findInventoryFields('host', source.configuration.fields); + const inventoryModelFields = findInventoryFields('host', source.configuration.fields); - const params = { - index: source.configuration.metricAlias, - body: { - query: { - range: { - [source.configuration.fields.timestamp]: { - gte: overviewRequest.timerange.from, - lte: overviewRequest.timerange.to, - format: 'epoch_millis', - }, + const params = { + index: source.configuration.metricAlias, + body: { + query: { + range: { + [source.configuration.fields.timestamp]: { + gte: overviewRequest.timerange.from, + lte: overviewRequest.timerange.to, + format: 'epoch_millis', }, }, - aggs: { - hosts: { - cardinality: { - field: inventoryModelFields.id, - }, + }, + aggs: { + hosts: { + cardinality: { + field: inventoryModelFields.id, }, - cpu: { - avg: { - field: 'system.cpu.total.norm.pct', - }, + }, + cpu: { + avg: { + field: 'system.cpu.total.norm.pct', }, - memory: { - avg: { - field: 'system.memory.actual.used.pct', - }, + }, + memory: { + avg: { + field: 'system.memory.actual.used.pct', }, }, }, - }; + }, + }; - const esResponse = await client<{}, OverviewESAggResponse>(params); + const esResponse = await client<{}, OverviewESAggResponse>(params); - return response.ok({ - body: { - stats: { - hosts: { - type: 'number', - value: esResponse.aggregations?.hosts.value ?? 0, - }, - cpu: { - type: 'percent', - value: esResponse.aggregations?.cpu.value ?? 0, - }, - memory: { - type: 'percent', - value: esResponse.aggregations?.memory.value ?? 0, - }, + return response.ok({ + body: { + stats: { + hosts: { + type: 'number', + value: esResponse.aggregations?.hosts.value ?? 0, + }, + cpu: { + type: 'percent', + value: esResponse.aggregations?.cpu.value ?? 0, + }, + memory: { + type: 'percent', + value: esResponse.aggregations?.memory.value ?? 0, }, }, - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + }, + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/process_list/index.ts b/x-pack/plugins/infra/server/routes/process_list/index.ts index ec4ec21fc16749..f1ba7a7be03600 100644 --- a/x-pack/plugins/infra/server/routes/process_list/index.ts +++ b/x-pack/plugins/infra/server/routes/process_list/index.ts @@ -35,23 +35,17 @@ export const initProcessListRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const options = pipe( - ProcessListAPIRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + const options = pipe( + ProcessListAPIRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - const client = createSearchClient(requestContext, framework); - const processListResponse = await getProcessList(client, options); + const client = createSearchClient(requestContext, framework); + const processListResponse = await getProcessList(client, options); - return response.ok({ - body: ProcessListAPIResponseRT.encode(processListResponse), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: ProcessListAPIResponseRT.encode(processListResponse), + }); } ); @@ -64,23 +58,17 @@ export const initProcessListRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const options = pipe( - ProcessListAPIChartRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + const options = pipe( + ProcessListAPIChartRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - const client = createSearchClient(requestContext, framework); - const processListResponse = await getProcessListChart(client, options); + const client = createSearchClient(requestContext, framework); + const processListResponse = await getProcessListChart(client, options); - return response.ok({ - body: ProcessListAPIChartResponseRT.encode(processListResponse), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: ProcessListAPIChartResponseRT.encode(processListResponse), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/snapshot/index.ts b/x-pack/plugins/infra/server/routes/snapshot/index.ts index 07402a3f3ab5d9..aaf23085d0d600 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/index.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/index.ts @@ -31,29 +31,23 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const snapshotRequest = pipe( - SnapshotRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); + const snapshotRequest = pipe( + SnapshotRequestRT.decode(request.body), + fold(throwErrors(Boom.badRequest), identity) + ); - const source = await libs.sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - snapshotRequest.sourceId - ); + const source = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + snapshotRequest.sourceId + ); - UsageCollector.countNode(snapshotRequest.nodeType); - const client = createSearchClient(requestContext, framework); - const snapshotResponse = await getNodes(client, snapshotRequest, source); + UsageCollector.countNode(snapshotRequest.nodeType); + const client = createSearchClient(requestContext, framework); + const snapshotResponse = await getNodes(client, snapshotRequest, source); - return response.ok({ - body: SnapshotNodeResponseRT.encode(snapshotResponse), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: SnapshotNodeResponseRT.encode(snapshotResponse), + }); } ); }; diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/create_timerange_with_interval.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/create_timerange_with_interval.ts index 35decbacf2a529..7473907b7410b8 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/create_timerange_with_interval.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/create_timerange_with_interval.ts @@ -8,7 +8,6 @@ import { uniq } from 'lodash'; import { InfraTimerangeInput } from '../../../../common/http_api'; import { ESSearchClient } from '../../../lib/metrics/types'; -import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { calculateMetricInterval } from '../../../utils/calculate_metric_interval'; import { getMetricsAggregations, InfraSnapshotRequestOptions } from './get_metrics_aggregations'; import { @@ -19,9 +18,6 @@ import { getDatasetForField } from '../../metrics_explorer/lib/get_dataset_for_f const createInterval = async (client: ESSearchClient, options: InfraSnapshotRequestOptions) => { const { timerange } = options; - if (timerange.forceInterval && timerange.interval) { - return getIntervalInSeconds(timerange.interval); - } const aggregations = getMetricsAggregations(options); const modules = await aggregationsToModules(client, aggregations, options); return Math.max( @@ -44,14 +40,21 @@ export const createTimeRangeWithInterval = async ( options: InfraSnapshotRequestOptions ): Promise => { const { timerange } = options; - const calculatedInterval = await createInterval(client, options); + if (timerange.forceInterval) { + return { + interval: timerange.interval, + from: timerange.from, + to: timerange.to, + }; + } if (timerange.ignoreLookback) { return { - interval: `${calculatedInterval}s`, + interval: 'modules', from: timerange.from, to: timerange.to, }; } + const calculatedInterval = await createInterval(client, options); const lookbackSize = Math.max(timerange.lookbackSize || 5, 5); return { interval: `${calculatedInterval}s`, diff --git a/x-pack/plugins/infra/server/routes/source/index.ts b/x-pack/plugins/infra/server/routes/source/index.ts index 5c3827e56ce79c..5ab3275f9ea9e0 100644 --- a/x-pack/plugins/infra/server/routes/source/index.ts +++ b/x-pack/plugins/infra/server/routes/source/index.ts @@ -44,34 +44,28 @@ export const initSourceRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const { type, sourceId } = request.params; + const { type, sourceId } = request.params; - const [source, logIndexStatus, metricIndicesExist, indexFields] = await Promise.all([ - libs.sources.getSourceConfiguration(requestContext.core.savedObjects.client, sourceId), - libs.sourceStatus.getLogIndexStatus(requestContext, sourceId), - libs.sourceStatus.hasMetricIndices(requestContext, sourceId), - libs.fields.getFields(requestContext, sourceId, typeToInfraIndexType(type)), - ]); + const [source, logIndexStatus, metricIndicesExist, indexFields] = await Promise.all([ + libs.sources.getSourceConfiguration(requestContext.core.savedObjects.client, sourceId), + libs.sourceStatus.getLogIndexStatus(requestContext, sourceId), + libs.sourceStatus.hasMetricIndices(requestContext, sourceId), + libs.fields.getFields(requestContext, sourceId, typeToInfraIndexType(type)), + ]); - if (!source) { - return response.notFound(); - } + if (!source) { + return response.notFound(); + } - const status: InfraSourceStatus = { - logIndicesExist: logIndexStatus !== 'missing', - metricIndicesExist, - indexFields, - }; + const status: InfraSourceStatus = { + logIndicesExist: logIndexStatus !== 'missing', + metricIndicesExist, + indexFields, + }; - return response.ok({ - body: SourceResponseRuntimeType.encode({ source: { ...source, status } }), - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + return response.ok({ + body: SourceResponseRuntimeType.encode({ source: { ...source, status } }), + }); } ); @@ -169,26 +163,20 @@ export const initSourceRoute = (libs: InfraBackendLibs) => { }, }, async (requestContext, request, response) => { - try { - const { type, sourceId } = request.params; - - const client = createSearchClient(requestContext, framework); - const source = await libs.sources.getSourceConfiguration( - requestContext.core.savedObjects.client, - sourceId - ); - const indexPattern = - type === 'metrics' ? source.configuration.metricAlias : source.configuration.logAlias; - const results = await hasData(indexPattern, client); - - return response.ok({ - body: { hasData: results }, - }); - } catch (error) { - return response.internalError({ - body: error.message, - }); - } + const { type, sourceId } = request.params; + + const client = createSearchClient(requestContext, framework); + const source = await libs.sources.getSourceConfiguration( + requestContext.core.savedObjects.client, + sourceId + ); + const indexPattern = + type === 'metrics' ? source.configuration.metricAlias : source.configuration.logAlias; + const results = await hasData(indexPattern, client); + + return response.ok({ + body: { hasData: results }, + }); } ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.helpers.tsx index 55188805d571cd..fabb6a46c49435 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.helpers.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.helpers.tsx @@ -154,6 +154,17 @@ const createActions = (testBed: TestBed) => { find(`${processorSelector}.moreMenu.duplicateButton`).simulate('click'); }); }, + openProcessorEditor: (processorSelector: string) => { + act(() => { + find(`${processorSelector}.manageItemButton`).simulate('click'); + }); + component.update(); + }, + submitProcessorForm: async () => { + await act(async () => { + find('editProcessorForm.submitButton').simulate('click'); + }); + }, }; }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.test.tsx index 1c698043a8bc78..e89e91c1cbaa91 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/pipeline_processors_editor.test.tsx @@ -22,6 +22,13 @@ const testProcessors: Pick = { replacement: '$17$2', }, }, + { + set: { + field: 'test', + value: 'test', + unknown_field_foo: 'unknown_value', + }, + }, ], }; @@ -79,11 +86,37 @@ describe('Pipeline Editor', () => { await actions.addProcessor('processors', 'test', { if: '1 == 1' }); const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; const { processors } = onUpdateResult.getData(); - expect(processors.length).toBe(3); - const [a, b, c] = processors; + expect(processors.length).toBe(4); + const [a, b, c, d] = processors; expect(a).toEqual(testProcessors.processors[0]); expect(b).toEqual(testProcessors.processors[1]); - expect(c).toEqual({ test: { if: '1 == 1' } }); + expect(c).toEqual(testProcessors.processors[2]); + expect(d).toEqual({ test: { if: '1 == 1' } }); + }); + + it('edits a processor without removing unknown processor.options', async () => { + const { actions, exists, form } = testBed; + // Open the edit processor form for the set processor + actions.openProcessorEditor('processors>2'); + expect(exists('editProcessorForm')).toBeTruthy(); + form.setInputValue('editProcessorForm.valueFieldInput', 'test44'); + await actions.submitProcessorForm(); + const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; + const { + processors: { 2: setProcessor }, + } = onUpdateResult.getData(); + // The original field should still be unchanged + expect(testProcessors.processors[2].set.value).toBe('test'); + expect(setProcessor.set).toEqual({ + description: undefined, + field: 'test', + ignore_empty_value: undefined, + ignore_failure: undefined, + override: undefined, + // This unknown_field is not supported in the form + unknown_field_foo: 'unknown_value', + value: 'test44', + }); }); it('removes a processor', () => { @@ -92,7 +125,7 @@ describe('Pipeline Editor', () => { actions.removeProcessor('processors>0'); const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; const { processors } = onUpdateResult.getData(); - expect(processors.length).toBe(1); + expect(processors.length).toBe(2); expect(processors[0]).toEqual({ gsub: { field: '_index', @@ -107,7 +140,11 @@ describe('Pipeline Editor', () => { actions.moveProcessor('processors>0', 'dropButtonBelow-processors>1'); const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; const { processors } = onUpdateResult.getData(); - expect(processors).toEqual(testProcessors.processors.slice(0).reverse()); + expect(processors).toEqual([ + testProcessors.processors[1], + testProcessors.processors[0], + testProcessors.processors[2], + ]); }); it('adds an on-failure processor to a processor', async () => { @@ -121,7 +158,7 @@ describe('Pipeline Editor', () => { expect(exists(`${processorSelector}.addProcessor`)); const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; const { processors } = onUpdateResult.getData(); - expect(processors.length).toBe(2); + expect(processors.length).toBe(3); expect(processors[0]).toEqual(testProcessors.processors[0]); // should be unchanged expect(processors[1].gsub).toEqual({ ...testProcessors.processors[1].gsub, @@ -135,7 +172,7 @@ describe('Pipeline Editor', () => { actions.moveProcessor('processors>0', 'dropButtonBelow-processors>1>onFailure>0'); const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; const { processors } = onUpdateResult.getData(); - expect(processors.length).toBe(1); + expect(processors.length).toBe(2); expect(processors[0].gsub.on_failure).toEqual([ { test: { if: '1 == 3' }, @@ -150,7 +187,7 @@ describe('Pipeline Editor', () => { actions.duplicateProcessor('processors>1'); const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; const { processors } = onUpdateResult.getData(); - expect(processors.length).toBe(3); + expect(processors.length).toBe(4); const duplicatedProcessor = { gsub: { ...testProcessors.processors[1].gsub, @@ -161,6 +198,7 @@ describe('Pipeline Editor', () => { testProcessors.processors[0], duplicatedProcessor, duplicatedProcessor, + testProcessors.processors[2], ]); }); @@ -182,14 +220,17 @@ describe('Pipeline Editor', () => { actions.moveProcessor('processors>0', 'dropButtonBelow-onFailure>0'); const [onUpdateResult1] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; const data1 = onUpdateResult1.getData(); - expect(data1.processors.length).toBe(1); + expect(data1.processors.length).toBe(2); expect(data1.on_failure.length).toBe(2); - expect(data1.processors).toEqual([testProcessors.processors[1]]); + expect(data1.processors).toEqual([ + testProcessors.processors[1], + testProcessors.processors[2], + ]); expect(data1.on_failure).toEqual([{ test: { if: '1 == 5' } }, testProcessors.processors[0]]); actions.moveProcessor('onFailure>1', 'dropButtonAbove-processors>0'); const [onUpdateResult2] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; const data2 = onUpdateResult2.getData(); - expect(data2.processors.length).toBe(2); + expect(data2.processors.length).toBe(3); expect(data2.on_failure.length).toBe(1); expect(data2.processors).toEqual(testProcessors.processors); expect(data2.on_failure).toEqual([{ test: { if: '1 == 5' } }]); @@ -208,7 +249,7 @@ describe('Pipeline Editor', () => { actions.moveProcessor('processors>0', 'onFailure.dropButtonEmptyTree'); const [onUpdateResult2] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; const data = onUpdateResult2.getData(); - expect(data.processors).toEqual([testProcessors.processors[1]]); + expect(data.processors).toEqual([testProcessors.processors[1], testProcessors.processors[2]]); expect(data.on_failure).toEqual([testProcessors.processors[0]]); }); }); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/load_from_json/modal_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/load_from_json/modal_provider.tsx index 10da326322ad4d..c0f9c758fc2785 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/load_from_json/modal_provider.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/load_from_json/modal_provider.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { FunctionComponent, useRef, useState, useCallback } from 'react'; -import { EuiConfirmModal, EuiOverlayMask, EuiSpacer, EuiText, EuiCallOut } from '@elastic/eui'; +import { EuiConfirmModal, EuiSpacer, EuiText, EuiCallOut } from '@elastic/eui'; import { JsonEditor, OnJsonEditorUpdateHandler } from '../../../../../shared_imports'; @@ -78,64 +78,62 @@ export const ModalProvider: FunctionComponent = ({ onDone, children }) => <> {children(() => setIsModalVisible(true))} {isModalVisible ? ( - - { + { + setIsModalVisible(false); + }} + onConfirm={async () => { + try { + const json = jsonContent.current.data.format(); + const { processors, on_failure: onFailure } = json; + // This function will throw if it cannot parse the pipeline object + deserialize({ processors, onFailure }); + onDone(json as any); setIsModalVisible(false); - }} - onConfirm={async () => { - try { - const json = jsonContent.current.data.format(); - const { processors, on_failure: onFailure } = json; - // This function will throw if it cannot parse the pipeline object - deserialize({ processors, onFailure }); - onDone(json as any); - setIsModalVisible(false); - } catch (e) { - setError(e); - } - }} - cancelButtonText={i18nTexts.buttons.cancel} - confirmButtonDisabled={!isValidJson} - confirmButtonText={i18nTexts.buttons.confirm} - maxWidth={600} - > -
    - - - + } catch (e) { + setError(e); + } + }} + cancelButtonText={i18nTexts.buttons.cancel} + confirmButtonDisabled={!isValidJson} + confirmButtonText={i18nTexts.buttons.confirm} + maxWidth={600} + > +
    + + + - + - {error && ( - <> - - {i18nTexts.error.body} - - - - )} + {error && ( + <> + + {i18nTexts.error.body} + + + + )} - -
    - - + +
    +
    ) : undefined} ); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processor_form.container.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processor_form.container.tsx index 5ce8cf09cc5942..cce473ec9a214f 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processor_form.container.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processor_form.container.tsx @@ -7,7 +7,13 @@ import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react'; -import { useForm, OnFormUpdateArg, FormData, useKibana } from '../../../../../shared_imports'; +import { + useForm, + OnFormUpdateArg, + FormData, + FormOptions, + useKibana, +} from '../../../../../shared_imports'; import { ProcessorInternal } from '../../types'; import { EditProcessorForm } from './edit_processor_form'; @@ -33,6 +39,14 @@ interface Props { processor?: ProcessorInternal; } +const formOptions: FormOptions = { + /** + * This is important for allowing configuration of empty text fields in certain processors that + * remove values from their inputs. + */ + stripEmptyFields: false, +}; + export const ProcessorFormContainer: FunctionComponent = ({ processor, onFormUpdate, @@ -81,6 +95,7 @@ export const ProcessorFormContainer: FunctionComponent = ({ const { form } = useForm({ defaultValue: { fields: getProcessor().options }, serializer: formSerializer, + options: formOptions, }); const { subscribe } = form; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/common_processor_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/common_processor_fields.tsx index 12226608a46821..f6449c3cc24d42 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/common_processor_fields.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/common_processor_fields.tsx @@ -38,6 +38,7 @@ const ignoreFailureConfig: FieldConfig = { }; const ifConfig: FieldConfig = { + serializer: from.emptyStringToUndefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.ifFieldLabel', { defaultMessage: 'Condition (optional)', }), @@ -48,6 +49,7 @@ const ifConfig: FieldConfig = { }; const tagConfig: FieldConfig = { + serializer: from.emptyStringToUndefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.tagFieldLabel', { defaultMessage: 'Tag (optional)', }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/target_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/target_field.tsx index 1caf5ffd3fb1e5..b603a131e10b09 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/target_field.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/common_fields/target_field.tsx @@ -10,12 +10,13 @@ import { i18n } from '@kbn/i18n'; import { Field, FIELD_TYPES, UseField, FieldConfig } from '../../../../../../../shared_imports'; -import { FieldsConfig } from '../shared'; +import { FieldsConfig, from } from '../shared'; const fieldsConfig: FieldsConfig = { target_field: { type: FIELD_TYPES.TEXT, deserializer: String, + serializer: from.emptyStringToUndefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.targetFieldLabel', { defaultMessage: 'Target field (optional)', }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/csv.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/csv.tsx index d6cf2d0ae05e89..b192ee0494bb3c 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/csv.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/csv.tsx @@ -24,7 +24,7 @@ import { FieldsConfig } from './shared'; import { IgnoreMissingField } from './common_fields/ignore_missing_field'; import { FieldNameField } from './common_fields/field_name_field'; -import { to } from './shared'; +import { to, from } from './shared'; const { minLengthField } = fieldValidators; @@ -72,7 +72,7 @@ const fieldsConfig: FieldsConfig = { /* Optional fields config */ separator: { type: FIELD_TYPES.TEXT, - serializer: (v) => (v ? v : undefined), + serializer: from.emptyStringToUndefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.convertForm.separatorFieldLabel', { defaultMessage: 'Separator (optional)', }), @@ -85,13 +85,13 @@ const fieldsConfig: FieldsConfig = { {','} }} + values={{ value: {','} }} /> ), }, quote: { type: FIELD_TYPES.TEXT, - serializer: (v) => (v ? v : undefined), + serializer: from.emptyStringToUndefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.convertForm.quoteFieldLabel', { defaultMessage: 'Quote (optional)', }), @@ -104,7 +104,7 @@ const fieldsConfig: FieldsConfig = { {'"'} }} + values={{ value: {'"'} }} /> ), }, @@ -121,6 +121,7 @@ const fieldsConfig: FieldsConfig = { }, empty_value: { type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.convertForm.emptyValueFieldLabel', { defaultMessage: 'Empty value (optional)', }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date.tsx index 17af47f1569e04..ca541a9e6d6195 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date.tsx @@ -18,7 +18,7 @@ import { ComboBoxField, } from '../../../../../../shared_imports'; -import { FieldsConfig, to } from './shared'; +import { FieldsConfig, to, from } from './shared'; import { FieldNameField } from './common_fields/field_name_field'; import { TargetField } from './common_fields/target_field'; @@ -53,7 +53,7 @@ const fieldsConfig: FieldsConfig = { /* Optional fields config */ timezone: { type: FIELD_TYPES.TEXT, - serializer: (v) => (v ? v : undefined), + serializer: from.emptyStringToUndefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.dateForm.timezoneFieldLabel', { defaultMessage: 'Timezone (optional)', }), @@ -61,13 +61,13 @@ const fieldsConfig: FieldsConfig = { {'UTC'} }} + values={{ timezone: {'UTC'} }} /> ), }, locale: { type: FIELD_TYPES.TEXT, - serializer: (v) => (v ? v : undefined), + serializer: from.emptyStringToUndefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.dateForm.localeFieldLabel', { defaultMessage: 'Locale (optional)', }), @@ -75,7 +75,7 @@ const fieldsConfig: FieldsConfig = { {'ENGLISH'} }} + values={{ timezone: {'ENGLISH'} }} /> ), }, @@ -102,7 +102,7 @@ export const DateProcessor: FunctionComponent = () => { id="xpack.ingestPipelines.pipelineEditor.dateForm.targetFieldHelpText" defaultMessage="Output field. If empty, the input field is updated in place. Defaults to {defaultField}." values={{ - defaultField: {'@timestamp'}, + defaultField: {'@timestamp'}, }} /> } diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date_index_name.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date_index_name.tsx index d4b1ec876bfd50..5c5b5ff89fd20c 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date_index_name.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/date_index_name.tsx @@ -19,7 +19,7 @@ import { SelectField, } from '../../../../../../shared_imports'; -import { FieldsConfig, to } from './shared'; +import { FieldsConfig, to, from } from './shared'; import { FieldNameField } from './common_fields/field_name_field'; const { emptyField } = fieldValidators; @@ -57,7 +57,7 @@ const fieldsConfig: FieldsConfig = { /* Optional fields config */ index_name_prefix: { type: FIELD_TYPES.TEXT, - serializer: (v) => (v ? v : undefined), + serializer: from.emptyStringToUndefined, label: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.indexNamePrefixFieldLabel', { @@ -71,7 +71,7 @@ const fieldsConfig: FieldsConfig = { }, index_name_format: { type: FIELD_TYPES.TEXT, - serializer: (v) => (v ? v : undefined), + serializer: from.emptyStringToUndefined, label: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.indexNameFormatFieldLabel', { @@ -82,7 +82,7 @@ const fieldsConfig: FieldsConfig = { {'yyyy-MM-dd'} }} + values={{ value: {'yyyy-MM-dd'} }} /> ), }, @@ -102,13 +102,13 @@ const fieldsConfig: FieldsConfig = { {"yyyy-MM-dd'T'HH:mm:ss.SSSXX"} }} + values={{ value: {"yyyy-MM-dd'T'HH:mm:ss.SSSXX"} }} /> ), }, timezone: { type: FIELD_TYPES.TEXT, - serializer: (v) => (v ? v : undefined), + serializer: from.emptyStringToUndefined, label: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.timezoneFieldLabel', { @@ -119,13 +119,13 @@ const fieldsConfig: FieldsConfig = { {'UTC'} }} + values={{ timezone: {'UTC'} }} /> ), }, locale: { type: FIELD_TYPES.TEXT, - serializer: (v) => (v ? v : undefined), + serializer: from.emptyStringToUndefined, label: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.dateIndexNameForm.localeFieldLabel', { @@ -136,7 +136,7 @@ const fieldsConfig: FieldsConfig = { {'ENGLISH'} }} + values={{ locale: {'ENGLISH'} }} /> ), }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx index 609ce8a1f8ae63..6652ad277cc265 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dissect.tsx @@ -22,7 +22,7 @@ import { import { FieldNameField } from './common_fields/field_name_field'; import { IgnoreMissingField } from './common_fields/ignore_missing_field'; -import { EDITOR_PX_HEIGHT } from './shared'; +import { EDITOR_PX_HEIGHT, from } from './shared'; const { emptyField } = fieldValidators; @@ -72,6 +72,7 @@ const getFieldsConfig = (esDocUrl: string): Record => { /* Optional field config */ append_separator: { type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, label: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.dissectForm.appendSeparatorparaotrFieldLabel', { @@ -82,7 +83,7 @@ const getFieldsConfig = (esDocUrl: string): Record => { {'""'} }} + values={{ value: {'""'} }} /> ), }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dot_expander.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dot_expander.tsx index 0bbcb7a2eefb34..4bbc242cf0ef88 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dot_expander.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dot_expander.tsx @@ -12,9 +12,12 @@ import { FieldConfig, FIELD_TYPES, UseField, Field } from '../../../../../../sha import { FieldNameField } from './common_fields/field_name_field'; +import { from } from './shared'; + const fieldsConfig: Record = { path: { type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.dotExpanderForm.pathFieldLabel', { defaultMessage: 'Path', }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/geoip.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/geoip.tsx index 2f0699fac729d8..6a1f86977d8db5 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/geoip.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/geoip.tsx @@ -22,7 +22,7 @@ const fieldsConfig: FieldsConfig = { /* Optional field config */ database_file: { type: FIELD_TYPES.TEXT, - serializer: (v) => (v === 'GeoLite2-City.mmdb' ? undefined : v), + serializer: (v) => (v === 'GeoLite2-City.mmdb' || v === '' ? undefined : v), label: i18n.translate('xpack.ingestPipelines.pipelineEditor.geoIPForm.databaseFileLabel', { defaultMessage: 'Database file (optional)', }), @@ -31,8 +31,8 @@ const fieldsConfig: FieldsConfig = { id="xpack.ingestPipelines.pipelineEditor.geoIPForm.databaseFileHelpText" defaultMessage="GeoIP2 database file in the {ingestGeoIP} configuration directory. Defaults to {databaseFile}." values={{ - databaseFile: {'GeoLite2-City.mmdb'}, - ingestGeoIP: {'ingest-geoip'}, + databaseFile: {'GeoLite2-City.mmdb'}, + ingestGeoIP: {'ingest-geoip'}, }} /> ), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/gsub.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/gsub.tsx index 8835f3775a90f0..edfa59ea80281c 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/gsub.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/gsub.tsx @@ -41,6 +41,7 @@ const fieldsConfig: FieldsConfig = { ], }, + // This is a required field, but we exclude validation because we accept empty values as '' replacement: { type: FIELD_TYPES.TEXT, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.replacementFieldLabel', { @@ -48,17 +49,11 @@ const fieldsConfig: FieldsConfig = { }), helpText: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.gsubForm.replacementFieldHelpText', - { defaultMessage: 'Replacement text for matches.' } - ), - validations: [ { - validator: emptyField( - i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.replacementRequiredError', { - defaultMessage: 'A value is required.', - }) - ), - }, - ], + defaultMessage: + 'Replacement text for matches. A blank value will remove the matched text from the resulting text.', + } + ), }, }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/inference.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/inference.tsx index 2b14a79afb8df1..9575e6d690e006 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/inference.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/inference.tsx @@ -168,7 +168,7 @@ export const Inference: FunctionComponent = () => { {'ml.inference.'} }} + values={{ targetField: {'ml.inference.'} }} /> } /> diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/kv.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/kv.tsx index 2d17e3600cb79a..694ae4e07070d4 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/kv.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/kv.tsx @@ -107,6 +107,7 @@ const fieldsConfig: FieldsConfig = { prefix: { type: FIELD_TYPES.TEXT, deserializer: String, + serializer: from.emptyStringToUndefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.prefixFieldLabel', { defaultMessage: 'Prefix', }), @@ -118,6 +119,7 @@ const fieldsConfig: FieldsConfig = { trim_key: { type: FIELD_TYPES.TEXT, deserializer: String, + serializer: from.emptyStringToUndefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.trimKeyFieldLabel', { defaultMessage: 'Trim key', }), @@ -129,6 +131,7 @@ const fieldsConfig: FieldsConfig = { trim_value: { type: FIELD_TYPES.TEXT, deserializer: String, + serializer: from.emptyStringToUndefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.trimValueFieldLabel', { defaultMessage: 'Trim value', }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/script.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/script.tsx index 60871fa7ba4ab5..3c662793578439 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/script.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/script.tsx @@ -81,7 +81,7 @@ const fieldsConfig: FieldsConfig = { lang: { type: FIELD_TYPES.TEXT, deserializer: String, - serializer: from.undefinedIfValue('painless'), + serializer: from.emptyStringToUndefined, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.scriptForm.langFieldLabel', { defaultMessage: 'Language (optional)', }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/set.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/set.tsx index 9ccfe580a53aed..89ca373b9e6539 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/set.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/set.tsx @@ -10,40 +10,30 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCode } from '@elastic/eui'; -import { - FIELD_TYPES, - fieldValidators, - ToggleField, - UseField, - Field, -} from '../../../../../../shared_imports'; +import { FIELD_TYPES, ToggleField, UseField, Field } from '../../../../../../shared_imports'; import { FieldsConfig, to, from } from './shared'; import { FieldNameField } from './common_fields/field_name_field'; -const { emptyField } = fieldValidators; - const fieldsConfig: FieldsConfig = { /* Required fields config */ + // This is a required field, but we exclude validation because we accept empty values as '' value: { type: FIELD_TYPES.TEXT, deserializer: String, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueFieldLabel', { defaultMessage: 'Value', }), - helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueFieldHelpText', { - defaultMessage: 'Value for the field.', - }), - validations: [ - { - validator: emptyField( - i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError', { - defaultMessage: 'A value is required.', - }) - ), - }, - ], + helpText: ( + {'""'}, + }} + /> + ), }, /* Optional fields config */ override: { @@ -101,7 +91,16 @@ export const SetProcessor: FunctionComponent = () => { })} /> - + diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/shared.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/shared.ts index daa0e548ab7280..399da3c05c7831 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/shared.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/shared.ts @@ -64,9 +64,11 @@ export const from = { // Ignore } } + return undefined; }, optionalArrayOfStrings: (v: string[]) => (v.length ? v : undefined), - undefinedIfValue: (value: any) => (v: boolean) => (v === value ? undefined : v), + undefinedIfValue: (value: unknown) => (v: boolean) => (v === value ? undefined : v), + emptyStringToUndefined: (v: unknown) => (v === '' ? undefined : v), }; export const EDITOR_PX_HEIGHT = { @@ -78,4 +80,6 @@ export const EDITOR_PX_HEIGHT = { export type FieldsConfig = Record>; -export type FormFieldsComponent = FunctionComponent<{ initialFieldValues?: Record }>; +export type FormFieldsComponent = FunctionComponent<{ + initialFieldValues?: Record; +}>; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/sort.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/sort.tsx index 82589e8777589f..3239f546820417 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/sort.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/sort.tsx @@ -12,7 +12,7 @@ import { FIELD_TYPES, UseField, SelectField } from '../../../../../../shared_imp import { FieldNameField } from './common_fields/field_name_field'; import { TargetField } from './common_fields/target_field'; -import { FieldsConfig, from } from './shared'; +import { FieldsConfig } from './shared'; const fieldsConfig: FieldsConfig = { /* Optional fields config */ @@ -20,7 +20,7 @@ const fieldsConfig: FieldsConfig = { type: FIELD_TYPES.SELECT, defaultValue: 'asc', deserializer: (v) => (v === 'asc' || v === 'desc' ? v : 'asc'), - serializer: from.undefinedIfValue('asc'), + serializer: (v) => (v === 'asc' || v === '' ? undefined : v), label: i18n.translate('xpack.ingestPipelines.pipelineEditor.sortForm.orderFieldLabel', { defaultMessage: 'Order', }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/user_agent.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/user_agent.tsx index 4309df214410b2..893e52bcc0073e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/user_agent.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/user_agent.tsx @@ -13,7 +13,7 @@ import { i18n } from '@kbn/i18n'; import { EuiComboBoxOptionOption } from '@elastic/eui'; import { FIELD_TYPES, UseField, Field } from '../../../../../../shared_imports'; -import { FieldsConfig } from './shared'; +import { FieldsConfig, from } from './shared'; import { IgnoreMissingField } from './common_fields/ignore_missing_field'; import { FieldNameField } from './common_fields/field_name_field'; import { TargetField } from './common_fields/target_field'; @@ -31,6 +31,7 @@ const fieldsConfig: FieldsConfig = { /* Optional fields config */ regex_file: { type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, deserializer: String, label: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.userAgentForm.regexFileFieldLabel', @@ -66,7 +67,7 @@ export const UserAgent: FunctionComponent = () => { id="xpack.ingestPipelines.pipelineEditor.userAgentForm.targetFieldHelpText" defaultMessage="Output field. Defaults to {defaultField}." values={{ - defaultField: {'user_agent'}, + defaultField: {'user_agent'}, }} /> } diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_remove_modal.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_remove_modal.tsx index eb5a1baac78fb9..26ae69ead3b5b6 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_remove_modal.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_remove_modal.tsx @@ -7,7 +7,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { ProcessorInternal, ProcessorSelector } from '../types'; interface Props { @@ -18,39 +18,37 @@ interface Props { export const ProcessorRemoveModal = ({ processor, onResult, selector }: Props) => { return ( - - - } - onCancel={() => onResult({ confirmed: false, selector })} - onConfirm={() => onResult({ confirmed: true, selector })} - cancelButtonText={ - - } - confirmButtonText={ - - } - > -

    - -

    -
    -
    + + } + onCancel={() => onResult({ confirmed: false, selector })} + onConfirm={() => onResult({ confirmed: true, selector })} + cancelButtonText={ + + } + confirmButtonText={ + + } + > +

    + +

    +
    ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx index aac4da7c16bbf0..9095ab1927cb98 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx @@ -135,7 +135,7 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { {'my-index-yyyy-MM-dd'} }} + values={{ value: {'my-index-yyyy-MM-dd'} }} /> ), }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/reset_documents_modal.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/reset_documents_modal.tsx index 305ccce4e31b5a..d71a6fb80bde1e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/reset_documents_modal.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/test_pipeline/test_pipeline_tabs/tab_documents/reset_documents_modal.tsx @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import React, { FunctionComponent } from 'react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; interface Props { confirmResetTestOutput: () => void; @@ -46,18 +46,16 @@ export const ResetDocumentsModal: FunctionComponent = ({ closeModal, }) => { return ( - - -

    {i18nTexts.modalDescription}

    -
    -
    + +

    {i18nTexts.modalDescription}

    +
    ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/context/processors_context.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/context/processors_context.tsx index 0302ff017f09f9..0c43297e811d3c 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/context/processors_context.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/context/processors_context.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { omit } from 'lodash'; import React, { createContext, FunctionComponent, @@ -150,12 +150,21 @@ export const PipelineProcessorsContextProvider: FunctionComponent = ({ }); break; case 'managingProcessor': + // These are the option names we get back from our UI + const knownOptionNames = Object.keys(processorTypeAndOptions.options); + // The processor that we are updating may have options configured the UI does not know about + const unknownOptions = omit(mode.arg.processor.options, knownOptionNames); + // In order to keep the options we don't get back from our UI, we merge the known and unknown options + const updatedProcessorOptions = { + ...processorTypeAndOptions.options, + ...unknownOptions, + }; processorsDispatch({ type: 'updateProcessor', payload: { processor: { ...mode.arg.processor, - ...processorTypeAndOptions, + options: updatedProcessorOptions, }, selector: mode.arg.selector, }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/delete_modal.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/delete_modal.tsx index 230cc52a1c1696..63cf7af2737aa3 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/delete_modal.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/delete_modal.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -78,49 +78,47 @@ export const PipelineDeleteModal = ({ }; return ( - - + } + onCancel={handleOnCancel} + onConfirm={handleDeletePipelines} + cancelButtonText={ + + } + confirmButtonText={ + + } + > + <> +

    - } - onCancel={handleOnCancel} - onConfirm={handleDeletePipelines} - cancelButtonText={ - - } - confirmButtonText={ - - } - > - <> -

    - -

    +

    -
      - {pipelinesToDelete.map((name) => ( -
    • {name}
    • - ))} -
    - -
    -
    +
      + {pipelinesToDelete.map((name) => ( +
    • {name}
    • + ))} +
    + +
    ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts index d951e2f2a0768a..4afd434b893726 100644 --- a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts +++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts @@ -51,6 +51,7 @@ export { ValidationFunc, ValidationConfig, useFormData, + FormOptions, } from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; export { diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts index 363254d63a2c7e..afa36e5abe31a1 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts @@ -83,7 +83,7 @@ export const registerCreateRoute = ({ }); } - return res.internalError({ body: error }); + throw error; } }) ); diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/documents.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/documents.ts index b23a42b895af92..635ee015be5162 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/documents.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/documents.ts @@ -51,7 +51,7 @@ export const registerDocumentsRoute = ({ }); } - return res.internalError({ body: error }); + throw error; } }) ); diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts index 6237f4b6911bdb..3995448d13fbb9 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/get.ts @@ -44,7 +44,7 @@ export const registerGetRoutes = ({ }); } - return res.internalError({ body: error }); + throw error; } }) ); @@ -78,7 +78,7 @@ export const registerGetRoutes = ({ }); } - return res.internalError({ body: error }); + throw error; } }) ); diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts index 52492c3ee6d27a..527b4d4277bf5f 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/privileges.ts @@ -44,28 +44,24 @@ export const registerPrivilegesRoute = ({ license, router, config }: RouteDepend }, } = ctx; - try { - const { has_all_requested: hasAllPrivileges, cluster } = await client.callAsCurrentUser( - 'transport.request', - { - path: '/_security/user/_has_privileges', - method: 'POST', - body: { - cluster: APP_CLUSTER_REQUIRED_PRIVILEGES, - }, - } - ); - - if (!hasAllPrivileges) { - privilegesResult.missingPrivileges.cluster = extractMissingPrivileges(cluster); + const { has_all_requested: hasAllPrivileges, cluster } = await client.callAsCurrentUser( + 'transport.request', + { + path: '/_security/user/_has_privileges', + method: 'POST', + body: { + cluster: APP_CLUSTER_REQUIRED_PRIVILEGES, + }, } + ); - privilegesResult.hasAllPrivileges = hasAllPrivileges; - - return res.ok({ body: privilegesResult }); - } catch (e) { - return res.internalError({ body: e }); + if (!hasAllPrivileges) { + privilegesResult.missingPrivileges.cluster = extractMissingPrivileges(cluster); } + + privilegesResult.hasAllPrivileges = hasAllPrivileges; + + return res.ok({ body: privilegesResult }); }) ); }; diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts index efa2a84daca28c..f02aa0a8d5ed6d 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/simulate.ts @@ -52,7 +52,7 @@ export const registerSimulateRoute = ({ }); } - return res.internalError({ body: error }); + throw error; } }) ); diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts index 30cfe1b2505c2c..8776aace5ad789 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/update.ts @@ -65,7 +65,7 @@ export const registerUpdateRoute = ({ }); } - return res.internalError({ body: error }); + throw error; } }) ); diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap b/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap index 158c7fa4aeec37..992301af13ad04 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap +++ b/x-pack/plugins/lens/public/datatable_visualization/components/__snapshots__/table_basic.test.tsx.snap @@ -8,6 +8,11 @@ exports[`DatatableComponent it renders actions column when there are row actions { + const table: Datatable = { + type: 'datatable', + columns: [ + { + id: 'a', + name: 'a', + meta: { + type: 'number', + }, + }, + ], + rows: [{ a: 123 }], + }; + const CellRenderer = createGridCell( + { + a: { convert: (x) => `formatted ${x}` } as FieldFormat, + }, + DataContext + ); + + it('renders formatted value', () => { + const instance = mountWithIntl( + + {}} + isExpandable={false} + isDetails={false} + isExpanded={false} + /> + + ); + expect(instance.text()).toEqual('formatted 123'); + }); + + it('set class with text alignment', () => { + const cell = mountWithIntl( + + {}} + isExpandable={false} + isDetails={false} + isExpanded={false} + /> + + ); + expect(cell.find('.lnsTableCell').prop('className')).toContain('--right'); + }); +}); diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.tsx index a6e1e3386bcf35..2261dd06b532ba 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/cell_value.tsx @@ -14,19 +14,21 @@ export const createGridCell = ( formatters: Record>, DataContext: React.Context ) => ({ rowIndex, columnId }: EuiDataGridCellValueElementProps) => { - const { table } = useContext(DataContext); + const { table, alignments } = useContext(DataContext); const rowValue = table?.rows[rowIndex][columnId]; const content = formatters[columnId]?.convert(rowValue, 'html'); + const currentAlignment = alignments && alignments[columnId]; + const alignmentClassName = `lnsTableCell--${currentAlignment}`; return ( - ); }; diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx new file mode 100644 index 00000000000000..e0d31a3ed02012 --- /dev/null +++ b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.test.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButtonGroup } from '@elastic/eui'; +import { FramePublicAPI, VisualizationDimensionEditorProps } from '../../types'; +import { DatatableVisualizationState } from '../visualization'; +import { createMockDatasource, createMockFramePublicAPI } from '../../editor_frame_service/mocks'; +import { mountWithIntl } from '@kbn/test/jest'; +import { TableDimensionEditor } from './dimension_editor'; + +describe('data table dimension editor', () => { + let frame: FramePublicAPI; + let state: DatatableVisualizationState; + let setState: (newState: DatatableVisualizationState) => void; + let props: VisualizationDimensionEditorProps; + + function testState(): DatatableVisualizationState { + return { + layerId: 'first', + columns: [ + { + columnId: 'foo', + }, + ], + }; + } + + beforeEach(() => { + state = testState(); + frame = createMockFramePublicAPI(); + frame.datasourceLayers = { + first: createMockDatasource('test').publicAPIMock, + }; + frame.activeData = { + first: { + type: 'datatable', + columns: [ + { + id: 'foo', + name: 'foo', + meta: { + type: 'string', + }, + }, + ], + rows: [], + }, + }; + setState = jest.fn(); + props = { + accessor: 'foo', + frame, + groupId: 'columns', + layerId: 'first', + state, + setState, + }; + }); + + it('should render default alignment', () => { + const instance = mountWithIntl(); + expect(instance.find(EuiButtonGroup).prop('idSelected')).toEqual( + expect.stringContaining('left') + ); + }); + + it('should render default alignment for number', () => { + frame.activeData!.first.columns[0].meta.type = 'number'; + const instance = mountWithIntl(); + expect(instance.find(EuiButtonGroup).prop('idSelected')).toEqual( + expect.stringContaining('right') + ); + }); + + it('should render specific alignment', () => { + state.columns[0].alignment = 'center'; + const instance = mountWithIntl(); + expect(instance.find(EuiButtonGroup).prop('idSelected')).toEqual( + expect.stringContaining('center') + ); + }); + + it('should set state for the right column', () => { + state.columns = [ + { + columnId: 'foo', + }, + { + columnId: 'bar', + }, + ]; + const instance = mountWithIntl(); + instance.find(EuiButtonGroup).prop('onChange')('center'); + expect(setState).toHaveBeenCalledWith({ + ...state, + columns: [ + { + columnId: 'foo', + alignment: 'center', + }, + { + columnId: 'bar', + }, + ], + }); + }); +}); diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx index 008b805bc8fed3..9c60cd47af3e34 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/dimension_editor.tsx @@ -7,55 +7,121 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiSwitch, EuiFormRow } from '@elastic/eui'; +import { EuiFormRow, EuiSwitch, EuiButtonGroup, htmlIdGenerator } from '@elastic/eui'; import { VisualizationDimensionEditorProps } from '../../types'; import { DatatableVisualizationState } from '../visualization'; +const idPrefix = htmlIdGenerator()(); + export function TableDimensionEditor( props: VisualizationDimensionEditorProps ) { - const { state, setState, accessor } = props; - const column = state.columns.find((c) => c.columnId === accessor); + const { state, setState, frame, accessor } = props; + const column = state.columns.find(({ columnId }) => accessor === columnId); - const visibleColumnsCount = state.columns.filter((c) => !c.hidden).length; + if (!column) return null; - if (!column) { - return null; - } + // either read config state or use same logic as chart itself + const currentAlignment = + column?.alignment || + (frame.activeData && + frame.activeData[state.layerId].columns.find((col) => col.id === accessor)?.meta.type === + 'number' + ? 'right' + : 'left'); + + const visibleColumnsCount = state.columns.filter((c) => !c.hidden).length; return ( - + + ); } diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.scss b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.scss index 5e5db2c6458095..b99ffb6dce8101 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.scss +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.scss @@ -1,3 +1,19 @@ .lnsDataTableContainer { height: 100%; } + +.lnsTableCell { + @include euiTextTruncate; +} + +.lnsTableCell--left { + text-align: left; +} + +.lnsTableCell--right { + text-align: right; +} + +.lnsTableCell--center { + text-align: center; +} \ No newline at end of file diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx index 588340fbe97fa9..22577e8ef5fd31 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.test.tsx @@ -12,7 +12,7 @@ import { EuiDataGrid } from '@elastic/eui'; import { IAggType, IFieldFormat } from 'src/plugins/data/public'; import { EmptyPlaceholder } from '../../shared_components'; import { LensIconChartDatatable } from '../../assets/chart_datatable'; -import { DatatableComponent } from './table_basic'; +import { DataContext, DatatableComponent } from './table_basic'; import { LensMultiTable } from '../../types'; import { DatatableProps } from '../expression'; @@ -427,6 +427,39 @@ describe('DatatableComponent', () => { expect(wrapper.find(EuiDataGrid).prop('columns')!.length).toEqual(2); }); + test('it adds alignment data to context', () => { + const { data, args } = sampleArgs(); + + const wrapper = shallow( + ({ convert: (x) => x } as IFieldFormat)} + dispatchEvent={onDispatchEvent} + getType={jest.fn()} + renderMode="display" + /> + ); + + expect(wrapper.find(DataContext.Provider).prop('value').alignments).toEqual({ + // set via args + a: 'center', + // default for date + b: 'left', + // default for number + c: 'right', + }); + }); + test('it should refresh the table header when the datatable data changes', () => { const { data, args } = sampleArgs(); diff --git a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx index f685990f12dd26..e1687ba28f07bb 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/components/table_basic.tsx @@ -40,7 +40,7 @@ import { createGridSortingConfig, } from './table_actions'; -const DataContext = React.createContext({}); +export const DataContext = React.createContext({}); const gridStyle: EuiDataGridStyle = { border: 'horizontal', @@ -192,6 +192,21 @@ export const DatatableComponent = (props: DatatableRenderProps) => { ] ); + const alignments: Record = useMemo(() => { + const alignmentMap: Record = {}; + columnConfig.columns.forEach((column) => { + if (column.alignment) { + alignmentMap[column.columnId] = column.alignment; + } else { + const isNumeric = + firstLocalTable.columns.find((dataColumn) => dataColumn.id === column.columnId)?.meta + .type === 'number'; + alignmentMap[column.columnId] = isNumeric ? 'right' : 'left'; + } + }); + return alignmentMap; + }, [firstLocalTable, columnConfig]); + const trailingControlColumns: EuiDataGridControlColumn[] = useMemo(() => { if (!hasAtLeastOneRowClickAction || !onRowContextMenuClick) { return []; @@ -259,6 +274,7 @@ export const DatatableComponent = (props: DatatableRenderProps) => { value={{ table: firstLocalTable, rowHasRowClickTriggerActions: props.rowHasRowClickTriggerActions, + alignments, }} > ; } diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx index 7ead7be67947c9..f6a38541cda271 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx @@ -138,6 +138,7 @@ export const datatableColumn: ExpressionFunctionDefinition< inputTypes: ['null'], args: { columnId: { types: ['string'], help: '' }, + alignment: { types: ['string'], help: '' }, hidden: { types: ['boolean'], help: '' }, width: { types: ['number'], help: '' }, }, diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx index ad5e1e552ccd2b..92136c557ad38d 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.test.tsx @@ -419,11 +419,13 @@ describe('Datatable Visualization', () => { columnId: ['c'], hidden: [], width: [], + alignment: [], }); expect(columnArgs[1].arguments).toEqual({ columnId: ['b'], hidden: [], width: [], + alignment: [], }); }); diff --git a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx index 47f8ce09aea68c..fc69c914deb680 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/visualization.tsx @@ -23,6 +23,7 @@ export interface ColumnState { columnId: string; width?: number; hidden?: boolean; + alignment?: 'left' | 'right' | 'center'; } export interface SortingState { @@ -264,6 +265,7 @@ export const datatableVisualization: Visualization columnId: [column.columnId], hidden: typeof column.hidden === 'undefined' ? [] : [column.hidden], width: typeof column.width === 'undefined' ? [] : [column.width], + alignment: typeof column.alignment === 'undefined' ? [] : [column.alignment], }, }, ], diff --git a/x-pack/plugins/lens/public/drag_drop/announcements.tsx b/x-pack/plugins/lens/public/drag_drop/announcements.tsx index 61785310bdcf37..3c65008f8f38b7 100644 --- a/x-pack/plugins/lens/public/drag_drop/announcements.tsx +++ b/x-pack/plugins/lens/public/drag_drop/announcements.tsx @@ -11,6 +11,7 @@ export interface HumanData { label: string; groupLabel?: string; position?: number; + nextLabel?: string; } type AnnouncementFunction = (draggedElement: HumanData, dropElement: HumanData) => string; @@ -25,7 +26,7 @@ const selectedTargetReplace = ( { label: dropLabel, groupLabel, position }: HumanData ) => i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.replace', { - defaultMessage: `Selected {dropLabel} in {groupLabel} group at position {position}. Press space or enter to replace {dropLabel} with {label}.`, + defaultMessage: `Replace {dropLabel} in {groupLabel} group at position {position} with {label}. Press space or enter to replace`, values: { label, dropLabel, @@ -34,65 +35,103 @@ const selectedTargetReplace = ( }, }); -const droppedReplace = ({ label }: HumanData, { label: dropLabel, groupLabel }: HumanData) => +const droppedReplace = ( + { label }: HumanData, + { label: dropLabel, groupLabel, position }: HumanData +) => i18n.translate('xpack.lens.dragDrop.announce.duplicated.replace', { - defaultMessage: - 'You have dropped the item. You have replaced {dropLabel} with {label} in {groupLabel} group.', + defaultMessage: 'Replaced {dropLabel} with {label} in {groupLabel} at position {position}', values: { label, dropLabel, groupLabel, + position, }, }); export const announcements: CustomAnnouncementsType = { selectedTarget: { - reorder: ({ label, position: prevPosition }, { position }) => + reorder: ({ label, groupLabel, position: prevPosition }, { position }) => prevPosition === position ? i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.reorderedBack', { - defaultMessage: `You have moved the item {label} back to position {prevPosition}`, + defaultMessage: `{label} returned to its initial position {prevPosition}`, values: { label, prevPosition, }, }) : i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.reordered', { - defaultMessage: `You have moved the item {label} from position {prevPosition} to position {position}`, + defaultMessage: `Reorder {label} in {groupLabel} group from position {prevPosition} to position {position}. Press space or enter to reorder`, values: { + groupLabel, label, position, prevPosition, }, }), - duplicate_in_group: ({ label }, { label: dropLabel, groupLabel, position }) => + duplicate_in_group: ({ label }, { groupLabel, position }) => i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.duplicated', { - defaultMessage: `Selected {dropLabel} in {groupLabel} group at position {position}. Press space or enter to duplicate {label}.`, + defaultMessage: `Duplicate {label} to {groupLabel} group at position {position}. Press space or enter to duplicate`, values: { label, - dropLabel, groupLabel, position, }, }), field_replace: selectedTargetReplace, replace_compatible: selectedTargetReplace, - replace_incompatible: selectedTargetReplace, + replace_incompatible: ( + { label }: HumanData, + { label: dropLabel, groupLabel, position, nextLabel }: HumanData + ) => + i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.replaceIncompatible', { + defaultMessage: `Convert {label} to {nextLabel} and replace {dropLabel} in {groupLabel} group at position {position}. Press space or enter to replace`, + values: { + label, + nextLabel, + dropLabel, + groupLabel, + position, + }, + }), + move_incompatible: ( + { label }: HumanData, + { label: groupLabel, position, nextLabel }: HumanData + ) => + i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.moveIncompatible', { + defaultMessage: `Convert {label} to {nextLabel} and move to {groupLabel} group at position {position}. Press space or enter to move`, + values: { + label, + nextLabel, + groupLabel, + position, + }, + }), + move_compatible: ({ label }: HumanData, { groupLabel, position }: HumanData) => + i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.moveCompatible', { + defaultMessage: `Move {label} to {groupLabel} group at position {position}. Press space or enter to move`, + values: { + label, + groupLabel, + position, + }, + }), }, dropped: { - reorder: ({ label, position: prevPosition }, { position }) => + reorder: ({ label, groupLabel, position: prevPosition }, { position }) => i18n.translate('xpack.lens.dragDrop.announce.dropped.reordered', { defaultMessage: - 'You have dropped the item {label}. You have moved the item from position {prevPosition} to positon {position}', + 'Reordered {label} in {groupLabel} group from position {prevPosition} to positon {position}', values: { label, + groupLabel, position, prevPosition, }, }), duplicate_in_group: ({ label }, { groupLabel, position }) => i18n.translate('xpack.lens.dragDrop.announce.dropped.duplicated', { - defaultMessage: - 'You have dropped the item. You have duplicated {label} in {groupLabel} group at position {position}', + defaultMessage: 'Duplicated {label} in {groupLabel} group at position {position}', values: { label, groupLabel, @@ -101,7 +140,42 @@ export const announcements: CustomAnnouncementsType = { }), field_replace: droppedReplace, replace_compatible: droppedReplace, - replace_incompatible: droppedReplace, + replace_incompatible: ( + { label }: HumanData, + { label: dropLabel, groupLabel, position, nextLabel }: HumanData + ) => + i18n.translate('xpack.lens.dragDrop.announce.dropped.replaceIncompatible', { + defaultMessage: + 'Converted {label} to {nextLabel} and replaced {dropLabel} in {groupLabel} group at position {position}', + values: { + label, + nextLabel, + dropLabel, + groupLabel, + position, + }, + }), + move_incompatible: ({ label }: HumanData, { groupLabel, position, nextLabel }: HumanData) => + i18n.translate('xpack.lens.dragDrop.announce.dropped.moveIncompatible', { + defaultMessage: + 'Converted {label} to {nextLabel} and moved to {groupLabel} group at position {position}', + values: { + label, + nextLabel, + groupLabel, + position, + }, + }), + + move_compatible: ({ label }: HumanData, { groupLabel, position }: HumanData) => + i18n.translate('xpack.lens.dragDrop.announce.dropped.moveCompatible', { + defaultMessage: 'Moved {label} to {groupLabel} group at position {position}', + values: { + label, + groupLabel, + position, + }, + }), }, }; @@ -113,13 +187,29 @@ const defaultAnnouncements = { label, }, }), - cancelled: () => - i18n.translate('xpack.lens.dragDrop.announce.cancelled', { - defaultMessage: 'Movement cancelled', - }), + cancelled: ({ label, groupLabel, position }: HumanData) => { + if (!groupLabel || !position) { + return i18n.translate('xpack.lens.dragDrop.announce.cancelled', { + defaultMessage: 'Movement cancelled. {label} returned to its initial position', + values: { + label, + }, + }); + } + return i18n.translate('xpack.lens.dragDrop.announce.cancelledItem', { + defaultMessage: + 'Movement cancelled. {label} returned to {groupLabel} group at position {position}', + values: { + label, + groupLabel, + position, + }, + }); + }, + noTarget: () => { return i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.noSelected', { - defaultMessage: `No target selected. Use arrow keys to select a target.`, + defaultMessage: `No target selected. Use arrow keys to select a target`, }); }, @@ -129,17 +219,15 @@ const defaultAnnouncements = { ) => dropGroupLabel && position ? i18n.translate('xpack.lens.dragDrop.announce.droppedDefault', { - defaultMessage: - 'You have dropped {label} to {dropLabel} in {dropGroupLabel} group at position {position}', + defaultMessage: 'Added {label} in {dropGroupLabel} group at position {position}', values: { label, dropGroupLabel, position, - dropLabel, }, }) : i18n.translate('xpack.lens.dragDrop.announce.droppedNoPosition', { - defaultMessage: 'You have dropped {label} to {dropLabel}', + defaultMessage: 'Added {label} to {dropLabel}', values: { label, dropLabel, @@ -151,16 +239,15 @@ const defaultAnnouncements = { ) => { return dropGroupLabel && position ? i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.default', { - defaultMessage: `Selected {dropLabel} in {dropGroupLabel} group at position {position}. Press space or enter to drop {label}`, + defaultMessage: `Add {label} to {dropGroupLabel} group at position {position}. Press space or enter to add`, values: { - dropLabel, label, dropGroupLabel, position, }, }) : i18n.translate('xpack.lens.dragDrop.announce.selectedTarget.defaultNoPosition', { - defaultMessage: `Selected {dropLabel}. Press space or enter to drop {label}`, + defaultMessage: `Add {label} to {dropLabel}. Press space or enter to add`, values: { dropLabel, label, diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx index f9bae547a563bf..f2a2fda730388d 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx @@ -110,7 +110,7 @@ describe('DragDrop', () => { const component = mount( @@ -126,7 +126,7 @@ describe('DragDrop', () => { expect(preventDefault).toBeCalled(); expect(stopPropagation).toBeCalled(); expect(setDragging).toBeCalledWith(undefined); - expect(onDrop).toBeCalledWith({ id: '2', humanData: { label: 'label1' } }, 'field_add'); + expect(onDrop).toBeCalledWith({ id: '2', humanData: { label: 'Label1' } }, 'field_add'); }); test('drop function is not called on dropType undefined', async () => { @@ -138,7 +138,7 @@ describe('DragDrop', () => { const component = mount( @@ -195,7 +195,7 @@ describe('DragDrop', () => { }); test('additional styles are reflected in the className until drop', () => { - let dragging: { id: '1'; humanData: { label: 'label1' } } | undefined; + let dragging: { id: '1'; humanData: { label: 'Label1' } } | undefined; const getAdditionalClassesOnEnter = jest.fn().mockReturnValue('additional'); const getAdditionalClassesOnDroppable = jest.fn().mockReturnValue('droppable'); const setA11yMessage = jest.fn(); @@ -206,7 +206,7 @@ describe('DragDrop', () => { dragging={dragging} setA11yMessage={setA11yMessage} setDragging={() => { - dragging = { id: '1', humanData: { label: 'label1' } }; + dragging = { id: '1', humanData: { label: 'Label1' } }; }} > { }); test('additional enter styles are reflected in the className until dragleave', () => { - let dragging: { id: '1'; humanData: { label: 'label1' } } | undefined; + let dragging: { id: '1'; humanData: { label: 'Label1' } } | undefined; const getAdditionalClasses = jest.fn().mockReturnValue('additional'); const getAdditionalClassesOnDroppable = jest.fn().mockReturnValue('droppable'); const setActiveDropTarget = jest.fn(); @@ -252,7 +252,7 @@ describe('DragDrop', () => { setA11yMessage={jest.fn()} dragging={dragging} setDragging={() => { - dragging = { id: '1', humanData: { label: 'label1' } }; + dragging = { id: '1', humanData: { label: 'Label1' } }; }} setActiveDropTarget={setActiveDropTarget} activeDropTarget={ @@ -303,7 +303,7 @@ describe('DragDrop', () => { draggable: true, value: { id: '1', - humanData: { label: 'label1', position: 1 }, + humanData: { label: 'Label1', position: 1 }, }, children: '1', order: [2, 0, 0, 0], @@ -326,7 +326,7 @@ describe('DragDrop', () => { dragType: 'move' as 'copy' | 'move', value: { id: '3', - humanData: { label: 'label3', position: 1 }, + humanData: { label: 'label3', position: 1, groupLabel: 'Y' }, }, onDrop, dropType: 'replace_compatible' as DropType, @@ -337,7 +337,7 @@ describe('DragDrop', () => { dragType: 'move' as 'copy' | 'move', value: { id: '4', - humanData: { label: 'label4', position: 2 }, + humanData: { label: 'label4', position: 2, groupLabel: 'Y' }, }, order: [2, 0, 2, 1], }, @@ -380,11 +380,11 @@ describe('DragDrop', () => { }); keyboardHandler.simulate('keydown', { key: 'Enter' }); expect(setA11yMessage).toBeCalledWith( - 'Selected label3 in group at position 1. Press space or enter to replace label3 with label1.' + 'Replace label3 in Y group at position 1 with Label1. Press space or enter to replace' ); expect(setActiveDropTarget).toBeCalledWith(undefined); expect(onDrop).toBeCalledWith( - { humanData: { label: 'label1', position: 1 }, id: '1' }, + { humanData: { label: 'Label1', position: 1 }, id: '1' }, 'move_compatible' ); }); @@ -437,7 +437,7 @@ describe('DragDrop', () => { draggable: true, value: { id: '1', - humanData: { label: 'label1', position: 1 }, + humanData: { label: 'Label1', position: 1 }, }, children: '1', order: [2, 0, 0, 0], @@ -488,19 +488,19 @@ describe('DragDrop', () => { const items = [ { id: '1', - humanData: { label: 'label1', position: 1 }, + humanData: { label: 'Label1', position: 1, groupLabel: 'X' }, onDrop, dropType: 'reorder' as DropType, }, { id: '2', - humanData: { label: 'label2', position: 2 }, + humanData: { label: 'label2', position: 2, groupLabel: 'X' }, onDrop, dropType: 'reorder' as DropType, }, { id: '3', - humanData: { label: 'label3', position: 3 }, + humanData: { label: 'label3', position: 3, groupLabel: 'X' }, onDrop, dropType: 'reorder' as DropType, }, @@ -583,7 +583,7 @@ describe('DragDrop', () => { }); expect(setDragging).toBeCalledWith({ ...items[0] }); - expect(setA11yMessage).toBeCalledWith('Lifted label1'); + expect(setA11yMessage).toBeCalledWith('Lifted Label1'); expect( component .find('[data-test-subj="lnsDragDrop-reorderableGroup"]') @@ -652,7 +652,7 @@ describe('DragDrop', () => { jest.runAllTimers(); expect(setA11yMessage).toBeCalledWith( - 'You have dropped the item label1. You have moved the item from position 1 to positon 3' + 'Reordered Label1 in X group from position 1 to positon 3' ); expect(preventDefault).toBeCalled(); expect(stopPropagation).toBeCalled(); @@ -687,7 +687,7 @@ describe('DragDrop', () => { expect(setActiveDropTarget).toBeCalledWith(items[1]); expect(setA11yMessage).toBeCalledWith( - 'You have moved the item label1 from position 1 to position 2' + 'Reorder Label1 in X group from position 1 to position 2. Press space or enter to reorder' ); }); test(`Keyboard navigation: user can drop element to an activeDropTarget`, () => { @@ -729,13 +729,17 @@ describe('DragDrop', () => { jest.runAllTimers(); expect(onDropHandler).not.toHaveBeenCalled(); - expect(setA11yMessage).toBeCalledWith('Movement cancelled'); + expect(setA11yMessage).toBeCalledWith( + 'Movement cancelled. Label1 returned to X group at position 1' + ); keyboardHandler.simulate('keydown', { key: 'Space' }); keyboardHandler.simulate('keydown', { key: 'ArrowDown' }); keyboardHandler.simulate('blur'); expect(onDropHandler).not.toHaveBeenCalled(); - expect(setA11yMessage).toBeCalledWith('Movement cancelled'); + expect(setA11yMessage).toBeCalledWith( + 'Movement cancelled. Label1 returned to X group at position 1' + ); }); test(`Keyboard Navigation: Reordered elements get extra styles to show the reorder effect`, () => { @@ -772,7 +776,7 @@ describe('DragDrop', () => { component.find('[data-test-subj="lnsDragDrop-translatableDrop"]').at(1).prop('style') ).toEqual(undefined); expect(setA11yMessage).toBeCalledWith( - 'You have moved the item label1 from position 1 to position 2' + 'Reorder Label1 in X group from position 1 to position 2. Press space or enter to reorder' ); component @@ -837,7 +841,7 @@ describe('DragDrop', () => { keyboardHandler.simulate('keydown', { key: 'Space' }); keyboardHandler.simulate('keydown', { key: 'ArrowUp' }); expect(setActiveDropTarget).toBeCalledWith(undefined); - expect(setA11yMessage).toBeCalledWith('You have moved the item label1 back to position 1'); + expect(setA11yMessage).toBeCalledWith('Label1 returned to its initial position 1'); }); }); }); diff --git a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx index 4b250643203279..6c6a65ab421b33 100644 --- a/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx +++ b/x-pack/plugins/lens/public/drag_drop/drag_drop.tsx @@ -267,7 +267,7 @@ const DragInner = memo(function DragInner({ setDragging(undefined); setActiveDropTarget(undefined); setKeyboardMode(false); - setA11yMessage(announce.cancelled()); + setA11yMessage(announce.cancelled(value.humanData)); if (onDragEnd) { onDragEnd(); } diff --git a/x-pack/plugins/lens/public/drag_drop/providers.tsx b/x-pack/plugins/lens/public/drag_drop/providers.tsx index 1a056902a474d6..deb9bf6cb17aec 100644 --- a/x-pack/plugins/lens/public/drag_drop/providers.tsx +++ b/x-pack/plugins/lens/public/drag_drop/providers.tsx @@ -204,12 +204,12 @@ export function RootDragDropProvider({ children }: { children: React.ReactNode }

    {i18n.translate('xpack.lens.dragDrop.keyboardInstructionsReorder', { - defaultMessage: `Press enter or space to dragging. When dragging, use the up/down arrow keys to reorder items in the group and left/right arrow keys to choose drop targets outside of the group. Press enter or space again to finish.`, + defaultMessage: `Press space or enter to start dragging. When dragging, use the up/down arrow keys to reorder items in the group and left/right arrow keys to choose drop targets outside of the group. Press space or enter again to finish.`, })}

    {i18n.translate('xpack.lens.dragDrop.keyboardInstructions', { - defaultMessage: `Press enter or space to start dragging. When dragging, use the left/right arrow keys to move between drop targets. Press enter or space again to finish.`, + defaultMessage: `Press space or enter to start dragging. When dragging, use the left/right arrow keys to move between drop targets. Press space or enter again to finish.`, })}

    diff --git a/x-pack/plugins/lens/public/drag_drop/readme.md b/x-pack/plugins/lens/public/drag_drop/readme.md index 55a9e3157c2471..01cc4c7bc85a52 100644 --- a/x-pack/plugins/lens/public/drag_drop/readme.md +++ b/x-pack/plugins/lens/public/drag_drop/readme.md @@ -56,7 +56,7 @@ const { dragging } = useContext(DragContext); return ( onChange([...items, item])} > {items.map((x) => ( diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx index f493000aa587a9..1cbd41fff2a8fb 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/draggable_dimension_button.tsx @@ -64,13 +64,16 @@ export function DraggableDimensionButton({ columnId: string; registerNewButtonRef: (id: string, instance: HTMLDivElement | null) => void; }) { - const dropType = layerDatasource.getDropTypes({ + const dropProps = layerDatasource.getDropProps({ ...layerDatasourceDropProps, columnId, filterOperations: group.filterOperations, groupId: group.groupId, }); + const dropType = dropProps?.dropType; + const nextLabel = dropProps?.nextLabel; + const value = useMemo( () => ({ columnId, @@ -82,9 +85,10 @@ export function DraggableDimensionButton({ label, groupLabel: group.groupLabel, position: accessorIndex + 1, + nextLabel: nextLabel || '', }, }), - [columnId, group.groupId, accessorIndex, layerId, dropType, label, group.groupLabel] + [columnId, group.groupId, accessorIndex, layerId, dropType, label, group.groupLabel, nextLabel] ); // todo: simplify by id and use drop targets? diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx index a83d4bde0383c6..c9d0a7b002870c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/empty_dimension_button.tsx @@ -54,13 +54,16 @@ export function EmptyDimensionButton({ setNewColumnId(generateId()); }, [itemIndex]); - const dropType = layerDatasource.getDropTypes({ + const dropProps = layerDatasource.getDropProps({ ...layerDatasourceDropProps, columnId: newColumnId, filterOperations: group.filterOperations, groupId: group.groupId, }); + const dropType = dropProps?.dropType; + const nextLabel = dropProps?.nextLabel; + const value = useMemo( () => ({ columnId: newColumnId, @@ -72,9 +75,10 @@ export function EmptyDimensionButton({ label, groupLabel: group.groupLabel, position: itemIndex + 1, + nextLabel: nextLabel || '', }, }), - [dropType, newColumnId, group.groupId, layerId, group.groupLabel, itemIndex] + [dropType, newColumnId, group.groupId, layerId, group.groupLabel, itemIndex, nextLabel] ); return ( diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx index 1f97399fdd2929..619147987cdd55 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.test.tsx @@ -440,7 +440,10 @@ describe('LayerPanel', () => { ], }); - mockDatasource.getDropTypes.mockReturnValue('field_add'); + mockDatasource.getDropProps.mockReturnValue({ + dropType: 'field_add', + nextLabel: '', + }); const draggingField = { field: { name: 'dragged' }, @@ -459,7 +462,7 @@ describe('LayerPanel', () => { ); - expect(mockDatasource.getDropTypes).toHaveBeenCalledWith( + expect(mockDatasource.getDropProps).toHaveBeenCalledWith( expect.objectContaining({ dragDropContext: expect.objectContaining({ dragging: draggingField, @@ -492,8 +495,8 @@ describe('LayerPanel', () => { ], }); - mockDatasource.getDropTypes.mockImplementation(({ columnId }) => - columnId !== 'a' ? 'field_replace' : undefined + mockDatasource.getDropProps.mockImplementation(({ columnId }) => + columnId !== 'a' ? { dropType: 'field_replace', nextLabel: '' } : undefined ); const draggingField = { @@ -513,7 +516,7 @@ describe('LayerPanel', () => { ); - expect(mockDatasource.getDropTypes).toHaveBeenCalledWith( + expect(mockDatasource.getDropProps).toHaveBeenCalledWith( expect.objectContaining({ columnId: 'a' }) ); @@ -554,7 +557,10 @@ describe('LayerPanel', () => { ], }); - mockDatasource.getDropTypes.mockReturnValue('replace_compatible'); + mockDatasource.getDropProps.mockReturnValue({ + dropType: 'replace_compatible', + nextLabel: '', + }); const draggingOperation = { layerId: 'first', @@ -574,7 +580,7 @@ describe('LayerPanel', () => { ); - expect(mockDatasource.getDropTypes).toHaveBeenCalledWith( + expect(mockDatasource.getDropProps).toHaveBeenCalledWith( expect.objectContaining({ dragDropContext: expect.objectContaining({ dragging: draggingOperation, diff --git a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx index 36c2d7128460cd..db3b29bb74d317 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/mocks.tsx @@ -88,7 +88,7 @@ export function createMockDatasource(id: string): DatasourceMock { uniqueLabels: jest.fn((_state) => ({})), renderDimensionTrigger: jest.fn(), renderDimensionEditor: jest.fn(), - getDropTypes: jest.fn(), + getDropProps: jest.fn(), onDrop: jest.fn(), // this is an additional property which doesn't exist on real datasources diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index c26d35c4d9a5d1..5eaa798f459e33 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -8,7 +8,14 @@ import { ReactWrapper, ShallowWrapper } from 'enzyme'; import React, { ChangeEvent, MouseEvent } from 'react'; import { act } from 'react-dom/test-utils'; -import { EuiComboBox, EuiListGroupItemProps, EuiListGroup, EuiRange } from '@elastic/eui'; +import { + EuiComboBox, + EuiListGroupItemProps, + EuiListGroup, + EuiRange, + EuiSelect, + EuiButtonIcon, +} from '@elastic/eui'; import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; import { IndexPatternDimensionEditorComponent, @@ -24,8 +31,6 @@ import { OperationMetadata } from '../../types'; import { DateHistogramIndexPatternColumn } from '../operations/definitions/date_histogram'; import { getFieldByNameFactory } from '../pure_helpers'; import { TimeScaling } from './time_scaling'; -import { EuiSelect } from '@elastic/eui'; -import { EuiButtonIcon } from '@elastic/eui'; import { DimensionEditor } from './dimension_editor'; jest.mock('../loader'); @@ -742,6 +747,8 @@ describe('IndexPatternDimensionEditorPanel', () => { }); it('should leave error state when switching from incomplete state to fieldless operation', () => { + // @ts-expect-error + window['__react-beautiful-dnd-disable-dev-warnings'] = true; // issue with enzyme & react-beautiful-dnd throwing errors: https://github.com/atlassian/react-beautiful-dnd/issues/1593 wrapper = mount(); wrapper diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts index 1f0381d92ce640..17f069b8831e7b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.test.ts @@ -6,7 +6,7 @@ */ import { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; import { IndexPatternDimensionEditorProps } from './dimension_panel'; -import { onDrop, getDropTypes } from './droppable'; +import { onDrop, getDropProps } from './droppable'; import { DragContextState } from '../../drag_drop'; import { createMockedDragDropContext } from '../mocks'; import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup, CoreSetup } from 'kibana/public'; @@ -91,7 +91,7 @@ const draggingField = { * - Dimension trigger: Not tested here * - Dimension editor component: First half of the tests * - * - getDropTypes: Returns drop types that are possible for the current dragging field or other dimension + * - getDropProps: Returns drop types that are possible for the current dragging field or other dimension * - onDrop: Correct application of drop logic */ describe('IndexPatternDimensionEditorPanel', () => { @@ -174,14 +174,14 @@ describe('IndexPatternDimensionEditorPanel', () => { }); const groupId = 'a'; - describe('getDropTypes', () => { + describe('getDropProps', () => { it('returns undefined if no drag is happening', () => { - expect(getDropTypes({ ...defaultProps, groupId, dragDropContext })).toBe(undefined); + expect(getDropProps({ ...defaultProps, groupId, dragDropContext })).toBe(undefined); }); it('returns undefined if the dragged item has no field', () => { expect( - getDropTypes({ + getDropProps({ ...defaultProps, groupId, dragDropContext: { @@ -198,7 +198,7 @@ describe('IndexPatternDimensionEditorPanel', () => { it('returns undefined if field is not supported by filterOperations', () => { expect( - getDropTypes({ + getDropProps({ ...defaultProps, groupId, dragDropContext: { @@ -217,7 +217,7 @@ describe('IndexPatternDimensionEditorPanel', () => { it('returns remove_add if the field is supported by filterOperations and the dropTarget is an existing column', () => { expect( - getDropTypes({ + getDropProps({ ...defaultProps, groupId, dragDropContext: { @@ -226,12 +226,12 @@ describe('IndexPatternDimensionEditorPanel', () => { }, filterOperations: (op: OperationMetadata) => op.dataType === 'number', }) - ).toBe('field_replace'); + ).toEqual({ dropType: 'field_replace', nextLabel: 'Intervals' }); }); it('returns undefined if the field belongs to another index pattern', () => { expect( - getDropTypes({ + getDropProps({ ...defaultProps, groupId, dragDropContext: { @@ -250,7 +250,7 @@ describe('IndexPatternDimensionEditorPanel', () => { it('returns undefined if the dragged field is already in use by this operation', () => { expect( - getDropTypes({ + getDropProps({ ...defaultProps, groupId, dragDropContext: { @@ -275,7 +275,7 @@ describe('IndexPatternDimensionEditorPanel', () => { it('returns move if the dragged column is compatible', () => { expect( - getDropTypes({ + getDropProps({ ...defaultProps, groupId, dragDropContext: { @@ -290,7 +290,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }, columnId: 'col2', }) - ).toBe('move_compatible'); + ).toEqual({ dropType: 'move_compatible' }); }); it('returns undefined if the dragged column from different group uses the same field as the dropTarget', () => { @@ -318,7 +318,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }; expect( - getDropTypes({ + getDropProps({ ...defaultProps, groupId, dragDropContext: { @@ -357,7 +357,7 @@ describe('IndexPatternDimensionEditorPanel', () => { }; expect( - getDropTypes({ + getDropProps({ ...defaultProps, groupId, dragDropContext: { @@ -373,7 +373,7 @@ describe('IndexPatternDimensionEditorPanel', () => { columnId: 'col2', filterOperations: (op: OperationMetadata) => op.isBucketed === false, }) - ).toEqual('replace_incompatible'); + ).toEqual({ dropType: 'replace_incompatible', nextLabel: 'Unique count' }); }); }); describe('onDrop', () => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts index 69c7e8c3c2ae61..be791b3c7f7cec 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/droppable.ts @@ -10,21 +10,29 @@ import { DatasourceDimensionDropHandlerProps, isDraggedOperation, DraggedOperation, + DropType, } from '../../types'; import { IndexPatternColumn } from '../indexpattern'; -import { insertOrReplaceColumn, deleteColumn, getOperationTypesForField } from '../operations'; +import { + insertOrReplaceColumn, + deleteColumn, + getOperationTypesForField, + getOperationDisplay, +} from '../operations'; import { mergeLayer } from '../state_helpers'; import { hasField, isDraggedField } from '../utils'; -import { IndexPatternPrivateState, IndexPatternField, DraggedField } from '../types'; +import { IndexPatternPrivateState, DraggedField } from '../types'; import { trackUiEvent } from '../../lens_ui_telemetry'; type DropHandlerProps = DatasourceDimensionDropHandlerProps & { droppedItem: T; }; -export function getDropTypes( +const operationLabels = getOperationDisplay(); + +export function getDropProps( props: DatasourceDimensionDropProps & { groupId: string } -) { +): { dropType: DropType; nextLabel?: string } | undefined { const { dragging } = props.dragDropContext; if (!dragging) { return; @@ -32,23 +40,27 @@ export function getDropTypes( const layerIndexPatternId = props.state.layers[props.layerId].indexPatternId; - function hasOperationForField(field: IndexPatternField) { - const operationsForNewField = getOperationTypesForField(field, props.filterOperations); - return !!operationsForNewField.length; - } - const currentColumn = props.state.layers[props.layerId].columns[props.columnId]; if (isDraggedField(dragging)) { - if ( - !!(layerIndexPatternId === dragging.indexPatternId && hasOperationForField(dragging.field)) - ) { + const operationsForNewField = getOperationTypesForField(dragging.field, props.filterOperations); + + if (!!(layerIndexPatternId === dragging.indexPatternId && operationsForNewField.length)) { + const highestPriorityOperationLabel = operationLabels[operationsForNewField[0]].displayName; if (!currentColumn) { - return 'field_add'; + return { dropType: 'field_add', nextLabel: highestPriorityOperationLabel }; } else if ( (hasField(currentColumn) && currentColumn.sourceField !== dragging.field.name) || !hasField(currentColumn) ) { - return 'field_replace'; + const persistingOperationLabel = + currentColumn && + operationsForNewField.includes(currentColumn.operationType) && + operationLabels[currentColumn.operationType].displayName; + + return { + dropType: 'field_replace', + nextLabel: persistingOperationLabel || highestPriorityOperationLabel, + }; } } return; @@ -62,9 +74,9 @@ export function getDropTypes( // same group if (props.groupId === dragging.groupId) { if (currentColumn) { - return 'reorder'; + return { dropType: 'reorder' }; } - return 'duplicate_in_group'; + return { dropType: 'duplicate_in_group' }; } // compatible group @@ -80,20 +92,34 @@ export function getDropTypes( } if (props.filterOperations(op)) { if (currentColumn) { - return 'replace_compatible'; // in the future also 'swap_compatible' and 'duplicate_compatible' + return { dropType: 'replace_compatible' }; // in the future also 'swap_compatible' and 'duplicate_compatible' } else { - return 'move_compatible'; // in the future also 'duplicate_compatible' + return { dropType: 'move_compatible' }; // in the future also 'duplicate_compatible' } } // suggest const field = hasField(op) && props.state.indexPatterns[layerIndexPatternId].getFieldByName(op.sourceField); - if (field && hasOperationForField(field)) { + const operationsForNewField = field && getOperationTypesForField(field, props.filterOperations); + + if (operationsForNewField && operationsForNewField?.length) { + const highestPriorityOperationLabel = operationLabels[operationsForNewField[0]].displayName; + if (currentColumn) { - return 'replace_incompatible'; // in the future also 'swap_incompatible', 'duplicate_incompatible' + const persistingOperationLabel = + currentColumn && + operationsForNewField.includes(currentColumn.operationType) && + operationLabels[currentColumn.operationType].displayName; + return { + dropType: 'replace_incompatible', + nextLabel: persistingOperationLabel || highestPriorityOperationLabel, + }; // in the future also 'swap_incompatible', 'duplicate_incompatible' } else { - return 'move_incompatible'; // in the future also 'duplicate_incompatible' + return { + dropType: 'move_incompatible', + nextLabel: highestPriorityOperationLabel, + }; // in the future also 'duplicate_incompatible' } } } @@ -178,6 +204,12 @@ function onMoveDropToNonCompatibleGroup(props: DropHandlerProps { ); }); - it('should request field stats without a time field, if the index pattern has none', async () => { - indexPattern.timeFieldName = undefined; - core.http.post.mockImplementationOnce(() => { - return Promise.resolve({}); - }); - const wrapper = mountWithIntl(); - - await act(async () => { - clickField(wrapper, 'bytes'); - }); - - expect(core.http.post).toHaveBeenCalledWith( - '/api/lens/index_stats/my-fake-index-pattern/field', - expect.anything() - ); - // Function argument types not detected correctly (https://github.com/microsoft/TypeScript/issues/26591) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const { body } = (core.http.post.mock.calls[0] as any)[1]; - expect(JSON.parse(body)).not.toHaveProperty('timeFieldName'); - }); - it('should request field stats every time the button is clicked', async () => { let resolveFunction: (arg: unknown) => void; @@ -150,31 +129,21 @@ describe('IndexPattern Field Item', () => { clickField(wrapper, 'bytes'); - expect(core.http.post).toHaveBeenCalledWith( - `/api/lens/index_stats/my-fake-index-pattern/field`, - { - body: JSON.stringify({ - dslQuery: { - bool: { - must: [{ match_all: {} }], - filter: [], - should: [], - must_not: [], - }, + expect(core.http.post).toHaveBeenCalledWith(`/api/lens/index_stats/1/field`, { + body: JSON.stringify({ + dslQuery: { + bool: { + must: [{ match_all: {} }], + filter: [], + should: [], + must_not: [], }, - fromDate: 'now-7d', - toDate: 'now', - timeFieldName: 'timestamp', - field: { - name: 'bytes', - displayName: 'bytesLabel', - type: 'number', - aggregatable: true, - searchable: true, - }, - }), - } - ); + }, + fromDate: 'now-7d', + toDate: 'now', + fieldName: 'bytes', + }), + }); expect(wrapper.find(EuiPopover).prop('isOpen')).toEqual(true); @@ -227,40 +196,30 @@ describe('IndexPattern Field Item', () => { clickField(wrapper, 'bytes'); expect(core.http.post).toHaveBeenCalledTimes(2); - expect(core.http.post).toHaveBeenLastCalledWith( - `/api/lens/index_stats/my-fake-index-pattern/field`, - { - body: JSON.stringify({ - dslQuery: { - bool: { - must: [], - filter: [ - { - bool: { - should: [{ match_phrase: { 'geo.src': 'US' } }], - minimum_should_match: 1, - }, - }, - { - match: { phrase: { 'geo.dest': 'US' } }, + expect(core.http.post).toHaveBeenLastCalledWith(`/api/lens/index_stats/1/field`, { + body: JSON.stringify({ + dslQuery: { + bool: { + must: [], + filter: [ + { + bool: { + should: [{ match_phrase: { 'geo.src': 'US' } }], + minimum_should_match: 1, }, - ], - should: [], - must_not: [], - }, + }, + { + match: { phrase: { 'geo.dest': 'US' } }, + }, + ], + should: [], + must_not: [], }, - fromDate: 'now-14d', - toDate: 'now-7d', - timeFieldName: 'timestamp', - field: { - name: 'bytes', - displayName: 'bytesLabel', - type: 'number', - aggregatable: true, - searchable: true, - }, - }), - } - ); + }, + fromDate: 'now-14d', + toDate: 'now-7d', + fieldName: 'bytes', + }), + }); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index e0198d6d7903e7..e5d46b4a7a0737 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -129,7 +129,7 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { setState((s) => ({ ...s, isLoading: true })); core.http - .post(`/api/lens/index_stats/${indexPattern.title}/field`, { + .post(`/api/lens/index_stats/${indexPattern.id}/field`, { body: JSON.stringify({ dslQuery: esQuery.buildEsQuery( indexPattern as IIndexPattern, @@ -139,8 +139,7 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { ), fromDate: dateRange.fromDate, toDate: dateRange.toDate, - timeFieldName: indexPattern.timeFieldName, - field, + fieldName: field.name, }), }) .then((results: FieldStatsResponse) => { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 6cc89d3dab119f..b8b5eb4c1e6f86 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -31,7 +31,7 @@ import { toExpression } from './to_expression'; import { IndexPatternDimensionTrigger, IndexPatternDimensionEditor, - getDropTypes, + getDropProps, onDrop, } from './dimension_panel'; import { IndexPatternDataPanel } from './datapanel'; @@ -308,7 +308,7 @@ export function getIndexPatternDatasource({ domElement ); }, - getDropTypes, + getDropProps, onDrop, // Reset the temporary invalid state when closing the editor, but don't diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx index d8c4c5fd8ca89c..62c729aa2b3f19 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx @@ -33,6 +33,19 @@ import { RangePopover } from './advanced_editor'; import { DragDropBuckets } from '../shared_components'; import { getFieldByNameFactory } from '../../../pure_helpers'; +// mocking random id generator function +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + htmlIdGenerator: (fn: unknown) => { + let counter = 0; + return () => counter++; + }, + }; +}); + const dataPluginMockValue = dataPluginMock.createStartContract(); // need to overwrite the formatter field first dataPluginMockValue.fieldFormats.deserialize = jest.fn().mockImplementation(({ params }) => { diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 19ce943335a677..419354117eda2e 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -189,9 +189,9 @@ export interface Datasource { renderDimensionTrigger: (domElement: Element, props: DatasourceDimensionTriggerProps) => void; renderDimensionEditor: (domElement: Element, props: DatasourceDimensionEditorProps) => void; renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => void; - getDropTypes: ( + getDropProps: ( props: DatasourceDimensionDropProps & { groupId: string } - ) => DropType | undefined; + ) => { dropType: DropType; nextLabel?: string } | undefined; onDrop: (props: DatasourceDimensionDropHandlerProps) => false | true | { deleted: string }; updateStateOnCloseDimension?: (props: { layerId: string; diff --git a/x-pack/plugins/lens/server/routes/existing_fields.test.ts b/x-pack/plugins/lens/server/routes/existing_fields.test.ts index c6364eca0ff499..3f3e94099f6669 100644 --- a/x-pack/plugins/lens/server/routes/existing_fields.test.ts +++ b/x-pack/plugins/lens/server/routes/existing_fields.test.ts @@ -61,6 +61,20 @@ describe('existingFields', () => { expect(result).toEqual(['bar']); }); + it('supports runtime fields', () => { + const result = existingFields( + [searchResults({ runtime_foo: ['scriptvalue'] })], + [ + field({ + name: 'runtime_foo', + runtimeField: { type: 'long', script: { source: '2+2' } }, + }), + ] + ); + + expect(result).toEqual(['runtime_foo']); + }); + it('supports meta fields', () => { const result = existingFields( [{ _mymeta: 'abc', ...searchResults({ bar: ['scriptvalue'] }) }], @@ -78,6 +92,11 @@ describe('buildFieldList', () => { typeMeta: 'typemeta', fields: [ { name: 'foo', scripted: true, lang: 'painless', script: '2+2' }, + { + name: 'runtime_foo', + isMapped: false, + runtimeField: { type: 'long', script: { source: '2+2' } }, + }, { name: 'bar' }, { name: '@bar' }, { name: 'baz' }, @@ -95,6 +114,14 @@ describe('buildFieldList', () => { }); }); + it('supports runtime fields', () => { + const fields = buildFieldList((indexPattern as unknown) as IndexPattern, []); + expect(fields.find((f) => f.runtimeField)).toMatchObject({ + name: 'runtime_foo', + runtimeField: { type: 'long', script: { source: '2+2' } }, + }); + }); + it('supports meta fields', () => { const fields = buildFieldList((indexPattern as unknown) as IndexPattern, ['_mymeta']); expect(fields.find((f) => f.isMeta)).toMatchObject({ diff --git a/x-pack/plugins/lens/server/routes/existing_fields.ts b/x-pack/plugins/lens/server/routes/existing_fields.ts index e76abf4598efa9..8a2db992a839da 100644 --- a/x-pack/plugins/lens/server/routes/existing_fields.ts +++ b/x-pack/plugins/lens/server/routes/existing_fields.ts @@ -10,7 +10,7 @@ import { errors } from '@elastic/elasticsearch'; import { schema } from '@kbn/config-schema'; import { RequestHandlerContext, ElasticsearchClient } from 'src/core/server'; import { CoreSetup, Logger } from 'src/core/server'; -import { IndexPattern, IndexPatternsService } from 'src/plugins/data/common'; +import { IndexPattern, IndexPatternsService, RuntimeField } from 'src/plugins/data/common'; import { BASE_API_URL } from '../../common'; import { UI_SETTINGS } from '../../../../../src/plugins/data/server'; import { PluginStartContract } from '../plugin'; @@ -30,6 +30,7 @@ export interface Field { isMeta: boolean; lang?: string; script?: string; + runtimeField?: RuntimeField; } export async function existingFieldsRoute(setup: CoreSetup, logger: Logger) { @@ -77,11 +78,9 @@ export async function existingFieldsRoute(setup: CoreSetup, if (e.output.statusCode === 404) { return res.notFound({ body: e.output.payload.message }); } - return res.internalError({ body: e.output.payload.message }); + throw new Error(e.output.payload.message); } else { - return res.internalError({ - body: Boom.internal(e.message || e.name), - }); + throw e; } } } @@ -138,6 +137,7 @@ export function buildFieldList(indexPattern: IndexPattern, metaFields: string[]) // id is a special case - it doesn't show up in the meta field list, // but as it's not part of source, it has to be handled separately. isMeta: metaFields.includes(field.name) || field.name === '_id', + runtimeField: !field.isMapped ? field.runtimeField : undefined, }; }); } @@ -181,6 +181,7 @@ async function fetchIndexPatternStats({ }; const scriptedFields = fields.filter((f) => f.isScript); + const runtimeFields = fields.filter((f) => f.runtimeField); const { body: result } = await client.search({ index, body: { @@ -189,6 +190,11 @@ async function fetchIndexPatternStats({ sort: timeFieldName && fromDate && toDate ? [{ [timeFieldName]: 'desc' }] : [], fields: ['*'], _source: false, + runtime_mappings: runtimeFields.reduce((acc, field) => { + if (!field.runtimeField) return acc; + acc[field.name] = field.runtimeField; + return acc; + }, {} as Record), script_fields: scriptedFields.reduce((acc, field) => { acc[field.name] = { script: { diff --git a/x-pack/plugins/lens/server/routes/field_stats.ts b/x-pack/plugins/lens/server/routes/field_stats.ts index 7fd884755d86df..57b3e59f4ad5c1 100644 --- a/x-pack/plugins/lens/server/routes/field_stats.ts +++ b/x-pack/plugins/lens/server/routes/field_stats.ts @@ -5,12 +5,12 @@ * 2.0. */ -import Boom from '@hapi/boom'; import { errors } from '@elastic/elasticsearch'; import DateMath from '@elastic/datemath'; import { schema } from '@kbn/config-schema'; import { CoreSetup } from 'src/core/server'; import { IFieldType } from 'src/plugins/data/common'; +import { SavedObjectNotFound } from '../../../../../src/plugins/kibana_utils/common'; import { ESSearchResponse } from '../../../../typings/elasticsearch'; import { FieldStatsResponse, BASE_API_URL } from '../../common'; import { PluginStartContract } from '../plugin'; @@ -21,28 +21,17 @@ export async function initFieldsRoute(setup: CoreSetup) { const router = setup.http.createRouter(); router.post( { - path: `${BASE_API_URL}/index_stats/{indexPatternTitle}/field`, + path: `${BASE_API_URL}/index_stats/{indexPatternId}/field`, validate: { params: schema.object({ - indexPatternTitle: schema.string(), + indexPatternId: schema.string(), }), body: schema.object( { dslQuery: schema.object({}, { unknowns: 'allow' }), fromDate: schema.string(), toDate: schema.string(), - timeFieldName: schema.maybe(schema.string()), - field: schema.object( - { - name: schema.string(), - type: schema.string(), - esTypes: schema.maybe(schema.arrayOf(schema.string())), - scripted: schema.maybe(schema.boolean()), - lang: schema.maybe(schema.string()), - script: schema.maybe(schema.string()), - }, - { unknowns: 'allow' } - ), + fieldName: schema.string(), }, { unknowns: 'allow' } ), @@ -50,9 +39,26 @@ export async function initFieldsRoute(setup: CoreSetup) { }, async (context, req, res) => { const requestClient = context.core.elasticsearch.client.asCurrentUser; - const { fromDate, toDate, timeFieldName, field, dslQuery } = req.body; + const { fromDate, toDate, fieldName, dslQuery } = req.body; + + const [{ savedObjects, elasticsearch }, { data }] = await setup.getStartServices(); + const savedObjectsClient = savedObjects.getScopedClient(req); + const esClient = elasticsearch.client.asScoped(req).asCurrentUser; + const indexPatternsService = await data.indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + esClient + ); try { + const indexPattern = await indexPatternsService.get(req.params.indexPatternId); + + const timeFieldName = indexPattern.timeFieldName; + const field = indexPattern.fields.find((f) => f.name === fieldName); + + if (!field) { + throw new Error(`Field {fieldName} not found in index pattern ${indexPattern.title}`); + } + const filter = timeFieldName ? [ { @@ -75,11 +81,12 @@ export async function initFieldsRoute(setup: CoreSetup) { const search = async (aggs: unknown) => { const { body: result } = await requestClient.search({ - index: req.params.indexPatternTitle, + index: indexPattern.title, track_total_hits: true, body: { query, aggs, + runtime_mappings: field.runtimeField ? { [fieldName]: field.runtimeField } : {}, }, size: 0, }); @@ -104,6 +111,9 @@ export async function initFieldsRoute(setup: CoreSetup) { body: await getStringSamples(search, field), }); } catch (e) { + if (e instanceof SavedObjectNotFound) { + return res.notFound(); + } if (e instanceof errors.ResponseError && e.statusCode === 404) { return res.notFound(); } @@ -111,11 +121,9 @@ export async function initFieldsRoute(setup: CoreSetup) { if (e.output.statusCode === 404) { return res.notFound(); } - return res.internalError(e.output.message); + throw new Error(e.output.message); } else { - return res.internalError({ - body: Boom.internal(e.message || e.name), - }); + throw e; } } } diff --git a/x-pack/plugins/lens/server/routes/telemetry.ts b/x-pack/plugins/lens/server/routes/telemetry.ts index cb8cf4b15f8d90..efcde9d14ebbe2 100644 --- a/x-pack/plugins/lens/server/routes/telemetry.ts +++ b/x-pack/plugins/lens/server/routes/telemetry.ts @@ -5,7 +5,6 @@ * 2.0. */ -import Boom from '@hapi/boom'; import { errors } from '@elastic/elasticsearch'; import { CoreSetup } from 'src/core/server'; import { schema } from '@kbn/config-schema'; @@ -84,11 +83,9 @@ export async function initLensUsageRoute(setup: CoreSetup) if (e.output.statusCode === 404) { return res.notFound(); } - return res.internalError(e.output.message); + throw new Error(e.output.message); } else { - return res.internalError({ - body: Boom.internal(e.message || e.name), - }); + throw e; } } } diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap index 9db44bd8225ea2..bc69ab5352a4f5 100644 --- a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap +++ b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap @@ -296,140 +296,140 @@ exports[`UploadLicense should display a modal when license requires acknowledgem className="euiSpacer euiSpacer--l" />
    - - -
    + + } + confirmButtonText={ + + } + onCancel={[Function]} + onConfirm={[Function]} + title={ + + } + > + + + -
    -
    - Confirm License Upload -
    -
    -
    +
    -
    -
    - } - > - - } - confirmButtonText={ - - } - onCancel={[Function]} - onConfirm={[Function]} - title={ - - } - > -
    -
    -
    - - + + + +
    - - } - onCancel={cancelStartBasicLicense} - onConfirm={() => startBasicLicense(licenseType, true)} - cancelButtonText={ - - } - confirmButtonText={ - - } - > -
    - {firstLine} - -
      - {messages.map((message) => ( -
    • {message}
    • - ))} -
    -
    -
    -
    - + + } + onCancel={cancelStartBasicLicense} + onConfirm={() => startBasicLicense(licenseType, true)} + cancelButtonText={ + + } + confirmButtonText={ + + } + > +
    + {firstLine} + +
      + {messages.map((message) => ( +
    • {message}
    • + ))} +
    +
    +
    +
    ); } render() { diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx b/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx index 4f7d1ab4365b68..36af5c3b9c7adc 100644 --- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx +++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx @@ -14,7 +14,6 @@ import { EuiFlexItem, EuiCard, EuiLink, - EuiOverlayMask, EuiText, EuiModal, EuiModalFooter, @@ -78,154 +77,152 @@ export class StartTrial extends Component { } return ( - - - - - - - - - -
    - -

    - + + + + + + + +

    + +

    + - - - ), - }} + values={{ + subscriptionFeaturesLinkText: ( + + + + ), + }} + /> +

    +
      +
    • + -

      -
        -
      • - -
      • -
      • - -
      • -
      • - -
      • -
      • - -
      • -
      -

      +

    • +
    • - - - ), - }} + id="xpack.licenseMgmt.licenseDashboard.startTrial.confirmModalDescription.alertingFeatureTitle" + defaultMessage="Alerting" + /> +
    • +
    • + -

      -

      +

    • +
    • - - - ), + jdbcStandard: 'JDBC', + odbcStandard: 'ODBC', + sqlDataBase: 'SQL', }} /> -

      - -
    -
    - - - - - - {shouldShowTelemetryOptIn(telemetry) && ( - + +

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

    +

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

    + +
    +
    +
    + + + + + {shouldShowTelemetryOptIn(telemetry) && ( + + )} + + + + + + + + + + + + + + + + + +
    ); } diff --git a/x-pack/plugins/license_management/public/application/sections/upload_license/upload_license.js b/x-pack/plugins/license_management/public/application/sections/upload_license/upload_license.js index 77efe30bbb71ea..4d639ec3123dff 100644 --- a/x-pack/plugins/license_management/public/application/sections/upload_license/upload_license.js +++ b/x-pack/plugins/license_management/public/application/sections/upload_license/upload_license.js @@ -13,7 +13,6 @@ import { EuiForm, EuiSpacer, EuiConfirmModal, - EuiOverlayMask, EuiText, EuiTitle, EuiFlexGroup, @@ -62,41 +61,39 @@ export class UploadLicense extends React.PureComponent { return null; } return ( - - - } - onCancel={this.cancel} - onConfirm={() => this.send(true)} - cancelButtonText={ - - } - confirmButtonText={ - - } - > -
    - {firstLine} - -
      - {messages.map((message) => ( -
    • {message}
    • - ))} -
    -
    -
    -
    -
    + + } + onCancel={this.cancel} + onConfirm={() => this.send(true)} + cancelButtonText={ + + } + confirmButtonText={ + + } + > +
    + {firstLine} + +
      + {messages.map((message) => ( +
    • {message}
    • + ))} +
    +
    +
    +
    ); } errorMessage() { diff --git a/x-pack/plugins/license_management/server/routes/api/license/register_license_route.ts b/x-pack/plugins/license_management/server/routes/api/license/register_license_route.ts index 7fcb73ffeb0086..86f87506dfc2c7 100644 --- a/x-pack/plugins/license_management/server/routes/api/license/register_license_route.ts +++ b/x-pack/plugins/license_management/server/routes/api/license/register_license_route.ts @@ -23,18 +23,14 @@ export function registerLicenseRoute({ router, plugins: { licensing } }: RouteDe }, async (ctx, req, res) => { const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; - try { - return res.ok({ - body: await putLicense({ - acknowledge: Boolean(req.query.acknowledge), - callAsCurrentUser, - licensing, - license: req.body, - }), - }); - } catch (e) { - return res.internalError({ body: e }); - } + return res.ok({ + body: await putLicense({ + acknowledge: Boolean(req.query.acknowledge), + callAsCurrentUser, + licensing, + license: req.body, + }), + }); } ); } diff --git a/x-pack/plugins/license_management/server/routes/api/license/register_permissions_route.ts b/x-pack/plugins/license_management/server/routes/api/license/register_permissions_route.ts index c5cd11c022cfe6..dd441051872d2b 100644 --- a/x-pack/plugins/license_management/server/routes/api/license/register_permissions_route.ts +++ b/x-pack/plugins/license_management/server/routes/api/license/register_permissions_route.ts @@ -16,12 +16,8 @@ export function registerPermissionsRoute({ router.post({ path: addBasePath('/permissions'), validate: false }, async (ctx, req, res) => { const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; - try { - return res.ok({ - body: await getPermissions({ callAsCurrentUser, isSecurityEnabled }), - }); - } catch (e) { - return res.internalError({ body: e }); - } + return res.ok({ + body: await getPermissions({ callAsCurrentUser, isSecurityEnabled }), + }); }); } diff --git a/x-pack/plugins/license_management/server/routes/api/license/register_start_basic_route.ts b/x-pack/plugins/license_management/server/routes/api/license/register_start_basic_route.ts index 820330c6a12044..bc5fb70f7dadd3 100644 --- a/x-pack/plugins/license_management/server/routes/api/license/register_start_basic_route.ts +++ b/x-pack/plugins/license_management/server/routes/api/license/register_start_basic_route.ts @@ -18,17 +18,13 @@ export function registerStartBasicRoute({ router, plugins: { licensing } }: Rout }, async (ctx, req, res) => { const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; - try { - return res.ok({ - body: await startBasic({ - acknowledge: Boolean(req.query.acknowledge), - callAsCurrentUser, - licensing, - }), - }); - } catch (e) { - return res.internalError({ body: e }); - } + return res.ok({ + body: await startBasic({ + acknowledge: Boolean(req.query.acknowledge), + callAsCurrentUser, + licensing, + }), + }); } ); } diff --git a/x-pack/plugins/license_management/server/routes/api/license/register_start_trial_routes.ts b/x-pack/plugins/license_management/server/routes/api/license/register_start_trial_routes.ts index 570ae73d8aa613..6986e85e7d280d 100644 --- a/x-pack/plugins/license_management/server/routes/api/license/register_start_trial_routes.ts +++ b/x-pack/plugins/license_management/server/routes/api/license/register_start_trial_routes.ts @@ -12,21 +12,13 @@ import { addBasePath } from '../../helpers'; export function registerStartTrialRoutes({ router, plugins: { licensing } }: RouteDependencies) { router.get({ path: addBasePath('/start_trial'), validate: false }, async (ctx, req, res) => { const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; - try { - return res.ok({ body: await canStartTrial(callAsCurrentUser) }); - } catch (e) { - return res.internalError({ body: e }); - } + return res.ok({ body: await canStartTrial(callAsCurrentUser) }); }); router.post({ path: addBasePath('/start_trial'), validate: false }, async (ctx, req, res) => { const { callAsCurrentUser } = ctx.core.elasticsearch.legacy.client; - try { - return res.ok({ - body: await startTrial({ callAsCurrentUser, licensing }), - }); - } catch (e) { - return res.internalError({ body: e }); - } + return res.ok({ + body: await startTrial({ callAsCurrentUser, licensing }), + }); }); } diff --git a/x-pack/plugins/lists/common/shared_exports.ts b/x-pack/plugins/lists/common/shared_exports.ts index ba5891092fe124..6dcda5d1f8c24d 100644 --- a/x-pack/plugins/lists/common/shared_exports.ts +++ b/x-pack/plugins/lists/common/shared_exports.ts @@ -47,4 +47,4 @@ export { OsTypeArray, } from './schemas'; -export { ENDPOINT_LIST_ID } from './constants'; +export { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from './constants'; diff --git a/x-pack/plugins/lists/server/saved_objects/migrations.test.ts b/x-pack/plugins/lists/server/saved_objects/migrations.test.ts index 143443b9320923..f71109b9bb85dc 100644 --- a/x-pack/plugins/lists/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/lists/server/saved_objects/migrations.test.ts @@ -6,61 +6,102 @@ */ import { SavedObjectUnsanitizedDoc } from 'kibana/server'; +import uuid from 'uuid'; -import { ENDPOINT_LIST_ID } from '../../common/constants'; +import { ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../common/constants'; +import { ExceptionListSoSchema } from '../../common/schemas/saved_objects'; import { OldExceptionListSoSchema, migrations } from './migrations'; +const DEFAULT_EXCEPTION_LIST_SO: ExceptionListSoSchema = { + comments: undefined, + created_at: '2020-06-09T20:18:20.349Z', + created_by: 'user', + description: 'description', + entries: undefined, + immutable: false, + item_id: undefined, + list_id: 'some_list', + list_type: 'list', + meta: undefined, + name: 'name', + os_types: [], + tags: [], + tie_breaker_id: uuid.v4(), + type: 'endpoint', + updated_by: 'user', + version: undefined, +}; + +const DEFAULT_OLD_EXCEPTION_LIST_SO: OldExceptionListSoSchema = { + ...DEFAULT_EXCEPTION_LIST_SO, + _tags: [], +}; + +const createOldExceptionListSoSchemaSavedObject = ( + attributes: Partial +): SavedObjectUnsanitizedDoc => ({ + attributes: { ...DEFAULT_OLD_EXCEPTION_LIST_SO, ...attributes }, + id: 'abcd', + migrationVersion: {}, + references: [], + type: 'so-type', + updated_at: '2020-06-09T20:18:20.349Z', +}); + +const createExceptionListSoSchemaSavedObject = ( + attributes: Partial +): SavedObjectUnsanitizedDoc => ({ + attributes: { ...DEFAULT_EXCEPTION_LIST_SO, ...attributes }, + id: 'abcd', + migrationVersion: {}, + references: [], + type: 'so-type', + updated_at: '2020-06-09T20:18:20.349Z', +}); + describe('7.10.0 lists migrations', () => { const migration = migrations['7.10.0']; test('properly converts .text fields to .caseless', () => { - const doc = { - attributes: { - entries: [ - { - field: 'file.path.text', - operator: 'included', - type: 'match', - value: 'C:\\Windows\\explorer.exe', - }, - { - field: 'host.os.name', - operator: 'included', - type: 'match', - value: 'my-host', - }, - { - entries: [ - { - field: 'process.command_line.text', - operator: 'included', - type: 'match', - value: '/usr/bin/bash', - }, - { - field: 'process.parent.command_line.text', - operator: 'included', - type: 'match', - value: '/usr/bin/bash', - }, - ], - field: 'nested.field', - type: 'nested', - }, - ], - list_id: ENDPOINT_LIST_ID, - }, - id: 'abcd', - migrationVersion: {}, - references: [], - type: 'so-type', - updated_at: '2020-06-09T20:18:20.349Z', - }; - expect( - migration((doc as unknown) as SavedObjectUnsanitizedDoc) - ).toEqual({ - attributes: { + const doc = createOldExceptionListSoSchemaSavedObject({ + entries: [ + { + field: 'file.path.text', + operator: 'included', + type: 'match', + value: 'C:\\Windows\\explorer.exe', + }, + { + field: 'host.os.name', + operator: 'included', + type: 'match', + value: 'my-host', + }, + { + entries: [ + { + field: 'process.command_line.text', + operator: 'included', + type: 'match', + value: '/usr/bin/bash', + }, + { + field: 'process.parent.command_line.text', + operator: 'included', + type: 'match', + value: '/usr/bin/bash', + }, + ], + field: 'nested.field', + type: 'nested', + }, + ], + list_id: ENDPOINT_LIST_ID, + }); + + expect(migration(doc)).toEqual( + createOldExceptionListSoSchemaSavedObject({ entries: [ { field: 'file.path.caseless', @@ -94,40 +135,98 @@ describe('7.10.0 lists migrations', () => { }, ], list_id: ENDPOINT_LIST_ID, - }, - id: 'abcd', - migrationVersion: {}, - references: [], - type: 'so-type', - updated_at: '2020-06-09T20:18:20.349Z', - }); + }) + ); }); test('properly copies os tags to os_types', () => { - const doc = { - attributes: { - _tags: ['1234', 'os:windows'], - comments: [], - }, - id: 'abcd', - migrationVersion: {}, - references: [], - type: 'so-type', - updated_at: '2020-06-09T20:18:20.349Z', - }; - expect( - migration((doc as unknown) as SavedObjectUnsanitizedDoc) - ).toEqual({ - attributes: { + const doc = createOldExceptionListSoSchemaSavedObject({ + _tags: ['1234', 'os:windows'], + comments: [], + }); + + expect(migration(doc)).toEqual( + createOldExceptionListSoSchemaSavedObject({ _tags: ['1234', 'os:windows'], comments: [], os_types: ['windows'], - }, - id: 'abcd', - migrationVersion: {}, - references: [], - type: 'so-type', - updated_at: '2020-06-09T20:18:20.349Z', + }) + ); + }); +}); + +describe('7.12.0 lists migrations', () => { + const migration = migrations['7.12.0']; + + test('should not convert non trusted apps lists', () => { + const doc = createExceptionListSoSchemaSavedObject({ list_id: ENDPOINT_LIST_ID, tags: [] }); + + expect(migration(doc)).toEqual( + createExceptionListSoSchemaSavedObject({ + list_id: ENDPOINT_LIST_ID, + tags: [], + tie_breaker_id: expect.anything(), + }) + ); + }); + + test('converts empty tags to contain list containing "policy:all" tag', () => { + const doc = createExceptionListSoSchemaSavedObject({ + list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + tags: [], + }); + + expect(migration(doc)).toEqual( + createExceptionListSoSchemaSavedObject({ + list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + tags: ['policy:all'], + tie_breaker_id: expect.anything(), + }) + ); + }); + + test('preserves existing non policy related tags', () => { + const doc = createExceptionListSoSchemaSavedObject({ + list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + tags: ['tag1', 'tag2'], + }); + + expect(migration(doc)).toEqual( + createExceptionListSoSchemaSavedObject({ + list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + tags: ['tag1', 'tag2', 'policy:all'], + tie_breaker_id: expect.anything(), + }) + ); + }); + + test('preserves existing "policy:all" tag and does not add another one', () => { + const doc = createExceptionListSoSchemaSavedObject({ + list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + tags: ['policy:all', 'tag1', 'tag2'], + }); + + expect(migration(doc)).toEqual( + createExceptionListSoSchemaSavedObject({ + list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + tags: ['policy:all', 'tag1', 'tag2'], + tie_breaker_id: expect.anything(), + }) + ); + }); + + test('preserves existing policy reference tag and does not add "policy:all" tag', () => { + const doc = createExceptionListSoSchemaSavedObject({ + list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + tags: ['policy:056d2d4645421fb92e5cd39f33d70856', 'tag1', 'tag2'], }); + + expect(migration(doc)).toEqual( + createExceptionListSoSchemaSavedObject({ + list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + tags: ['policy:056d2d4645421fb92e5cd39f33d70856', 'tag1', 'tag2'], + tie_breaker_id: expect.anything(), + }) + ); }); }); diff --git a/x-pack/plugins/lists/server/saved_objects/migrations.ts b/x-pack/plugins/lists/server/saved_objects/migrations.ts index 43faa7a5e8fb64..2fa19a6810a8ad 100644 --- a/x-pack/plugins/lists/server/saved_objects/migrations.ts +++ b/x-pack/plugins/lists/server/saved_objects/migrations.ts @@ -40,6 +40,9 @@ const reduceOsTypes = (acc: string[], tag: string): string[] => { return [...acc]; }; +const containsPolicyTags = (tags: string[]): boolean => + tags.some((tag) => tag.startsWith('policy:')); + export type OldExceptionListSoSchema = ExceptionListSoSchema & { _tags: string[]; }; @@ -64,4 +67,25 @@ export const migrations = { }, references: doc.references || [], }), + '7.12.0': ( + doc: SavedObjectUnsanitizedDoc + ): SavedObjectSanitizedDoc => { + if (doc.attributes.list_id === ENDPOINT_TRUSTED_APPS_LIST_ID) { + return { + ...doc, + ...{ + attributes: { + ...doc.attributes, + tags: [ + ...(doc.attributes.tags || []), + ...(containsPolicyTags(doc.attributes.tags) ? [] : ['policy:all']), + ], + }, + }, + references: doc.references || [], + }; + } else { + return { ...doc, references: doc.references || [] }; + } + }, }; diff --git a/x-pack/plugins/logstash/public/application/components/pipeline_editor/__snapshots__/confirm_delete_pipeline_modal.test.js.snap b/x-pack/plugins/logstash/public/application/components/pipeline_editor/__snapshots__/confirm_delete_pipeline_modal.test.js.snap index 8fc0ecacd4a3c1..31b8be8aab9ce1 100644 --- a/x-pack/plugins/logstash/public/application/components/pipeline_editor/__snapshots__/confirm_delete_pipeline_modal.test.js.snap +++ b/x-pack/plugins/logstash/public/application/components/pipeline_editor/__snapshots__/confirm_delete_pipeline_modal.test.js.snap @@ -1,41 +1,39 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ConfirmDeletePipelineModal component renders as expected 1`] = ` - - - } - confirmButtonText={ - - } - defaultFocusedButton="cancel" - onCancel={[MockFunction]} - onConfirm={[MockFunction]} - title={ - + } + confirmButtonText={ + + } + defaultFocusedButton="cancel" + onCancel={[MockFunction]} + onConfirm={[MockFunction]} + title={ + - } - > -

    - You cannot recover a deleted pipeline. -

    -
    -
    + } + /> + } +> +

    + You cannot recover a deleted pipeline. +

    + `; diff --git a/x-pack/plugins/logstash/public/application/components/pipeline_editor/confirm_delete_pipeline_modal.js b/x-pack/plugins/logstash/public/application/components/pipeline_editor/confirm_delete_pipeline_modal.js index 37ce05f42073af..d8cf85919bd425 100644 --- a/x-pack/plugins/logstash/public/application/components/pipeline_editor/confirm_delete_pipeline_modal.js +++ b/x-pack/plugins/logstash/public/application/components/pipeline_editor/confirm_delete_pipeline_modal.js @@ -7,41 +7,39 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { EuiConfirmModal, EUI_MODAL_CANCEL_BUTTON, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal, EUI_MODAL_CANCEL_BUTTON } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { PIPELINE_EDITOR } from './constants'; export function ConfirmDeletePipelineModal({ id, cancelDeleteModal, confirmDeletePipeline }) { return ( - - - } - confirmButtonText={ - - } - defaultFocusedButton={EUI_MODAL_CANCEL_BUTTON} - onCancel={cancelDeleteModal} - onConfirm={confirmDeletePipeline} - title={ - - } - > -

    {PIPELINE_EDITOR.DELETE_PIPELINE_MODAL_MESSAGE}

    -
    -
    + + } + confirmButtonText={ + + } + defaultFocusedButton={EUI_MODAL_CANCEL_BUTTON} + onCancel={cancelDeleteModal} + onConfirm={confirmDeletePipeline} + title={ + + } + > +

    {PIPELINE_EDITOR.DELETE_PIPELINE_MODAL_MESSAGE}

    +
    ); } diff --git a/x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/confirm_delete_modal.test.js.snap b/x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/confirm_delete_modal.test.js.snap index c58337612f2871..9eabf4120ef233 100644 --- a/x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/confirm_delete_modal.test.js.snap +++ b/x-pack/plugins/logstash/public/application/components/pipeline_list/__snapshots__/confirm_delete_modal.test.js.snap @@ -1,93 +1,89 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ConfirmDeleteModal component confirms delete for multiple pipelines 1`] = ` - - - } - confirmButtonText={ - + } + confirmButtonText={ + - } - defaultFocusedButton="cancel" - onCancel={[MockFunction]} - onConfirm={[MockFunction]} - title={ - + } + defaultFocusedButton="cancel" + onCancel={[MockFunction]} + onConfirm={[MockFunction]} + title={ + - } - > -

    - -

    -
    -
    + } + /> + } +> +

    + +

    + `; exports[`ConfirmDeleteModal component confirms delete for single pipeline 1`] = ` - - - } - confirmButtonText={ - - } - defaultFocusedButton="cancel" - onCancel={[MockFunction]} - onConfirm={[MockFunction]} - title={ - + } + confirmButtonText={ + + } + defaultFocusedButton="cancel" + onCancel={[MockFunction]} + onConfirm={[MockFunction]} + title={ + - } - > -

    - -

    -
    -
    + } + /> + } +> +

    + +

    + `; diff --git a/x-pack/plugins/logstash/public/application/components/pipeline_list/confirm_delete_modal.js b/x-pack/plugins/logstash/public/application/components/pipeline_list/confirm_delete_modal.js index c20db3d3fc5796..5dbefd2ae58e89 100644 --- a/x-pack/plugins/logstash/public/application/components/pipeline_list/confirm_delete_modal.js +++ b/x-pack/plugins/logstash/public/application/components/pipeline_list/confirm_delete_modal.js @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiConfirmModal, EUI_MODAL_CANCEL_BUTTON, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal, EUI_MODAL_CANCEL_BUTTON } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; export function ConfirmDeleteModal({ @@ -67,23 +67,21 @@ export function ConfirmDeleteModal({ }; return ( - - - } - confirmButtonText={confirmText.button} - defaultFocusedButton={EUI_MODAL_CANCEL_BUTTON} - onCancel={cancelDeletePipelines} - onConfirm={deleteSelectedPipelines} - title={confirmText.title} - > -

    {confirmText.message}

    -
    -
    + + } + confirmButtonText={confirmText.button} + defaultFocusedButton={EUI_MODAL_CANCEL_BUTTON} + onCancel={cancelDeletePipelines} + onConfirm={deleteSelectedPipelines} + title={confirmText.title} + > +

    {confirmText.message}

    +
    ); } diff --git a/x-pack/plugins/logstash/server/routes/cluster/load.ts b/x-pack/plugins/logstash/server/routes/cluster/load.ts index f820ecdbeb4f34..ac7bc245e51ebc 100644 --- a/x-pack/plugins/logstash/server/routes/cluster/load.ts +++ b/x-pack/plugins/logstash/server/routes/cluster/load.ts @@ -29,7 +29,7 @@ export function registerClusterLoadRoute(router: LogstashPluginRouter) { if (err.status === 403) { return response.ok(); } - return response.internalError(); + throw err; } }) ); diff --git a/x-pack/plugins/logstash/tsconfig.json b/x-pack/plugins/logstash/tsconfig.json new file mode 100644 index 00000000000000..6f21cfdb0b1919 --- /dev/null +++ b/x-pack/plugins/logstash/tsconfig.json @@ -0,0 +1,26 @@ + +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json"}, + { "path": "../../../src/plugins/management/tsconfig.json"}, + + { "path": "../features/tsconfig.json" }, + { "path": "../licensing/tsconfig.json"}, + { "path": "../monitoring/tsconfig.json"}, + { "path": "../security/tsconfig.json"}, + ] + } diff --git a/x-pack/plugins/maps/public/actions/map_actions.test.js b/x-pack/plugins/maps/public/actions/map_actions.test.js index c0ad934c232e22..fafafa6b6a0711 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.test.js +++ b/x-pack/plugins/maps/public/actions/map_actions.test.js @@ -277,6 +277,9 @@ describe('map_actions', () => { require('../selectors/map_selectors').getSearchSessionId = () => { return searchSessionId; }; + require('../selectors/map_selectors').getSearchSessionMapBuffer = () => { + return undefined; + }; require('../selectors/map_selectors').getMapSettings = () => { return { autoFitToDataBounds: false, diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index 33c79c793974b4..9682306852ba92 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -22,6 +22,7 @@ import { getTimeFilters, getLayerList, getSearchSessionId, + getSearchSessionMapBuffer, } from '../selectors/map_selectors'; import { CLEAR_GOTO, @@ -229,12 +230,14 @@ export function setQuery({ filters = [], forceRefresh = false, searchSessionId, + searchSessionMapBuffer, }: { filters?: Filter[]; query?: Query; timeFilters?: TimeRange; forceRefresh?: boolean; searchSessionId?: string; + searchSessionMapBuffer?: MapExtent; }) { return async ( dispatch: ThunkDispatch, @@ -255,6 +258,7 @@ export function setQuery({ }, filters: filters ? filters : getFilters(getState()), searchSessionId, + searchSessionMapBuffer, }; const prevQueryContext = { @@ -262,6 +266,7 @@ export function setQuery({ query: getQuery(getState()), filters: getFilters(getState()), searchSessionId: getSearchSessionId(getState()), + searchSessionMapBuffer: getSearchSessionMapBuffer(getState()), }; if (_.isEqual(nextQueryContext, prevQueryContext)) { diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index 89c6d70a217c95..e3a21b596afe14 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -77,7 +77,7 @@ export interface ILayer { canShowTooltip(): boolean; syncLayerWithMB(mbMap: MbMap): void; getLayerTypeIconName(): string; - isDataLoaded(): boolean; + isInitialDataLoadComplete(): boolean; getIndexPatternIds(): string[]; getQueryableIndexPatternIds(): string[]; getType(): string | undefined; @@ -446,7 +446,7 @@ export class AbstractLayer implements ILayer { throw new Error('should implement Layer#getLayerTypeIconName'); } - isDataLoaded(): boolean { + isInitialDataLoadComplete(): boolean { const sourceDataRequest = this.getSourceDataRequest(); return sourceDataRequest ? sourceDataRequest.hasData() : false; } diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index 7e87d99fd4f93f..2373ed3ba2062f 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -174,7 +174,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { return this.getValidJoins().length > 0; } - isDataLoaded() { + isInitialDataLoadComplete() { const sourceDataRequest = this.getSourceDataRequest(); if (!sourceDataRequest || !sourceDataRequest.hasData()) { return false; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx index 25bd589cda658a..5ca370f7d54c8e 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx @@ -84,6 +84,8 @@ export class MBMap extends Component { private _checker?: ResizeChecker; private _isMounted: boolean = false; private _containerRef: HTMLDivElement | null = null; + private _prevDisableInteractive?: boolean; + private _navigationControl = new mapboxgl.NavigationControl({ showCompass: false }); state: State = { prevLayerList: undefined, @@ -181,7 +183,6 @@ export class MBMap extends Component { style: mbStyle, scrollZoom: this.props.scrollZoom, preserveDrawingBuffer: getPreserveDrawingBuffer(), - interactive: !this.props.settings.disableInteractive, maxZoom: this.props.settings.maxZoom, minZoom: this.props.settings.minZoom, }; @@ -197,9 +198,6 @@ export class MBMap extends Component { const mbMap = new mapboxgl.Map(options); mbMap.dragRotate.disable(); mbMap.touchZoomRotate.disableRotation(); - if (!this.props.settings.disableInteractive) { - mbMap.addControl(new mapboxgl.NavigationControl({ showCompass: false }), 'top-left'); - } const tooManyFeaturesImageSrc = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAA7DgAAOw4BzLahgwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAARLSURBVHic7ZnPbxRVAMe/7735sWO3293ZlUItJsivCxEE0oTYRgu1FqTQoFSwKTYx8SAH/wHjj4vRozGGi56sMcW2UfqTEuOhppE0KJc2GIuKQFDY7qzdtrudX88D3YTUdFuQN8+k87ltZt7uZz958/bNLAGwBWsYKltANmEA2QKyCQPIFpBNGEC2gGzCALIFZBMGkC0gmzCAbAHZhAFkC8gmDCBbQDZhANkCslnzARQZH6oDpNs0D5UDSUIInePcOpPLfdfnODNBuwQWIAWwNOABwHZN0x8npE6hNLJ4DPWRyFSf40wE5VOEQPBjcR0g3YlE4ybGmtK+/1NzJtOZA/xSYwZMs3nG962T2ez3It2AANaA/kSidYuivOQBs5WM1fUnk6f0u+GXJUqIuUtVXx00zRbRfkIDfBqL7a1WlIYbjvNtTTr99jXXHVpH6dMjK0R4cXq6c9rzxjcx9sKX8XitSEdhAToMI7VP10/97fsTh7PZrgWAN1lW72KE2vOm2b5chDTgtWQyn93x/bEEIetEOQIC14CxVOr1CkKefH929t0v8vn0vcdGEoljGxXl4C3PGz2YyXy+AHARDqtByAxoUdWKBKV70r4/vvTLA0CjZfX+5nkDGxirKzUTgkBIgNaysh3gnF627R+XO+dQJvP1ddcdrmSsbtA020pF+CAW21qrqmUiXIUEqGRsIwD0FQq/lzqv0bJ6rrvucBVjzwyb5ivLRTiiaW+8VV7eIEBVTAANiIIQd9RxZlc6t9Gyem647vn1jD07ZJonl4sQASoevqmgABzwwHnJzc69PGdZ3X+47sgGxuqHTPPE0ggeVtg5/QeEBMhxPg1Aa1DV2GrHPG9ZXy1G2D+wNALn9jyQEeHKAJgP+033Kgrdqij7AFwZtu3bqx3XWShMHtV1o1pRGo4YxiNd+fyEB2DKdX/4aG5u0hbwcylkBryTy/3scT6zW9Nq7ndso2Wdvea6Q1WUHuiPx1/WAXLBcWZXun94UMRcAoD/p+ddTFK6u8MwUvc7vsmyem+67oVqVT0wkEgcF+FYRNhW+L25uX6f84XThtHxIBudE5bVY/t++jFVrU/dvVSFICzAqG3PX/S8rihj2/61qK1AOUB7ksl2jdLUL7Z9rvgcQQRCFsEi5wqFmw26XnhCUQ63GcZmCly95Lrzpca0G0byk3j8tEnpU1c975tmyxoU5QcE8EAEAM5WVOzfoarHAeC2749dcpzxMwsLv07Ztg0AOzVNf03Ttu/S9T2PMlbjc25fdpyutmx2TLRbIAEA4M1otKo1EjmaoHQn4ZwBgA/kAVAK6MXXdzxv/ONcrq/HcbJBeAUWoEizqsaORaPbKglZrxMSZZyrM76f/ovzWx/m85PFWREUgQf4v7Hm/xcIA8gWkE0YQLaAbMIAsgVkEwaQLSCbMIBsAdmEAWQLyCYMIFtANmEA2QKyCQPIFpDNmg/wD3OFdEybUvJjAAAAAElFTkSuQmCC'; @@ -357,6 +355,28 @@ export class MBMap extends Component { return; } + if ( + this._prevDisableInteractive === undefined || + this._prevDisableInteractive !== this.props.settings.disableInteractive + ) { + this._prevDisableInteractive = this.props.settings.disableInteractive; + if (this.props.settings.disableInteractive) { + this.state.mbMap.boxZoom.disable(); + this.state.mbMap.doubleClickZoom.disable(); + this.state.mbMap.dragPan.disable(); + try { + this.state.mbMap.removeControl(this._navigationControl); + } catch (error) { + // ignore removeControl errors + } + } else { + this.state.mbMap.boxZoom.enable(); + this.state.mbMap.doubleClickZoom.enable(); + this.state.mbMap.dragPan.enable(); + this.state.mbMap.addControl(this._navigationControl, 'top-left'); + } + } + let zoomRangeChanged = false; if (this.props.settings.minZoom !== this.state.mbMap.getMinZoom()) { this.state.mbMap.setMinZoom(this.props.settings.minZoom); diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js index 89eef907b22593..9e5a6080c830d8 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/view.js @@ -8,7 +8,7 @@ import React from 'react'; import classNames from 'classnames'; -import { EuiIcon, EuiOverlayMask, EuiButtonIcon, EuiConfirmModal } from '@elastic/eui'; +import { EuiIcon, EuiButtonIcon, EuiConfirmModal } from '@elastic/eui'; import { TOCEntryActionsPopover } from './toc_entry_actions_popover'; import { i18n } from '@kbn/i18n'; @@ -100,20 +100,18 @@ export class TOCEntry extends React.Component { }; return ( - - -

    There are unsaved changes to your layer.

    -

    Are you sure you want to proceed?

    -
    -
    + +

    There are unsaved changes to your layer.

    +

    Are you sure you want to proceed?

    +
    ); } diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index b769ac489f565e..b7e50815fd1f71 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -29,6 +29,7 @@ import { } from '../../../../../src/plugins/data/public'; import { replaceLayerList, + setMapSettings, setQuery, setRefreshConfig, disableScrollZoom, @@ -43,6 +44,7 @@ import { } from '../reducers/non_serializable_instances'; import { getMapCenter, + getMapBuffer, getMapZoom, getHiddenLayerIds, getQueryableUniqueIndexPatternIds, @@ -60,6 +62,7 @@ import { getCoreI18n, getHttp, getChartsPaletteServiceGetColor, + getSearchService, } from '../kibana_services'; import { LayerDescriptor } from '../../common/descriptor_types'; import { MapContainer } from '../connected_components/map_container'; @@ -77,6 +80,14 @@ import { } from './types'; export { MapEmbeddableInput, MapEmbeddableOutput }; +function getIsRestore(searchSessionId?: string) { + if (!searchSessionId) { + return false; + } + const searchSessionOptions = getSearchService().session.getSearchOptions(searchSessionId); + return searchSessionOptions ? searchSessionOptions.isRestore : false; +} + export class MapEmbeddable extends Embeddable implements ReferenceOrValueEmbeddable { @@ -85,6 +96,7 @@ export class MapEmbeddable private _savedMap: SavedMap; private _renderTooltipContent?: RenderToolTipContent; private _subscription: Subscription; + private _prevIsRestore: boolean = false; private _prevTimeRange?: TimeRange; private _prevQuery?: Query; private _prevRefreshConfig?: RefreshInterval; @@ -140,11 +152,7 @@ export class MapEmbeddable store.dispatch(disableScrollZoom()); this._dispatchSetQuery({ - query: this.input.query, - timeRange: this.input.timeRange, - filters: this.input.filters, forceRefresh: false, - searchSessionId: this.input.searchSessionId, }); if (this.input.refreshConfig) { this._dispatchSetRefreshConfig(this.input.refreshConfig); @@ -219,11 +227,7 @@ export class MapEmbeddable this.input.searchSessionId !== this._prevSearchSessionId ) { this._dispatchSetQuery({ - query: this.input.query, - timeRange: this.input.timeRange, - filters: this.input.filters, forceRefresh: false, - searchSessionId: this.input.searchSessionId, }); } @@ -234,32 +238,37 @@ export class MapEmbeddable if (this.input.syncColors !== this._prevSyncColors) { this._dispatchSetChartsPaletteServiceGetColor(this.input.syncColors); } + + const isRestore = getIsRestore(this.input.searchSessionId); + if (isRestore !== this._prevIsRestore) { + this._prevIsRestore = isRestore; + this._savedMap.getStore().dispatch( + setMapSettings({ + disableInteractive: isRestore, + hideToolbarOverlay: isRestore, + }) + ); + } } - _dispatchSetQuery({ - query, - timeRange, - filters = [], - forceRefresh, - searchSessionId, - }: { - query?: Query; - timeRange?: TimeRange; - filters?: Filter[]; - forceRefresh: boolean; - searchSessionId?: string; - }) { - this._prevTimeRange = timeRange; - this._prevQuery = query; - this._prevFilters = filters; - this._prevSearchSessionId = searchSessionId; + _dispatchSetQuery({ forceRefresh }: { forceRefresh: boolean }) { + this._prevTimeRange = this.input.timeRange; + this._prevQuery = this.input.query; + this._prevFilters = this.input.filters; + this._prevSearchSessionId = this.input.searchSessionId; + const enabledFilters = this.input.filters + ? this.input.filters.filter((filter) => !filter.meta.disabled) + : []; this._savedMap.getStore().dispatch( setQuery({ - filters: filters.filter((filter) => !filter.meta.disabled), - query, - timeFilters: timeRange, + filters: enabledFilters, + query: this.input.query, + timeFilters: this.input.timeRange, forceRefresh, - searchSessionId, + searchSessionId: this.input.searchSessionId, + searchSessionMapBuffer: getIsRestore(this.input.searchSessionId) + ? this.input.mapBuffer + : undefined, }) ); } @@ -410,11 +419,7 @@ export class MapEmbeddable reload() { this._dispatchSetQuery({ - query: this.input.query, - timeRange: this.input.timeRange, - filters: this.input.filters, forceRefresh: true, - searchSessionId: this.input.searchSessionId, }); } @@ -435,6 +440,7 @@ export class MapEmbeddable lon: center.lon, zoom, }, + mapBuffer: getMapBuffer(this._savedMap.getStore().getState()), }); } diff --git a/x-pack/plugins/maps/public/embeddable/types.ts b/x-pack/plugins/maps/public/embeddable/types.ts index 67489802bc31d1..7cd4fa8e1253bd 100644 --- a/x-pack/plugins/maps/public/embeddable/types.ts +++ b/x-pack/plugins/maps/public/embeddable/types.ts @@ -12,7 +12,7 @@ import { SavedObjectEmbeddableInput, } from '../../../../../src/plugins/embeddable/public'; import { RefreshInterval, Query, Filter, TimeRange } from '../../../../../src/plugins/data/common'; -import { MapCenterAndZoom } from '../../common/descriptor_types'; +import { MapCenterAndZoom, MapExtent } from '../../common/descriptor_types'; import { MapSavedObjectAttributes } from '../../common/map_saved_object_type'; import { MapSettings } from '../reducers/map'; @@ -25,6 +25,7 @@ interface MapEmbeddableState { isLayerTOCOpen?: boolean; openTOCDetails?: string[]; mapCenter?: MapCenterAndZoom; + mapBuffer?: MapExtent; mapSettings?: Partial; hiddenLayers?: string[]; hideFilterActions?: boolean; diff --git a/x-pack/plugins/maps/public/reducers/map.d.ts b/x-pack/plugins/maps/public/reducers/map.d.ts index 6a51d4feeb9dfe..1cf37561609640 100644 --- a/x-pack/plugins/maps/public/reducers/map.d.ts +++ b/x-pack/plugins/maps/public/reducers/map.d.ts @@ -37,6 +37,7 @@ export type MapContext = { refreshTimerLastTriggeredAt?: string; drawState?: DrawState; searchSessionId?: string; + searchSessionMapBuffer?: MapExtent; }; export type MapSettings = { diff --git a/x-pack/plugins/maps/public/reducers/map.js b/x-pack/plugins/maps/public/reducers/map.js index fa7e1308bac4f4..9bf0df612bac40 100644 --- a/x-pack/plugins/maps/public/reducers/map.js +++ b/x-pack/plugins/maps/public/reducers/map.js @@ -241,7 +241,7 @@ export function map(state = DEFAULT_MAP_STATE, action) { }; return { ...state, mapState: { ...state.mapState, ...newMapState } }; case SET_QUERY: - const { query, timeFilters, filters, searchSessionId } = action; + const { query, timeFilters, filters, searchSessionId, searchSessionMapBuffer } = action; return { ...state, mapState: { @@ -250,6 +250,7 @@ export function map(state = DEFAULT_MAP_STATE, action) { timeFilters, filters, searchSessionId, + searchSessionMapBuffer, }, }; case SET_REFRESH_CONFIG: diff --git a/x-pack/plugins/maps/public/reducers/store.js b/x-pack/plugins/maps/public/reducers/store.js index 76199de5b24c92..f1020b2d6cdb69 100644 --- a/x-pack/plugins/maps/public/reducers/store.js +++ b/x-pack/plugins/maps/public/reducers/store.js @@ -10,7 +10,6 @@ import thunk from 'redux-thunk'; import { ui, DEFAULT_MAP_UI_STATE } from './ui'; import { map, DEFAULT_MAP_STATE } from './map'; // eslint-disable-line import/named import { nonSerializableInstances } from './non_serializable_instances'; -import { MAP_DESTROYED } from '../actions'; export const DEFAULT_MAP_STORE_STATE = { ui: { ...DEFAULT_MAP_UI_STATE }, @@ -26,16 +25,7 @@ export function createMapStore() { nonSerializableInstances, }); - const rootReducer = (state, action) => { - // Reset store on map destroyed - if (action.type === MAP_DESTROYED) { - state = undefined; - } - - return combinedReducers(state, action); - }; - const storeConfig = {}; const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; - return createStore(rootReducer, storeConfig, composeEnhancers(...enhancers)); + return createStore(combinedReducers, storeConfig, composeEnhancers(...enhancers)); } diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.test.ts b/x-pack/plugins/maps/public/selectors/map_selectors.test.ts index 89cd80f4daab50..58268b6ea9d82c 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.test.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.test.ts @@ -26,9 +26,67 @@ jest.mock('../kibana_services', () => ({ })); import { DEFAULT_MAP_STORE_STATE } from '../reducers/store'; -import { areLayersLoaded, getTimeFilters } from './map_selectors'; +import { areLayersLoaded, getDataFilters, getTimeFilters } from './map_selectors'; import { LayerDescriptor } from '../../common/descriptor_types'; import { ILayer } from '../classes/layers/layer'; +import { Filter } from '../../../../../src/plugins/data/public'; + +describe('getDataFilters', () => { + const mapExtent = { + maxLat: 1, + maxLon: 1, + minLat: 0, + minLon: 0, + }; + const mapBuffer = { + maxLat: 1.5, + maxLon: 1.5, + minLat: -0.5, + minLon: -0.5, + }; + const mapZoom = 4; + const timeFilters = { to: '2001-01-01', from: '2001-12-31' }; + const refreshTimerLastTriggeredAt = '2001-01-01T00:00:00'; + const query = undefined; + const filters: Filter[] = []; + const searchSessionId = '12345'; + const searchSessionMapBuffer = { + maxLat: 1.25, + maxLon: 1.25, + minLat: -0.25, + minLon: -0.25, + }; + + test('should set buffer as searchSessionMapBuffer when using searchSessionId', () => { + const dataFilters = getDataFilters.resultFunc( + mapExtent, + mapBuffer, + mapZoom, + timeFilters, + refreshTimerLastTriggeredAt, + query, + filters, + searchSessionId, + searchSessionMapBuffer + ); + expect(dataFilters.buffer).toEqual(searchSessionMapBuffer); + }); + + test('should fall back to screen buffer when using searchSessionId and searchSessionMapBuffer is not provided', () => { + const dataFilters = getDataFilters.resultFunc( + mapExtent, + mapBuffer, + mapZoom, + timeFilters, + refreshTimerLastTriggeredAt, + query, + filters, + searchSessionId, + undefined + ); + expect(dataFilters.buffer).toEqual(mapBuffer); + }); +}); describe('getTimeFilters', () => { test('should return timeFilters when contained in state', () => { @@ -66,12 +124,12 @@ describe('getTimeFilters', () => { describe('areLayersLoaded', () => { function createLayerMock({ hasErrors = false, - isDataLoaded = false, + isInitialDataLoadComplete = false, isVisible = true, showAtZoomLevel = true, }: { hasErrors?: boolean; - isDataLoaded?: boolean; + isInitialDataLoadComplete?: boolean; isVisible?: boolean; showAtZoomLevel?: boolean; }) { @@ -79,8 +137,8 @@ describe('areLayersLoaded', () => { hasErrors: () => { return hasErrors; }, - isDataLoaded: () => { - return isDataLoaded; + isInitialDataLoadComplete: () => { + return isInitialDataLoadComplete; }, isVisible: () => { return isVisible; @@ -99,35 +157,37 @@ describe('areLayersLoaded', () => { }); test('layer should not be counted as loaded if it has not loaded', () => { - const layerList = [createLayerMock({ isDataLoaded: false })]; + const layerList = [createLayerMock({ isInitialDataLoadComplete: false })]; const waitingForMapReadyLayerList: LayerDescriptor[] = []; const zoom = 4; expect(areLayersLoaded.resultFunc(layerList, waitingForMapReadyLayerList, zoom)).toBe(false); }); test('layer should be counted as loaded if its not visible', () => { - const layerList = [createLayerMock({ isVisible: false, isDataLoaded: false })]; + const layerList = [createLayerMock({ isVisible: false, isInitialDataLoadComplete: false })]; const waitingForMapReadyLayerList: LayerDescriptor[] = []; const zoom = 4; expect(areLayersLoaded.resultFunc(layerList, waitingForMapReadyLayerList, zoom)).toBe(true); }); test('layer should be counted as loaded if its not shown at zoom level', () => { - const layerList = [createLayerMock({ showAtZoomLevel: false, isDataLoaded: false })]; + const layerList = [ + createLayerMock({ showAtZoomLevel: false, isInitialDataLoadComplete: false }), + ]; const waitingForMapReadyLayerList: LayerDescriptor[] = []; const zoom = 4; expect(areLayersLoaded.resultFunc(layerList, waitingForMapReadyLayerList, zoom)).toBe(true); }); test('layer should be counted as loaded if it has a loading error', () => { - const layerList = [createLayerMock({ hasErrors: true, isDataLoaded: false })]; + const layerList = [createLayerMock({ hasErrors: true, isInitialDataLoadComplete: false })]; const waitingForMapReadyLayerList: LayerDescriptor[] = []; const zoom = 4; expect(areLayersLoaded.resultFunc(layerList, waitingForMapReadyLayerList, zoom)).toBe(true); }); test('layer should be counted as loaded if its loaded', () => { - const layerList = [createLayerMock({ isDataLoaded: true })]; + const layerList = [createLayerMock({ isInitialDataLoadComplete: true })]; const waitingForMapReadyLayerList: LayerDescriptor[] = []; const zoom = 4; expect(areLayersLoaded.resultFunc(layerList, waitingForMapReadyLayerList, zoom)).toBe(true); diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts index b16ac704c3715b..35c73a2bd2f1c7 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts @@ -183,6 +183,9 @@ export const getFilters = ({ map }: MapStoreState): Filter[] => map.mapState.fil export const getSearchSessionId = ({ map }: MapStoreState): string | undefined => map.mapState.searchSessionId; +export const getSearchSessionMapBuffer = ({ map }: MapStoreState): MapExtent | undefined => + map.mapState.searchSessionMapBuffer; + export const isUsingSearch = (state: MapStoreState): boolean => { const filters = getFilters(state).filter((filter) => !filter.meta.disabled); const queryString = _.get(getQuery(state), 'query', ''); @@ -235,6 +238,7 @@ export const getDataFilters = createSelector( getQuery, getFilters, getSearchSessionId, + getSearchSessionMapBuffer, ( mapExtent, mapBuffer, @@ -243,11 +247,12 @@ export const getDataFilters = createSelector( refreshTimerLastTriggeredAt, query, filters, - searchSessionId + searchSessionId, + searchSessionMapBuffer ) => { return { extent: mapExtent, - buffer: mapBuffer, + buffer: searchSessionId && searchSessionMapBuffer ? searchSessionMapBuffer : mapBuffer, zoom: mapZoom, timeFilters, refreshTimerLastTriggeredAt, @@ -432,7 +437,7 @@ export const areLayersLoaded = createSelector( layer.isVisible() && layer.showAtZoomLevel(zoom) && !layer.hasErrors() && - !layer.isDataLoaded() + !layer.isInitialDataLoadComplete() ) { return false; } diff --git a/x-pack/plugins/ml/common/types/alerts.ts b/x-pack/plugins/ml/common/types/alerts.ts index 51d06b99062300..7e6e9d89c5a651 100644 --- a/x-pack/plugins/ml/common/types/alerts.ts +++ b/x-pack/plugins/ml/common/types/alerts.ts @@ -15,7 +15,7 @@ export type TopHitsResultsKeys = 'top_record_hits' | 'top_bucket_hits' | 'top_in export interface AlertExecutionResult { count: number; key: number; - key_as_string: string; + alertInstanceKey: string; isInterim: boolean; jobIds: string[]; timestamp: number; @@ -47,10 +47,13 @@ interface BaseAnomalyAlertDoc { export interface RecordAnomalyAlertDoc extends BaseAnomalyAlertDoc { result_type: typeof ANOMALY_RESULT_TYPE.RECORD; function: string; - field_name: string; - by_field_value: string | number; - over_field_value: string | number; - partition_field_value: string | number; + field_name?: string; + by_field_name?: string; + by_field_value?: string | number; + over_field_name?: string; + over_field_value?: string | number; + partition_field_name?: string; + partition_field_value?: string | number; } export interface BucketAnomalyAlertDoc extends BaseAnomalyAlertDoc { diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts index bb6b331c10fc18..09f5c37ac9aeaf 100644 --- a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts +++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/summary_job.ts @@ -49,6 +49,7 @@ export type MlSummaryJobs = MlSummaryJob[]; export interface MlJobWithTimeRange extends CombinedJobWithStats { id: string; + isRunning?: boolean; isNotSingleMetricViewerJobMessage?: string; timeRange: { from: number; diff --git a/x-pack/plugins/ml/common/types/data_frame_analytics.ts b/x-pack/plugins/ml/common/types/data_frame_analytics.ts index cacc5acb9768f4..95d82932a1212e 100644 --- a/x-pack/plugins/ml/common/types/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/types/data_frame_analytics.ts @@ -34,9 +34,10 @@ interface Regression { } interface Classification { + class_assignment_objective?: string; dependent_variable: string; training_percent?: number; - num_top_classes?: string; + num_top_classes?: number; num_top_feature_importance_values?: number; prediction_field_name?: string; } diff --git a/x-pack/plugins/ml/common/types/feature_importance.ts b/x-pack/plugins/ml/common/types/feature_importance.ts index 2e45c3cd4d8c44..964ce8c3257838 100644 --- a/x-pack/plugins/ml/common/types/feature_importance.ts +++ b/x-pack/plugins/ml/common/types/feature_importance.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { isPopulatedObject } from '../util/object_utils'; + export type FeatureImportanceClassName = string | number | boolean; export interface ClassFeatureImportance { @@ -87,7 +89,7 @@ export function isClassificationFeatureImportanceBaseline( baselineData: any ): baselineData is ClassificationFeatureImportanceBaseline { return ( - typeof baselineData === 'object' && + isPopulatedObject(baselineData) && baselineData.hasOwnProperty('classes') && Array.isArray(baselineData.classes) ); @@ -96,5 +98,5 @@ export function isClassificationFeatureImportanceBaseline( export function isRegressionFeatureImportanceBaseline( baselineData: any ): baselineData is RegressionFeatureImportanceBaseline { - return typeof baselineData === 'object' && baselineData.hasOwnProperty('baseline'); + return isPopulatedObject(baselineData) && baselineData.hasOwnProperty('baseline'); } diff --git a/x-pack/plugins/ml/common/types/fields.ts b/x-pack/plugins/ml/common/types/fields.ts index ae157cef5735fc..047852534965c2 100644 --- a/x-pack/plugins/ml/common/types/fields.ts +++ b/x-pack/plugins/ml/common/types/fields.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ES_FIELD_TYPES, RuntimeField } from '../../../../../src/plugins/data/common'; +import { ES_FIELD_TYPES } from '../../../../../src/plugins/data/common'; import { ML_JOB_AGGREGATION, KIBANA_AGGREGATION, @@ -106,4 +106,18 @@ export interface AggCardinality { } export type RollupFields = Record]>; + +// Replace this with import once #88995 is merged +const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; +type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; + +export interface RuntimeField { + type: RuntimeType; + script?: + | string + | { + source: string; + }; +} + export type RuntimeMappings = Record; diff --git a/x-pack/plugins/ml/common/util/anomaly_utils.ts b/x-pack/plugins/ml/common/util/anomaly_utils.ts index 028afee2524c93..68605f29c7be96 100644 --- a/x-pack/plugins/ml/common/util/anomaly_utils.ts +++ b/x-pack/plugins/ml/common/util/anomaly_utils.ts @@ -230,8 +230,6 @@ export function getEntityFieldName(record: AnomalyRecordDoc): string | undefined if (record.partition_field_name !== undefined) { return record.partition_field_name; } - - return undefined; } // Returns the value of the field to use as the entity value from the source record @@ -249,8 +247,6 @@ export function getEntityFieldValue(record: AnomalyRecordDoc): string | number | if (record.partition_field_value !== undefined) { return record.partition_field_value; } - - return undefined; } // Returns the list of partitioning entity fields for the source record as a list diff --git a/x-pack/plugins/ml/common/util/datafeed_utils.ts b/x-pack/plugins/ml/common/util/datafeed_utils.ts index fa1a940ba5492c..c0579ce947992a 100644 --- a/x-pack/plugins/ml/common/util/datafeed_utils.ts +++ b/x-pack/plugins/ml/common/util/datafeed_utils.ts @@ -20,7 +20,7 @@ export const getDatafeedAggregations = ( }; export const getAggregationBucketsName = (aggregations: any): string | undefined => { - if (typeof aggregations === 'object') { + if (aggregations !== null && typeof aggregations === 'object') { const keys = Object.keys(aggregations); return keys.length > 0 ? keys[0] : undefined; } diff --git a/x-pack/plugins/ml/common/util/job_utils.ts b/x-pack/plugins/ml/common/util/job_utils.ts index 711103b499ec90..ab56726e160f7c 100644 --- a/x-pack/plugins/ml/common/util/job_utils.ts +++ b/x-pack/plugins/ml/common/util/job_utils.ts @@ -28,6 +28,7 @@ import { getDatafeedAggregations, } from './datafeed_utils'; import { findAggField } from './validation_utils'; +import { isPopulatedObject } from './object_utils'; export interface ValidationResults { valid: boolean; @@ -51,17 +52,9 @@ export function calculateDatafeedFrequencyDefaultSeconds(bucketSpanSeconds: numb } export function hasRuntimeMappings(job: CombinedJob): boolean { - const hasDatafeed = - typeof job.datafeed_config === 'object' && Object.keys(job.datafeed_config).length > 0; + const hasDatafeed = isPopulatedObject(job.datafeed_config); if (hasDatafeed) { - const runtimeMappings = - typeof job.datafeed_config.runtime_mappings === 'object' - ? Object.keys(job.datafeed_config.runtime_mappings) - : undefined; - - if (Array.isArray(runtimeMappings) && runtimeMappings.length > 0) { - return true; - } + return isPopulatedObject(job.datafeed_config.runtime_mappings); } return false; } @@ -114,7 +107,11 @@ export function isSourceDataChartableForDetector(job: CombinedJob, detectorIndex // If the datafeed uses script fields, we can only plot the time series if // model plot is enabled. Without model plot it will be very difficult or impossible // to invert to a reverse search of the underlying metric data. - if (isSourceDataChartable === true && typeof job.datafeed_config?.script_fields === 'object') { + if ( + isSourceDataChartable === true && + job.datafeed_config?.script_fields !== null && + typeof job.datafeed_config?.script_fields === 'object' + ) { // Perform extra check to see if the detector is using a scripted field. const scriptFields = Object.keys(job.datafeed_config.script_fields); isSourceDataChartable = @@ -123,8 +120,7 @@ export function isSourceDataChartableForDetector(job: CombinedJob, detectorIndex scriptFields.indexOf(dtr.over_field_name!) === -1; } - const hasDatafeed = - typeof job.datafeed_config === 'object' && Object.keys(job.datafeed_config).length > 0; + const hasDatafeed = isPopulatedObject(job.datafeed_config); if (hasDatafeed) { // We cannot plot the source data for some specific aggregation configurations const aggs = getDatafeedAggregations(job.datafeed_config); diff --git a/x-pack/plugins/ml/common/util/object_utils.ts b/x-pack/plugins/ml/common/util/object_utils.ts new file mode 100644 index 00000000000000..4bbd0c1c2810fe --- /dev/null +++ b/x-pack/plugins/ml/common/util/object_utils.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const isPopulatedObject = >(arg: any): arg is T => { + return typeof arg === 'object' && arg !== null && Object.keys(arg).length > 0; +}; diff --git a/x-pack/plugins/ml/common/util/validation_utils.ts b/x-pack/plugins/ml/common/util/validation_utils.ts index 7f0208e726ab0b..66084f83ea87d1 100644 --- a/x-pack/plugins/ml/common/util/validation_utils.ts +++ b/x-pack/plugins/ml/common/util/validation_utils.ts @@ -45,7 +45,7 @@ export function findAggField( value = returnParent === true ? aggs : aggs[k]; return true; } - if (aggs.hasOwnProperty(k) && typeof aggs[k] === 'object') { + if (aggs.hasOwnProperty(k) && aggs[k] !== null && typeof aggs[k] === 'object') { value = findAggField(aggs[k], fieldName, returnParent); return value !== undefined; } diff --git a/x-pack/plugins/ml/public/application/components/annotations/delete_annotation_modal/index.tsx b/x-pack/plugins/ml/public/application/components/annotations/delete_annotation_modal/index.tsx index 8469d42c16c519..9999fad89d0e1c 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/delete_annotation_modal/index.tsx +++ b/x-pack/plugins/ml/public/application/components/annotations/delete_annotation_modal/index.tsx @@ -8,7 +8,7 @@ import PropTypes from 'prop-types'; import React, { Fragment } from 'react'; -import { EUI_MODAL_CONFIRM_BUTTON, EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EUI_MODAL_CONFIRM_BUTTON, EuiConfirmModal } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -26,33 +26,31 @@ export const DeleteAnnotationModal: React.FC = ({ return ( {isVisible === true && ( - - - } - onCancel={cancelAction} - onConfirm={deleteAction} - cancelButtonText={ - - } - confirmButtonText={ - - } - buttonColor="danger" - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - className="eui-textBreakWord" - /> - + + } + onCancel={cancelAction} + onConfirm={deleteAction} + cancelButtonText={ + + } + confirmButtonText={ + + } + buttonColor="danger" + defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + className="eui-textBreakWord" + /> )} ); 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 index f169c56205e08e..069c13df2470f7 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -27,7 +27,11 @@ import { import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants/data_frame_analytics'; import { extractErrorMessage } from '../../../../common/util/errors'; -import { FeatureImportance, TopClasses } from '../../../../common/types/feature_importance'; +import { + FeatureImportance, + FeatureImportanceClassName, + TopClasses, +} from '../../../../common/types/feature_importance'; import { BASIC_NUMERICAL_TYPES, @@ -44,6 +48,9 @@ import { getNestedProperty } from '../../util/object_utils'; import { mlFieldFormatService } from '../../services/field_format_service'; import { DataGridItem, IndexPagination, RenderCellValue } from './types'; +import type { RuntimeField } from '../../../../../../../src/plugins/data/common/index_patterns'; +import { RuntimeMappings } from '../../../../common/types/fields'; +import { isPopulatedObject } from '../../../../common/util/object_utils'; export const INIT_MAX_COLUMNS = 10; @@ -82,6 +89,37 @@ export const getFieldsFromKibanaIndexPattern = (indexPattern: IndexPattern): str return indexPatternFields; }; +/** + * Return a map of runtime_mappings for each of the index pattern field provided + * to provide in ES search queries + * @param indexPatternFields + * @param indexPattern + * @param clonedRuntimeMappings + */ +export const getRuntimeFieldsMapping = ( + indexPatternFields: string[] | undefined, + indexPattern: IndexPattern | undefined, + clonedRuntimeMappings?: RuntimeMappings +) => { + if (!Array.isArray(indexPatternFields) || indexPattern === undefined) return {}; + const ipRuntimeMappings = indexPattern.getComputedFields().runtimeFields; + let combinedRuntimeMappings: RuntimeMappings = {}; + + if (isPopulatedObject(ipRuntimeMappings)) { + indexPatternFields.forEach((ipField) => { + if (ipRuntimeMappings.hasOwnProperty(ipField)) { + combinedRuntimeMappings[ipField] = ipRuntimeMappings[ipField]; + } + }); + } + if (isPopulatedObject(clonedRuntimeMappings)) { + combinedRuntimeMappings = { ...combinedRuntimeMappings, ...clonedRuntimeMappings }; + } + return Object.keys(combinedRuntimeMappings).length > 0 + ? { runtime_mappings: combinedRuntimeMappings } + : {}; +}; + export interface FieldTypes { [key: string]: ES_FIELD_TYPES; } @@ -131,6 +169,45 @@ export const getDataGridSchemasFromFieldTypes = (fieldTypes: FieldTypes, results }; export const NON_AGGREGATABLE = 'non-aggregatable'; + +export const getDataGridSchemaFromESFieldType = ( + fieldType: ES_FIELD_TYPES | undefined | RuntimeField['type'] +): string | 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 (fieldType) { + 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 schema; +}; + export const getDataGridSchemaFromKibanaFieldType = ( field: IFieldType | undefined ): string | undefined => { @@ -168,8 +245,9 @@ const getClassName = (className: string, isClassTypeBoolean: boolean) => { return className; }; + /** - * Helper to transform feature importance flattened fields with arrays back to object structure + * Helper to transform feature importance fields with arrays back to primitive value * * @param row - EUI data grid data row * @param mlResultsField - Data frame analytics results field @@ -180,69 +258,44 @@ export const getFeatureImportance = ( mlResultsField: string, isClassTypeBoolean = false ): FeatureImportance[] => { - const featureNames: string[] | undefined = - row[`${mlResultsField}.feature_importance.feature_name`]; - const classNames: string[] | undefined = - row[`${mlResultsField}.feature_importance.classes.class_name`]; - const classImportance: number[] | undefined = - row[`${mlResultsField}.feature_importance.classes.importance`]; - - if (featureNames === undefined) { - return []; - } - - // return object structure for classification job - if (classNames !== undefined && classImportance !== undefined) { - const overallClassNames = classNames?.slice(0, classNames.length / featureNames.length); - - return featureNames.map((fName, index) => { - const offset = overallClassNames.length * index; - const featureClassImportance = classImportance.slice( - offset, - offset + overallClassNames.length - ); - return { - feature_name: fName, - classes: overallClassNames.map((fClassName, fIndex) => { + const featureImportance: Array<{ + feature_name: string[]; + classes?: Array<{ class_name: FeatureImportanceClassName[]; importance: number[] }>; + importance?: number | number[]; + }> = row[`${mlResultsField}.feature_importance`]; + if (featureImportance === undefined) return []; + + return featureImportance.map((fi) => ({ + feature_name: Array.isArray(fi.feature_name) ? fi.feature_name[0] : fi.feature_name, + classes: Array.isArray(fi.classes) + ? fi.classes.map((c) => { + const processedClass = getProcessedFields(c); return { - class_name: getClassName(fClassName, isClassTypeBoolean), - importance: featureClassImportance[fIndex], + importance: processedClass.importance, + class_name: getClassName(processedClass.class_name, isClassTypeBoolean), }; - }), - }; - }); - } - - // return object structure for regression job - const importance: number[] = row[`${mlResultsField}.feature_importance.importance`]; - return featureNames.map((fName, index) => ({ - feature_name: fName, - importance: importance[index], + }) + : fi.classes, + importance: Array.isArray(fi.importance) ? fi.importance[0] : fi.importance, })); }; /** - * Helper to transforms top classes flattened fields with arrays back to object structure + * Helper to transforms top classes fields with arrays back to original primitive value * * @param row - EUI data grid data row * @param mlResultsField - Data frame analytics results field * @returns nested object structure of feature importance values */ export const getTopClasses = (row: Record, mlResultsField: string): TopClasses => { - const classNames: string[] | undefined = row[`${mlResultsField}.top_classes.class_name`]; - const classProbabilities: number[] | undefined = - row[`${mlResultsField}.top_classes.class_probability`]; - const classScores: number[] | undefined = row[`${mlResultsField}.top_classes.class_score`]; - - if (classNames === undefined || classProbabilities === undefined || classScores === undefined) { - return []; - } - - return classNames.map((className, index) => ({ - class_name: className, - class_probability: classProbabilities[index], - class_score: classScores[index], - })); + const topClasses: Array<{ + class_name: FeatureImportanceClassName[]; + class_probability: number[]; + class_score: number[]; + }> = row[`${mlResultsField}.top_classes`]; + + if (topClasses === undefined) return []; + return topClasses.map((tc) => getProcessedFields(tc)) as TopClasses; }; export const useRenderCellValue = ( 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 index da34e0f1bc9fb9..5dad9801eb644b 100644 --- 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 @@ -35,7 +35,7 @@ import { getTopClasses, } from './common'; import { UseIndexDataReturnType } from './types'; -import { DecisionPathPopover } from './feature_importance/decision_path_popover'; +import { DecisionPathPopover } from '../../data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_popover'; import { FeatureImportanceBaseline, FeatureImportance, 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 index ccd2f3f56e45df..79a8d65f9905a2 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/index.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/index.ts @@ -7,8 +7,10 @@ export { getDataGridSchemasFromFieldTypes, + getDataGridSchemaFromESFieldType, getDataGridSchemaFromKibanaFieldType, getFieldsFromKibanaIndexPattern, + getRuntimeFieldsMapping, multiColumnSortFactory, showDataGridColumnChartErrorMessageToast, useRenderCellValue, diff --git a/x-pack/plugins/ml/public/application/components/delete_job_check_modal/delete_job_check_modal.tsx b/x-pack/plugins/ml/public/application/components/delete_job_check_modal/delete_job_check_modal.tsx index eda0509d417ca7..972ed06ba13859 100644 --- a/x-pack/plugins/ml/public/application/components/delete_job_check_modal/delete_job_check_modal.tsx +++ b/x-pack/plugins/ml/public/application/components/delete_job_check_modal/delete_job_check_modal.tsx @@ -13,7 +13,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiModal, - EuiOverlayMask, EuiModalHeader, EuiModalHeaderTitle, EuiModalBody, @@ -230,69 +229,67 @@ export const DeleteJobCheckModal: FC = ({ }; return ( - - - {isLoading === true && ( - <> - - - - - - - - - )} - {isLoading === false && ( - <> - - - - - + + {isLoading === true && ( + <> + + + + + + + + + )} + {isLoading === false && ( + <> + + + + + - {modalContent} + {modalContent} - - - - {!hasUntagged && + + + + {!hasUntagged && + jobCheckRespSummary?.canTakeAnyAction && + jobCheckRespSummary?.canRemoveFromSpace && + jobCheckRespSummary?.canDelete && ( + + {shouldUnTagLabel} + + )} + + + - {shouldUnTagLabel} - - )} - - - - {buttonContent} - - - - - - )} - - + !jobCheckRespSummary?.canDelete + ? onUntagClick + : onClick + } + fill + > + {buttonContent} + + + + + + )} + ); }; diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/close_job_confirm.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/close_job_confirm.tsx index 1d2bda90516b99..8fc4a0d636bce7 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/close_job_confirm.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/close_job_confirm/close_job_confirm.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { COMBINED_JOB_STATE } from '../model_snapshots_table'; @@ -25,56 +25,51 @@ export const CloseJobConfirm: FC = ({ forceCloseJob, }) => { return ( - - +

    + {combinedJobState === COMBINED_JOB_STATE.OPEN_AND_RUNNING && ( + )} - confirmButtonText={ - combinedJobState === COMBINED_JOB_STATE.OPEN_AND_RUNNING - ? i18n.translate('xpack.ml.modelSnapshotTable.closeJobConfirm.stopAndClose.button', { - defaultMessage: 'Force stop and close', - }) - : i18n.translate('xpack.ml.modelSnapshotTable.closeJobConfirm.close.button', { - defaultMessage: 'Force close', - }) - } - defaultFocusedButton="confirm" - > -

    - {combinedJobState === COMBINED_JOB_STATE.OPEN_AND_RUNNING && ( - - )} - {combinedJobState === COMBINED_JOB_STATE.OPEN_AND_STOPPED && ( - - )} -
    + {combinedJobState === COMBINED_JOB_STATE.OPEN_AND_STOPPED && ( -

    -
    -
    + )} +
    + +

    + ); }; diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx index 833e70fc86f4c4..20c98255930b4d 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/edit_model_snapshot_flyout/edit_model_snapshot_flyout.tsx @@ -22,7 +22,6 @@ import { EuiFormRow, EuiSwitch, EuiConfirmModal, - EuiOverlayMask, EuiCallOut, } from '@elastic/eui'; @@ -190,23 +189,21 @@ export const EditModelSnapshotFlyout: FC = ({ snapshot, job, closeFlyout {deleteModalVisible && ( - - - + )} ); diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx index 1929cddaca6b56..6dd4e6c14589b2 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/revert_model_snapshot_flyout.tsx @@ -22,7 +22,6 @@ import { EuiFormRow, EuiSwitch, EuiConfirmModal, - EuiOverlayMask, EuiCallOut, EuiHorizontalRule, EuiSuperSelect, @@ -368,34 +367,32 @@ export const RevertModelSnapshotFlyout: FC = ({ {revertModalVisible && ( - - - - - + + + )} ); diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap b/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap index a132e6682ee250..3a11531f6c4bc1 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap +++ b/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/__snapshots__/delete_rule_modal.test.js.snap @@ -42,34 +42,32 @@ exports[`DeleteRuleModal renders modal after clicking delete rule link 1`] = ` values={Object {}} /> - - - } - confirmButtonText={ - - } - defaultFocusedButton="confirm" - onCancel={[Function]} - onConfirm={[Function]} - title={ - - } - /> - + + } + confirmButtonText={ + + } + defaultFocusedButton="confirm" + onCancel={[Function]} + onConfirm={[Function]} + title={ + + } + /> `; diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js b/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js index 809bb780c33239..6caa6592e96c1e 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/select_rule_action/delete_rule_modal.js @@ -12,7 +12,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { EuiConfirmModal, EuiLink, EuiOverlayMask, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; +import { EuiConfirmModal, EuiLink, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; export class DeleteRuleModal extends Component { @@ -43,32 +43,30 @@ export class DeleteRuleModal extends Component { if (this.state.isModalVisible) { modal = ( - - - } - onCancel={this.closeModal} - onConfirm={this.deleteRule} - buttonColor="danger" - cancelButtonText={ - - } - confirmButtonText={ - - } - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - /> - + + } + onCancel={this.closeModal} + onConfirm={this.deleteRule} + buttonColor="danger" + cancelButtonText={ + + } + confirmButtonText={ + + } + defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + /> ); } diff --git a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js index a93264c852dd15..2b7c89db15e2e1 100644 --- a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js +++ b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js @@ -19,7 +19,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiSpacer, EuiText, EuiFlexGroup, @@ -161,24 +160,19 @@ const LoadingSpinner = () => ( ); const Modal = ({ close, title, children }) => ( - - - - {title} - - - {children} - - - - - - - - + + + {title} + + + {children} + + + + + + + ); Modal.propType = { close: PropTypes.func.isRequired, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_details.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_details.tsx index 469327323bee98..fc8798a3af0f57 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_details.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_details.tsx @@ -131,20 +131,6 @@ export const AdvancedStepDetails: FC<{ setCurrentStep: any; state: State }> = ({ } if (isRegOrClassJob) { - if (jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION) { - advancedFirstCol.push({ - title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.numTopClasses', { - defaultMessage: 'Top classes', - }), - description: - numTopClasses === -1 - ? i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.allClasses', { - defaultMessage: 'All classes', - }) - : getStringValue(numTopClasses), - }); - } - advancedFirstCol.push({ title: i18n.translate( 'xpack.ml.dataframe.analytics.create.configDetails.numTopFeatureImportanceValues', @@ -170,15 +156,23 @@ export const AdvancedStepDetails: FC<{ setCurrentStep: any; state: State }> = ({ } ); - advancedSecondCol.push({ - title: i18n.translate( - 'xpack.ml.dataframe.analytics.create.configDetails.predictionFieldName', - { - defaultMessage: 'Prediction field name', - } - ), - description: predictionFieldName ? predictionFieldName : `${dependentVariable}_prediction`, - }); + advancedSecondCol.push( + { + title: i18n.translate( + 'xpack.ml.dataframe.analytics.create.configDetails.predictionFieldName', + { + defaultMessage: 'Prediction field name', + } + ), + description: predictionFieldName ? predictionFieldName : `${dependentVariable}_prediction`, + }, + { + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.randomizedSeed', { + defaultMessage: 'Randomized seed', + }), + description: getStringValue(randomizeSeed), + } + ); hyperSecondCol.push( { @@ -205,20 +199,26 @@ export const AdvancedStepDetails: FC<{ setCurrentStep: any; state: State }> = ({ description: `${modelMemoryLimit}`, }); - hyperThirdCol.push( - { - title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.gamma', { - defaultMessage: 'Gamma', - }), - description: getStringValue(gamma), - }, - { - title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.randomizedSeed', { - defaultMessage: 'Randomized seed', - }), - description: getStringValue(randomizeSeed), - } - ); + hyperThirdCol.push({ + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.gamma', { + defaultMessage: 'Gamma', + }), + description: getStringValue(gamma), + }); + } + + if (jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION) { + advancedThirdCol.push({ + title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.numTopClasses', { + defaultMessage: 'Top classes', + }), + description: + numTopClasses === -1 + ? i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.allClasses', { + defaultMessage: 'All classes', + }) + : getStringValue(numTopClasses), + }); } if (maxNumThreads !== undefined) { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx index 8e25fc961c7c28..71770dcf952d97 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/advanced_step_form.tsx @@ -409,6 +409,34 @@ export const AdvancedStepForm: FC = ({ /> + + + + setFormState({ randomizeSeed: e.target.value === '' ? undefined : +e.target.value }) + } + isInvalid={randomizeSeed !== undefined && typeof randomizeSeed !== 'number'} + value={getNumberValue(randomizeSeed)} + step={1} + /> + + ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/hyper_parameters.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/hyper_parameters.tsx index cd4dd44edfa507..704b2cc77a7f93 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/hyper_parameters.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/advanced_step/hyper_parameters.tsx @@ -31,7 +31,6 @@ export const HyperParameters: FC = ({ actions, state, advancedParamErrors lambda, maxOptimizationRoundsPerHyperparameter, maxTrees, - randomizeSeed, softTreeDepthLimit, softTreeDepthTolerance, } = state.form; @@ -184,34 +183,6 @@ export const HyperParameters: FC = ({ actions, state, advancedParamErrors /> - - - - setFormState({ randomizeSeed: e.target.value === '' ? undefined : +e.target.value }) - } - isInvalid={randomizeSeed !== undefined && typeof randomizeSeed !== 'number'} - value={getNumberValue(randomizeSeed)} - step={1} - /> - - = ({ setCurrentStep, state }) = }), description: includes.length > MAX_INCLUDES_LENGTH - ? `${includes.slice(0, MAX_INCLUDES_LENGTH).join(', ')} ... (and ${ - includes.length - MAX_INCLUDES_LENGTH - } more)` + ? i18n.translate( + 'xpack.ml.dataframe.analytics.create.configDetails.includedFieldsAndMoreDescription', + { + defaultMessage: '{includedFields} ... (and {extraCount} more)', + values: { + extraCount: includes.length - MAX_INCLUDES_LENGTH, + includedFields: includes.slice(0, MAX_INCLUDES_LENGTH).join(', '), + }, + } + ) : includes.join(', '), }, ]; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx index 6ad874d3abd6c9..0432094c30c500 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx @@ -8,6 +8,7 @@ import React, { FC, Fragment, useEffect, useMemo, useRef, useState } from 'react'; import { EuiBadge, + EuiCallOut, EuiComboBox, EuiComboBoxOptionOption, EuiFormRow, @@ -19,6 +20,7 @@ import { import { i18n } from '@kbn/i18n'; import { debounce } from 'lodash'; +import { FormattedMessage } from '@kbn/i18n/react'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; import { useMlContext } from '../../../../../contexts/ml'; @@ -62,6 +64,8 @@ const requiredFieldsErrorText = i18n.translate( } ); +const maxRuntimeFieldsDisplayCount = 5; + export const ConfigurationStepForm: FC = ({ actions, state, @@ -314,6 +318,15 @@ export const ConfigurationStepForm: FC = ({ }; }, [jobType, dependentVariable, trainingPercent, JSON.stringify(includes), jobConfigQueryString]); + const unsupportedRuntimeFields = useMemo( + () => + currentIndexPattern.fields + .getAll() + .filter((f) => f.runtimeField) + .map((f) => `'${f.displayName}'`), + [currentIndexPattern.fields] + ); + return ( @@ -445,6 +458,36 @@ export const ConfigurationStepForm: FC = ({ > + {Array.isArray(unsupportedRuntimeFields) && unsupportedRuntimeFields.length > 0 && ( + <> + + 0 ? ( + + ) : ( + '' + ), + unsupportedRuntimeFields: unsupportedRuntimeFields + .slice(0, maxRuntimeFieldsDisplayCount) + .join(', '), + }} + /> + + + + )} + { - const indexPatternFields = getFieldsFromKibanaIndexPattern(indexPattern); + const indexPatternFields = useMemo(() => getFieldsFromKibanaIndexPattern(indexPattern), [ + indexPattern, + ]); // EuiDataGrid State const columns: EuiDataGridColumn[] = [ @@ -75,7 +78,6 @@ export const useIndexData = ( s[column.id] = { order: column.direction }; return s; }, {} as EsSorting); - const esSearchRequest = { index: indexPattern.title, body: { @@ -86,6 +88,7 @@ export const useIndexData = ( fields: ['*'], _source: false, ...(Object.keys(sort).length > 0 ? { sort } : {}), + ...getRuntimeFieldsMapping(indexPatternFields, indexPattern), }, }; @@ -105,7 +108,7 @@ export const useIndexData = ( useEffect(() => { getIndexData(); // custom comparison - }, [indexPattern.title, JSON.stringify([query, pagination, sortingColumns])]); + }, [indexPattern.title, indexPatternFields, JSON.stringify([query, pagination, sortingColumns])]); const dataLoader = useMemo(() => new DataLoader(indexPattern, toastNotifications), [ indexPattern, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section.scss b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section.scss index c1c80e8dbd2c42..3a548b40d3a912 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section.scss +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/expandable_section/expandable_section.scss @@ -5,3 +5,9 @@ .mlExpandableSection-contentPadding { padding: $euiSizeS; } + +// Make sure the charts tooltip in popover +// have higher zIndex than Eui popover cells +[id^='echTooltipPortal'] { + z-index: $euiZLevel9 !important; +} diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_chart.tsx similarity index 95% rename from x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_chart.tsx index a711d672975aae..5e508df7c6ae5e 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_chart.tsx @@ -25,12 +25,12 @@ import { EuiIcon } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import euiVars from '@elastic/eui/dist/eui_theme_light.json'; -import { DecisionPathPlotData } from './use_classification_path_data'; -import { formatSingleValue } from '../../../formatters/format_value'; +import type { DecisionPathPlotData } from './use_classification_path_data'; +import { formatSingleValue } from '../../../../../formatters/format_value'; import { FeatureImportanceBaseline, isRegressionFeatureImportanceBaseline, -} from '../../../../../common/types/feature_importance'; +} from '../../../../../../../common/types/feature_importance'; const { euiColorFullShade, euiColorMediumShade } = euiVars; const axisColor = euiColorMediumShade; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_classification.tsx similarity index 97% rename from x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_classification.tsx index 48a0c0871f6865..d10755b32d7a75 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_classification.tsx @@ -14,11 +14,11 @@ import { useDecisionPathData, getStringBasedClassName, } from './use_classification_path_data'; -import { +import type { FeatureImportance, FeatureImportanceBaseline, TopClasses, -} from '../../../../../common/types/feature_importance'; +} from '../../../../../../../common/types/feature_importance'; import { DecisionPathChart } from './decision_path_chart'; import { MissingDecisionPathCallout } from './missing_decision_path_callout'; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_json_viewer.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_json_viewer.tsx similarity index 86% rename from x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_json_viewer.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_json_viewer.tsx index 93b7bd6bd012fb..1110ef8171b96a 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_json_viewer.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_json_viewer.tsx @@ -7,7 +7,7 @@ import React, { FC } from 'react'; import { EuiCodeBlock } from '@elastic/eui'; -import { FeatureImportance } from '../../../../../common/types/feature_importance'; +import type { FeatureImportance } from '../../../../../../../common/types/feature_importance'; interface DecisionPathJSONViewerProps { featureImportance: FeatureImportance[]; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_popover.tsx similarity index 94% rename from x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_popover.tsx index 3aed0f56d5a76d..e1ad6a68639081 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_popover.tsx @@ -16,11 +16,11 @@ import { isClassificationFeatureImportanceBaseline, isRegressionFeatureImportanceBaseline, TopClasses, -} from '../../../../../common/types/feature_importance'; -import { ANALYSIS_CONFIG_TYPE } from '../../../data_frame_analytics/common'; +} from '../../../../../../../common/types/feature_importance'; +import { ANALYSIS_CONFIG_TYPE } from '../../../../common'; import { ClassificationDecisionPath } from './decision_path_classification'; -import { useMlKibana } from '../../../contexts/kibana'; -import { DataFrameAnalysisConfigType } from '../../../../../common/types/data_frame_analytics'; +import { useMlKibana } from '../../../../../contexts/kibana'; +import type { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics'; import { getStringBasedClassName } from './use_classification_path_data'; interface DecisionPathPopoverProps { diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_regression.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_regression.tsx similarity index 97% rename from x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_regression.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_regression.tsx index ccb7870fd79dc2..bb9cdd861788c3 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_regression.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_regression.tsx @@ -9,11 +9,11 @@ import React, { FC, useMemo } from 'react'; import { EuiCallOut } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import d3 from 'd3'; -import { +import type { FeatureImportance, FeatureImportanceBaseline, TopClasses, -} from '../../../../../common/types/feature_importance'; +} from '../../../../../../../common/types/feature_importance'; import { useDecisionPathData, isDecisionPathData } from './use_classification_path_data'; import { DecisionPathChart } from './decision_path_chart'; import { MissingDecisionPathCallout } from './missing_decision_path_callout'; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/missing_decision_path_callout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/missing_decision_path_callout.tsx similarity index 100% rename from x-pack/plugins/ml/public/application/components/data_grid/feature_importance/missing_decision_path_callout.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/missing_decision_path_callout.tsx diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/use_classification_path_data.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/use_classification_path_data.test.tsx similarity index 98% rename from x-pack/plugins/ml/public/application/components/data_grid/feature_importance/use_classification_path_data.test.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/use_classification_path_data.test.tsx index 18bc02ae638473..70c62294cae009 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/use_classification_path_data.test.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/use_classification_path_data.test.tsx @@ -9,7 +9,7 @@ import { buildClassificationDecisionPathData, buildRegressionDecisionPathData, } from './use_classification_path_data'; -import { FeatureImportance } from '../../../../../common/types/feature_importance'; +import type { FeatureImportance } from '../../../../../../../common/types/feature_importance'; describe('buildClassificationDecisionPathData()', () => { test('should return correct prediction probability for binary classification', () => { diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/use_classification_path_data.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/use_classification_path_data.tsx similarity index 98% rename from x-pack/plugins/ml/public/application/components/data_grid/feature_importance/use_classification_path_data.tsx rename to x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/use_classification_path_data.tsx index ccee43a8c971d1..5d61d8b3ef0c49 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/use_classification_path_data.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/use_classification_path_data.tsx @@ -14,8 +14,8 @@ import { isClassificationFeatureImportanceBaseline, isRegressionFeatureImportanceBaseline, TopClasses, -} from '../../../../../common/types/feature_importance'; -import { ExtendedFeatureImportance } from './decision_path_popover'; +} from '../../../../../../../common/types/feature_importance'; +import type { ExtendedFeatureImportance } from './decision_path_popover'; export type DecisionPathPlotData = Array<[string, number, number]>; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_modal.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_modal.tsx index bc99330a444ae5..e2e1ec852d1a95 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_modal.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/delete_action_modal.tsx @@ -9,7 +9,6 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiConfirmModal, - EuiOverlayMask, EuiSwitch, EuiFlexGroup, EuiFlexItem, @@ -37,67 +36,62 @@ export const DeleteActionModal: FC = ({ const indexName = item.config.dest.index; return ( - - - - - {userCanDeleteIndex && ( - - )} - - - {userCanDeleteIndex && indexPatternExists && ( - - )} - - - - + + + + {userCanDeleteIndex && ( + + )} + + + {userCanDeleteIndex && indexPatternExists && ( + + )} + + + ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_action_modal.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_action_modal.tsx index 2a19068ca6f4e1..d63e60e43e9094 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_action_modal.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_start/start_action_modal.tsx @@ -7,7 +7,7 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiConfirmModal, EuiOverlayMask, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; +import { EuiConfirmModal, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; import { StartAction } from './use_start_action'; @@ -15,37 +15,35 @@ export const StartActionModal: FC = ({ closeModal, item, startAndCl return ( <> {item !== undefined && ( - - +

    + {i18n.translate('xpack.ml.dataframe.analyticsList.startModalBody', { + defaultMessage: + 'A data frame analytics job increases search and indexing load in your cluster. If excessive load occurs, stop the job.', })} - onCancel={closeModal} - onConfirm={startAndCloseModal} - cancelButtonText={i18n.translate( - 'xpack.ml.dataframe.analyticsList.startModalCancelButton', - { - defaultMessage: 'Cancel', - } - )} - confirmButtonText={i18n.translate( - 'xpack.ml.dataframe.analyticsList.startModalStartButton', - { - defaultMessage: 'Start', - } - )} - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - buttonColor="primary" - > -

    - {i18n.translate('xpack.ml.dataframe.analyticsList.startModalBody', { - defaultMessage: - 'A data frame analytics job increases search and indexing load in your cluster. If excessive load occurs, stop the job.', - })} -

    -
    -
    +

    +
    )} ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_action_modal.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_action_modal.tsx index a10c0c59abd973..8ee7350245be43 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_action_modal.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_stop/stop_action_modal.tsx @@ -8,7 +8,7 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiConfirmModal, EuiOverlayMask, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; +import { EuiConfirmModal, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; import { StopAction } from './use_stop_action'; @@ -16,37 +16,35 @@ export const StopActionModal: FC = ({ closeModal, item, forceStopAnd return ( <> {item !== undefined && ( - - -

    - -

    -
    -
    + +

    + +

    +
    )} ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/delete_models_modal.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/delete_models_modal.tsx index 7ff77f21a8623e..d93baee97c5330 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/delete_models_modal.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/delete_models_modal.tsx @@ -8,7 +8,6 @@ import React, { FC } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { - EuiOverlayMask, EuiModal, EuiModalHeader, EuiModalHeaderTitle, @@ -31,57 +30,55 @@ export const DeleteModelsModal: FC = ({ models, onClose .map((model) => model.model_id); return ( - - - - + + + + + + + + + {modelsWithPipelines.length > 0 && ( + - - - - - {modelsWithPipelines.length > 0 && ( - - - - )} - + + )} + - - - - + + + + - - - - - - + + + + + ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx index c9f78e9b0dab1a..40f97690d7790b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx @@ -9,13 +9,7 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiModal, - EuiModalBody, - EuiModalHeader, - EuiModalHeaderTitle, - EuiOverlayMask, -} from '@elastic/eui'; +import { EuiModal, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui'; import { SavedObjectFinderUi } from '../../../../../../../../../../src/plugins/saved_objects/public'; import { useMlKibana, useNavigateToPath } from '../../../../../contexts/kibana'; @@ -41,66 +35,62 @@ export const SourceSelection: FC = ({ onClose }) => { }; return ( - <> - - - - - {' '} - /{' '} - - - - - + + + {' '} + /{' '} + + + + + 'search', + name: i18n.translate( + 'xpack.ml.dataFrame.analytics.create.searchSelection.savedObjectType.search', { - defaultMessage: 'No matching indices or saved searches found.', + defaultMessage: 'Saved search', } - )} - savedObjectMetaData={[ + ), + }, + { + type: 'index-pattern', + getIconForSavedObject: () => 'indexPatternApp', + name: i18n.translate( + 'xpack.ml.dataFrame.analytics.create.searchSelection.savedObjectType.indexPattern', { - type: 'search', - getIconForSavedObject: () => 'search', - name: i18n.translate( - 'xpack.ml.dataFrame.analytics.create.searchSelection.savedObjectType.search', - { - defaultMessage: 'Saved search', - } - ), - }, - { - type: 'index-pattern', - getIconForSavedObject: () => 'indexPatternApp', - name: i18n.translate( - 'xpack.ml.dataFrame.analytics.create.searchSelection.savedObjectType.indexPattern', - { - defaultMessage: 'Index pattern', - } - ), - }, - ]} - fixedPageSize={fixedPageSize} - uiSettings={uiSettings} - savedObjects={savedObjects} - /> - - - - + defaultMessage: 'Index pattern', + } + ), + }, + ]} + fixedPageSize={fixedPageSize} + uiSettings={uiSettings} + savedObjects={savedObjects} + /> + + ); }; 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 131da93a2328a0..40e13ea0e6867e 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 @@ -16,6 +16,7 @@ import { DataFrameAnalyticsId, DataFrameAnalysisConfigType, } from '../../../../../../../common/types/data_frame_analytics'; +import { isClassificationAnalysis } from '../../../../../../../common/util/analytics_utils'; import { ANALYSIS_CONFIG_TYPE } from '../../../../../../../common/constants/data_frame_analytics'; export enum DEFAULT_MODEL_MEMORY_LIMIT { regression = '100mb', @@ -50,6 +51,7 @@ export interface State { alpha: undefined | number; computeFeatureInfluence: string; createIndexPattern: boolean; + classAssignmentObjective: undefined | string; dependentVariable: DependentVariable; description: string; destinationIndex: EsIndexName; @@ -126,6 +128,7 @@ export const getInitialState = (): State => ({ alpha: undefined, computeFeatureInfluence: 'true', createIndexPattern: true, + classAssignmentObjective: undefined, dependentVariable: '', description: '', destinationIndex: '', @@ -278,13 +281,14 @@ export const getJobConfigFromFormState = ( }; } - if ( - formState.jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && - jobConfig?.analysis?.classification !== undefined && - formState.numTopClasses !== undefined - ) { - // @ts-ignore - jobConfig.analysis.classification.num_top_classes = formState.numTopClasses; + if (jobConfig?.analysis !== undefined && isClassificationAnalysis(jobConfig?.analysis)) { + if (formState.numTopClasses !== undefined) { + jobConfig.analysis.classification.num_top_classes = formState.numTopClasses; + } + if (formState.classAssignmentObjective !== undefined) { + jobConfig.analysis.classification.class_assignment_objective = + formState.classAssignmentObjective; + } } if (formState.jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION) { diff --git a/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx b/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx index 3401c72a3b8549..2330eafd87825e 100644 --- a/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx +++ b/x-pack/plugins/ml/public/application/explorer/add_to_dashboard_control.tsx @@ -14,7 +14,6 @@ import { EuiModal, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiSpacer, EuiButtonEmpty, EuiButton, @@ -215,103 +214,99 @@ export const AddToDashboardControl: FC = ({ const noSwimlaneSelected = Object.values(selectedSwimlanes).every((isSelected) => !isSelected); return ( - - - - + + + + + + + + - - - - - } - > - { - const newSelection = { - ...selectedSwimlanes, - [optionId]: !selectedSwimlanes[optionId as SwimlaneType], - }; - setSelectedSwimlanes(newSelection); - }} - data-test-subj="mlAddToDashboardSwimlaneTypeSelector" - /> - + } + > + { + const newSelection = { + ...selectedSwimlanes, + [optionId]: !selectedSwimlanes[optionId as SwimlaneType], + }; + setSelectedSwimlanes(newSelection); + }} + data-test-subj="mlAddToDashboardSwimlaneTypeSelector" + /> + - + - - } - data-test-subj="mlDashboardSelectionContainer" - > - - - - - - - - { - onClose(async () => { - const selectedDashboardId = selectedItems[0].id; - await addSwimlaneToDashboardCallback(); - await navigateToUrl( - await dashboardService.getDashboardEditUrl(selectedDashboardId) - ); - }); - }} - data-test-subj="mlAddAndEditDashboardButton" - > - - - + - - - - + } + data-test-subj="mlDashboardSelectionContainer" + > + + + + + + + + { + onClose(async () => { + const selectedDashboardId = selectedItems[0].id; + await addSwimlaneToDashboardCallback(); + await navigateToUrl(await dashboardService.getDashboardEditUrl(selectedDashboardId)); + }); + }} + data-test-subj="mlAddAndEditDashboardButton" + > + + + + + + + ); }; diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_results_found/__snapshots__/explorer_no_results_found.test.js.snap b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_results_found/__snapshots__/explorer_no_results_found.test.js.snap index dc7e567380fdf9..388e2f590edf28 100644 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_results_found/__snapshots__/explorer_no_results_found.test.js.snap +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_results_found/__snapshots__/explorer_no_results_found.test.js.snap @@ -14,14 +14,6 @@ exports[`ExplorerNoInfluencersFound snapshot 1`] = ` } iconType="iInCircle" - title={ -

    - -

    - } + title={

    } /> `; diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_results_found/explorer_no_results_found.js b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_results_found/explorer_no_results_found.js index 6e058a8fc8c610..799437e1799f00 100644 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_results_found/explorer_no_results_found.js +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_results_found/explorer_no_results_found.js @@ -14,26 +14,48 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiEmptyPrompt } from '@elastic/eui'; -export const ExplorerNoResultsFound = () => ( - - -

    - } - body={ - -

    - -

    -
    - } - /> -); +export const ExplorerNoResultsFound = ({ hasResults, selectedJobsRunning }) => { + const resultsHaveNoAnomalies = hasResults === true; + const noResults = hasResults === false; + return ( + + {resultsHaveNoAnomalies && ( + + )} + {noResults && ( + + )} + + } + body={ + + {selectedJobsRunning && noResults && ( +

    + +

    + )} + {!selectedJobsRunning && ( +

    + +

    + )} +
    + } + /> + ); +}; diff --git a/x-pack/plugins/ml/public/application/explorer/components/no_overall_data.tsx b/x-pack/plugins/ml/public/application/explorer/components/no_overall_data.tsx index fe77fdf235b58d..65935050ee218a 100644 --- a/x-pack/plugins/ml/public/application/explorer/components/no_overall_data.tsx +++ b/x-pack/plugins/ml/public/application/explorer/components/no_overall_data.tsx @@ -12,7 +12,7 @@ export const NoOverallData: FC = () => { return ( ); }; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index 9f77260ab3320f..abf8197f51634d 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -142,6 +142,7 @@ export class Explorer extends React.Component { setSelectedCells: PropTypes.func.isRequired, severity: PropTypes.number.isRequired, showCharts: PropTypes.bool.isRequired, + selectedJobsRunning: PropTypes.bool.isRequired, }; state = { filterIconTriggeredQuery: undefined, language: DEFAULT_QUERY_LANG }; @@ -223,7 +224,7 @@ export class Explorer extends React.Component { updateLanguage = (language) => this.setState({ language }); render() { - const { showCharts, severity, stoppedPartitions } = this.props; + const { showCharts, severity, stoppedPartitions, selectedJobsRunning } = this.props; const { annotations, @@ -248,6 +249,9 @@ export class Explorer extends React.Component { const noJobsFound = selectedJobs === null || selectedJobs.length === 0; const hasResults = overallSwimlaneData.points && overallSwimlaneData.points.length > 0; + const hasResultsWithAnomalies = + (hasResults && overallSwimlaneData.points.some((v) => v.value > 0)) || + tableData.anomalies?.length > 0; if (noJobsFound && !loading) { return ( @@ -257,10 +261,13 @@ export class Explorer extends React.Component { ); } - if (noJobsFound && hasResults === false && !loading) { + if (hasResultsWithAnomalies === false && !loading) { return ( - + ); } diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx index 3bec404276ca26..a67863ea5f803b 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/delete_job_modal/delete_job_modal.tsx @@ -10,7 +10,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiSpacer, EuiModal, - EuiOverlayMask, EuiModalHeader, EuiModalHeaderTitle, EuiModalBody, @@ -77,74 +76,72 @@ export const DeleteJobModal: FC = ({ setShowFunction, unsetShowFunction, if (canDelete) { return ( - - - - - - - - -

    - {deleting === true ? ( -

    - - -
    - -
    + + + + + + + +

    + {deleting === true ? ( +

    + + +
    +
    - ) : ( - - - - )} -

    - - <> - - - - - + + )} +

    + + <> + + + + + - - - - - - - + + + +
    + + ); } else { return ( diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js index 8769c2c3cca20e..b23bbedb7413a2 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js @@ -21,7 +21,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiTabbedContent, - EuiOverlayMask, EuiConfirmModal, } from '@elastic/eui'; @@ -443,38 +442,36 @@ export class EditJobFlyoutUI extends Component { if (this.state.isConfirmationModalVisible) { confirmationModal = ( - - - } - onCancel={() => this.closeFlyout(true)} - onConfirm={() => this.save()} - cancelButtonText={ - - } - confirmButtonText={ - - } - defaultFocusedButton="confirm" - > -

    - -

    -
    -
    + + } + onCancel={() => this.closeFlyout(true)} + onConfirm={() => this.save()} + cancelButtonText={ + + } + confirmButtonText={ + + } + defaultFocusedButton="confirm" + > +

    + +

    +
    ); } diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx index a1bac4b6a35979..da4c9b0b0cc004 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx @@ -14,7 +14,6 @@ import { EuiFlexItem, EuiPanel, EuiSpacer, - EuiOverlayMask, EuiModal, EuiModalBody, EuiModalHeader, @@ -282,30 +281,28 @@ class CustomUrlsUI extends Component { ) : ( - - - - - - - + + + + + + - {editor} + {editor} - - {testButton} - {addButton} - - - + + {testButton} + {addButton} + + ); } diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js index 5f5759e49208cb..361e8956c714e3 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js @@ -16,7 +16,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiHorizontalRule, EuiCheckbox, } from '@elastic/eui'; @@ -138,78 +137,76 @@ export class StartDatafeedModal extends Component { if (this.state.isModalVisible) { modal = ( - - - - - - - - - - + + + - {this.state.endTime === undefined && ( -
    - - - } - checked={createAlert} - onChange={this.setCreateAlert} - /> -
    - )} -
    - - - - - - - - + + + + + {this.state.endTime === undefined && ( +
    + + + } + checked={createAlert} + onChange={this.setCreateAlert} /> - - - - +
    + )} +
    + + + + + + + + + + +
    ); } return
    {modal}
    ; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js index 98d8b5eaf912a7..5b8fa5c672c6e0 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js @@ -19,6 +19,7 @@ import { stringMatch } from '../../../util/string_utils'; import { JOB_STATE, DATAFEED_STATE } from '../../../../../common/constants/states'; import { parseInterval } from '../../../../../common/util/parse_interval'; import { mlCalendarService } from '../../../services/calendar_service'; +import { isPopulatedObject } from '../../../../../common/util/object_utils'; export function loadFullJob(jobId) { return new Promise((resolve, reject) => { @@ -379,7 +380,7 @@ export function checkForAutoStartDatafeed() { mlJobService.tempJobCloningObjects.datafeed = undefined; mlJobService.tempJobCloningObjects.createdBy = undefined; - const hasDatafeed = typeof datafeed === 'object' && Object.keys(datafeed).length > 0; + const hasDatafeed = isPopulatedObject(datafeed); const datafeedId = hasDatafeed ? datafeed.datafeed_id : ''; return { id: job.job_id, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts index ca2c2204fb0c11..ec5cb59964ffd5 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts @@ -36,6 +36,7 @@ import { SHARED_RESULTS_INDEX_NAME, } from '../../../../../../common/constants/new_job'; import { collectAggs } from './util/general'; +import { filterRuntimeMappings } from './util/filter_runtime_mappings'; import { parseInterval } from '../../../../../../common/util/parse_interval'; import { Calendar } from '../../../../../../common/types/calendars'; import { mlCalendarService } from '../../../../services/calendar_service'; @@ -65,6 +66,7 @@ export class JobCreator { protected _scriptFields: Field[] = []; protected _runtimeFields: Field[] = []; protected _runtimeMappings: RuntimeMappings | null = null; + protected _filterRuntimeMappingsOnSave: boolean = true; protected _aggregationFields: Field[] = []; protected _sparseData: boolean = false; private _stopAllRefreshPolls: { @@ -546,7 +548,8 @@ export class JobCreator { public async createDatafeed(): Promise { try { - return await mlJobService.saveNewDatafeed(this._datafeed_config, this._job_config.job_id); + const tempDatafeed = this._getDatafeedWithFilteredRuntimeMappings(); + return await mlJobService.saveNewDatafeed(tempDatafeed, this._job_config.job_id); } catch (error) { throw error; } @@ -559,6 +562,23 @@ export class JobCreator { return jobRunner; } + private _getDatafeedWithFilteredRuntimeMappings(): Datafeed { + if (this._filterRuntimeMappingsOnSave === false) { + return this._datafeed_config; + } + + const { runtime_mappings: filteredRuntimeMappings } = filterRuntimeMappings( + this._job_config, + this._datafeed_config + ); + + return { + ...this._datafeed_config, + runtime_mappings: + Object.keys(filteredRuntimeMappings).length > 0 ? filteredRuntimeMappings : undefined, + }; + } + public subscribeToProgress(func: ProgressSubscriber) { this._subscribers.push(func); } @@ -645,6 +665,14 @@ export class JobCreator { return JSON.stringify(this._datafeed_config, null, 2); } + public set filterRuntimeMappingsOnSave(filter: boolean) { + this._filterRuntimeMappingsOnSave = filter; + } + + public get filterRuntimeMappingsOnSave(): boolean { + return this._filterRuntimeMappingsOnSave; + } + protected _initPerPartitionCategorization() { if (this._job_config.analysis_config.per_partition_categorization === undefined) { this._job_config.analysis_config.per_partition_categorization = {}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/filter_runtime_mappings.test.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/filter_runtime_mappings.test.ts new file mode 100644 index 00000000000000..43e7d4e45b6e0c --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/filter_runtime_mappings.test.ts @@ -0,0 +1,183 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Job, Datafeed } from '../../../../../../../common/types/anomaly_detection_jobs'; +import { filterRuntimeMappings } from './filter_runtime_mappings'; + +function getJob(): Job { + return { + job_id: 'test', + description: '', + groups: [], + analysis_config: { + bucket_span: '15m', + detectors: [ + { + function: 'mean', + field_name: 'responsetime', + }, + ], + influencers: [], + }, + data_description: { + time_field: '@timestamp', + }, + analysis_limits: { + model_memory_limit: '11MB', + }, + model_plot_config: { + enabled: false, + annotations_enabled: false, + }, + }; +} + +function getDatafeed(): Datafeed { + return { + datafeed_id: 'datafeed-test', + job_id: 'dds', + indices: ['farequote-*'], + query: { + bool: { + must: [ + { + match_all: {}, + }, + ], + }, + }, + runtime_mappings: { + responsetime_big: { + type: 'double', + script: { + source: "emit(doc['responsetime'].value * 100.0)", + }, + }, + airline_lower: { + type: 'keyword', + script: { + source: "emit(doc['airline'].value.toLowerCase())", + }, + }, + }, + }; +} + +function getAggs() { + return { + buckets: { + date_histogram: { + field: '@timestamp', + fixed_interval: '90000ms', + }, + aggregations: { + responsetime: { + avg: { + field: 'responsetime_big', + }, + }, + '@timestamp': { + max: { + field: '@timestamp', + }, + }, + }, + }, + }; +} + +describe('filter_runtime_mappings', () => { + describe('filterRuntimeMappings()', () => { + let job: Job; + let datafeed: Datafeed; + beforeEach(() => { + job = getJob(); + datafeed = getDatafeed(); + }); + + test('returns no runtime mappings, no mappings in aggs', () => { + const resp = filterRuntimeMappings(job, datafeed); + expect(Object.keys(resp.runtime_mappings).length).toEqual(0); + + expect(Object.keys(resp.discarded_mappings).length).toEqual(2); + expect(resp.discarded_mappings.responsetime_big).not.toEqual(undefined); + expect(resp.discarded_mappings.airline_lower).not.toEqual(undefined); + }); + + test('returns no runtime mappings, no runtime mappings in datafeed', () => { + datafeed.runtime_mappings = undefined; + const resp = filterRuntimeMappings(job, datafeed); + expect(Object.keys(resp.runtime_mappings).length).toEqual(0); + expect(resp.runtime_mappings.responsetime_big).toEqual(undefined); + + expect(Object.keys(resp.discarded_mappings).length).toEqual(0); + expect(resp.discarded_mappings.airline_lower).toEqual(undefined); + }); + + test('return one runtime mapping and one unused mapping, mappings in aggs', () => { + datafeed.aggregations = getAggs(); + const resp = filterRuntimeMappings(job, datafeed); + expect(Object.keys(resp.runtime_mappings).length).toEqual(1); + expect(resp.runtime_mappings.responsetime_big).not.toEqual(undefined); + + expect(Object.keys(resp.discarded_mappings).length).toEqual(1); + expect(resp.discarded_mappings.airline_lower).not.toEqual(undefined); + }); + + test('return no runtime mappings, no mappings in aggs', () => { + datafeed.aggregations = getAggs(); + datafeed.aggregations!.buckets!.aggregations!.responsetime!.avg!.field! = 'responsetime'; + + const resp = filterRuntimeMappings(job, datafeed); + expect(Object.keys(resp.runtime_mappings).length).toEqual(0); + + expect(Object.keys(resp.discarded_mappings).length).toEqual(2); + expect(resp.discarded_mappings.responsetime_big).not.toEqual(undefined); + expect(resp.discarded_mappings.airline_lower).not.toEqual(undefined); + }); + + test('return one runtime mapping and one unused mapping, no mappings in aggs', () => { + // set the detector field to be a runtime mapping + job.analysis_config.detectors[0].field_name = 'responsetime_big'; + const resp = filterRuntimeMappings(job, datafeed); + expect(Object.keys(resp.runtime_mappings).length).toEqual(1); + expect(resp.runtime_mappings.responsetime_big).not.toEqual(undefined); + + expect(Object.keys(resp.discarded_mappings).length).toEqual(1); + expect(resp.discarded_mappings.airline_lower).not.toEqual(undefined); + }); + + test('return two runtime mappings, no mappings in aggs', () => { + // set the detector field to be a runtime mapping + job.analysis_config.detectors[0].field_name = 'responsetime_big'; + // set the detector by field to be a runtime mapping + job.analysis_config.detectors[0].by_field_name = 'airline_lower'; + const resp = filterRuntimeMappings(job, datafeed); + expect(Object.keys(resp.runtime_mappings).length).toEqual(2); + expect(resp.runtime_mappings.responsetime_big).not.toEqual(undefined); + expect(resp.runtime_mappings.airline_lower).not.toEqual(undefined); + + expect(Object.keys(resp.discarded_mappings).length).toEqual(0); + }); + + test('return two runtime mappings, no mappings in aggs, categorization job', () => { + job.analysis_config.detectors[0].function = 'count'; + // set the detector field to be a runtime mapping + job.analysis_config.detectors[0].field_name = undefined; + // set the detector by field to be a runtime mapping + job.analysis_config.detectors[0].by_field_name = 'mlcategory'; + job.analysis_config.categorization_field_name = 'airline_lower'; + + const resp = filterRuntimeMappings(job, datafeed); + expect(Object.keys(resp.runtime_mappings).length).toEqual(1); + expect(resp.runtime_mappings.airline_lower).not.toEqual(undefined); + + expect(Object.keys(resp.discarded_mappings).length).toEqual(1); + expect(resp.discarded_mappings.responsetime_big).not.toEqual(undefined); + }); + }); +}); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/filter_runtime_mappings.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/filter_runtime_mappings.ts new file mode 100644 index 00000000000000..5319cd3c3aabc9 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/filter_runtime_mappings.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { RuntimeMappings } from '../../../../../../../common/types/fields'; +import type { Datafeed, Job } from '../../../../../../../common/types/anomaly_detection_jobs'; + +interface Response { + runtime_mappings: RuntimeMappings; + discarded_mappings: RuntimeMappings; +} + +export function filterRuntimeMappings(job: Job, datafeed: Datafeed): Response { + if (datafeed.runtime_mappings === undefined) { + return { + runtime_mappings: {}, + discarded_mappings: {}, + }; + } + + const usedFields = findFieldsInJob(job, datafeed); + + const { runtimeMappings, discardedMappings } = createMappings( + datafeed.runtime_mappings, + usedFields + ); + + return { runtime_mappings: runtimeMappings, discarded_mappings: discardedMappings }; +} + +function findFieldsInJob(job: Job, datafeed: Datafeed) { + const usedFields = new Set(); + job.analysis_config.detectors.forEach((d) => { + if (d.field_name !== undefined) { + usedFields.add(d.field_name); + } + if (d.by_field_name !== undefined) { + usedFields.add(d.by_field_name); + } + if (d.over_field_name !== undefined) { + usedFields.add(d.over_field_name); + } + if (d.partition_field_name !== undefined) { + usedFields.add(d.partition_field_name); + } + }); + + if (job.analysis_config.categorization_field_name !== undefined) { + usedFields.add(job.analysis_config.categorization_field_name); + } + + if (job.analysis_config.summary_count_field_name !== undefined) { + usedFields.add(job.analysis_config.summary_count_field_name); + } + + if (job.analysis_config.influencers !== undefined) { + job.analysis_config.influencers.forEach((i) => usedFields.add(i)); + } + + const aggs = datafeed.aggregations ?? datafeed.aggs; + if (aggs !== undefined) { + findFieldsInAgg(aggs).forEach((f) => usedFields.add(f)); + } + + return [...usedFields]; +} + +function findFieldsInAgg(obj: Record) { + const fields: string[] = []; + Object.entries(obj).forEach(([key, val]) => { + if (typeof val === 'object' && val !== null) { + fields.push(...findFieldsInAgg(val)); + } else if (typeof val === 'string' && key === 'field') { + fields.push(val); + } + }); + return fields; +} + +function createMappings(rm: RuntimeMappings, usedFieldNames: string[]) { + return { + runtimeMappings: usedFieldNames.reduce((acc, cur) => { + if (rm[cur] !== undefined) { + acc[cur] = rm[cur]; + } + return acc; + }, {} as RuntimeMappings), + discardedMappings: Object.keys(rm).reduce((acc, cur) => { + if (usedFieldNames.includes(cur) === false && rm[cur] !== undefined) { + acc[cur] = rm[cur]; + } + return acc; + }, {} as RuntimeMappings), + }; +} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx index 6afc1122fcdab1..916a25271c63b8 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/datafeed_preview_flyout/datafeed_preview.tsx @@ -21,6 +21,7 @@ import { CombinedJob } from '../../../../../../../../common/types/anomaly_detect import { MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor'; import { mlJobService } from '../../../../../../services/job_service'; import { ML_DATA_PREVIEW_COUNT } from '../../../../../../../../common/util/job_utils'; +import { isPopulatedObject } from '../../../../../../../../common/util/object_utils'; export const DatafeedPreview: FC<{ combinedJob: CombinedJob | null; @@ -64,7 +65,7 @@ export const DatafeedPreview: FC<{ const resp = await mlJobService.searchPreview(combinedJob); let data = resp.hits.hits; // the first item under aggregations can be any name - if (typeof resp.aggregations === 'object' && Object.keys(resp.aggregations).length > 0) { + if (isPopulatedObject(resp.aggregations)) { const accessor = Object.keys(resp.aggregations)[0]; data = resp.aggregations[accessor].buckets.slice(0, ML_DATA_PREVIEW_COUNT); } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/reset_query/reset_query.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/reset_query/reset_query.tsx index 02f53c77c088c0..e42ec414e9641f 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/reset_query/reset_query.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/reset_query/reset_query.tsx @@ -8,13 +8,7 @@ import React, { FC, useContext, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiButtonEmpty, - EuiConfirmModal, - EuiOverlayMask, - EuiCodeBlock, - EuiSpacer, -} from '@elastic/eui'; +import { EuiButtonEmpty, EuiConfirmModal, EuiCodeBlock, EuiSpacer } from '@elastic/eui'; import { JobCreatorContext } from '../../../job_creator_context'; import { getDefaultDatafeedQuery } from '../../../../../utils/new_job_utils'; @@ -34,35 +28,33 @@ export const ResetQueryButton: FC = () => { return ( <> {confirmModalVisible && ( - - - + + - + - - {defaultQueryString} - - - + + {defaultQueryString} + + )} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/modal_wrapper.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/modal_wrapper.tsx index 3f4a0f6ea6b3d2..aaed47cc7a02bb 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/modal_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/modal_wrapper.tsx @@ -9,7 +9,6 @@ import React, { FC } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, - EuiOverlayMask, EuiModal, EuiModalHeader, EuiModalHeaderTitle, @@ -28,44 +27,42 @@ interface Props { export const ModalWrapper: FC = ({ onCreateClick, closeModal, saveEnabled, children }) => { return ( - - - - - - - + + + + + + - {children} + {children} - - - - + + + + - - - - - - + + + + + ); }; diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index 052be41ca1eb70..e65ca22effd768 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -87,6 +87,9 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim const timefilter = useTimefilter({ timeRangeSelector: true, autoRefreshSelector: true }); const { jobIds } = useJobSelection(jobsWithTimeRange); + const selectedJobsRunning = jobsWithTimeRange.some( + (job) => jobIds.includes(job.id) && job.isRunning === true + ); const explorerAppState = useObservable(explorerService.appState$); const explorerState = useObservable(explorerService.state$); @@ -261,6 +264,7 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim severity: tableSeverity.val, stoppedPartitions, invalidTimeRangeError, + selectedJobsRunning, }} /> diff --git a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts index ec1d36a1ced4c2..a8ae42658f3689 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts +++ b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts @@ -24,6 +24,7 @@ import { findAggField } from '../../../../common/util/validation_utils'; import { getDatafeedAggregations } from '../../../../common/util/datafeed_utils'; import { aggregationTypeTransform } from '../../../../common/util/anomaly_utils'; import { ES_AGGREGATION } from '../../../../common/constants/aggregation_types'; +import { isPopulatedObject } from '../../../../common/util/object_utils'; interface ResultResponse { success: boolean; @@ -175,7 +176,7 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) { // when the field is an aggregation field, because the field doesn't actually exist in the indices // we need to pass all the sub aggs from the original datafeed config // so that we can access the aggregated field - if (typeof aggFields === 'object' && Object.keys(aggFields).length > 0) { + if (isPopulatedObject(aggFields)) { // first item under aggregations can be any name, not necessarily 'buckets' const accessor = Object.keys(aggFields)[0]; const tempAggs = { ...(aggFields[accessor].aggs ?? aggFields[accessor].aggregations) }; diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js index 7f3c8ce9a1a6e5..42d8b32691c205 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js @@ -10,7 +10,7 @@ import { PropTypes } from 'prop-types'; import { i18n } from '@kbn/i18n'; -import { EuiPage, EuiPageBody, EuiPageContent, EuiOverlayMask } from '@elastic/eui'; +import { EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; import { NavigationMenu } from '../../../components/navigation_menu'; @@ -336,19 +336,13 @@ class NewCalendarUI extends Component { let modal = ''; if (isNewEventModalVisible) { - modal = ( - - - - ); + modal = ; } else if (isImportModalVisible) { modal = ( - - - + ); } diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.js b/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.js index afd1433b7ae698..bba28ab481ea11 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/calendars_list.js @@ -10,7 +10,6 @@ import { PropTypes } from 'prop-types'; import { EuiConfirmModal, - EuiOverlayMask, EuiPage, EuiPageBody, EuiPageContent, @@ -111,37 +110,35 @@ export class CalendarsListUI extends Component { if (this.state.isDestroyModalVisible) { destroyModal = ( - - c.calendar_id).join(', '), - }} - /> - } - onCancel={this.closeDestroyModal} - onConfirm={this.deleteCalendars} - cancelButtonText={ - - } - confirmButtonText={ - - } - buttonColor="danger" - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - /> - + c.calendar_id).join(', '), + }} + /> + } + onCancel={this.closeDestroyModal} + onConfirm={this.deleteCalendars} + cancelButtonText={ + + } + confirmButtonText={ + + } + buttonColor="danger" + defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + /> ); } diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/__snapshots__/delete_filter_list_modal.test.js.snap b/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/__snapshots__/delete_filter_list_modal.test.js.snap index 93ca044cb0c830..8cadb8270f680a 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/__snapshots__/delete_filter_list_modal.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/__snapshots__/delete_filter_list_modal.test.js.snap @@ -92,41 +92,39 @@ exports[`DeleteFilterListModal renders modal after clicking delete button 1`] = values={Object {}} /> - - - } - className="eui-textBreakWord" - confirmButtonText={ - - } - data-test-subj="mlFilterListDeleteConfirmation" - defaultFocusedButton="confirm" - onCancel={[Function]} - onConfirm={[Function]} - title={ - + } + className="eui-textBreakWord" + confirmButtonText={ + + } + data-test-subj="mlFilterListDeleteConfirmation" + defaultFocusedButton="confirm" + onCancel={[Function]} + onConfirm={[Function]} + title={ + - } - /> - + } + /> + } + /> `; diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js b/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js index bed0e7ca281e5b..20b716586b97d1 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js @@ -9,7 +9,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButton, EuiConfirmModal, EuiOverlayMask, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; +import { EuiButton, EuiConfirmModal, EUI_MODAL_CONFIRM_BUTTON } from '@elastic/eui'; import { deleteFilterLists } from './delete_filter_lists'; @@ -67,29 +67,27 @@ export class DeleteFilterListModal extends Component { /> ); modal = ( - - - } - confirmButtonText={ - - } - buttonColor="danger" - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - data-test-subj={'mlFilterListDeleteConfirmation'} - /> - + + } + confirmButtonText={ + + } + buttonColor="danger" + defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + data-test-subj={'mlFilterListDeleteConfirmation'} + /> ); } diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/modal.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/modal.js index 613bd51bc16c3c..3261846a5fdd5b 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/modal.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/modal.js @@ -19,7 +19,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiSpacer, } from '@elastic/eui'; @@ -31,48 +30,42 @@ import { FormattedMessage } from '@kbn/i18n/react'; export function Modal(props) { return ( - - - - - - - + + + + + + - - {props.messages.map((message, i) => ( - - - - - ))} + + {props.messages.map((message, i) => ( + + + + + ))} - {props.forecasts.length > 0 && ( - - - - - )} - - + {props.forecasts.length > 0 && ( + + + + + )} + + - - - - - - - + + + + + + ); } diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 33e5183fa79493..06a0f7e17e1649 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -1000,7 +1000,6 @@ export class TimeSeriesExplorer extends React.Component { }} /> } - color="warning" iconType="help" size="s" /> diff --git a/x-pack/plugins/ml/public/shared.ts b/x-pack/plugins/ml/public/shared.ts index a0107ce8e049c5..7fb27f889c4173 100644 --- a/x-pack/plugins/ml/public/shared.ts +++ b/x-pack/plugins/ml/public/shared.ts @@ -17,8 +17,8 @@ export * from '../common/types/audit_message'; export * from '../common/util/anomaly_utils'; export * from '../common/util/errors'; export * from '../common/util/validators'; +export * from '../common/util/date_utils'; export * from './application/formatters/metric_change_description'; export * from './application/components/data_grid'; export * from './application/data_frame_analytics/common'; -export * from '../common/util/date_utils'; diff --git a/x-pack/plugins/ml/server/lib/alerts/alerting_service.test.ts b/x-pack/plugins/ml/server/lib/alerts/alerting_service.test.ts index 261fac7b620ba9..f029fa24f96078 100644 --- a/x-pack/plugins/ml/server/lib/alerts/alerting_service.test.ts +++ b/x-pack/plugins/ml/server/lib/alerts/alerting_service.test.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { resolveTimeInterval } from './alerting_service'; +import { resolveBucketSpanInSeconds } from './alerting_service'; describe('Alerting Service', () => { test('should resolve maximum bucket interval', () => { - expect(resolveTimeInterval(['15m', '1h', '6h', '90s'])).toBe('43200s'); + expect(resolveBucketSpanInSeconds(['15m', '1h', '6h', '90s'])).toBe(43200); }); }); diff --git a/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts b/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts index 5ef883cc50fbb3..6e7cd77e450bcc 100644 --- a/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts +++ b/x-pack/plugins/ml/server/lib/alerts/alerting_service.ts @@ -7,6 +7,9 @@ import Boom from '@hapi/boom'; import rison from 'rison-node'; +import { ElasticsearchClient } from 'kibana/server'; +import moment from 'moment'; +import { Duration } from 'moment/moment'; import { MlClient } from '../ml_client'; import { MlAnomalyDetectionAlertParams, @@ -25,6 +28,8 @@ import { import { parseInterval } from '../../../common/util/parse_interval'; import { AnomalyDetectionAlertContext } from './register_anomaly_detection_alert_type'; import { MlJobsResponse } from '../../../common/types/job_service'; +import { ANOMALY_SCORE_MATCH_GROUP_ID } from '../../../common/constants/alerts'; +import { getEntityFieldName, getEntityFieldValue } from '../../../common/util/anomaly_utils'; function isDefined(argument: T | undefined | null): argument is T { return argument !== undefined && argument !== null; @@ -34,22 +39,23 @@ function isDefined(argument: T | undefined | null): argument is T { * Resolves the longest bucket span from the list and multiply it by 2. * @param bucketSpans Collection of bucket spans */ -export function resolveTimeInterval(bucketSpans: string[]): string { - return `${ +export function resolveBucketSpanInSeconds(bucketSpans: string[]): number { + return ( Math.max( ...bucketSpans .map((b) => parseInterval(b)) .filter(isDefined) .map((v) => v.asSeconds()) ) * 2 - }s`; + ); } /** * Alerting related server-side methods * @param mlClient + * @param esClient */ -export function alertingServiceProvider(mlClient: MlClient) { +export function alertingServiceProvider(mlClient: MlClient, esClient: ElasticsearchClient) { const getAggResultsLabel = (resultType: AnomalyResultType) => { return { aggGroupLabel: `${resultType}_results` as PreviewResultsKeys, @@ -177,10 +183,14 @@ export function alertingServiceProvider(mlClient: MlClient) { 'is_interim', 'function', 'field_name', + 'by_field_name', 'by_field_value', + 'over_field_name', 'over_field_value', + 'partition_field_name', 'partition_field_value', 'job_id', + 'detector_index', ], }, size: 3, @@ -257,14 +267,31 @@ export function alertingServiceProvider(mlClient: MlClient) { }; }; + /** + * Provides unique key for the anomaly result. + */ + const getAlertInstanceKey = (source: any): string => { + let alertInstanceKey = `${source.job_id}_${source.timestamp}`; + if (source.result_type === ANOMALY_RESULT_TYPE.INFLUENCER) { + alertInstanceKey += `_${source.influencer_field_name}_${source.influencer_field_value}`; + } else if (source.result_type === ANOMALY_RESULT_TYPE.RECORD) { + const fieldName = getEntityFieldName(source); + const fieldValue = getEntityFieldValue(source); + alertInstanceKey += `_${source.detector_index}_${source.function}_${fieldName}_${fieldValue}`; + } + return alertInstanceKey; + }; + /** * Builds a request body - * @param params - * @param previewTimeInterval + * @param params - Alert params + * @param previewTimeInterval - Relative time interval to test the alert condition + * @param checkIntervalGap - Interval between alert executions */ const fetchAnomalies = async ( params: MlAnomalyDetectionAlertParams, - previewTimeInterval?: string + previewTimeInterval?: string, + checkIntervalGap?: Duration ): Promise => { const jobAndGroupIds = [ ...(params.jobSelection.jobIds ?? []), @@ -281,9 +308,14 @@ export function alertingServiceProvider(mlClient: MlClient) { return; } - const lookBackTimeInterval = resolveTimeInterval( - jobsResponse.map((v) => v.analysis_config.bucket_span) - ); + /** + * The check interval might be bigger than the 2x bucket span. + * We need to check the biggest time range to make sure anomalies are not missed. + */ + const lookBackTimeInterval = `${Math.max( + resolveBucketSpanInSeconds(jobsResponse.map((v) => v.analysis_config.bucket_span)), + checkIntervalGap ? checkIntervalGap.asSeconds() : 0 + )}s`; const jobIds = jobsResponse.map((v) => v.job_id); @@ -370,19 +402,22 @@ export function alertingServiceProvider(mlClient: MlClient) { const aggTypeResults = v[resultsLabel.aggGroupLabel]; const requestedAnomalies = aggTypeResults[resultsLabel.topHitsLabel].hits.hits; + const topAnomaly = requestedAnomalies[0]; + const alertInstanceKey = getAlertInstanceKey(topAnomaly._source); + return { count: aggTypeResults.doc_count, key: v.key, - key_as_string: v.key_as_string, + alertInstanceKey, jobIds: [...new Set(requestedAnomalies.map((h) => h._source.job_id))], isInterim: requestedAnomalies.some((h) => h._source.is_interim), - timestamp: requestedAnomalies[0]._source.timestamp, - timestampIso8601: requestedAnomalies[0].fields.timestamp_iso8601[0], - timestampEpoch: requestedAnomalies[0].fields.timestamp_epoch[0], - score: requestedAnomalies[0].fields.score[0], + timestamp: topAnomaly._source.timestamp, + timestampIso8601: topAnomaly.fields.timestamp_iso8601[0], + timestampEpoch: topAnomaly.fields.timestamp_epoch[0], + score: topAnomaly.fields.score[0], bucketRange: { - start: requestedAnomalies[0].fields.start[0], - end: requestedAnomalies[0].fields.end[0], + start: topAnomaly.fields.start[0], + end: topAnomaly.fields.end[0], }, topRecords: v.record_results.top_record_hits.hits.hits.map((h) => ({ ...h._source, @@ -479,13 +514,24 @@ export function alertingServiceProvider(mlClient: MlClient) { /** * Return the result of an alert condition execution. * - * @param params + * @param params - Alert params + * @param publicBaseUrl + * @param alertId - Alert ID + * @param startedAt + * @param previousStartedAt */ execute: async ( params: MlAnomalyDetectionAlertParams, - publicBaseUrl: string | undefined + publicBaseUrl: string | undefined, + alertId: string, + startedAt: Date, + previousStartedAt: Date | null ): Promise => { - const res = await fetchAnomalies(params); + const checkIntervalGap = previousStartedAt + ? moment.duration(moment(startedAt).diff(previousStartedAt)) + : undefined; + + const res = await fetchAnomalies(params, undefined, checkIntervalGap); if (!res) { throw new Error('No results found'); @@ -496,12 +542,65 @@ export function alertingServiceProvider(mlClient: MlClient) { const anomalyExplorerUrl = buildExplorerUrl(result, params.resultType as AnomalyResultType); - return { + const executionResult = { ...result, - name: result.key_as_string, + name: result.alertInstanceKey, anomalyExplorerUrl, kibanaBaseUrl: publicBaseUrl!, }; + + let kibanaEventLogCount = 0; + try { + // Check kibana-event-logs for presence of this alert instance + const kibanaLogResults = await esClient.count({ + index: '.kibana-event-log-*', + body: { + query: { + bool: { + must: [ + { + term: { + 'kibana.alerting.action_group_id': { + value: ANOMALY_SCORE_MATCH_GROUP_ID, + }, + }, + }, + { + term: { + 'kibana.alerting.instance_id': { + value: executionResult.name, + }, + }, + }, + { + nested: { + path: 'kibana.saved_objects', + query: { + term: { + 'kibana.saved_objects.id': { + value: alertId, + }, + }, + }, + }, + }, + ], + }, + }, + }, + }); + + kibanaEventLogCount = kibanaLogResults.body.count; + } catch (e) { + // eslint-disable-next-line no-console + console.log('Unable to check kibana event logs', e); + } + + if (kibanaEventLogCount > 0) { + return; + } + + return executionResult; }, /** * Checks how often the alert condition will fire an alert instance diff --git a/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts b/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts index 6f8fa59aa231e7..30a92c02cefc3a 100644 --- a/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts +++ b/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts @@ -123,13 +123,19 @@ export function registerAnomalyDetectionAlertType({ }, producer: PLUGIN_ID, minimumLicenseRequired: MINIMUM_FULL_LICENSE, - async executor({ services, params }) { + async executor({ services, params, alertId, state, previousStartedAt, startedAt }) { const fakeRequest = {} as KibanaRequest; const { execute } = mlSharedServices.alertingServiceProvider( services.savedObjectsClient, fakeRequest ); - const executionResult = await execute(params, publicBaseUrl); + const executionResult = await execute( + params, + publicBaseUrl, + alertId, + startedAt, + previousStartedAt + ); if (executionResult) { const alertInstanceName = executionResult.name; diff --git a/x-pack/plugins/ml/server/lib/alerts/register_ml_alerts.ts b/x-pack/plugins/ml/server/lib/alerts/register_ml_alerts.ts index 5c9106d78595fc..371c5435f91de9 100644 --- a/x-pack/plugins/ml/server/lib/alerts/register_ml_alerts.ts +++ b/x-pack/plugins/ml/server/lib/alerts/register_ml_alerts.ts @@ -5,12 +5,14 @@ * 2.0. */ +import { Logger } from 'kibana/server'; import { AlertingPlugin } from '../../../../alerts/server'; import { registerAnomalyDetectionAlertType } from './register_anomaly_detection_alert_type'; import { SharedServices } from '../../shared_services'; export interface RegisterAlertParams { alerts: AlertingPlugin['setup']; + logger: Logger; mlSharedServices: SharedServices; publicBaseUrl: string | undefined; } diff --git a/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts b/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts index d075f0cc4e6605..278dd19f74acc3 100644 --- a/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts +++ b/x-pack/plugins/ml/server/lib/ml_client/ml_client.ts @@ -196,9 +196,6 @@ export function getMlClient( await jobIdsCheck('data-frame-analytics', p); return mlClient.explainDataFrameAnalytics(...p); }, - async findFileStructure(...p: Parameters) { - return mlClient.findFileStructure(...p); - }, async flushJob(...p: Parameters) { await jobIdsCheck('anomaly-detector', p); return mlClient.flushJob(...p); diff --git a/x-pack/plugins/ml/server/lib/ml_client/types.ts b/x-pack/plugins/ml/server/lib/ml_client/types.ts index 6618e19dfe2663..7ff1acf4ac0ce2 100644 --- a/x-pack/plugins/ml/server/lib/ml_client/types.ts +++ b/x-pack/plugins/ml/server/lib/ml_client/types.ts @@ -29,7 +29,6 @@ export type MlClientParams = | Parameters | Parameters | Parameters - | Parameters | Parameters | Parameters | Parameters diff --git a/x-pack/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts b/x-pack/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts index 2dd3373f2e42f5..6e57e997e5f008 100644 --- a/x-pack/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts +++ b/x-pack/plugins/ml/server/models/file_data_visualizer/file_data_visualizer.ts @@ -5,20 +5,22 @@ * 2.0. */ +import { IScopedClusterClient } from 'kibana/server'; import { AnalysisResult, FormattedOverrides, InputOverrides, FindFileStructureResponse, } from '../../../common/types/file_datavisualizer'; -import type { MlClient } from '../../lib/ml_client'; export type InputData = any[]; -export function fileDataVisualizerProvider(mlClient: MlClient) { +export function fileDataVisualizerProvider(client: IScopedClusterClient) { async function analyzeFile(data: InputData, overrides: InputOverrides): Promise { overrides.explain = overrides.explain === undefined ? 'true' : overrides.explain; - const { body } = await mlClient.findFileStructure({ + const { + body, + } = await client.asInternalUser.textStructure.findStructure({ body: data, ...overrides, }); diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.ts b/x-pack/plugins/ml/server/models/job_service/jobs.ts index 0af8f1e1ec1cab..dc2c04540ef21d 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.ts +++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts @@ -39,6 +39,7 @@ import { } from '../../../common/util/job_utils'; import { groupsProvider } from './groups'; import type { MlClient } from '../../lib/ml_client'; +import { isPopulatedObject } from '../../../common/util/object_utils'; interface Results { [id: string]: { @@ -172,8 +173,7 @@ export function jobsProvider(client: IScopedClusterClient, mlClient: MlClient) { }); const jobs = fullJobsList.map((job) => { - const hasDatafeed = - typeof job.datafeed_config === 'object' && Object.keys(job.datafeed_config).length > 0; + const hasDatafeed = isPopulatedObject(job.datafeed_config); const dataCounts = job.data_counts; const errorMessage = getSingleMetricViewerJobErrorMessage(job); @@ -233,8 +233,7 @@ export function jobsProvider(client: IScopedClusterClient, mlClient: MlClient) { const jobs = fullJobsList.map((job) => { jobsMap[job.job_id] = job.groups || []; - const hasDatafeed = - typeof job.datafeed_config === 'object' && Object.keys(job.datafeed_config).length > 0; + const hasDatafeed = isPopulatedObject(job.datafeed_config); const timeRange: { to?: number; from?: number } = {}; const dataCounts = job.data_counts; diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 10ed70d7f73969..24fac9184cc27c 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -211,6 +211,7 @@ export class MlServerPlugin if (plugins.alerts) { registerMlAlerts({ alerts: plugins.alerts, + logger: this.log, mlSharedServices: sharedServices, publicBaseUrl: coreSetup.http.basePath.publicBaseUrl, }); diff --git a/x-pack/plugins/ml/server/routes/alerting.ts b/x-pack/plugins/ml/server/routes/alerting.ts index 7b7f3a7db9723a..a268a5200b35e7 100644 --- a/x-pack/plugins/ml/server/routes/alerting.ts +++ b/x-pack/plugins/ml/server/routes/alerting.ts @@ -30,9 +30,9 @@ export function alertingRoutes({ router, routeGuard }: RouteInitialization) { tags: ['access:ml:canGetJobs'], }, }, - routeGuard.fullLicenseAPIGuard(async ({ mlClient, request, response }) => { + routeGuard.fullLicenseAPIGuard(async ({ mlClient, request, response, client }) => { try { - const alertingService = alertingServiceProvider(mlClient); + const alertingService = alertingServiceProvider(mlClient, client.asInternalUser); const result = await alertingService.preview(request.body); diff --git a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts index fa56a6854de62d..6b200c59f57d5a 100644 --- a/x-pack/plugins/ml/server/routes/file_data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/file_data_visualizer.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { IScopedClusterClient } from 'kibana/server'; import { schema } from '@kbn/config-schema'; import { MAX_FILE_SIZE_BYTES } from '../../../file_upload/common'; import { InputOverrides } from '../../common/types/file_datavisualizer'; @@ -13,10 +14,9 @@ import { InputData, fileDataVisualizerProvider } from '../models/file_data_visua import { RouteInitialization } from '../types'; import { analyzeFileQuerySchema } from './schemas/file_data_visualizer_schema'; -import type { MlClient } from '../lib/ml_client'; -function analyzeFiles(mlClient: MlClient, data: InputData, overrides: InputOverrides) { - const { analyzeFile } = fileDataVisualizerProvider(mlClient); +function analyzeFiles(client: IScopedClusterClient, data: InputData, overrides: InputOverrides) { + const { analyzeFile } = fileDataVisualizerProvider(client); return analyzeFile(data, overrides); } @@ -48,9 +48,9 @@ export function fileDataVisualizerRoutes({ router, routeGuard }: RouteInitializa tags: ['access:ml:canFindFileStructure'], }, }, - routeGuard.basicLicenseAPIGuard(async ({ mlClient, request, response }) => { + routeGuard.basicLicenseAPIGuard(async ({ client, request, response }) => { try { - const result = await analyzeFiles(mlClient, request.body, request.query); + const result = await analyzeFiles(client, request.body, request.query); return response.ok({ body: result }); } catch (e) { return response.customError(wrapError(e)); diff --git a/x-pack/plugins/ml/server/shared_services/providers/alerting_service.ts b/x-pack/plugins/ml/server/shared_services/providers/alerting_service.ts index 318dac200a8772..cbe22478e12d6c 100644 --- a/x-pack/plugins/ml/server/shared_services/providers/alerting_service.ts +++ b/x-pack/plugins/ml/server/shared_services/providers/alerting_service.ts @@ -20,7 +20,9 @@ export function getAlertingServiceProvider(getGuards: GetGuards) { return await getGuards(request, savedObjectsClient) .isFullLicense() .hasMlCapabilities(['canGetJobs']) - .ok(({ mlClient }) => alertingServiceProvider(mlClient).preview(...args)); + .ok(({ mlClient, scopedClient }) => + alertingServiceProvider(mlClient, scopedClient.asInternalUser).preview(...args) + ); }, execute: async ( ...args: Parameters @@ -28,7 +30,9 @@ export function getAlertingServiceProvider(getGuards: GetGuards) { return await getGuards(request, savedObjectsClient) .isFullLicense() .hasMlCapabilities(['canGetJobs']) - .ok(({ mlClient }) => alertingServiceProvider(mlClient).execute(...args)); + .ok(({ mlClient, scopedClient }) => + alertingServiceProvider(mlClient, scopedClient.asInternalUser).execute(...args) + ); }, }; }, diff --git a/x-pack/plugins/monitoring/common/constants.ts b/x-pack/plugins/monitoring/common/constants.ts index 8b1e91e2caf67e..18c96106566486 100644 --- a/x-pack/plugins/monitoring/common/constants.ts +++ b/x-pack/plugins/monitoring/common/constants.ts @@ -580,6 +580,11 @@ export const ALERT_ACTION_TYPE_EMAIL = '.email'; */ export const ALERT_ACTION_TYPE_LOG = '.server-log'; +/** + * To enable modifing of alerts in under actions + */ +export const ALERT_REQUIRES_APP_CONTEXT = false; + export const ALERT_EMAIL_SERVICES = ['gmail', 'hotmail', 'icloud', 'outlook365', 'ses', 'yahoo']; /** diff --git a/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx index f0b58b41015052..3088e2a75c3492 100644 --- a/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx @@ -9,7 +9,11 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { Expression, Props } from '../components/param_details_form/expression'; import { AlertTypeModel, ValidationResult } from '../../../../triggers_actions_ui/public'; -import { ALERT_CCR_READ_EXCEPTIONS, ALERT_DETAILS } from '../../../common/constants'; +import { + ALERT_CCR_READ_EXCEPTIONS, + ALERT_DETAILS, + ALERT_REQUIRES_APP_CONTEXT, +} from '../../../common/constants'; import { AlertTypeParams } from '../../../../alerts/common'; interface ValidateOptions extends AlertTypeParams { @@ -45,6 +49,6 @@ export function createCCRReadExceptionsAlertType(): AlertTypeModel { return { @@ -26,6 +30,6 @@ export function createDiskUsageAlertType(): AlertTypeModel ), validate, defaultActionMessage: '{{context.internalFullMessage}}', - requiresAppContext: true, + requiresAppContext: ALERT_REQUIRES_APP_CONTEXT, }; } diff --git a/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx index 3f23035f526496..87850f893797a2 100644 --- a/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx +++ b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx @@ -10,7 +10,11 @@ import { i18n } from '@kbn/i18n'; import { EuiTextColor, EuiSpacer } from '@elastic/eui'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; -import { LEGACY_ALERTS, LEGACY_ALERT_DETAILS } from '../../../common/constants'; +import { + LEGACY_ALERTS, + LEGACY_ALERT_DETAILS, + ALERT_REQUIRES_APP_CONTEXT, +} from '../../../common/constants'; export function createLegacyAlertTypes(): AlertTypeModel[] { return LEGACY_ALERTS.map((legacyAlert) => { @@ -34,7 +38,7 @@ export function createLegacyAlertTypes(): AlertTypeModel[] { ), defaultActionMessage: '{{context.internalFullMessage}}', validate: () => ({ errors: {} }), - requiresAppContext: true, + requiresAppContext: ALERT_REQUIRES_APP_CONTEXT, }; }); } diff --git a/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx index 8437faf2258b97..6639b4f2f6a489 100644 --- a/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx +++ b/x-pack/plugins/monitoring/public/alerts/memory_usage_alert/index.tsx @@ -11,7 +11,11 @@ import { Expression, Props } from '../components/param_details_form/expression'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; -import { ALERT_MEMORY_USAGE, ALERT_DETAILS } from '../../../common/constants'; +import { + ALERT_MEMORY_USAGE, + ALERT_DETAILS, + ALERT_REQUIRES_APP_CONTEXT, +} from '../../../common/constants'; export function createMemoryUsageAlertType(): AlertTypeModel { return { @@ -26,6 +30,6 @@ export function createMemoryUsageAlertType(): AlertTypeModel { }); it('should fetch from the alerts client', async () => { - const status = await fetchStatus( - alertsClient as any, - licenseService as any, - alertTypes, - defaultClusterState.clusterUuid - ); + const status = await fetchStatus(alertsClient as any, licenseService as any, alertTypes, [ + defaultClusterState.clusterUuid, + ]); expect(status).toEqual({ monitoring_alert_cpu_usage: { rawAlert: { id: 1 }, @@ -99,24 +96,18 @@ describe('fetchStatus', () => { }, ]; - const status = await fetchStatus( - alertsClient as any, - licenseService as any, - alertTypes, - defaultClusterState.clusterUuid - ); + const status = await fetchStatus(alertsClient as any, licenseService as any, alertTypes, [ + defaultClusterState.clusterUuid, + ]); expect(Object.values(status).length).toBe(1); expect(Object.keys(status)).toEqual(alertTypes); expect(status[alertType].states[0].state.ui.isFiring).toBe(true); }); it('should pass in the right filter to the alerts client', async () => { - await fetchStatus( - alertsClient as any, - licenseService as any, - alertTypes, - defaultClusterState.clusterUuid - ); + await fetchStatus(alertsClient as any, licenseService as any, alertTypes, [ + defaultClusterState.clusterUuid, + ]); expect((alertsClient.find as jest.Mock).mock.calls[0][0].options.filter).toBe( `alert.attributes.alertTypeId:${alertType}` ); @@ -127,12 +118,9 @@ describe('fetchStatus', () => { alertTypeState: null, })) as any; - const status = await fetchStatus( - alertsClient as any, - licenseService as any, - alertTypes, - defaultClusterState.clusterUuid - ); + const status = await fetchStatus(alertsClient as any, licenseService as any, alertTypes, [ + defaultClusterState.clusterUuid, + ]); expect(status[alertType].states.length).toEqual(0); }); @@ -142,12 +130,9 @@ describe('fetchStatus', () => { data: [], })) as any; - const status = await fetchStatus( - alertsClient as any, - licenseService as any, - alertTypes, - defaultClusterState.clusterUuid - ); + const status = await fetchStatus(alertsClient as any, licenseService as any, alertTypes, [ + defaultClusterState.clusterUuid, + ]); expect(status).toEqual({}); }); @@ -163,7 +148,7 @@ describe('fetchStatus', () => { alertsClient as any, customLicenseService as any, [ALERT_CLUSTER_HEALTH], - defaultClusterState.clusterUuid + [defaultClusterState.clusterUuid] ); expect(customLicenseService.getWatcherFeature).toHaveBeenCalled(); }); @@ -200,7 +185,7 @@ describe('fetchStatus', () => { customAlertsClient as any, licenseService as any, [ALERT_CPU_USAGE, ALERT_DISK_USAGE, ALERT_MISSING_MONITORING_DATA], - defaultClusterState.clusterUuid + [defaultClusterState.clusterUuid] ); expect(Object.keys(status)).toEqual([ ALERT_CPU_USAGE, diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts index 399b26a6c5c314..3ccb4d3a9c4d5d 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_status.ts @@ -20,7 +20,7 @@ export async function fetchStatus( alertsClient: AlertsClient, licenseService: MonitoringLicenseService, alertTypes: string[] | undefined, - clusterUuid: string, + clusterUuids: string[], filters: CommonAlertFilter[] = [] ): Promise<{ [type: string]: CommonAlertStatus }> { const types: Array<{ type: string; result: CommonAlertStatus }> = []; @@ -57,7 +57,7 @@ export async function fetchStatus( } for (const state of alertInstanceState.alertStates) { const meta = instance.meta; - if (clusterUuid && state.cluster.clusterUuid !== clusterUuid) { + if (clusterUuids && !clusterUuids.includes(state.cluster.clusterUuid)) { return accum; } diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js index 47e3cef0674110..0bed25a70d0480 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js @@ -120,6 +120,13 @@ export async function getClustersFromRequest( // add alerts data if (isInCodePath(codePaths, [CODE_PATH_ALERTS])) { const alertsClient = req.getAlertsClient(); + const alertStatus = await fetchStatus( + alertsClient, + req.server.plugins.monitoring.info, + undefined, + clusters.map((cluster) => cluster.cluster_uuid) + ); + for (const cluster of clusters) { const verification = verifyMonitoringLicense(req.server); if (!verification.enabled) { @@ -154,12 +161,20 @@ export async function getClustersFromRequest( if (prodLicenseInfo.clusterAlerts.enabled) { try { cluster.alerts = { - list: await fetchStatus( - alertsClient, - req.server.plugins.monitoring.info, - undefined, - cluster.cluster_uuid - ), + list: Object.keys(alertStatus).reduce((accum, alertName) => { + const value = alertStatus[alertName]; + if (value.states && value.states.length) { + accum[alertName] = { + ...value, + states: value.states.filter( + (state) => state.state.cluster.clusterUuid === cluster.cluster_uuid + ), + }; + } else { + accum[alertName] = value; + } + return accum; + }, {}), alertsMeta: { enabled: true, }, diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 654c3de7d81a96..4fada2d17bf5d5 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -368,7 +368,7 @@ export class MonitoringPlugin if (Boom.isBoom(err) || statusCode !== 500) { return res.customError({ statusCode, body: err }); } - return res.internalError(wrapError(err)); + throw wrapError(err).body; } }; diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts index d0a4de7b5b3783..95e2cb63bec86d 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/status.ts @@ -43,7 +43,7 @@ export function alertStatusRoute(server: any, npRoute: RouteDependencies) { alertsClient, npRoute.licenseService, alertTypeIds, - clusterUuid, + [clusterUuid], filters as CommonAlertFilter[] ); return response.ok({ body: status }); diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/__mocks__/fixtures/logstash_state_results_1n1p.json b/x-pack/plugins/monitoring/server/telemetry_collection/__mocks__/fixtures/logstash_state_results_1n1p.json new file mode 100644 index 00000000000000..71656e3468c098 --- /dev/null +++ b/x-pack/plugins/monitoring/server/telemetry_collection/__mocks__/fixtures/logstash_state_results_1n1p.json @@ -0,0 +1,168 @@ +[ + { + "hits" : { + "hits": [ + { + "_index" : ".monitoring-logstash-7-2021.02.01", + "_type" : "_doc", + "_id" : "ezNjX3cB1VO1nvgv72Wz", + "_score" : 0.0, + "_source" : { + "logstash_state" : { + "pipeline" : { + "batch_size" : 125, + "workers" : 1, + "representation" : { + "graph" : { + "vertices" : [ + { + "config_name" : "stdin", + "meta" : { + "source" : { + "protocol" : "file" + } + }, + "plugin_type" : "input" + }, + { + "config_name" : "elasticsearch", + "meta" : { + "source" : { + "protocol" : "file" + } + }, + "plugin_type" : "input" + }, + { + "config_name" : "mutate", + "meta" : { + "source" : { + "protocol" : "file" + } + }, + "plugin_type" : "filter" + }, + { + "config_name" : "ruby", + "meta" : { + "source" : { + "protocol" : "file" + } + }, + "plugin_type" : "filter" + }, + { + "config_name" : "split", + "meta" : { + "source" : { + "protocol" : "file" + } + }, + "plugin_type" : "filter" + }, + { + "config_name" : "elasticsearch", + "meta" : { + "source" : { + "protocol" : "file" + } + }, + "plugin_type" : "filter" + }, + { + "config_name" : "ruby", + "meta" : { + "source" : { + "protocol" : "file" + } + }, + "plugin_type" : "filter" + }, + { + "config_name" : "ruby", + "meta" : { + "source" : { + "protocol" : "file" + } + }, + "plugin_type" : "filter" + }, + { + "config_name" : "mutate", + "meta" : { + "source" : { + "protocol" : "file" + } + }, + "plugin_type" : "filter" + }, + { + "config_name" : "aggregate", + "meta" : { + "source" : { + "protocol" : "file" + } + }, + "plugin_type" : "filter" + }, + { + "config_name" : "drop", + "meta" : { + "source" : { + "protocol" : "file" + } + }, + "plugin_type" : "filter" + }, + { + "meta" : { + "source" : { + "protocol" : "file" + } + } + }, + { + "config_name" : "mutate", + "meta" : { + "source" : { + "protocol" : "file" + } + }, + "plugin_type" : "filter" + }, + { + "config_name" : "elasticsearch", + "meta" : { + "source" : { + "protocol" : "file" + } + }, + "plugin_type" : "output" + }, + { + "config_name" : "stdout", + "meta" : { + "source" : { + "protocol" : "file" + } + }, + "plugin_type" : "output" + } + ] + } + } + } + }, + "type" : "logstash_state", + "timestamp" : "2021-02-01T15:58:40.943-0500" + }, + "fields" : { + "logstash_state.pipeline.ephemeral_id" : [ + "994e68cd-d607-40e6-a54c-02a51caa17e0" + ] + } + } + ] + } +} +] diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/__mocks__/fixtures/logstash_state_results_1nmp.json b/x-pack/plugins/monitoring/server/telemetry_collection/__mocks__/fixtures/logstash_state_results_1nmp.json new file mode 100644 index 00000000000000..f2e3b1e038476c --- /dev/null +++ b/x-pack/plugins/monitoring/server/telemetry_collection/__mocks__/fixtures/logstash_state_results_1nmp.json @@ -0,0 +1,187 @@ +[ + { + "hits" : { + "hits" : [ + { + "_index" : ".monitoring-logstash-7-mb-2021.01.28", + "_type" : "_doc", + "_id" : "CiziSXcB1VO1nvgvRr-E", + "_score" : 0.0, + "_source" : { + "logstash_state" : { + "pipeline" : { + "batch_size" : 125, + "representation" : { + "graph" : { + "vertices" : [ + { + "config_name" : "stdin", + "meta" : { + "source" : { + "protocol" : "x-pack-config-management" + } + }, + "plugin_type" : "input" + }, + { + "config_name" : "stdout", + "meta" : { + "source" : { + "protocol" : "x-pack-config-management" + } + }, + "plugin_type" : "output" + } + ] + } + }, + "workers" : 12 + } + }, + "type" : "logstash_state", + "timestamp" : "2021-01-28T11:45:01.937-0500" + }, + "fields" : { + "logstash_state.pipeline.ephemeral_id" : [ + "47a70feb-3cb5-4618-8670-2c0bada61acd" + ] + } + }, + { + "_index" : ".monitoring-logstash-7-2021.01.12", + "_type" : "_doc", + "_id" : "DSziSXcB1VO1nvgvRr-Z", + "_score" : 0.0, + "_source" : { + "logstash_state" : { + "pipeline" : { + "batch_size" : 125, + "representation" : { + "graph" : { + "vertices" : [ + { + "config_name" : "stdin", + "meta" : { + "source" : { + "protocol" : "x-pack-config-management" + } + }, + "plugin_type" : "input" + }, + { + "config_name" : "stdout", + "meta" : { + "source" : { + "protocol" : "x-pack-config-management" + } + }, + "plugin_type" : "output" + } + ] + } + }, + "workers" : 1 + } + }, + "type" : "logstash_state", + "timestamp" : "2021-01-28T11:45:01.955-0500" + }, + "fields" : { + "logstash_state.pipeline.ephemeral_id" : [ + "5a65d966-0330-4bd7-82f2-ee81040c13cf" + ] + } + }, + { + "_index" : ".monitoring-logstash-7-2021.01.12", + "_type" : "_doc", + "_id" : "DCziSXcB1VO1nvgvRr-Z", + "_score" : 0.0, + "_source" : { + "logstash_state" : { + "pipeline" : { + "batch_size" : 125, + "representation" : { + "graph" : { + "vertices" : [ + { + "config_name" : "stdin", + "meta" : { + "source" : { + "protocol" : "x-pack-config-management" + } + }, + "plugin_type" : "input" + }, + { + "config_name" : "stdout", + "meta" : { + "source" : { + "protocol" : "x-pack-config-management" + } + }, + "plugin_type" : "output" + } + ] + } + }, + "workers" : 44 + } + }, + "type" : "logstash_state", + "timestamp" : "2021-01-28T11:45:01.937-0500" + }, + "fields" : { + "logstash_state.pipeline.ephemeral_id" : [ + "8d33fe25-a2c0-4c54-9ecf-d218cb8dbfe4" + ] + } + }, + { + "_index" : ".monitoring-logstash-7-2021.01.12", + "_type" : "_doc", + "_id" : "CyziSXcB1VO1nvgvRr-E", + "_score" : 0.0, + "_source" : { + "logstash_state" : { + "pipeline" : { + "batch_size" : 1251, + "representation" : { + "graph" : { + "vertices" : [ + { + "config_name" : "stdin", + "meta" : { + "source" : { + "protocol" : "x-pack-config-management" + } + }, + "plugin_type" : "input" + }, + { + "config_name" : "stdout", + "meta" : { + "source" : { + "protocol" : "x-pack-config-management" + } + }, + "plugin_type" : "output" + } + ] + } + }, + "workers" : 12 + } + }, + "type" : "logstash_state", + "timestamp" : "2021-01-28T11:45:01.937-0500" + }, + "fields" : { + "logstash_state.pipeline.ephemeral_id" : [ + "f4167a94-20a8-43e7-828e-4cf38d906187" + ] + } + } + ] + } + }] diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/__mocks__/fixtures/logstash_state_results_mnmp.json b/x-pack/plugins/monitoring/server/telemetry_collection/__mocks__/fixtures/logstash_state_results_mnmp.json new file mode 100644 index 00000000000000..65cb704529efd0 --- /dev/null +++ b/x-pack/plugins/monitoring/server/telemetry_collection/__mocks__/fixtures/logstash_state_results_mnmp.json @@ -0,0 +1,926 @@ +[ + { + "hits" : { + "hits" : [ + { + "_index" : ".monitoring-logstash-7-2021.02.01", + "_type" : "_doc", + "_id" : "ZzNmX3cB1VO1nvgv9Wlp", + "_score" : 0.0, + "_source" : { + "logstash_state" : { + "pipeline" : { + "batch_size" : 125, + "representation" : { + "graph" : { + "vertices" : [ + { + "config_name" : "stdin", + "meta" : { + "source" : { + "protocol" : "file" + } + }, + "plugin_type" : "input" + }, + { + "config_name" : "clone", + "meta" : { + "source" : { + "protocol" : "file" + } + }, + "plugin_type" : "filter" + }, + { + "config_name" : "pipeline", + "meta" : { + "source" : { + "protocol" : "file" + } + }, + "plugin_type" : "output" + } + ] + } + }, + "workers" : 1 + } + }, + "type" : "logstash_state", + "timestamp" : "2021-02-01T16:01:58.852-0500" + }, + "fields" : { + "logstash_state.pipeline.ephemeral_id" : [ + "cf37c6fa-2f1a-41e2-9a89-36b420a8b9a5" + ] + } + }, + { + "_index" : ".monitoring-logstash-7-2021.02.01", + "_type" : "_doc", + "_id" : "ujNxX3cB1VO1nvgvv3em", + "_score" : 0.0, + "_source" : { + "logstash_state" : { + "pipeline" : { + "batch_size" : 100, + "workers" : 1, + "representation" : { + "graph" : { + "vertices" : [ + { + "config_name" : "pipeline", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "input" + }, + { + "config_name" : "sleep", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "filter" + }, + { + "config_name" : "stdout", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "output" + } + ] + } + } + } + }, + "type" : "logstash_state", + "timestamp" : "2021-02-01T16:13:45.947-0500" + }, + "fields" : { + "logstash_state.pipeline.ephemeral_id" : [ + "bc6ef6f2-ecce-4328-96a2-002de41a144d" + ] + } + }, + { + "_index" : ".monitoring-logstash-7-2021.02.01", + "_type" : "_doc", + "_id" : "ODNtX3cB1VO1nvgvo3IK", + "_score" : 0.0, + "_source" : { + "logstash_state" : { + "pipeline" : { + "batch_size" : 1, + "representation" : { + "graph" : { + "vertices" : [ + { + "config_name" : "pipeline", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "input" + }, + { + "config_name" : "sleep", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "filter" + }, + { + "config_name" : "stdout", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "output" + } + ] + } + }, + "workers" : 10 + } + }, + "type" : "logstash_state", + "timestamp" : "2021-02-01T16:09:16.534-0500" + }, + "fields" : { + "logstash_state.pipeline.ephemeral_id" : [ + "3193df5f-2a34-4fe3-816e-6b05999aa5ce" + ] + } + }, + { + "_index" : ".monitoring-logstash-7-2021.02.01", + "_type" : "_doc", + "_id" : "LDNtX3cB1VO1nvgvonIZ", + "_score" : 0.0, + "_source" : { + "logstash_state" : { + "pipeline" : { + "batch_size" : 6, + "representation" : { + "graph" : { + "vertices" : [ + { + "config_name" : "generator", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "input" + }, + { + "config_name" : "sleep", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "filter" + }, + { + "config_name" : "stdout", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "output" + } + ] + } + }, + "workers" : 6 + } + }, + "type" : "logstash_state", + "timestamp" : "2021-02-01T16:09:16.322-0500" + }, + "fields" : { + "logstash_state.pipeline.ephemeral_id" : [ + "602936f5-98a3-4f8c-9471-cf389a519f4b" + ] + } + }, + { + "_index" : ".monitoring-logstash-7-2021.02.01", + "_type" : "_doc", + "_id" : "LTNtX3cB1VO1nvgvonIZ", + "_score" : 0.0, + "_source" : { + "logstash_state" : { + "pipeline" : { + "batch_size" : 12, + "representation" : { + "graph" : { + "vertices" : [ + { + "config_name" : "generator", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "input" + }, + { + "config_name" : "sleep", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "filter" + }, + { + "config_name" : "stdout", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "output" + } + ] + } + }, + "workers" : 12 + } + }, + "type" : "logstash_state", + "timestamp" : "2021-02-01T16:09:16.332-0500" + }, + "fields" : { + "logstash_state.pipeline.ephemeral_id" : [ + "41258219-b129-4fad-a629-f244826281f8" + ] + } + }, + { + "_index" : ".monitoring-logstash-7-2021.02.01", + "_type" : "_doc", + "_id" : "LjNtX3cB1VO1nvgvonI3", + "_score" : 0.0, + "_source" : { + "logstash_state" : { + "pipeline" : { + "batch_size" : 7, + "representation" : { + "graph" : { + "vertices" : [ + { + "config_name" : "generator", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "input" + }, + { + "config_name" : "sleep", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "filter" + }, + { + "config_name" : "stdout", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "output" + } + ] + } + }, + "workers" : 7 + } + }, + "type" : "logstash_state", + "timestamp" : "2021-02-01T16:09:16.335-0500" + }, + "fields" : { + "logstash_state.pipeline.ephemeral_id" : [ + "18593052-c021-4158-860d-d8122981a0ac" + ] + } + }, + { + "_index" : ".monitoring-logstash-7-2021.02.01", + "_type" : "_doc", + "_id" : "LzNtX3cB1VO1nvgvonI3", + "_score" : 0.0, + "_source" : { + "logstash_state" : { + "pipeline" : { + "batch_size" : 10, + "representation" : { + "graph" : { + "vertices" : [ + { + "config_name" : "generator", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "input" + }, + { + "config_name" : "sleep", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "filter" + }, + { + "config_name" : "stdout", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "output" + } + ] + } + }, + "workers" : 10 + } + }, + "type" : "logstash_state", + "timestamp" : "2021-02-01T16:09:16.337-0500" + }, + "fields" : { + "logstash_state.pipeline.ephemeral_id" : [ + "8b300988-62cc-4bc6-9ee0-9194f3f78e27" + ] + } + }, + { + "_index" : ".monitoring-logstash-7-2021.02.01", + "_type" : "_doc", + "_id" : "MDNtX3cB1VO1nvgvonJL", + "_score" : 0.0, + "_source" : { + "logstash_state" : { + "pipeline" : { + "batch_size" : 8, + "representation" : { + "graph" : { + "vertices" : [ + { + "config_name" : "generator", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "input" + }, + { + "config_name" : "sleep", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "filter" + }, + { + "config_name" : "stdout", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "output" + } + ] + } + }, + "workers" : 8 + } + }, + "type" : "logstash_state", + "timestamp" : "2021-02-01T16:09:16.339-0500" + }, + "fields" : { + "logstash_state.pipeline.ephemeral_id" : [ + "802a5994-a03c-44b8-a650-47c0f71c2e48" + ] + } + }, + { + "_index" : ".monitoring-logstash-7-2021.02.01", + "_type" : "_doc", + "_id" : "MTNtX3cB1VO1nvgvonJL", + "_score" : 0.0, + "_source" : { + "logstash_state" : { + "pipeline" : { + "batch_size" : 4, + "representation" : { + "graph" : { + "vertices" : [ + { + "config_name" : "generator", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "input" + }, + { + "config_name" : "sleep", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "filter" + }, + { + "config_name" : "stdout", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "output" + } + ] + } + }, + "workers" : 4 + } + }, + "type" : "logstash_state", + "timestamp" : "2021-02-01T16:09:16.468-0500" + }, + "fields" : { + "logstash_state.pipeline.ephemeral_id" : [ + "6ab60531-fb6f-478c-9063-82f2b0af2bed" + ] + } + }, + { + "_index" : ".monitoring-logstash-7-2021.02.01", + "_type" : "_doc", + "_id" : "MjNtX3cB1VO1nvgvonJi", + "_score" : 0.0, + "_source" : { + "logstash_state" : { + "pipeline" : { + "batch_size" : 5, + "representation" : { + "graph" : { + "vertices" : [ + { + "config_name" : "generator", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "input" + }, + { + "config_name" : "sleep", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "filter" + }, + { + "config_name" : "stdout", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "output" + } + ] + } + }, + "workers" : 5 + } + }, + "type" : "logstash_state", + "timestamp" : "2021-02-01T16:09:16.470-0500" + }, + "fields" : { + "logstash_state.pipeline.ephemeral_id" : [ + "0ec4702d-b5e5-4c60-91e9-6fa6a836f0d1" + ] + } + }, + { + "_index" : ".monitoring-logstash-7-2021.02.01", + "_type" : "_doc", + "_id" : "MzNtX3cB1VO1nvgvonJi", + "_score" : 0.0, + "_source" : { + "logstash_state" : { + "pipeline" : { + "batch_size" : 2, + "representation" : { + "graph" : { + "vertices" : [ + { + "config_name" : "generator", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "input" + }, + { + "config_name" : "sleep", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "filter" + }, + { + "config_name" : "pipeline", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "output" + } + ] + } + }, + "workers" : 2 + } + }, + "type" : "logstash_state", + "timestamp" : "2021-02-01T16:09:16.489-0500" + }, + "fields" : { + "logstash_state.pipeline.ephemeral_id" : [ + "ddf882b7-be26-4a93-8144-0aeb35122651" + ] + } + }, + { + "_index" : ".monitoring-logstash-7-2021.02.01", + "_type" : "_doc", + "_id" : "NDNtX3cB1VO1nvgvonJ3", + "_score" : 0.0, + "_source" : { + "logstash_state" : { + "pipeline" : { + "batch_size" : 13, + "representation" : { + "graph" : { + "vertices" : [ + { + "config_name" : "generator", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "input" + }, + { + "config_name" : "sleep", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "filter" + }, + { + "config_name" : "stdout", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "output" + } + ] + } + }, + "workers" : 13 + } + }, + "type" : "logstash_state", + "timestamp" : "2021-02-01T16:09:16.490-0500" + }, + "fields" : { + "logstash_state.pipeline.ephemeral_id" : [ + "6070b400-5c10-4c5e-b5c5-a5bd9be6d321" + ] + } + }, + { + "_index" : ".monitoring-logstash-7-2021.02.01", + "_type" : "_doc", + "_id" : "NTNtX3cB1VO1nvgvonJ3", + "_score" : 0.0, + "_source" : { + "logstash_state" : { + "pipeline" : { + "batch_size" : 3, + "representation" : { + "graph" : { + "vertices" : [ + { + "config_name" : "generator", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "input" + }, + { + "config_name" : "sleep", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "filter" + }, + { + "config_name" : "stdout", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "output" + } + ] + } + }, + "workers" : 3 + } + }, + "type" : "logstash_state", + "timestamp" : "2021-02-01T16:09:16.512-0500" + }, + "fields" : { + "logstash_state.pipeline.ephemeral_id" : [ + "e73bc63d-561a-4acd-a0c4-d5f70c4603df" + ] + } + }, + { + "_index" : ".monitoring-logstash-7-2021.02.01", + "_type" : "_doc", + "_id" : "NjNtX3cB1VO1nvgvonKM", + "_score" : 0.0, + "_source" : { + "logstash_state" : { + "pipeline" : { + "batch_size" : 11, + "representation" : { + "graph" : { + "vertices" : [ + { + "config_name" : "generator", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "input" + }, + { + "config_name" : "sleep", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "filter" + }, + { + "config_name" : "stdout", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "output" + } + ] + } + }, + "workers" : 11 + } + }, + "type" : "logstash_state", + "timestamp" : "2021-02-01T16:09:16.513-0500" + }, + "fields" : { + "logstash_state.pipeline.ephemeral_id" : [ + "4207025c-9b00-4bea-a36c-6fbf2d3c215e" + ] + } + }, + { + "_index" : ".monitoring-logstash-7-2021.02.01", + "_type" : "_doc", + "_id" : "NzNtX3cB1VO1nvgvonKM", + "_score" : 0.0, + "_source" : { + "logstash_state" : { + "pipeline" : { + "batch_size" : 9, + "representation" : { + "graph" : { + "vertices" : [ + { + "config_name" : "generator", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "input" + }, + { + "config_name" : "sleep", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "filter" + }, + { + "config_name" : "stdout", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "output" + } + ] + } + }, + "workers" : 9 + } + }, + "type" : "logstash_state", + "timestamp" : "2021-02-01T16:09:16.533-0500" + }, + "fields" : { + "logstash_state.pipeline.ephemeral_id" : [ + "72058ad1-68a1-45f6-a8e8-10621ffc7288" + ] + } + }, + { + "_index" : ".monitoring-logstash-7-2021.02.01", + "_type" : "_doc", + "_id" : "uDNxX3cB1VO1nvgvv3cb", + "_score" : 0.0, + "_source" : { + "logstash_state" : { + "pipeline" : { + "batch_size" : 125, + "workers" : 16, + "representation" : { + "graph" : { + "vertices" : [ + { + "config_name" : "generator", + "meta" : { + "source" : { + "protocol" : "file" + } + }, + "plugin_type" : "input" + }, + { + "config_name" : "sleep", + "meta" : { + "source" : { + "protocol" : "file" + } + }, + "plugin_type" : "filter" + }, + { + "config_name" : "stdout", + "meta" : { + "source" : { + "protocol" : "file" + } + }, + "plugin_type" : "output" + } + ] + } + } + } + }, + "type" : "logstash_state", + "timestamp" : "2021-02-01T16:13:45.932-0500" + }, + "fields" : { + "logstash_state.pipeline.ephemeral_id" : [ + "c6785d63-6e5f-42c2-839d-5edf139b7c19" + ] + } + }, + { + "_index" : ".monitoring-logstash-7-2021.02.01", + "_type" : "_doc", + "_id" : "uTNxX3cB1VO1nvgvv3cb", + "_score" : 0.0, + "_source" : { + "logstash_state" : { + "pipeline" : { + "batch_size" : 125, + "workers" : 8, + "representation" : { + "graph" : { + "vertices" : [ + { + "config_name" : "generator", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "input" + }, + { + "config_name" : "sleep", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "filter" + }, + { + "config_name" : "pipeline", + "meta" : { + "source" : { + "protocol" : "string" + } + }, + "plugin_type" : "output" + } + ] + } + } + } + }, + "type" : "logstash_state", + "timestamp" : "2021-02-01T16:13:45.945-0500" + }, + "fields" : { + "logstash_state.pipeline.ephemeral_id" : [ + "2fcd4161-e08f-4eea-818b-703ea3ec6389" + ] + } + } + ] + } +} +] diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/__mocks__/fixtures/logstash_stats_results.json b/x-pack/plugins/monitoring/server/telemetry_collection/__mocks__/fixtures/logstash_stats_results.json new file mode 100644 index 00000000000000..f9b54f62100dc7 --- /dev/null +++ b/x-pack/plugins/monitoring/server/telemetry_collection/__mocks__/fixtures/logstash_stats_results.json @@ -0,0 +1,323 @@ +[ + { + "hits" : { + "hits": [ + { + "_index": ".monitoring-logstash-7-2021.02.01", + "_type": "_doc", + "_id": "6DN5X3cB1VO1nvgvb4Ft", + "_score": null, + "_source": { + "logstash_stats": { + "logstash": { + "version": "7.10.0", + "snapshot": false, + "status": "green" + }, + "pipelines": [ + { + "id": "main", + "ephemeral_id": "cf37c6fa-2f1a-41e2-9a89-36b420a8b9a5", + "queue": { + "max_queue_size_in_bytes": 0, + "type": "memory" + } + } + ] + }, + "cluster_uuid": "1n1p", + "type": "logstash_stats" + }, + "fields": { + "logstash_stats.logstash.uuid": [ + "12a3b26f-9b8c-4d38-b4d0-54649c1dd244" + ] + }, + "sort": [ + 1612214529897 + ] + }, + { + "_index" : ".monitoring-logstash-7-mb-2021.01.28", + "_type" : "_doc", + "_id" : "rC2jSncB1VO1nvgvsKx6", + "_score" : null, + "_source" : { + "agent" : { + "type" : "metricbeat" + }, + "logstash_stats" : { + "logstash" : { + "version" : "7.8.0", + "snapshot" : false, + "status" : "green" + }, + "pipelines" : [ + { + "id" : "test2", + "ephemeral_id" : "47a70feb-3cb5-4618-8670-2c0bada61acd", + "queue" : { + "max_queue_size_in_bytes" : 0, + "type" : "memory" + } + }, + { + "id" : "main", + "ephemeral_id" : "5a65d966-0330-4bd7-82f2-ee81040c13cf", + "queue" : { + "max_queue_size_in_bytes" : 0, + "type" : "memory" + } + }, + { + "id" : "main2", + "ephemeral_id" : "8d33fe25-a2c0-4c54-9ecf-d218cb8dbfe4", + "queue" : { + "max_queue_size_in_bytes" : 0, + "type" : "memory" + } + }, + { + "id" : "main3", + "ephemeral_id" : "f4167a94-20a8-43e7-828e-4cf38d906187", + "queue" : { + "max_queue_size_in_bytes" : 1.19185342464E11, + "type" : "persisted" + } + } + ] + }, + "cluster_uuid" : "1nmp", + "type" : "logstash_stats" + }, + "fields" : { + "logstash_stats.logstash.uuid" : [ + "1232122-f2b6-4b6c-8cea-22661f9c4134" + ] + }, + "sort" : [ + 1611864976527 + ] + }, + { + "_index": ".monitoring-logstash-7-2021.02.01", + "_type": "_doc", + "_id": "JzNyX3cB1VO1nvgvDXi5", + "_score": null, + "_source": { + "logstash_stats": { + "logstash": { + "version": "7.9.2", + "snapshot": false, + "status": "green" + }, + "pipelines": [ + { + "id": "t2", + "ephemeral_id": "2fcd4161-e08f-4eea-818b-703ea3ec6389", + "queue": { + "max_queue_size_in_bytes": 0, + "type": "memory" + } + }, + { + "id": "aggregate-test", + "ephemeral_id": "c6785d63-6e5f-42c2-839d-5edf139b7c19", + "queue": { + "max_queue_size_in_bytes": 0, + "type": "memory" + } + }, + { + "id": "t1", + "ephemeral_id": "bc6ef6f2-ecce-4328-96a2-002de41a144d", + "queue": { + "max_queue_size_in_bytes": 1073741824, + "type": "persisted" + } + } + ] + }, + "cluster_uuid": "mnmp", + "type": "logstash_stats" + }, + "fields": { + "logstash_stats.logstash.uuid": [ + "701ce2f6-4e6a-426b-a959-80e7cdd608aa" + ] + }, + "sort": [ + 1612214046021 + ] + }, + { + "_index": ".monitoring-logstash-7-2021.02.01", + "_type": "_doc", + "_id": "sTNuX3cB1VO1nvgvtXN6", + "_score": null, + "_source": { + "logstash_stats": { + "logstash": { + "version": "7.9.1", + "snapshot": false, + "status": "green" + }, + "pipelines": [ + { + "id": "t9", + "ephemeral_id": "72058ad1-68a1-45f6-a8e8-10621ffc7288", + "queue": { + "max_queue_size_in_bytes": 1073741824, + "type": "persisted" + } + }, + { + "id": "t7", + "ephemeral_id": "18593052-c021-4158-860d-d8122981a0ac", + "queue": { + "max_queue_size_in_bytes": 1073741824, + "type": "persisted" + } + }, + { + "id": "t11", + "ephemeral_id": "4207025c-9b00-4bea-a36c-6fbf2d3c215e", + "queue": { + "max_queue_size_in_bytes": 1073741824, + "type": "persisted" + } + }, + { + "id": "t5", + "ephemeral_id": "0ec4702d-b5e5-4c60-91e9-6fa6a836f0d1", + "queue": { + "max_queue_size_in_bytes": 1073741824, + "type": "persisted" + } + }, + { + "id": "t12", + "ephemeral_id": "41258219-b129-4fad-a629-f244826281f8", + "queue": { + "max_queue_size_in_bytes": 1073741824, + "type": "persisted" + } + }, + { + "id": "t3", + "ephemeral_id": "e73bc63d-561a-4acd-a0c4-d5f70c4603df", + "queue": { + "max_queue_size_in_bytes": 1073741824, + "type": "persisted" + } + }, + { + "id": "t2", + "ephemeral_id": "ddf882b7-be26-4a93-8144-0aeb35122651", + "queue": { + "max_queue_size_in_bytes": 1073741824, + "type": "persisted" + } + }, + { + "id": "t6", + "ephemeral_id": "602936f5-98a3-4f8c-9471-cf389a519f4b", + "queue": { + "max_queue_size_in_bytes": 1073741824, + "type": "persisted" + } + }, + { + "id": "t10", + "ephemeral_id": "8b300988-62cc-4bc6-9ee0-9194f3f78e27", + "queue": { + "max_queue_size_in_bytes": 1073741824, + "type": "persisted" + } + }, + { + "id": "t4", + "ephemeral_id": "6ab60531-fb6f-478c-9063-82f2b0af2bed", + "queue": { + "max_queue_size_in_bytes": 1073741824, + "type": "persisted" + } + }, + { + "id": "t8", + "ephemeral_id": "802a5994-a03c-44b8-a650-47c0f71c2e48", + "queue": { + "max_queue_size_in_bytes": 1073741824, + "type": "persisted" + } + }, + { + "id": "t13", + "ephemeral_id": "6070b400-5c10-4c5e-b5c5-a5bd9be6d321", + "queue": { + "max_queue_size_in_bytes": 1073741824, + "type": "persisted" + } + }, + { + "id": "t1", + "ephemeral_id": "3193df5f-2a34-4fe3-816e-6b05999aa5ce", + "queue": { + "max_queue_size_in_bytes": 1073741824, + "type": "persisted" + } + } + ] + }, + "cluster_uuid": "mnmp", + "type": "logstash_stats" + }, + "fields": { + "logstash_stats.logstash.uuid": [ + "255eab6a-e15c-4e21-8d81-a9474eadb3eb" + ] + }, + "sort": [ + 1612213826805 + ] + }, + { + "_index": ".monitoring-logstash-7-2021.02.01", + "_type": "_doc", + "_id": "FDNkX3cB1VO1nvgvZWaA", + "_score": null, + "_source": { + "logstash_stats": { + "logstash": { + "version": "7.10.0", + "snapshot": false, + "status": "green" + }, + "pipelines": [ + { + "id": "main", + "ephemeral_id": "994e68cd-d607-40e6-a54c-02a51caa17e0", + "queue": { + "max_queue_size_in_bytes": 0, + "type": "memory" + } + } + ] + }, + "cluster_uuid": "mnmp", + "type": "logstash_stats" + }, + "fields": { + "logstash_stats.logstash.uuid": [ + "beb45ee3-ad5f-4895-87f0-97625a6e5295" + ] + }, + "sort": [ + 1612213150986 + ] + } + ] + } + } +] + diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts index cee95869602649..33487ecafd8c52 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.test.ts @@ -9,7 +9,7 @@ import sinon from 'sinon'; import { getStackStats, getAllStats, handleAllStats } from './get_all_stats'; import { ESClusterStats } from './get_es_stats'; import { KibanaStats } from './get_kibana_stats'; -import { ClustersHighLevelStats } from './get_high_level_stats'; +import { LogstashStatsByClusterUuid } from './get_logstash_stats'; describe('get_all_stats', () => { const timestamp = Date.now(); @@ -145,13 +145,13 @@ describe('get_all_stats', () => { logstash: { count: 1, versions: [{ version: '2.3.4-beta2', count: 1 }], - os: { - platforms: [], - platformReleases: [], - distros: [], - distroReleases: [], + cluster_stats: { + collection_types: { + internal_collection: 1, + }, + pipelines: {}, + plugins: [], }, - cloud: undefined, }, }, }, @@ -188,7 +188,7 @@ describe('get_all_stats', () => { it('handles response', () => { const clusters = handleAllStats(esClusters as ESClusterStats[], { kibana: (kibanaStats as unknown) as KibanaStats, - logstash: (logstashStats as unknown) as ClustersHighLevelStats, + logstash: (logstashStats as unknown) as LogstashStatsByClusterUuid, beats: {}, }); diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.ts index 67a21dc04fd558..60b107cb293426 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.ts @@ -19,7 +19,7 @@ import { import { getElasticsearchStats, ESClusterStats } from './get_es_stats'; import { getKibanaStats, KibanaStats } from './get_kibana_stats'; import { getBeatsStats, BeatsStatsByClusterUuid } from './get_beats_stats'; -import { getHighLevelStats, ClustersHighLevelStats } from './get_high_level_stats'; +import { getLogstashStats, LogstashStatsByClusterUuid } from './get_logstash_stats'; /** * Get statistics for all products joined by Elasticsearch cluster. @@ -38,7 +38,7 @@ export async function getAllStats( const [esClusters, kibana, logstash, beats] = await Promise.all([ getElasticsearchStats(callCluster, clusterUuids, maxBucketSize), // cluster_stats, stack_stats.xpack, cluster_name/uuid, license, version getKibanaStats(callCluster, clusterUuids, start, end, maxBucketSize), // stack_stats.kibana - getHighLevelStats(callCluster, clusterUuids, start, end, LOGSTASH_SYSTEM_ID, maxBucketSize), // stack_stats.logstash + getLogstashStats(callCluster, clusterUuids), // stack_stats.logstash getBeatsStats(callCluster, clusterUuids, start, end), // stack_stats.beats ]); @@ -63,7 +63,7 @@ export function handleAllStats( beats, }: { kibana: KibanaStats; - logstash: ClustersHighLevelStats; + logstash: LogstashStatsByClusterUuid; beats: BeatsStatsByClusterUuid; } ) { diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts index f7252c9d893649..63188be142fdd6 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_high_level_stats.ts @@ -53,7 +53,7 @@ type Counter = Map; * @param {Map} map Map to update the counter for the {@code key}. * @param {String} key The key to increment a counter for. */ -function incrementByKey(map: Counter, key?: string) { +export function incrementByKey(map: Counter, key?: string) { if (!key) { return; } @@ -207,7 +207,7 @@ function groupInstancesByCluster( * { [keyName]: key2, count: value2 } * ] */ -function mapToList(map: Map, keyName: string): T[] { +export function mapToList(map: Map, keyName: string): T[] { const list: T[] = []; for (const [key, count] of map) { diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_logstash_stats.test.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_logstash_stats.test.ts new file mode 100644 index 00000000000000..f2f0c37255d924 --- /dev/null +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_logstash_stats.test.ts @@ -0,0 +1,401 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + fetchLogstashStats, + fetchLogstashState, + processStatsResults, + processLogstashStateResults, +} from './get_logstash_stats'; +import sinon from 'sinon'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const logstashStatsResultSet = require('./__mocks__/fixtures/logstash_stats_results'); + +const resultsMap = new Map(); + +// Load data for state results. +['1n1p', '1nmp', 'mnmp'].forEach((data) => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + resultsMap.set(data, require(`./__mocks__/fixtures/logstash_state_results_${data}`)); +}); + +const getBaseOptions = () => ({ + clusters: {}, + allEphemeralIds: {}, + versions: {}, + plugins: {}, +}); + +describe('Get Logstash Stats', () => { + const clusterUuids = ['aCluster', 'bCluster', 'cCluster']; + let callCluster = sinon.stub(); + + beforeEach(() => { + callCluster = sinon.stub(); + }); + + describe('fetchLogstashState', () => { + const clusterUuid = 'a'; + const ephemeralIds = ['a', 'b', 'c']; + it('should create the logstash state query correctly', async () => { + const expected = { + bool: { + filter: [ + { + terms: { + 'logstash_state.pipeline.ephemeral_id': ['a', 'b', 'c'], + }, + }, + { + bool: { + must: { + term: { type: 'logstash_state' }, + }, + }, + }, + ], + }, + }; + + await fetchLogstashState(callCluster, clusterUuid, ephemeralIds, {} as any); + const { args } = callCluster.firstCall; + const [api, { body }] = args; + expect(api).toEqual('search'); + expect(body.query).toEqual(expected); + }); + + it('should set `from: 0, to: 10000` in the query', async () => { + await fetchLogstashState(callCluster, clusterUuid, ephemeralIds, {} as any); + const { args } = callCluster.firstCall; + const [api, { body }] = args; + expect(api).toEqual('search'); + expect(body.from).toEqual(0); + expect(body.size).toEqual(10000); + }); + + it('should set `from: 10000, to: 10000` in the query', async () => { + await fetchLogstashState(callCluster, clusterUuid, ephemeralIds, { + page: 1, + } as any); + const { args } = callCluster.firstCall; + const [api, { body }] = args; + + expect(api).toEqual('search'); + expect(body.from).toEqual(10000); + expect(body.size).toEqual(10000); + }); + + it('should set `from: 20000, to: 10000` in the query', async () => { + await fetchLogstashState(callCluster, clusterUuid, ephemeralIds, { + page: 2, + } as any); + const { args } = callCluster.firstCall; + const [api, { body }] = args; + + expect(api).toEqual('search'); + expect(body.from).toEqual(20000); + expect(body.size).toEqual(10000); + }); + }); + + describe('fetchLogstashStats', () => { + it('should set `from: 0, to: 10000` in the query', async () => { + await fetchLogstashStats(callCluster, clusterUuids, {} as any); + const { args } = callCluster.firstCall; + const [api, { body }] = args; + + expect(api).toEqual('search'); + expect(body.from).toEqual(0); + expect(body.size).toEqual(10000); + }); + + it('should set `from: 10000, to: 10000` in the query', async () => { + await fetchLogstashStats(callCluster, clusterUuids, { page: 1 } as any); + const { args } = callCluster.firstCall; + const [api, { body }] = args; + + expect(api).toEqual('search'); + expect(body.from).toEqual(10000); + expect(body.size).toEqual(10000); + }); + + it('should set `from: 20000, to: 10000` in the query', async () => { + await fetchLogstashStats(callCluster, clusterUuids, { page: 2 } as any); + const { args } = callCluster.firstCall; + const [api, { body }] = args; + + expect(api).toEqual('search'); + expect(body.from).toEqual(20000); + expect(body.size).toEqual(10000); + }); + }); + + describe('processLogstashStatsResults', () => { + it('should summarize empty results', () => { + const resultsEmpty = undefined; + + const options = getBaseOptions(); + processStatsResults(resultsEmpty as any, options); + + expect(options.clusters).toStrictEqual({}); + }); + + it('should summarize single result with some missing fields', () => { + const results = { + hits: { + hits: [ + { + _source: { + type: 'logstash_stats', + cluster_uuid: 'FlV4ckTxQ0a78hmBkzzc9A', + logstash_stats: { + logstash: { + uuid: '61de393a-f2b6-4b6c-8cea-22661f9c4134', + }, + pipelines: [ + { + id: 'main', + ephemeral_id: 'cf37c6fa-2f1a-41e2-9a89-36b420a8b9a5', + queue: { + type: 'memory', + }, + }, + ], + }, + }, + }, + ], + }, + }; + + const options = getBaseOptions(); + processStatsResults(results as any, options); + + expect(options.clusters).toStrictEqual({ + FlV4ckTxQ0a78hmBkzzc9A: { + count: 1, + cluster_stats: { + plugins: [], + collection_types: { + internal_collection: 1, + }, + pipelines: {}, + queues: { + memory: 1, + }, + }, + versions: [], + }, + }); + }); + + it('should summarize stats from hits across multiple result objects', () => { + const options = getBaseOptions(); + + // logstashStatsResultSet is an array of many small query results + logstashStatsResultSet.forEach((results: any) => { + processStatsResults(results, options); + }); + + resultsMap.forEach((value: string[], clusterUuid: string) => { + value.forEach((results: any) => { + processLogstashStateResults(results, clusterUuid, options); + }); + }); + + expect(options.clusters).toStrictEqual({ + '1n1p': { + count: 1, + versions: [ + { + count: 1, + version: '7.10.0', + }, + ], + cluster_stats: { + collection_types: { + internal_collection: 1, + }, + pipelines: { + batch_size_avg: 125, + batch_size_max: 125, + batch_size_min: 125, + batch_size_total: 125, + count: 1, + sources: { + file: true, + }, + workers_avg: 1, + workers_max: 1, + workers_min: 1, + workers_total: 1, + }, + plugins: [ + { + count: 1, + name: 'logstash-input-stdin', + }, + { + count: 1, + name: 'logstash-input-elasticsearch', + }, + { + count: 3, + name: 'logstash-filter-mutate', + }, + { + count: 3, + name: 'logstash-filter-ruby', + }, + { + count: 1, + name: 'logstash-filter-split', + }, + { + count: 1, + name: 'logstash-filter-elasticsearch', + }, + { + count: 1, + name: 'logstash-filter-aggregate', + }, + { + count: 1, + name: 'logstash-filter-drop', + }, + { + count: 1, + name: 'logstash-output-elasticsearch', + }, + { + count: 1, + name: 'logstash-output-stdout', + }, + ], + queues: { + memory: 1, + }, + }, + }, + '1nmp': { + count: 1, + versions: [ + { + count: 1, + version: '7.8.0', + }, + ], + cluster_stats: { + collection_types: { + metricbeat: 1, + }, + pipelines: { + batch_size_avg: 406.5, + batch_size_max: 1251, + batch_size_min: 125, + batch_size_total: 1626, + count: 4, + sources: { + xpack: true, + }, + workers_avg: 17.25, + workers_max: 44, + workers_min: 1, + workers_total: 69, + }, + plugins: [ + { + count: 4, + name: 'logstash-input-stdin', + }, + { + count: 4, + name: 'logstash-output-stdout', + }, + ], + queues: { + memory: 3, + persisted: 1, + }, + }, + }, + mnmp: { + count: 3, + versions: [ + { + count: 1, + version: '7.9.2', + }, + { + count: 1, + version: '7.9.1', + }, + { + count: 1, + version: '7.10.0', + }, + ], + cluster_stats: { + collection_types: { + internal_collection: 3, + }, + pipelines: { + batch_size_avg: 33.294117647058826, + batch_size_max: 125, + batch_size_min: 1, + batch_size_total: 566, + count: 17, + sources: { + file: true, + string: true, + }, + workers_avg: 7.411764705882353, + workers_max: 16, + workers_min: 1, + workers_total: 126, + }, + plugins: [ + { + count: 1, + name: 'logstash-input-stdin', + }, + { + count: 1, + name: 'logstash-filter-clone', + }, + { + count: 3, + name: 'logstash-output-pipeline', + }, + { + count: 2, + name: 'logstash-input-pipeline', + }, + { + count: 16, + name: 'logstash-filter-sleep', + }, + { + count: 14, + name: 'logstash-output-stdout', + }, + { + count: 14, + name: 'logstash-input-generator', + }, + ], + queues: { + memory: 3, + persisted: 14, + }, + }, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_logstash_stats.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_logstash_stats.ts new file mode 100644 index 00000000000000..93c69c644c0649 --- /dev/null +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_logstash_stats.ts @@ -0,0 +1,418 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SearchResponse } from 'elasticsearch'; +import { LegacyAPICaller } from 'kibana/server'; +import { createQuery } from './create_query'; +import { mapToList } from './get_high_level_stats'; +import { incrementByKey } from './get_high_level_stats'; + +import { INDEX_PATTERN_LOGSTASH, TELEMETRY_QUERY_SOURCE } from '../../common/constants'; + +type Counter = Map; + +const HITS_SIZE = 10000; // maximum hits to receive from ES with each search + +export interface LogstashBaseStats { + // stats + versions: Array<{ version: string; count: number }>; + count: number; + + cluster_stats?: { + collection_types?: { [collection_type_type: string]: number }; + queues?: { [queue_type: string]: number }; + plugins?: Array<{ name: string; count: number }>; + pipelines?: { + count?: number; + batch_size_max?: number; + batch_size_avg?: number; + batch_size_min?: number; + batch_size_total?: number; + workers_max?: number; + workers_avg?: number; + workers_min?: number; + workers_total?: number; + sources?: { [source_type: string]: boolean }; + }; + }; +} + +const getLogstashBaseStats = () => ({ + versions: [], + count: 0, + cluster_stats: { + pipelines: {}, + plugins: [], + }, +}); + +export interface LogstashStats { + cluster_uuid: string; + source_node: string; + type: string; + agent?: { + type: string; + }; + logstash_stats?: { + pipelines?: [ + { + id?: string; + ephemeral_id: string; + queue?: { + type: string; + }; + } + ]; + logstash?: { + version?: string; + uuid?: string; + snapshot?: string; + }; + }; +} + +export interface LogstashState { + logstash_state?: { + pipeline?: { + batch_size?: number; + workers?: number; + representation?: { + graph?: { + vertices?: [ + { + config_name?: string; + plugin_type?: string; + meta?: { + source?: { + protocol?: string; + }; + }; + } + ]; + }; + }; + }; + }; +} + +export interface LogstashProcessOptions { + clusters: { [clusterUuid: string]: LogstashBaseStats }; + allEphemeralIds: { [clusterUuid: string]: string[] }; + versions: { [clusterUuid: string]: Counter }; + plugins: { [clusterUuid: string]: Counter }; +} + +/* + * Update a clusters object with processed Logstash stats + * @param {Array} results - array of LogstashStats docs from ES + * @param {Object} clusters - LogstashBaseStats in an object keyed by the cluster UUIDs + * @param {Object} allEphemeralIds - EphemeralIds in an object keyed by cluster UUIDs to track the pipelines for the cluster + * @param {Object} versions - Versions in an object keyed by cluster UUIDs to track the logstash versions for the cluster + * @param {Object} plugins - plugin information keyed by cluster UUIDs to count the unique plugins + */ +export function processStatsResults( + results: SearchResponse, + { clusters, allEphemeralIds, versions, plugins }: LogstashProcessOptions +) { + const currHits = results?.hits?.hits || []; + currHits.forEach((hit) => { + const clusterUuid = hit._source.cluster_uuid; + if (clusters[clusterUuid] === undefined) { + clusters[clusterUuid] = getLogstashBaseStats(); + versions[clusterUuid] = new Map(); + plugins[clusterUuid] = new Map(); + } + const logstashStats = hit._source.logstash_stats; + const clusterStats = clusters[clusterUuid].cluster_stats; + + if (clusterStats !== undefined && logstashStats !== undefined) { + clusters[clusterUuid].count = (clusters[clusterUuid].count || 0) + 1; + + const thisVersion = logstashStats.logstash?.version; + const a: Counter = versions[clusterUuid]; + incrementByKey(a, thisVersion); + clusters[clusterUuid].versions = mapToList(a, 'version'); + + // Internal Collection has no agent field, so default to 'internal_collection' + let thisCollectionType = hit._source.agent?.type; + if (thisCollectionType === undefined) { + thisCollectionType = 'internal_collection'; + } + if (!clusterStats.hasOwnProperty('collection_types')) { + clusterStats.collection_types = {}; + } + clusterStats.collection_types![thisCollectionType] = + (clusterStats.collection_types![thisCollectionType] || 0) + 1; + + const theseEphemeralIds: string[] = []; + const pipelines = logstashStats.pipelines || []; + + pipelines.forEach((pipeline) => { + const thisQueueType = pipeline.queue?.type; + if (thisQueueType !== undefined) { + if (!clusterStats.hasOwnProperty('queues')) { + clusterStats.queues = {}; + } + clusterStats.queues![thisQueueType] = (clusterStats.queues![thisQueueType] || 0) + 1; + } + + const ephemeralId = pipeline.ephemeral_id; + if (ephemeralId !== undefined) { + theseEphemeralIds.push(ephemeralId); + } + }); + allEphemeralIds[clusterUuid] = theseEphemeralIds; + } + }); +} + +/* + * Update a clusters object with logstash state details + * @param {Array} results - array of LogstashState docs from ES + * @param {Object} clusters - LogstashBaseStats in an object keyed by the cluster UUIDs + * @param {Object} plugins - plugin information keyed by cluster UUIDs to count the unique plugins + */ +export function processLogstashStateResults( + results: SearchResponse, + clusterUuid: string, + { clusters, plugins }: LogstashProcessOptions +) { + const currHits = results?.hits?.hits || []; + const clusterStats = clusters[clusterUuid].cluster_stats; + const pipelineStats = clusters[clusterUuid].cluster_stats?.pipelines; + + currHits.forEach((hit) => { + const thisLogstashStatePipeline = hit._source.logstash_state?.pipeline; + + if (pipelineStats !== undefined && thisLogstashStatePipeline !== undefined) { + pipelineStats.count = (pipelineStats.count || 0) + 1; + + const thisPipelineBatchSize = thisLogstashStatePipeline.batch_size; + + if (thisPipelineBatchSize !== undefined) { + pipelineStats.batch_size_total = + (pipelineStats.batch_size_total || 0) + thisPipelineBatchSize; + pipelineStats.batch_size_max = pipelineStats.batch_size_max || 0; + pipelineStats.batch_size_min = pipelineStats.batch_size_min || 0; + pipelineStats.batch_size_avg = pipelineStats.batch_size_total / pipelineStats.count; + + if (thisPipelineBatchSize > pipelineStats.batch_size_max) { + pipelineStats.batch_size_max = thisPipelineBatchSize; + } + if ( + pipelineStats.batch_size_min === 0 || + thisPipelineBatchSize < pipelineStats.batch_size_min + ) { + pipelineStats.batch_size_min = thisPipelineBatchSize; + } + } + + const thisPipelineWorkers = thisLogstashStatePipeline.workers; + if (thisPipelineWorkers !== undefined) { + pipelineStats.workers_total = (pipelineStats.workers_total || 0) + thisPipelineWorkers; + pipelineStats.workers_max = pipelineStats.workers_max || 0; + pipelineStats.workers_min = pipelineStats.workers_min || 0; + pipelineStats.workers_avg = pipelineStats.workers_total / pipelineStats.count; + + if (thisPipelineWorkers > pipelineStats.workers_max) { + pipelineStats.workers_max = thisPipelineWorkers; + } + if (pipelineStats.workers_min === 0 || thisPipelineWorkers < pipelineStats.workers_min) { + pipelineStats.workers_min = thisPipelineWorkers; + } + } + + // Extract the vertices object from the pipeline representation. From this, we can + // retrieve the source of the pipeline element on the configuration(from file, string, or + // x-pack-config-management), and the input, filter and output plugins from that pipeline. + const vertices = thisLogstashStatePipeline.representation?.graph?.vertices; + + if (vertices !== undefined) { + vertices.forEach((vertex) => { + const configName = vertex.config_name; + const pluginType = vertex.plugin_type; + let pipelineConfig = vertex.meta?.source?.protocol; + + if (pipelineConfig !== undefined) { + if (pipelineConfig === 'string' || pipelineConfig === 'str') { + pipelineConfig = 'string'; + } else if (pipelineConfig === 'x-pack-config-management') { + pipelineConfig = 'xpack'; + } else { + pipelineConfig = 'file'; + } + if (!pipelineStats.hasOwnProperty('sources')) { + pipelineStats.sources = {}; + } + pipelineStats.sources![pipelineConfig] = true; + } + if (configName !== undefined && pluginType !== undefined) { + incrementByKey(plugins[clusterUuid], `logstash-${pluginType}-${configName}`); + } + }); + } + } + }); + if (clusterStats !== undefined) { + clusterStats.plugins = mapToList(plugins[clusterUuid], 'name'); + } +} + +export async function fetchLogstashStats( + callCluster: LegacyAPICaller, + clusterUuids: string[], + { page = 0, ...options }: { page?: number } & LogstashProcessOptions +): Promise { + const params = { + headers: { + 'X-QUERY-SOURCE': TELEMETRY_QUERY_SOURCE, + }, + index: INDEX_PATTERN_LOGSTASH, + ignoreUnavailable: true, + filterPath: [ + 'hits.hits._source.cluster_uuid', + 'hits.hits._source.type', + 'hits.hits._source.source_node', + 'hits.hits._source.agent.type', + 'hits.hits._source.logstash_stats.pipelines.id', + 'hits.hits._source.logstash_stats.pipelines.ephemeral_id', + 'hits.hits._source.logstash_stats.pipelines.queue.type', + 'hits.hits._source.logstash_stats.logstash.version', + 'hits.hits._source.logstash_stats.logstash.uuid', + ], + body: { + query: createQuery({ + filters: [ + { terms: { cluster_uuid: clusterUuids } }, + { + bool: { + must: { term: { type: 'logstash_stats' } }, + }, + }, + ], + }), + from: page * HITS_SIZE, + collapse: { field: 'logstash_stats.logstash.uuid' }, + sort: [{ ['logstash_stats.timestamp']: { order: 'desc', unmapped_type: 'long' } }], + size: HITS_SIZE, + }, + }; + + const results = await callCluster>('search', params); + const hitsLength = results?.hits?.hits.length || 0; + + if (hitsLength > 0) { + // further augment the clusters object with more stats + processStatsResults(results, options); + + if (hitsLength === HITS_SIZE) { + // call recursively + const nextOptions = { + page: page + 1, + ...options, + }; + + // returns a promise and keeps the caller blocked from returning until the entire clusters object is built + return fetchLogstashStats(callCluster, clusterUuids, nextOptions); + } + } + return Promise.resolve(); +} + +export async function fetchLogstashState( + callCluster: LegacyAPICaller, + clusterUuid: string, + ephemeralIds: string[], + { page = 0, ...options }: { page?: number } & LogstashProcessOptions +): Promise { + const params = { + headers: { + 'X-QUERY-SOURCE': TELEMETRY_QUERY_SOURCE, + }, + index: INDEX_PATTERN_LOGSTASH, + ignoreUnavailable: true, + filterPath: [ + 'hits.hits._source.logstash_state.pipeline.batch_size', + 'hits.hits._source.logstash_state.pipeline.workers', + 'hits.hits._source.logstash_state.pipeline.representation.graph.vertices.config_name', + 'hits.hits._source.logstash_state.pipeline.representation.graph.vertices.plugin_type', + 'hits.hits._source.logstash_state.pipeline.representation.graph.vertices.meta.source.protocol', + 'hits.hits._source.logstash_state.pipeline.representation.graph.vertices', + 'hits.hits._source.type', + ], + body: { + query: createQuery({ + filters: [ + { terms: { 'logstash_state.pipeline.ephemeral_id': ephemeralIds } }, + { + bool: { + must: { term: { type: 'logstash_state' } }, + }, + }, + ], + }), + from: page * HITS_SIZE, + collapse: { field: 'logstash_state.pipeline.ephemeral_id' }, + sort: [{ ['timestamp']: { order: 'desc', unmapped_type: 'long' } }], + size: HITS_SIZE, + }, + }; + + const results = await callCluster>('search', params); + const hitsLength = results?.hits?.hits.length || 0; + if (hitsLength > 0) { + // further augment the clusters object with more stats + processLogstashStateResults(results, clusterUuid, options); + + if (hitsLength === HITS_SIZE) { + // call recursively + const nextOptions = { + page: page + 1, + ...options, + }; + + // returns a promise and keeps the caller blocked from returning until the entire clusters object is built + return fetchLogstashState(callCluster, clusterUuid, ephemeralIds, nextOptions); + } + } + return Promise.resolve(); +} + +export interface LogstashStatsByClusterUuid { + [clusterUuid: string]: LogstashBaseStats; +} + +/* + * Call the function for fetching and summarizing Logstash stats + * @return {Object} - Logstash stats in an object keyed by the cluster UUIDs + */ +export async function getLogstashStats( + callCluster: LegacyAPICaller, + clusterUuids: string[] +): Promise { + const options: LogstashProcessOptions = { + clusters: {}, // the result object to be built up + allEphemeralIds: {}, + versions: {}, + plugins: {}, + }; + + await fetchLogstashStats(callCluster, clusterUuids, options); + await Promise.all( + clusterUuids.map(async (clusterUuid) => { + if (options.clusters[clusterUuid] !== undefined) { + await fetchLogstashState( + callCluster, + clusterUuid, + options.allEphemeralIds[clusterUuid], + options + ); + } + }) + ); + return options.clusters; +} diff --git a/x-pack/plugins/monitoring/tsconfig.json b/x-pack/plugins/monitoring/tsconfig.json index 760ff188aacfc1..1835c4a75d9d47 100644 --- a/x-pack/plugins/monitoring/tsconfig.json +++ b/x-pack/plugins/monitoring/tsconfig.json @@ -7,25 +7,30 @@ "declaration": true, "declarationMap": true }, - "include": ["common/**/*", "public/**/*", "server/**/*"], + "include": [ + "common/**/*", + "public/**/*", + "server/**/*" + ], "references": [ { "path": "../../../src/core/tsconfig.json" }, { "path": "../../../src/plugins/data/tsconfig.json" }, { "path": "../../../src/plugins/home/tsconfig.json" }, { "path": "../../../src/plugins/kibana_legacy/tsconfig.json" }, - { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../../src/plugins/navigation/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, - { "path": "../actions/tsconfig.json" }, { "path": "../alerts/tsconfig.json" }, + { "path": "../actions/tsconfig.json" }, { "path": "../cloud/tsconfig.json" }, { "path": "../encrypted_saved_objects/tsconfig.json" }, { "path": "../features/tsconfig.json" }, { "path": "../infra/tsconfig.json" }, - { "path": "../license_management/tsconfig.json" }, { "path": "../licensing/tsconfig.json" }, + { "path": "../license_management/tsconfig.json" }, { "path": "../observability/tsconfig.json" }, { "path": "../telemetry_collection_xpack/tsconfig.json" }, - { "path": "../triggers_actions_ui/tsconfig.json" } + { "path": "../triggers_actions_ui/tsconfig.json" }, ] } diff --git a/x-pack/plugins/observability/server/index.ts b/x-pack/plugins/observability/server/index.ts index fa2aa5f4e60dff..e118d17e17c3fd 100644 --- a/x-pack/plugins/observability/server/index.ts +++ b/x-pack/plugins/observability/server/index.ts @@ -10,7 +10,7 @@ import { PluginInitializerContext } from 'src/core/server'; import { ObservabilityPlugin, ObservabilityPluginSetup } from './plugin'; import { createOrUpdateIndex, MappingsDefinition } from './utils/create_or_update_index'; import { ScopedAnnotationsClient } from './lib/annotations/bootstrap_annotations'; -import { unwrapEsResponse } from './utils/unwrap_es_response'; +import { unwrapEsResponse, WrappedElasticsearchClientError } from './utils/unwrap_es_response'; export const config = { schema: schema.object({ @@ -33,4 +33,5 @@ export { ObservabilityPluginSetup, ScopedAnnotationsClient, unwrapEsResponse, + WrappedElasticsearchClientError, }; diff --git a/x-pack/plugins/observability/server/utils/unwrap_es_response.ts b/x-pack/plugins/observability/server/utils/unwrap_es_response.ts index 0bbd53061f8512..81f8be4e0f696d 100644 --- a/x-pack/plugins/observability/server/utils/unwrap_es_response.ts +++ b/x-pack/plugins/observability/server/utils/unwrap_es_response.ts @@ -4,11 +4,45 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { ElasticsearchClientError, ResponseError } from '@elastic/elasticsearch/lib/errors'; import type { UnwrapPromise } from '@kbn/utility-types'; +import { inspect } from 'util'; + +export class WrappedElasticsearchClientError extends Error { + originalError: ElasticsearchClientError; + constructor(originalError: ElasticsearchClientError) { + super(originalError.message); + + const stack = this.stack; + + this.originalError = originalError; + + if (originalError instanceof ResponseError) { + // make sure ES response body is visible when logged to the console + // @ts-expect-error + this.stack = { + valueOf() { + const value = stack?.valueOf() ?? ''; + return value; + }, + toString() { + const value = + stack?.toString() + + `\nResponse: ${inspect(originalError.meta.body, { depth: null })}\n`; + return value; + }, + }; + } + } +} export function unwrapEsResponse>( responsePromise: T ): Promise['body']> { - return responsePromise.then((res) => res.body); + return responsePromise + .then((res) => res.body) + .catch((err) => { + // make sure stacktrace is relative to where client was called + throw new WrappedElasticsearchClientError(err); + }); } diff --git a/x-pack/plugins/osquery/common/ecs/rule/index.ts b/x-pack/plugins/osquery/common/ecs/rule/index.ts index 3a764a836e9b34..51d7722a3ecdec 100644 --- a/x-pack/plugins/osquery/common/ecs/rule/index.ts +++ b/x-pack/plugins/osquery/common/ecs/rule/index.ts @@ -28,7 +28,7 @@ export interface RuleEcs { tags?: string[]; threat?: unknown; threshold?: { - field: string; + field: string | string[]; value: number; }; type?: string[]; diff --git a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/components/remove_cluster_button_provider/remove_cluster_button_provider.js b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/components/remove_cluster_button_provider/remove_cluster_button_provider.js index fd9f4e3503d106..4db5d1b333b7cf 100644 --- a/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/components/remove_cluster_button_provider/remove_cluster_button_provider.js +++ b/x-pack/plugins/remote_clusters/public/application/sections/remote_cluster_list/components/remove_cluster_button_provider/remove_cluster_button_provider.js @@ -10,7 +10,7 @@ import PropTypes from 'prop-types'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; export class RemoveClusterButtonProvider extends Component { static propTypes = { @@ -84,7 +84,7 @@ export class RemoveClusterButtonProvider extends Component { ); modal = ( - + <> {/* eslint-disable-next-line jsx-a11y/mouse-events-have-key-events */} {!isSingleCluster && content} - + ); } diff --git a/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts index 1d9881c400ec8d..685aee16dc665c 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/add_route.ts @@ -88,7 +88,7 @@ export const register = (deps: RouteDependencies): void => { if (isEsError(error)) { return response.customError({ statusCode: error.statusCode, body: error }); } - return response.internalError({ body: error }); + throw error; } }; deps.router.post( diff --git a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts index 3a65bb2c54d952..89df5255d19e2e 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/delete_route.ts @@ -95,7 +95,7 @@ export const register = (deps: RouteDependencies): void => { if (isEsError(error)) { return response.customError({ statusCode: error.statusCode, body: error }); } - return response.internalError({ body: error }); + throw error; } }; @@ -132,7 +132,7 @@ export const register = (deps: RouteDependencies): void => { if (isEsError(error)) { return response.customError({ statusCode: error.statusCode, body: error }); } - return response.internalError({ body: error }); + throw error; } }; diff --git a/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts index 25d17d796b0ee4..cfec01da943ab7 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/get_route.test.ts @@ -78,10 +78,16 @@ describe('GET remote clusters', () => { const mockContext = xpackMocks.createRequestHandlerContext(); mockContext.core.elasticsearch.legacy.client = mockScopedClusterClient; - const response = await handler(mockContext, mockRequest, kibanaResponseFactory); + if (asserts.statusCode === 500) { + await expect(handler(mockContext, mockRequest, kibanaResponseFactory)).rejects.toThrowError( + asserts.result as Error + ); + } else { + const response = await handler(mockContext, mockRequest, kibanaResponseFactory); - expect(response.status).toBe(asserts.statusCode); - expect(response.payload).toEqual(asserts.result); + expect(response.status).toBe(asserts.statusCode); + expect(response.payload).toEqual(asserts.result); + } if (Array.isArray(asserts.apiArguments)) { for (const apiArguments of asserts.apiArguments) { diff --git a/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts index 1445316cfec37c..fbb345203e48a8 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/get_route.ts @@ -63,7 +63,7 @@ export const register = (deps: RouteDependencies): void => { if (isEsError(error)) { return response.customError({ statusCode: error.statusCode, body: error }); } - return response.internalError({ body: error }); + throw error; } }; diff --git a/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts b/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts index 5e1fdbb2bc0db1..99fb7dd01adb13 100644 --- a/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts +++ b/x-pack/plugins/remote_clusters/server/routes/api/update_route.ts @@ -100,7 +100,7 @@ export const register = (deps: RouteDependencies): void => { if (isEsError(error)) { return response.customError({ statusCode: error.statusCode, body: error }); } - return response.internalError({ body: error }); + throw error; } }; diff --git a/x-pack/plugins/reporting/public/components/buttons/__snapshots__/report_info_button.test.tsx.snap b/x-pack/plugins/reporting/public/components/buttons/__snapshots__/report_info_button.test.tsx.snap index 09e487591c164a..692b410bd7e5f3 100644 --- a/x-pack/plugins/reporting/public/components/buttons/__snapshots__/report_info_button.test.tsx.snap +++ b/x-pack/plugins/reporting/public/components/buttons/__snapshots__/report_info_button.test.tsx.snap @@ -58,7 +58,7 @@ Array [ >
    ,
    ,
    { }); return ( - - this.hideConfirm()} - onConfirm={() => this.props.performDelete()} - confirmButtonText={confirmButtonText} - cancelButtonText={cancelButtonText} - defaultFocusedButton="confirm" - buttonColor="danger" - > - {message} - - + this.hideConfirm()} + onConfirm={() => this.props.performDelete()} + confirmButtonText={confirmButtonText} + cancelButtonText={cancelButtonText} + defaultFocusedButton="confirm" + buttonColor="danger" + > + {message} + ); } diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_font.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_font.test.ts new file mode 100644 index 00000000000000..e2d44b43b674ba --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_font.test.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getFont } from './get_font'; + +describe('getFont', () => { + it(`returns 'noto-cjk' when matching cjk characters`, () => { + const cjkStrings = [ + 'vi-Hani: 关', + 'ko: 全', + 'ja: 入', + 'zh-Hant-HK: 免', + 'zh-Hant: 令', + 'zh-Hans: 令', + 'random: おあいい 漢字 あい 抵 令', + String.fromCharCode(0x4ee4), + String.fromCodePoint(0x9aa8), + ]; + + for (const cjkString of cjkStrings) { + expect(getFont(cjkString)).toBe('noto-cjk'); + } + }); + + it(`returns 'Roboto' for non Han characters`, () => { + expect(getFont('English text')).toBe('Roboto'); + expect(getFont('')).toBe('Roboto'); + expect(getFont(undefined!)).toBe('Roboto'); + }); +}); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_font.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_font.ts index e27489b5d66bc8..4997d37327102d 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_font.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/pdf/get_font.ts @@ -5,16 +5,11 @@ * 2.0. */ -// @ts-ignore: no module definition -import xRegExp from 'xregexp'; - export function getFont(text: string) { - // Once unicode regex scripts are fully supported we should be able to get rid of the dependency - // on xRegExp library. See https://github.com/tc39/proposal-regexp-unicode-property-escapes - // for more information. We are matching Han characters which is one of the supported unicode scripts + // We are matching Han characters which is one of the supported unicode scripts // (you can see the full list of supported scripts here: http://www.unicode.org/standard/supported.html). // This will match Chinese, Japanese, Korean and some other Asian languages. - const isCKJ = xRegExp('\\p{Han}').test(text, 'g'); + const isCKJ = /\p{Script=Han}/gu.test(text); if (isCKJ) { return 'noto-cjk'; } else { diff --git a/x-pack/plugins/rollup/public/crud_app/sections/components/job_action_menu/confirm_delete_modal/confirm_delete_modal.js b/x-pack/plugins/rollup/public/crud_app/sections/components/job_action_menu/confirm_delete_modal/confirm_delete_modal.js index 650d63f38eeb52..ec7473c69dec19 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/components/job_action_menu/confirm_delete_modal/confirm_delete_modal.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/components/job_action_menu/confirm_delete_modal/confirm_delete_modal.js @@ -10,7 +10,7 @@ import PropTypes from 'prop-types'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; export class ConfirmDeleteModal extends Component { static propTypes = { @@ -91,28 +91,26 @@ export class ConfirmDeleteModal extends Component { } return ( - - - {content} - - + + {content} + ); } } 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 index cd35552ed5ad71..694ab3c467c1f9 100644 --- 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 @@ -34,7 +34,7 @@ export const registerGetRoute = ({ if (isEsError(err)) { return response.customError({ statusCode: err.statusCode, body: err }); } - return response.internalError({ body: err }); + throw 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 index 854f4986e76867..90eabaa88b6410 100644 --- 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 @@ -136,7 +136,7 @@ export const registerValidateIndexPatternRoute = ({ return response.customError({ statusCode: err.statusCode, body: err }); } - return response.internalError({ body: err }); + throw err; } }) ); 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 index 14ff452a4dd546..bcb3a337aa7253 100644 --- 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 @@ -43,7 +43,7 @@ export const registerCreateRoute = ({ if (isEsError(err)) { return response.customError({ statusCode: err.statusCode, body: err }); } - return response.internalError({ body: err }); + throw 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 index e94a1a80ce134d..4bbe73753e96cf 100644 --- 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 @@ -45,7 +45,7 @@ export const registerDeleteRoute = ({ if (isEsError(err)) { return response.customError({ statusCode: err.statusCode, body: err }); } - return response.internalError({ body: err }); + throw 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 index 12b3f96e778358..a9a30c0370c5f3 100644 --- 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 @@ -26,7 +26,7 @@ export const registerGetRoute = ({ if (isEsError(err)) { return response.customError({ statusCode: err.statusCode, body: err }); } - return response.internalError({ body: err }); + throw 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 index c560b41cc4385b..2ebfcc437f41e5 100644 --- 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 @@ -42,7 +42,7 @@ export const registerStartRoute = ({ if (isEsError(err)) { return response.customError({ statusCode: err.statusCode, body: err }); } - return response.internalError({ body: err }); + throw 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 index 87cf2822d4f1b6..faaf377a2d833d 100644 --- 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 @@ -43,7 +43,7 @@ export const registerStopRoute = ({ if (isEsError(err)) { return response.customError({ statusCode: err.statusCode, body: err }); } - return response.internalError({ body: err }); + throw err; } }) ); 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 index 759e05dc2a3343..f77ae7829bb6c5 100644 --- 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 @@ -41,7 +41,7 @@ export const registerSearchRoute = ({ if (isEsError(err)) { return response.customError({ statusCode: err.statusCode, body: err }); } - return response.internalError({ body: err }); + throw err; } }) ); diff --git a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx index f6815a4264a5d4..023b620522282b 100644 --- a/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx +++ b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx @@ -8,7 +8,6 @@ import React, { useEffect, useState, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiCode } from '@elastic/eui'; import { PainlessLang, PainlessContext } from '@kbn/monaco'; import { EuiFlexGroup, @@ -19,6 +18,7 @@ import { EuiComboBoxOptionOption, EuiLink, EuiCallOut, + EuiCode, } from '@elastic/eui'; import { diff --git a/x-pack/plugins/security/public/components/confirm_modal.tsx b/x-pack/plugins/security/public/components/confirm_modal.tsx index d0ca1de07314e2..3802ee368d735b 100644 --- a/x-pack/plugins/security/public/components/confirm_modal.tsx +++ b/x-pack/plugins/security/public/components/confirm_modal.tsx @@ -18,7 +18,6 @@ import { EuiModalHeader, EuiModalHeaderTitle, EuiModalProps, - EuiOverlayMask, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -29,13 +28,11 @@ export interface ConfirmModalProps extends Omit = ({ children, @@ -45,51 +42,42 @@ export const ConfirmModal: FunctionComponent = ({ isDisabled, onCancel, onConfirm, - ownFocus = true, title, ...rest -}) => { - const modal = ( - - - {title} - - {children} - - - - - - - - - - {confirmButtonText} - - - - - - ); - - return ownFocus ? ( - {modal} - ) : ( - modal - ); -}; +}) => ( + + + {title} + + {children} + + + + + + + + + + {confirmButtonText} + + + + + +); diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/invalidate_provider/invalidate_provider.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/invalidate_provider/invalidate_provider.tsx index ae142e76877cef..232847b63cb1ab 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/invalidate_provider/invalidate_provider.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/invalidate_provider/invalidate_provider.tsx @@ -6,7 +6,7 @@ */ import React, { Fragment, useRef, useState } from 'react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import type { PublicMethodsOf } from '@kbn/utility-types'; import { i18n } from '@kbn/i18n'; import { NotificationsStart } from 'src/core/public'; @@ -127,58 +127,56 @@ export const InvalidateProvider: React.FunctionComponent = ({ const isSingle = apiKeys.length === 1; return ( - - - {!isSingle ? ( - -

    - {i18n.translate( - 'xpack.security.management.apiKeys.invalidateApiKey.confirmModal.invalidateMultipleListDescription', - { defaultMessage: 'You are about to invalidate these API keys:' } - )} -

    -
      - {apiKeys.map(({ name, id }) => ( -
    • {name}
    • - ))} -
    -
    - ) : null} -
    -
    + )} + buttonColor="danger" + data-test-subj="invalidateApiKeyConfirmationModal" + > + {!isSingle ? ( + +

    + {i18n.translate( + 'xpack.security.management.apiKeys.invalidateApiKey.confirmModal.invalidateMultipleListDescription', + { defaultMessage: 'You are about to invalidate these API keys:' } + )} +

    +
      + {apiKeys.map(({ name, id }) => ( +
    • {name}
    • + ))} +
    +
    + ) : null} + ); }; diff --git a/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx index c5e6e3cb9860d0..680a4a40a7d9a1 100644 --- a/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx @@ -6,7 +6,7 @@ */ import React, { Fragment, useRef, useState, ReactElement } from 'react'; -import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { PublicMethodsOf } from '@kbn/utility-types'; import { NotificationsStart } from 'src/core/public'; @@ -140,59 +140,57 @@ export const DeleteProvider: React.FunctionComponent = ({ const isSingle = roleMappings.length === 1; return ( - - - {!isSingle ? ( - -

    - {i18n.translate( - 'xpack.security.management.roleMappings.deleteRoleMapping.confirmModal.deleteMultipleListDescription', - { defaultMessage: 'You are about to delete these role mappings:' } - )} -

    -
      - {roleMappings.map(({ name }) => ( -
    • {name}
    • - ))} -
    -
    - ) : null} -
    -
    + )} + confirmButtonDisabled={isDeleteInProgress} + buttonColor="danger" + data-test-subj="deleteRoleMappingConfirmationModal" + > + {!isSingle ? ( + +

    + {i18n.translate( + 'xpack.security.management.roleMappings.deleteRoleMapping.confirmModal.deleteMultipleListDescription', + { defaultMessage: 'You are about to delete these role mappings:' } + )} +

    +
      + {roleMappings.map(({ name }) => ( +
    • {name}
    • + ))} +
    +
    + ) : null} + ); }; diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.tsx index b094f78a53e778..d027a1aeb7e1fc 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.tsx @@ -9,7 +9,6 @@ import React, { Component, Fragment } from 'react'; import { EuiSpacer, EuiConfirmModal, - EuiOverlayMask, EuiCallOut, EuiErrorBoundary, EuiIcon, @@ -228,40 +227,38 @@ export class RuleEditorPanel extends Component { return null; } return ( - - - } - onCancel={() => this.setState({ showConfirmModeChange: false })} - onConfirm={() => { - this.setState({ mode: 'visual', showConfirmModeChange: false }); - this.onValidityChange(true); - }} - cancelButtonText={ - - } - confirmButtonText={ - - } - > -

    - -

    -
    -
    + + } + onCancel={() => this.setState({ showConfirmModeChange: false })} + onConfirm={() => { + this.setState({ mode: 'visual', showConfirmModeChange: false }); + this.onValidityChange(true); + }} + cancelButtonText={ + + } + confirmButtonText={ + + } + > +

    + +

    +
    ); }; diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_title.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_title.tsx index 6e94abfb3f4a20..478e8d87abf95c 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_title.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_title.tsx @@ -12,7 +12,6 @@ import { EuiContextMenuItem, EuiLink, EuiIcon, - EuiOverlayMask, EuiConfirmModal, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -87,45 +86,43 @@ export const RuleGroupTitle = (props: Props) => { ); const confirmChangeModal = showConfirmChangeModal ? ( - - - } - onCancel={() => { - setShowConfirmChangeModal(false); - setPendingNewRule(null); - }} - onConfirm={() => { - setShowConfirmChangeModal(false); - changeRuleDiscardingSubRules(pendingNewRule!); - setPendingNewRule(null); - }} - cancelButtonText={ - - } - confirmButtonText={ - - } - > -

    - -

    -
    -
    + + } + onCancel={() => { + setShowConfirmChangeModal(false); + setPendingNewRule(null); + }} + onConfirm={() => { + setShowConfirmChangeModal(false); + changeRuleDiscardingSubRules(pendingNewRule!); + setPendingNewRule(null); + }} + cancelButtonText={ + + } + confirmButtonText={ + + } + > +

    + +

    +
    ) : null; return ( diff --git a/x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.tsx b/x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.tsx index bd3c86575c61a4..1b3a7fa024dd16 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiButtonEmpty, EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { EuiButtonEmpty, EuiConfirmModal } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; @@ -46,44 +46,42 @@ export class DeleteRoleButton extends Component { return null; } return ( - - - } - onCancel={this.closeModal} - onConfirm={this.onConfirmDelete} - cancelButtonText={ - - } - confirmButtonText={ - - } - buttonColor={'danger'} - > -

    - -

    -

    - -

    -
    -
    + + } + onCancel={this.closeModal} + onConfirm={this.onConfirmDelete} + cancelButtonText={ + + } + confirmButtonText={ + + } + buttonColor={'danger'} + > +

    + +

    +

    + +

    +
    ); }; diff --git a/x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/confirm_delete.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/confirm_delete.tsx index dbbb09f1598b63..81302465bb3732 100644 --- a/x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/confirm_delete.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/confirm_delete.tsx @@ -14,7 +14,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -55,65 +54,61 @@ export class ConfirmDelete extends Component { // to disable the buttons since this could be a long-running operation return ( - - - - - {title} - - - - - {moreThanOne ? ( - -

    - -

    -
      - {rolesToDelete.map((roleName) => ( -
    • {roleName}
    • - ))} -
    -
    - ) : null} -

    - -

    -
    -
    - - + + + {title} + + + + {moreThanOne ? ( + +

    + +

    +
      + {rolesToDelete.map((roleName) => ( +
    • {roleName}
    • + ))} +
    +
    + ) : null} +

    - +

    +
    +
    + + + + - - - - -
    -
    + + + + + ); } diff --git a/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx index c670a9ce99f5bb..38adca145dfc5d 100644 --- a/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx +++ b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx @@ -6,7 +6,7 @@ */ import React, { Component, Fragment } from 'react'; -import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; +import { EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import type { PublicMethodsOf } from '@kbn/utility-types'; @@ -35,46 +35,44 @@ export class ConfirmDeleteUsers extends Component { values: { userLength: usersToDelete[0] }, }); return ( - - -
    - {moreThanOne ? ( - -

    - -

    -
      - {usersToDelete.map((username) => ( -
    • {username}
    • - ))} -
    -
    - ) : null} -

    - -

    -
    -
    -
    + +
    + {moreThanOne ? ( + +

    + +

    +
      + {usersToDelete.map((username) => ( +
    • {username}
    • + ))} +
    +
    + ) : null} +

    + +

    +
    +
    ); } diff --git a/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx b/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx index 9e8745538e0ed3..189f0c3845d635 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/confirm_delete_users.tsx @@ -68,7 +68,6 @@ export const ConfirmDeleteUsers: FunctionComponent = ({ )} confirmButtonColor="danger" isLoading={state.loading} - ownFocus >

    diff --git a/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx b/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx index 793f0e6c2a420b..e0fb4e554ee3c4 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/confirm_disable_users.tsx @@ -80,7 +80,6 @@ export const ConfirmDisableUsers: FunctionComponent = } confirmButtonColor={isSystemUser ? 'danger' : undefined} isLoading={state.loading} - ownFocus > {isSystemUser ? ( diff --git a/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx b/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx index a1aac5bc0a8cb4..2cb4cf8b4a9e2c 100644 --- a/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/confirm_enable_users.tsx @@ -67,7 +67,6 @@ export const ConfirmEnableUsers: FunctionComponent = ({ } )} isLoading={state.loading} - ownFocus >

    diff --git a/x-pack/plugins/security/server/audit/audit_service.test.ts b/x-pack/plugins/security/server/audit/audit_service.test.ts index 3b07d766d7cb46..f59fd6ecdec919 100644 --- a/x-pack/plugins/security/server/audit/audit_service.test.ts +++ b/x-pack/plugins/security/server/audit/audit_service.test.ts @@ -76,9 +76,9 @@ describe('#setup', () => { config: { enabled: true, appender: { - kind: 'console', + type: 'console', layout: { - kind: 'pattern', + type: 'pattern', }, }, }, @@ -102,9 +102,9 @@ describe('#setup', () => { config: { enabled: true, appender: { - kind: 'console', + type: 'console', layout: { - kind: 'pattern', + type: 'pattern', }, }, }, @@ -251,9 +251,9 @@ describe('#createLoggingConfig', () => { createLoggingConfig({ enabled: true, appender: { - kind: 'console', + type: 'console', layout: { - kind: 'pattern', + type: 'pattern', }, }, }) @@ -264,10 +264,10 @@ describe('#createLoggingConfig', () => { Object { "appenders": Object { "auditTrailAppender": Object { - "kind": "console", "layout": Object { - "kind": "pattern", + "type": "pattern", }, + "type": "console", }, }, "loggers": Array [ @@ -275,8 +275,8 @@ describe('#createLoggingConfig', () => { "appenders": Array [ "auditTrailAppender", ], - "context": "audit.ecs", "level": "info", + "name": "audit.ecs", }, ], } @@ -293,9 +293,9 @@ describe('#createLoggingConfig', () => { createLoggingConfig({ enabled: false, appender: { - kind: 'console', + type: 'console', layout: { - kind: 'pattern', + type: 'pattern', }, }, }) @@ -331,9 +331,9 @@ describe('#createLoggingConfig', () => { createLoggingConfig({ enabled: true, appender: { - kind: 'console', + type: 'console', layout: { - kind: 'pattern', + type: 'pattern', }, }, }) diff --git a/x-pack/plugins/security/server/audit/audit_service.ts b/x-pack/plugins/security/server/audit/audit_service.ts index 42e36e50d6d42d..99dd2c82ec9fe5 100644 --- a/x-pack/plugins/security/server/audit/audit_service.ts +++ b/x-pack/plugins/security/server/audit/audit_service.ts @@ -224,16 +224,16 @@ export const createLoggingConfig = (config: ConfigType['audit']) => map, LoggerContextConfigInput>((features) => ({ appenders: { auditTrailAppender: config.appender ?? { - kind: 'console', + type: 'console', layout: { - kind: 'pattern', + type: 'pattern', highlight: true, }, }, }, loggers: [ { - context: 'audit.ecs', + name: 'audit.ecs', level: config.enabled && config.appender && features.allowAuditLogging ? 'info' : 'off', appenders: ['auditTrailAppender'], }, diff --git a/x-pack/plugins/security/server/authentication/authentication_service.test.ts b/x-pack/plugins/security/server/authentication/authentication_service.test.ts index 65dd15b627c7f1..a9c5fa3577476d 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.test.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.test.ts @@ -149,7 +149,6 @@ describe('AuthenticationService', () => { expect(mockAuthToolkit.authenticated).toHaveBeenCalledTimes(1); expect(mockAuthToolkit.authenticated).toHaveBeenCalledWith(); expect(mockAuthToolkit.redirected).not.toHaveBeenCalled(); - expect(mockResponse.internalError).not.toHaveBeenCalled(); expect(authenticate).not.toHaveBeenCalled(); }); @@ -172,7 +171,6 @@ describe('AuthenticationService', () => { requestHeaders: mockAuthHeaders, }); expect(mockAuthToolkit.redirected).not.toHaveBeenCalled(); - expect(mockResponse.internalError).not.toHaveBeenCalled(); expect(authenticate).toHaveBeenCalledTimes(1); expect(authenticate).toHaveBeenCalledWith(mockRequest); @@ -201,7 +199,6 @@ describe('AuthenticationService', () => { responseHeaders: mockAuthResponseHeaders, }); expect(mockAuthToolkit.redirected).not.toHaveBeenCalled(); - expect(mockResponse.internalError).not.toHaveBeenCalled(); expect(authenticate).toHaveBeenCalledTimes(1); expect(authenticate).toHaveBeenCalledWith(mockRequest); @@ -223,7 +220,6 @@ describe('AuthenticationService', () => { 'WWW-Authenticate': 'Negotiate', }); expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled(); - expect(mockResponse.internalError).not.toHaveBeenCalled(); }); it('rejects with `Internal Server Error` and log error when `authenticate` throws unhandled exception', async () => { @@ -231,15 +227,12 @@ describe('AuthenticationService', () => { const failureReason = new Error('something went wrong'); authenticate.mockRejectedValue(failureReason); - await authHandler(httpServerMock.createKibanaRequest(), mockResponse, mockAuthToolkit); - - expect(mockResponse.internalError).toHaveBeenCalledTimes(1); - const [[error]] = mockResponse.internalError.mock.calls; - expect(error).toBeUndefined(); + await expect( + authHandler(httpServerMock.createKibanaRequest(), mockResponse, mockAuthToolkit) + ).rejects.toThrow(failureReason); expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled(); expect(mockAuthToolkit.redirected).not.toHaveBeenCalled(); - expect(logger.error).toHaveBeenCalledWith(failureReason); }); it('rejects with original `badRequest` error when `authenticate` fails to authenticate user', async () => { diff --git a/x-pack/plugins/security/server/authentication/authentication_service.ts b/x-pack/plugins/security/server/authentication/authentication_service.ts index 0543e2abd60df3..6848d7a3c7df63 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.ts @@ -93,13 +93,7 @@ export class AuthenticationService { }); } - let authenticationResult; - try { - authenticationResult = await this.authenticator.authenticate(request); - } catch (err) { - this.logger.error(err); - return response.internalError(); - } + const authenticationResult = await this.authenticator.authenticate(request); if (authenticationResult.succeeded()) { return t.authenticated({ diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index d4dcca8bebb0c1..53e4152b3c8fbf 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -1558,21 +1558,21 @@ describe('createConfig()', () => { ConfigSchema.validate({ audit: { appender: { - kind: 'file', - path: '/path/to/file.txt', + type: 'file', + fileName: '/path/to/file.txt', layout: { - kind: 'json', + type: 'json', }, }, }, }).audit.appender ).toMatchInlineSnapshot(` Object { - "kind": "file", + "fileName": "/path/to/file.txt", "layout": Object { - "kind": "json", + "type": "json", }, - "path": "/path/to/file.txt", + "type": "file", } `); }); @@ -1583,12 +1583,12 @@ describe('createConfig()', () => { audit: { // no layout configured appender: { - kind: 'file', + type: 'file', path: '/path/to/file.txt', }, }, }) - ).toThrow('[audit.appender.2.kind]: expected value to equal [legacy-appender]'); + ).toThrow('[audit.appender.2.type]: expected value to equal [legacy-appender]'); }); it('rejects an ignore_filter when no appender is configured', () => { diff --git a/x-pack/plugins/security/server/config_deprecations.test.ts b/x-pack/plugins/security/server/config_deprecations.test.ts index bdb02d8ed99750..c4c7f399e7b5d3 100644 --- a/x-pack/plugins/security/server/config_deprecations.test.ts +++ b/x-pack/plugins/security/server/config_deprecations.test.ts @@ -52,6 +52,117 @@ describe('Config Deprecations', () => { `); }); + it('renames audit.appender.kind to audit.appender.type', () => { + const config = { + xpack: { + security: { + audit: { + appender: { + kind: 'console', + }, + }, + }, + }, + }; + const { messages, migrated } = applyConfigDeprecations(cloneDeep(config)); + expect(migrated.xpack.security.audit.appender.kind).not.toBeDefined(); + expect(migrated.xpack.security.audit.appender.type).toEqual('console'); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"xpack.security.audit.appender.kind\\" is deprecated and has been replaced by \\"xpack.security.audit.appender.type\\"", + ] + `); + }); + + it('renames audit.appender.layout.kind to audit.appender.layout.type', () => { + const config = { + xpack: { + security: { + audit: { + appender: { + layout: { kind: 'pattern' }, + }, + }, + }, + }, + }; + const { messages, migrated } = applyConfigDeprecations(cloneDeep(config)); + expect(migrated.xpack.security.audit.appender.layout.kind).not.toBeDefined(); + expect(migrated.xpack.security.audit.appender.layout.type).toEqual('pattern'); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"xpack.security.audit.appender.layout.kind\\" is deprecated and has been replaced by \\"xpack.security.audit.appender.layout.type\\"", + ] + `); + }); + + it('renames audit.appender.policy.kind to audit.appender.policy.type', () => { + const config = { + xpack: { + security: { + audit: { + appender: { + policy: { kind: 'time-interval' }, + }, + }, + }, + }, + }; + const { messages, migrated } = applyConfigDeprecations(cloneDeep(config)); + expect(migrated.xpack.security.audit.appender.policy.kind).not.toBeDefined(); + expect(migrated.xpack.security.audit.appender.policy.type).toEqual('time-interval'); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"xpack.security.audit.appender.policy.kind\\" is deprecated and has been replaced by \\"xpack.security.audit.appender.policy.type\\"", + ] + `); + }); + + it('renames audit.appender.strategy.kind to audit.appender.strategy.type', () => { + const config = { + xpack: { + security: { + audit: { + appender: { + strategy: { kind: 'numeric' }, + }, + }, + }, + }, + }; + const { messages, migrated } = applyConfigDeprecations(cloneDeep(config)); + expect(migrated.xpack.security.audit.appender.strategy.kind).not.toBeDefined(); + expect(migrated.xpack.security.audit.appender.strategy.type).toEqual('numeric'); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"xpack.security.audit.appender.strategy.kind\\" is deprecated and has been replaced by \\"xpack.security.audit.appender.strategy.type\\"", + ] + `); + }); + + it('renames audit.appender.path to audit.appender.fileName', () => { + const config = { + xpack: { + security: { + audit: { + appender: { + type: 'file', + path: './audit.log', + }, + }, + }, + }, + }; + const { messages, migrated } = applyConfigDeprecations(cloneDeep(config)); + expect(migrated.xpack.security.audit.appender.path).not.toBeDefined(); + expect(migrated.xpack.security.audit.appender.fileName).toEqual('./audit.log'); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"xpack.security.audit.appender.path\\" is deprecated and has been replaced by \\"xpack.security.audit.appender.fileName\\"", + ] + `); + }); + it(`warns that 'authorization.legacyFallback.enabled' is unused`, () => { const config = { xpack: { diff --git a/x-pack/plugins/security/server/config_deprecations.ts b/x-pack/plugins/security/server/config_deprecations.ts index 65d18f0a4e7eb3..a7bb5e09fb919d 100644 --- a/x-pack/plugins/security/server/config_deprecations.ts +++ b/x-pack/plugins/security/server/config_deprecations.ts @@ -12,6 +12,13 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ unused, }) => [ rename('sessionTimeout', 'session.idleTimeout'), + + rename('audit.appender.kind', 'audit.appender.type'), + rename('audit.appender.layout.kind', 'audit.appender.layout.type'), + rename('audit.appender.policy.kind', 'audit.appender.policy.type'), + rename('audit.appender.strategy.kind', 'audit.appender.strategy.type'), + rename('audit.appender.path', 'audit.appender.fileName'), + unused('authorization.legacyFallback.enabled'), unused('authc.saml.maxRedirectURLSize'), // Deprecation warning for the old array-based format of `xpack.security.authc.providers`. diff --git a/x-pack/plugins/security/server/routes/authentication/common.test.ts b/x-pack/plugins/security/server/routes/authentication/common.test.ts index 38f832cc051ddb..654e4fc18f195a 100644 --- a/x-pack/plugins/security/server/routes/authentication/common.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/common.test.ts @@ -413,11 +413,9 @@ describe('Common authentication routes', () => { body: { providerType: 'saml', providerName: 'saml1', currentURL: '/some-url' }, }); - await expect(routeHandler(mockContext, request, kibanaResponseFactory)).resolves.toEqual({ - status: 500, - payload: 'Internal Error', - options: {}, - }); + await expect(routeHandler(mockContext, request, kibanaResponseFactory)).rejects.toThrow( + unhandledException + ); }); it('returns 401 if login fails.', async () => { @@ -683,11 +681,9 @@ describe('Common authentication routes', () => { authc.acknowledgeAccessAgreement.mockRejectedValue(unhandledException); const request = httpServerMock.createKibanaRequest(); - await expect(routeHandler(mockContext, request, kibanaResponseFactory)).resolves.toEqual({ - status: 500, - payload: 'Internal Error', - options: {}, - }); + await expect(routeHandler(mockContext, request, kibanaResponseFactory)).rejects.toThrowError( + unhandledException + ); }); it('returns 204 if successfully acknowledged.', async () => { diff --git a/x-pack/plugins/security/server/routes/authentication/common.ts b/x-pack/plugins/security/server/routes/authentication/common.ts index 0b0915198f3d40..f1d9aab74548a4 100644 --- a/x-pack/plugins/security/server/routes/authentication/common.ts +++ b/x-pack/plugins/security/server/routes/authentication/common.ts @@ -142,28 +142,23 @@ export function defineCommonRoutes({ logger.info(`Logging in with provider "${providerName}" (${providerType})`); const redirectURL = parseNext(currentURL, basePath.serverBasePath); - try { - const authenticationResult = await getAuthenticationService().login(request, { - provider: { name: providerName }, - redirectURL, - value: getLoginAttemptForProviderType(providerType, redirectURL, params), - }); - - if (authenticationResult.redirected() || authenticationResult.succeeded()) { - return response.ok({ - body: { location: authenticationResult.redirectURL || redirectURL }, - headers: authenticationResult.authResponseHeaders, - }); - } - - return response.unauthorized({ - body: authenticationResult.error, + const authenticationResult = await getAuthenticationService().login(request, { + provider: { name: providerName }, + redirectURL, + value: getLoginAttemptForProviderType(providerType, redirectURL, params), + }); + + if (authenticationResult.redirected() || authenticationResult.succeeded()) { + return response.ok({ + body: { location: authenticationResult.redirectURL || redirectURL }, headers: authenticationResult.authResponseHeaders, }); - } catch (err) { - logger.error(err); - return response.internalError(); } + + return response.unauthorized({ + body: authenticationResult.error, + headers: authenticationResult.authResponseHeaders, + }); }) ); @@ -178,12 +173,7 @@ export function defineCommonRoutes({ }); } - try { - await getAuthenticationService().acknowledgeAccessAgreement(request); - } catch (err) { - logger.error(err); - return response.internalError(); - } + await getAuthenticationService().acknowledgeAccessAgreement(request); return response.noContent(); }) diff --git a/x-pack/plugins/security/server/routes/authentication/saml.test.ts b/x-pack/plugins/security/server/routes/authentication/saml.test.ts index 5a08c3e1797047..73cba46f46ea70 100644 --- a/x-pack/plugins/security/server/routes/authentication/saml.test.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.test.ts @@ -76,16 +76,14 @@ describe('SAML authentication routes', () => { const unhandledException = new Error('Something went wrong.'); authc.login.mockRejectedValue(unhandledException); - const internalServerErrorResponse = Symbol('error'); const responseFactory = httpServerMock.createResponseFactory(); - responseFactory.internalError.mockReturnValue(internalServerErrorResponse as any); const request = httpServerMock.createKibanaRequest({ body: { SAMLResponse: 'saml-response' }, }); - await expect(routeHandler({} as any, request, responseFactory)).resolves.toBe( - internalServerErrorResponse + await expect(routeHandler({} as any, request, responseFactory)).rejects.toThrow( + unhandledException ); expect(authc.login).toHaveBeenCalledWith(request, { diff --git a/x-pack/plugins/security/server/routes/authentication/saml.ts b/x-pack/plugins/security/server/routes/authentication/saml.ts index 9fee03cbc614a1..257b95ec707b40 100644 --- a/x-pack/plugins/security/server/routes/authentication/saml.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.ts @@ -30,28 +30,23 @@ export function defineSAMLRoutes({ options: { authRequired: false, xsrfRequired: false }, }, async (context, request, response) => { - try { - // When authenticating using SAML we _expect_ to redirect to the Kibana target location. - const authenticationResult = await getAuthenticationService().login(request, { - provider: { type: SAMLAuthenticationProvider.type }, - value: { - type: SAMLLogin.LoginWithSAMLResponse, - samlResponse: request.body.SAMLResponse, - relayState: request.body.RelayState, - }, - }); - - if (authenticationResult.redirected()) { - return response.redirected({ - headers: { location: authenticationResult.redirectURL! }, - }); - } + // When authenticating using SAML we _expect_ to redirect to the Kibana target location. + const authenticationResult = await getAuthenticationService().login(request, { + provider: { type: SAMLAuthenticationProvider.type }, + value: { + type: SAMLLogin.LoginWithSAMLResponse, + samlResponse: request.body.SAMLResponse, + relayState: request.body.RelayState, + }, + }); - return response.unauthorized({ body: authenticationResult.error }); - } catch (err) { - logger.error(err); - return response.internalError(); + if (authenticationResult.redirected()) { + return response.redirected({ + headers: { location: authenticationResult.redirectURL! }, + }); } + + return response.unauthorized({ body: authenticationResult.error }); } ); } diff --git a/x-pack/plugins/security/server/routes/session_management/info.test.ts b/x-pack/plugins/security/server/routes/session_management/info.test.ts index 6ed50b50c0eb91..84db94f38d5829 100644 --- a/x-pack/plugins/security/server/routes/session_management/info.test.ts +++ b/x-pack/plugins/security/server/routes/session_management/info.test.ts @@ -60,11 +60,7 @@ describe('Info session routes', () => { request, kibanaResponseFactory ) - ).resolves.toEqual({ - status: 500, - options: {}, - payload: 'Internal Error', - }); + ).rejects.toThrowError(unhandledException); expect(session.get).toHaveBeenCalledWith(request); }); diff --git a/x-pack/plugins/security/server/routes/session_management/info.ts b/x-pack/plugins/security/server/routes/session_management/info.ts index f47d896eb55e00..6cab44509f162e 100644 --- a/x-pack/plugins/security/server/routes/session_management/info.ts +++ b/x-pack/plugins/security/server/routes/session_management/info.ts @@ -11,30 +11,25 @@ import { RouteDefinitionParams } from '..'; /** * Defines routes required for the session info. */ -export function defineSessionInfoRoutes({ router, logger, getSession }: RouteDefinitionParams) { +export function defineSessionInfoRoutes({ router, getSession }: RouteDefinitionParams) { router.get( { path: '/internal/security/session', validate: false }, async (_context, request, response) => { - try { - const sessionValue = await getSession().get(request); - if (sessionValue) { - return response.ok({ - body: { - // We can't rely on the client's system clock, so in addition to returning expiration timestamps, we also return - // the current server time -- that way the client can calculate the relative time to expiration. - now: Date.now(), - idleTimeoutExpiration: sessionValue.idleTimeoutExpiration, - lifespanExpiration: sessionValue.lifespanExpiration, - provider: sessionValue.provider, - } as SessionInfo, - }); - } - - return response.noContent(); - } catch (err) { - logger.error(`Error retrieving user session: ${err.message}`); - return response.internalError(); + const sessionValue = await getSession().get(request); + if (sessionValue) { + return response.ok({ + body: { + // We can't rely on the client's system clock, so in addition to returning expiration timestamps, we also return + // the current server time -- that way the client can calculate the relative time to expiration. + now: Date.now(), + idleTimeoutExpiration: sessionValue.idleTimeoutExpiration, + lifespanExpiration: sessionValue.lifespanExpiration, + provider: sessionValue.provider, + } as SessionInfo, + }); } + + return response.noContent(); } ); } diff --git a/x-pack/plugins/security/server/routes/views/access_agreement.ts b/x-pack/plugins/security/server/routes/views/access_agreement.ts index ff67e1af1e7baa..daf697bd23448b 100644 --- a/x-pack/plugins/security/server/routes/views/access_agreement.ts +++ b/x-pack/plugins/security/server/routes/views/access_agreement.ts @@ -46,20 +46,15 @@ export function defineAccessAgreementRoutes({ // It's not guaranteed that we'll have session for the authenticated user (e.g. when user is // authenticated with the help of HTTP authentication), that means we should safely check if // we have it and can get a corresponding configuration. - try { - const sessionValue = await getSession().get(request); - const accessAgreement = - (sessionValue && - config.authc.providers[ - sessionValue.provider.type as keyof ConfigType['authc']['providers'] - ]?.[sessionValue.provider.name]?.accessAgreement?.message) || - ''; + const sessionValue = await getSession().get(request); + const accessAgreement = + (sessionValue && + config.authc.providers[ + sessionValue.provider.type as keyof ConfigType['authc']['providers'] + ]?.[sessionValue.provider.name]?.accessAgreement?.message) || + ''; - return response.ok({ body: { accessAgreement } }); - } catch (err) { - logger.error(err); - return response.internalError(); - } + return response.ok({ body: { accessAgreement } }); }) ); } diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 34fcfa5f0befdb..31b4cef1a9d451 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -45,6 +45,11 @@ export const DEFAULT_RULE_REFRESH_INTERVAL_VALUE = 60000; // ms export const DEFAULT_RULE_REFRESH_IDLE_VALUE = 2700000; // ms export const DEFAULT_RULE_NOTIFICATION_QUERY_SIZE = 100; +// Document path where threat indicator fields are expected. Used as +// both the source of enrichment fields and the destination for enrichment in +// the generated detection alert +export const DEFAULT_INDICATOR_PATH = 'threat.indicator'; + export enum SecurityPageName { detections = 'detections', overview = 'overview', @@ -166,7 +171,7 @@ export const ML_GROUP_IDS = [ML_GROUP_ID, LEGACY_ML_GROUP_ID]; /* Rule notifications options */ -export const ENABLE_CASE_CONNECTOR = false; +export const ENABLE_CASE_CONNECTOR = true; export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [ '.email', '.slack', diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index 5d1f7572672990..d97820f010a802 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -325,7 +325,7 @@ export const job_status = t.keyof({ succeeded: null, failed: null, 'going to run': null, - 'partial failure': null, + warning: null, }); export type JobStatus = t.TypeOf; @@ -459,12 +459,21 @@ export type Threats = t.TypeOf; export const threatsOrUndefined = t.union([threats, t.undefined]); export type ThreatsOrUndefined = t.TypeOf; -export const threshold = t.exact( - t.type({ - field: t.string, - value: PositiveIntegerGreaterThanZero, - }) -); +export const threshold = t.intersection([ + t.exact( + t.type({ + field: t.union([t.string, t.array(t.string)]), + value: PositiveIntegerGreaterThanZero, + }) + ), + t.exact( + t.partial({ + cardinality_field: t.union([t.string, t.array(t.string), t.undefined, t.null]), + cardinality_value: t.union([PositiveInteger, t.undefined, t.null]), // TODO: cardinality_value should be set if cardinality_field is set + }) + ), +]); +// TODO: codec to transform threshold field string to string[] ? export type Threshold = t.TypeOf; export const thresholdOrUndefined = t.union([threshold, t.undefined]); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts index b76a762ca6cbf0..981a5422a05949 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/add_prepackaged_rules_schema.ts @@ -55,6 +55,7 @@ import { threat_filters, threat_mapping, threat_language, + threat_indicator_path, } from '../types/threat_mapping'; import { @@ -133,6 +134,7 @@ export const addPrepackagedRulesSchema = t.intersection([ threat_query, // defaults to "undefined" if not set during decode threat_index, // defaults to "undefined" if not set during decode threat_language, // defaults "undefined" if not set during decode + threat_indicator_path, // defaults "undefined" if not set during decode concurrent_searches, // defaults to "undefined" if not set during decode items_per_search, // defaults to "undefined" if not set during decode }) diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts index 0a7b8b120ba7ef..8fa5809abe68b4 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/import_rules_schema.ts @@ -62,6 +62,7 @@ import { threat_filters, threat_mapping, threat_language, + threat_indicator_path, } from '../types/threat_mapping'; import { @@ -152,6 +153,7 @@ export const importRulesSchema = t.intersection([ threat_query, // defaults to "undefined" if not set during decode threat_index, // defaults to "undefined" if not set during decode threat_language, // defaults "undefined" if not set during decode + threat_indicator_path, // defaults to "undefined" if not set during decode concurrent_searches, // defaults to "undefined" if not set during decode items_per_search, // defaults to "undefined" if not set during decode }) diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts index 9d5331aeab8e4e..920fbaf4915c5c 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/patch_rules_schema.ts @@ -57,6 +57,7 @@ import { threat_filters, threat_mapping, threat_language, + threat_indicator_path, } from '../types/threat_mapping'; import { listArrayOrUndefined } from '../types/lists'; @@ -112,6 +113,7 @@ export const patchRulesSchema = t.exact( threat_filters, threat_mapping, threat_language, + threat_indicator_path, concurrent_searches, items_per_search, }) diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts index 87e5acb5428df7..3bae9551f4df79 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.mock.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { DEFAULT_INDICATOR_PATH } from '../../../constants'; import { MachineLearningCreateSchema, MachineLearningUpdateSchema, @@ -56,6 +57,7 @@ export const getCreateThreatMatchRulesSchemaMock = ( rule_id: ruleId, threat_query: '*:*', threat_index: ['list-index'], + threat_indicator_path: DEFAULT_INDICATOR_PATH, threat_mapping: [ { entries: [ diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts index 14b47c8b2b3280..6b8211b23088ca 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.test.ts @@ -1152,7 +1152,7 @@ describe('create rules schema', () => { }); }); - describe('threat_mapping', () => { + describe('threat_match', () => { test('You can set a threat query, index, mapping, filters when creating a rule', () => { const payload = getCreateThreatMatchRulesSchemaMock(); const decoded = createRulesSchema.decode(payload); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts index 1c9ebe00333157..5cf2b6242b2f89 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts @@ -13,6 +13,7 @@ import { threat_query, threat_mapping, threat_index, + threat_indicator_path, concurrent_searches, items_per_search, } from '../types/threat_mapping'; @@ -213,6 +214,7 @@ const threatMatchRuleParams = { filters, saved_id, threat_filters, + threat_indicator_path, threat_language: t.keyof({ kuery: null, lucene: null }), concurrent_searches, items_per_search, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts index b14c646e862d36..8dc7427ed09331 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.mocks.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { DEFAULT_INDICATOR_PATH } from '../../../constants'; import { getListArrayMock } from '../types/lists.mock'; import { RulesSchema } from './rules_schema'; @@ -150,6 +151,7 @@ export const getThreatMatchingSchemaPartialMock = (enabled = false): Partial { expect(fields).toEqual(expected); }); - test('should return 8 fields for a rule of type "threat_match"', () => { + test('should return nine (9) fields for a rule of type "threat_match"', () => { const fields = addThreatMatchFields({ type: 'threat_match' }); - expect(fields.length).toEqual(8); + expect(fields.length).toEqual(9); }); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts index bcdb0fa9b085d6..6bd54973e064f1 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts @@ -70,6 +70,7 @@ import { threat_filters, threat_mapping, threat_language, + threat_indicator_path, } from '../types/threat_mapping'; import { DefaultListArray } from '../types/lists_default_array'; @@ -151,6 +152,7 @@ export const dependentRulesSchema = t.partial({ items_per_search, threat_mapping, threat_language, + threat_indicator_path, }); /** @@ -286,6 +288,9 @@ export const addThreatMatchFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.exact(t.type({ threat_mapping: dependentRulesSchema.props.threat_mapping })), t.exact(t.partial({ threat_language: dependentRulesSchema.props.threat_language })), t.exact(t.partial({ threat_filters: dependentRulesSchema.props.threat_filters })), + t.exact( + t.partial({ threat_indicator_path: dependentRulesSchema.props.threat_indicator_path }) + ), t.exact(t.partial({ saved_id: dependentRulesSchema.props.saved_id })), t.exact(t.partial({ concurrent_searches: dependentRulesSchema.props.concurrent_searches })), t.exact( diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts index d3975df488de9e..aab06941686c26 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/types/threat_mapping.ts @@ -18,6 +18,11 @@ export type ThreatQuery = t.TypeOf; export const threatQueryOrUndefined = t.union([threat_query, t.undefined]); export type ThreatQueryOrUndefined = t.TypeOf; +export const threat_indicator_path = t.string; +export type ThreatIndicatorPath = t.TypeOf; +export const threatIndicatorPathOrUndefined = t.union([threat_indicator_path, t.undefined]); +export type ThreatIndicatorPathOrUndefined = t.TypeOf; + export const threat_filters = t.array(t.unknown); // Filters are not easily type-able yet export type ThreatFilters = t.TypeOf; export const threatFiltersOrUndefined = t.union([threat_filters, t.undefined]); diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts index 080b704e9c193b..725a2eb9fea7bb 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/utils.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.ts @@ -30,4 +30,5 @@ export const isEqlRule = (ruleType: Type | undefined): boolean => ruleType === ' export const isThresholdRule = (ruleType: Type | undefined): boolean => ruleType === 'threshold'; export const isQueryRule = (ruleType: Type | undefined): boolean => ruleType === 'query' || ruleType === 'saved_query'; -export const isThreatMatchRule = (ruleType: Type): boolean => ruleType === 'threat_match'; +export const isThreatMatchRule = (ruleType: Type | undefined): boolean => + ruleType === 'threat_match'; diff --git a/x-pack/plugins/security_solution/common/ecs/file/index.ts b/x-pack/plugins/security_solution/common/ecs/file/index.ts index 06abc7fd87541c..5e409b1095cf59 100644 --- a/x-pack/plugins/security_solution/common/ecs/file/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/file/index.ts @@ -5,14 +5,22 @@ * 2.0. */ +interface Original { + name?: string[]; + path?: string[]; +} + export interface CodeSignature { subject_name: string[]; trusted: string[]; } export interface Ext { - code_signature: CodeSignature[] | CodeSignature; + code_signature?: CodeSignature[] | CodeSignature; + original?: Original; } export interface Hash { + md5?: string[]; + sha1?: string[]; sha256: string[]; } diff --git a/x-pack/plugins/security_solution/common/ecs/index.ts b/x-pack/plugins/security_solution/common/ecs/index.ts index e3bcd11097cf79..4c57f6419d5dbf 100644 --- a/x-pack/plugins/security_solution/common/ecs/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/index.ts @@ -15,6 +15,7 @@ import { FileEcs } from './file'; import { GeoEcs } from './geo'; import { HostEcs } from './host'; import { NetworkEcs } from './network'; +import { RegistryEcs } from './registry'; import { RuleEcs } from './rule'; import { SignalEcs } from './signal'; import { SourceEcs } from './source'; @@ -27,6 +28,7 @@ import { UserEcs } from './user'; import { WinlogEcs } from './winlog'; import { ProcessEcs } from './process'; import { SystemEcs } from './system'; +import { Ransomware } from './ransomware'; export interface Ecs { _id: string; @@ -40,6 +42,7 @@ export interface Ecs { geo?: GeoEcs; host?: HostEcs; network?: NetworkEcs; + registry?: RegistryEcs; rule?: RuleEcs; signal?: SignalEcs; source?: SourceEcs; @@ -55,4 +58,7 @@ export interface Ecs { process?: ProcessEcs; file?: FileEcs; system?: SystemEcs; + // This should be temporary + eql?: { parentId: string; sequenceNumber: string }; + Ransomware?: Ransomware; } diff --git a/x-pack/plugins/security_solution/common/ecs/process/index.ts b/x-pack/plugins/security_solution/common/ecs/process/index.ts index 3a8ccc309aecb9..820ecc5560e6c5 100644 --- a/x-pack/plugins/security_solution/common/ecs/process/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/process/index.ts @@ -5,7 +5,10 @@ * 2.0. */ +import { Ext } from '../file'; + export interface ProcessEcs { + Ext?: Ext; entity_id?: string[]; exit_code?: number[]; hash?: ProcessHashData; @@ -28,6 +31,7 @@ export interface ProcessHashData { export interface ProcessParentData { name?: string[]; + pid?: number[]; } export interface Thread { diff --git a/x-pack/plugins/security_solution/common/ecs/ransomware/index.ts b/x-pack/plugins/security_solution/common/ecs/ransomware/index.ts new file mode 100644 index 00000000000000..1724a264f8a4ca --- /dev/null +++ b/x-pack/plugins/security_solution/common/ecs/ransomware/index.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface Ransomware { + feature?: string[]; + score?: string[]; + version?: number[]; + child_pids?: string[]; + files?: RansomwareFiles; +} + +export interface RansomwareFiles { + operation?: string[]; + entropy?: number[]; + metrics?: string[]; + extension?: string[]; + original?: OriginalRansomwareFiles; + path?: string[]; + data?: string[]; + score?: number[]; +} + +export interface OriginalRansomwareFiles { + path?: string[]; + extension?: string[]; +} diff --git a/x-pack/plugins/apm/common/utils/range_filter.ts b/x-pack/plugins/security_solution/common/ecs/registry/index.ts similarity index 60% rename from x-pack/plugins/apm/common/utils/range_filter.ts rename to x-pack/plugins/security_solution/common/ecs/registry/index.ts index 8d5b7d5e1beb1e..c756fb139199e7 100644 --- a/x-pack/plugins/apm/common/utils/range_filter.ts +++ b/x-pack/plugins/security_solution/common/ecs/registry/index.ts @@ -5,12 +5,9 @@ * 2.0. */ -export function rangeFilter(start: number, end: number) { - return { - '@timestamp': { - gte: start, - lte: end, - format: 'epoch_millis', - }, - }; +export interface RegistryEcs { + hive?: string[]; + key?: string[]; + path?: string[]; + value?: string[]; } diff --git a/x-pack/plugins/security_solution/common/ecs/rule/index.ts b/x-pack/plugins/security_solution/common/ecs/rule/index.ts index 3a764a836e9b34..5463b21f6b7f70 100644 --- a/x-pack/plugins/security_solution/common/ecs/rule/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/rule/index.ts @@ -27,10 +27,7 @@ export interface RuleEcs { severity?: string[]; tags?: string[]; threat?: unknown; - threshold?: { - field: string; - value: number; - }; + threshold?: unknown; type?: string[]; size?: string[]; to?: string[]; diff --git a/x-pack/plugins/security_solution/common/ecs/signal/index.ts b/x-pack/plugins/security_solution/common/ecs/signal/index.ts index eb5e629a1abcf7..45e1f04d2b405f 100644 --- a/x-pack/plugins/security_solution/common/ecs/signal/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/signal/index.ts @@ -14,4 +14,5 @@ export interface SignalEcs { group?: { id?: string[]; }; + threshold_result?: unknown; } diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts b/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts index b38cabf33a3db0..3bfe2a7410c080 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/resolver.ts @@ -76,6 +76,6 @@ export const validateEntities = { /** * Indices to search in. */ - indices: schema.arrayOf(schema.string()), + indices: schema.oneOf([schema.arrayOf(schema.string()), schema.string()]), }), }; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts index 7e19944ea5856c..11dc8ee2f6a825 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/common/index.ts @@ -70,7 +70,6 @@ export interface HostAggEsItem { cloud_machine_type?: HostBuckets; cloud_provider?: HostBuckets; cloud_region?: HostBuckets; - firstSeen?: HostValue; host_architecture?: HostBuckets; host_id?: HostBuckets; host_ip?: HostBuckets; @@ -80,7 +79,6 @@ export interface HostAggEsItem { host_os_version?: HostBuckets; host_type?: HostBuckets; key?: string; - lastSeen?: HostValue; os?: HostOsHitsItem; } diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/first_last_seen/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/first_last_seen/index.ts index a7e074058daa32..b3e7b14aed000a 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/first_last_seen/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/first_last_seen/index.ts @@ -13,7 +13,9 @@ import { HostsFields } from '../common'; export interface HostFirstLastSeenRequestOptions extends Partial> { hostName: string; + order: 'asc' | 'desc'; } + export interface HostFirstLastSeenStrategyResponse extends IEsSearchResponse { inspect?: Maybe; firstSeen?: Maybe; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts index a430c429d54dca..fa3029405dc225 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts @@ -17,7 +17,7 @@ export * from './uncommon_processes'; export enum HostsQueries { authentications = 'authentications', details = 'details', - firstLastSeen = 'firstLastSeen', + firstOrLastSeen = 'firstOrLastSeen', hosts = 'hosts', overview = 'overviewHost', uncommonProcesses = 'uncommonProcesses', diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts index de88409549f8a8..319933be5c79e5 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -15,7 +15,6 @@ import { HostAuthenticationsStrategyResponse, HostOverviewRequestOptions, HostFirstLastSeenStrategyResponse, - HostFirstLastSeenRequestOptions, HostsQueries, HostsRequestOptions, HostsStrategyResponse, @@ -28,6 +27,7 @@ import { HostsKpiHostsRequestOptions, HostsKpiUniqueIpsStrategyResponse, HostsKpiUniqueIpsRequestOptions, + HostFirstLastSeenRequestOptions, } from './hosts'; import { NetworkQueries, @@ -111,7 +111,7 @@ export type StrategyResponseType = T extends HostsQ ? HostsOverviewStrategyResponse : T extends HostsQueries.authentications ? HostAuthenticationsStrategyResponse - : T extends HostsQueries.firstLastSeen + : T extends HostsQueries.firstOrLastSeen ? HostFirstLastSeenStrategyResponse : T extends HostsQueries.uncommonProcesses ? HostsUncommonProcessesStrategyResponse @@ -159,7 +159,7 @@ export type StrategyRequestType = T extends HostsQu ? HostOverviewRequestOptions : T extends HostsQueries.authentications ? HostAuthenticationsRequestOptions - : T extends HostsQueries.firstLastSeen + : T extends HostsQueries.firstOrLastSeen ? HostFirstLastSeenRequestOptions : T extends HostsQueries.uncommonProcesses ? HostsUncommonProcessesRequestOptions diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts index df6543ddbce903..c71108d58d980f 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/matrix_histogram/index.ts @@ -36,7 +36,14 @@ export interface MatrixHistogramRequestOptions extends RequestBasicOptions { timerange: TimerangeInput; histogramType: MatrixHistogramType; stackByField: string; - threshold?: { field: string | undefined; value: number } | undefined; + threshold?: + | { + field: string | string[] | undefined; + value: number; + cardinality_field?: string | undefined; + cardinality_value?: number | undefined; + } + | undefined; inspect?: Maybe; isPtrIncluded?: boolean; } diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/all/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/all/index.ts index 6f5bea87f55088..ada22437a1530d 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/all/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/all/index.ts @@ -37,4 +37,5 @@ export interface TimelineEventsAllStrategyResponse extends IEsSearchResponse { export interface TimelineEventsAllRequestOptions extends TimelineRequestOptionsPaginated { fields: string[]; fieldRequested: string[]; + language: 'eql' | 'kuery' | 'lucene'; } diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/eql/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/eql/index.ts new file mode 100644 index 00000000000000..6bf01e478a9727 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/eql/index.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiComboBoxOptionOption } from '@elastic/eui'; +import { + EqlSearchStrategyRequest, + EqlSearchStrategyResponse, +} from '../../../../../../data_enhanced/common'; +import { Inspect, Maybe, PaginationInputPaginated } from '../../..'; +import { TimelineEdges, TimelineEventsAllRequestOptions } from '../..'; +import { EqlSearchResponse } from '../../../../detection_engine/types'; + +export interface TimelineEqlRequestOptions + extends EqlSearchStrategyRequest, + Omit { + eventCategoryField?: string; + tiebreakerField?: string; + timestampField?: string; + size?: number; +} + +export interface TimelineEqlResponse extends EqlSearchStrategyResponse> { + edges: TimelineEdges[]; + totalCount: number; + pageInfo: Pick; + inspect: Maybe; +} + +export interface EqlOptionsData { + keywordFields: EuiComboBoxOptionOption[]; + dateFields: EuiComboBoxOptionOption[]; + nonDateFields: EuiComboBoxOptionOption[]; +} + +export interface EqlOptionsSelected { + eventCategoryField?: string; + tiebreakerField?: string; + timestampField?: string; + query?: string; + size?: number; +} + +export type FieldsEqlOptions = keyof EqlOptionsSelected; diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/index.ts index 7ffbde2ebec028..c4d6f70a275871 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/index.ts @@ -8,6 +8,7 @@ export * from './all'; export * from './details'; export * from './last_event_time'; +export * from './eql'; export enum TimelineEventsQueries { all = 'eventsAll', diff --git a/x-pack/plugins/security_solution/common/shared_imports.ts b/x-pack/plugins/security_solution/common/shared_imports.ts index d6ec668e1b0f9f..988f0ad0c125d4 100644 --- a/x-pack/plugins/security_solution/common/shared_imports.ts +++ b/x-pack/plugins/security_solution/common/shared_imports.ts @@ -43,6 +43,7 @@ export { ExceptionListType, Type, ENDPOINT_LIST_ID, + ENDPOINT_TRUSTED_APPS_LIST_ID, osTypeArray, OsTypeArray, } from '../../lists/common'; diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index 58e3b9824d8fdb..5fb7d1a74fc367 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -103,6 +103,17 @@ const SavedFilterRuntimeType = runtimeTypes.partial({ script: unionWithNullType(runtimeTypes.string), }); +/* + * eqlOptionsQuery -> filterQuery Types + */ +const EqlOptionsRuntimeType = runtimeTypes.partial({ + eventCategoryField: unionWithNullType(runtimeTypes.string), + query: unionWithNullType(runtimeTypes.string), + tiebreakerField: unionWithNullType(runtimeTypes.string), + timestampField: unionWithNullType(runtimeTypes.string), + size: unionWithNullType(runtimeTypes.union([runtimeTypes.string, runtimeTypes.number])), +}); + /* * kqlQuery -> filterQuery Types */ @@ -180,10 +191,13 @@ export type TimelineStatusLiteralWithNull = runtimeTypes.TypeOf< >; export enum RowRendererId { + alerts = 'alerts', auditd = 'auditd', auditd_file = 'auditd_file', + library = 'library', netflow = 'netflow', plain = 'plain', + registry = 'registry', suricata = 'suricata', system = 'system', system_dns = 'system_dns', @@ -243,6 +257,7 @@ export const SavedTimelineRuntimeType = runtimeTypes.partial({ columns: unionWithNullType(runtimeTypes.array(SavedColumnHeaderRuntimeType)), dataProviders: unionWithNullType(runtimeTypes.array(SavedDataProviderRuntimeType)), description: unionWithNullType(runtimeTypes.string), + eqlOptions: unionWithNullType(EqlOptionsRuntimeType), eventType: unionWithNullType(runtimeTypes.string), excludedRowRendererIds: unionWithNullType(runtimeTypes.array(RowRendererIdRuntimeType)), favorite: unionWithNullType(runtimeTypes.array(SavedFavoriteRuntimeType)), @@ -410,13 +425,14 @@ export const importTimelineResultSchema = runtimeTypes.exact( export type ImportTimelineResultSchema = runtimeTypes.TypeOf; -export type TimelineEventsType = 'all' | 'raw' | 'alert' | 'signal' | 'custom'; +export type TimelineEventsType = 'all' | 'raw' | 'alert' | 'signal' | 'custom' | 'eql'; export enum TimelineTabs { query = 'query', graph = 'graph', notes = 'notes', pinned = 'pinned', + eql = 'eql', } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/x-pack/plugins/security_solution/common/utility_types.ts b/x-pack/plugins/security_solution/common/utility_types.ts index 3c13e6af837bc0..498b18dccaca56 100644 --- a/x-pack/plugins/security_solution/common/utility_types.ts +++ b/x-pack/plugins/security_solution/common/utility_types.ts @@ -36,6 +36,12 @@ export const stringEnum = (enumObj: T, enumName = 'enum') => * * Optionally you can avoid the use of this by using early returns and TypeScript will clear your type checking without complaints * but there are situations and times where this function might still be needed. + * + * If you see an error, DO NOT cast "as never" such as: + * assertUnreachable(x as never) // BUG IN YOUR CODE NOW AND IT WILL THROW DURING RUNTIME + * If you see code like that remove it, as that deactivates the intent of this utility. + * If you need to do that, then you should remove assertUnreachable from your code and + * use a default at the end of the switch instead. * @param x Unreachable field * @param message Message of error thrown */ diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts index 5a2cf5408b04de..64ce6be9ec457b 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/creation.spec.ts @@ -8,11 +8,10 @@ import { case1 } from '../../objects/case'; import { - ALL_CASES_CLOSE_ACTION, ALL_CASES_CLOSED_CASES_STATS, ALL_CASES_COMMENTS_COUNT, - ALL_CASES_DELETE_ACTION, ALL_CASES_IN_PROGRESS_CASES_STATS, + ALL_CASES_ITEM_ACTIONS_BTN, ALL_CASES_NAME, ALL_CASES_OPEN_CASES_COUNT, ALL_CASES_OPEN_CASES_STATS, @@ -91,8 +90,7 @@ describe('Cases', () => { cy.get(ALL_CASES_COMMENTS_COUNT).should('have.text', '0'); cy.get(ALL_CASES_OPENED_ON).should('include.text', 'ago'); cy.get(ALL_CASES_SERVICE_NOW_INCIDENT).should('have.text', 'Not pushed'); - cy.get(ALL_CASES_DELETE_ACTION).should('exist'); - cy.get(ALL_CASES_CLOSE_ACTION).should('exist'); + cy.get(ALL_CASES_ITEM_ACTIONS_BTN).should('exist'); goToCaseDetails(); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_detection_callouts_index_outdated.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_detection_callouts_index_outdated.spec.ts new file mode 100644 index 00000000000000..1c6c604b84fbb2 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_detection_callouts_index_outdated.spec.ts @@ -0,0 +1,196 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ROLES } from '../../../common/test'; +import { DETECTIONS_RULE_MANAGEMENT_URL, DETECTIONS_URL } from '../../urls/navigation'; +import { newRule } from '../../objects/rule'; +import { PAGE_TITLE } from '../../screens/common/page'; + +import { + login, + loginAndWaitForPageWithoutDateRange, + waitForPageWithoutDateRange, +} from '../../tasks/login'; +import { waitForAlertsIndexToBeCreated } from '../../tasks/alerts'; +import { goToRuleDetails } from '../../tasks/alerts_detection_rules'; +import { createCustomRule, deleteCustomRule } from '../../tasks/api_calls/rules'; +import { getCallOut, waitForCallOutToBeShown } from '../../tasks/common/callouts'; +import { cleanKibana } from '../../tasks/common'; + +const loadPageAsPlatformEngineerUser = (url: string) => { + waitForPageWithoutDateRange(url, ROLES.soc_manager); + waitForPageTitleToBeShown(); +}; + +const waitForPageTitleToBeShown = () => { + cy.get(PAGE_TITLE).should('be.visible'); +}; + +describe('Detections > Need Admin Callouts indicating an admin is needed to migrate the alert data set', () => { + const NEED_ADMIN_FOR_UPDATE_CALLOUT = 'need-admin-for-update-rules'; + + before(() => { + // First, we have to open the app on behalf of a privileged user in order to initialize it. + // Otherwise the app will be disabled and show a "welcome"-like page. + cleanKibana(); + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL, ROLES.platform_engineer); + waitForAlertsIndexToBeCreated(); + + // After that we can login as a soc manager. + login(ROLES.soc_manager); + }); + + context( + 'The users index_mapping_outdated is "true" and their admin callouts should show up', + () => { + beforeEach(() => { + // Index mapping outdated is forced to return true as being outdated so that we get the + // need admin callouts being shown. + cy.intercept('GET', '/api/detection_engine/index', { + index_mapping_outdated: true, + name: '.siem-signals-default', + }); + }); + context('On Detections home page', () => { + beforeEach(() => { + loadPageAsPlatformEngineerUser(DETECTIONS_URL); + }); + + it('We show the need admin primary callout', () => { + waitForCallOutToBeShown(NEED_ADMIN_FOR_UPDATE_CALLOUT, 'primary'); + }); + }); + + context('On Rules Management page', () => { + beforeEach(() => { + loadPageAsPlatformEngineerUser(DETECTIONS_RULE_MANAGEMENT_URL); + }); + + it('We show 1 primary callout of need admin', () => { + waitForCallOutToBeShown(NEED_ADMIN_FOR_UPDATE_CALLOUT, 'primary'); + }); + }); + + context('On Rule Details page', () => { + beforeEach(() => { + createCustomRule(newRule); + loadPageAsPlatformEngineerUser(DETECTIONS_RULE_MANAGEMENT_URL); + waitForPageTitleToBeShown(); + goToRuleDetails(); + }); + + afterEach(() => { + deleteCustomRule(); + }); + + it('We show 1 primary callout', () => { + waitForCallOutToBeShown(NEED_ADMIN_FOR_UPDATE_CALLOUT, 'primary'); + }); + }); + } + ); + + context( + 'The users index_mapping_outdated is "false" and their admin callouts should not show up ', + () => { + beforeEach(() => { + // Index mapping outdated is forced to return true as being outdated so that we get the + // need admin callouts being shown. + cy.intercept('GET', '/api/detection_engine/index', { + index_mapping_outdated: false, + name: '.siem-signals-default', + }); + }); + context('On Detections home page', () => { + beforeEach(() => { + loadPageAsPlatformEngineerUser(DETECTIONS_URL); + }); + + it('We show the need admin primary callout', () => { + getCallOut(NEED_ADMIN_FOR_UPDATE_CALLOUT).should('not.exist'); + }); + }); + + context('On Rules Management page', () => { + beforeEach(() => { + loadPageAsPlatformEngineerUser(DETECTIONS_RULE_MANAGEMENT_URL); + }); + + it('We show 1 primary callout of need admin', () => { + getCallOut(NEED_ADMIN_FOR_UPDATE_CALLOUT).should('not.exist'); + }); + }); + + context('On Rule Details page', () => { + beforeEach(() => { + createCustomRule(newRule); + loadPageAsPlatformEngineerUser(DETECTIONS_RULE_MANAGEMENT_URL); + waitForPageTitleToBeShown(); + goToRuleDetails(); + }); + + afterEach(() => { + deleteCustomRule(); + }); + + it('We show 1 primary callout', () => { + getCallOut(NEED_ADMIN_FOR_UPDATE_CALLOUT).should('not.exist'); + }); + }); + } + ); + + context( + 'The users index_mapping_outdated is "null" and their admin callouts should not show up ', + () => { + beforeEach(() => { + // Index mapping outdated is forced to return true as being outdated so that we get the + // need admin callouts being shown. + cy.intercept('GET', '/api/detection_engine/index', { + index_mapping_outdated: null, + name: '.siem-signals-default', + }); + }); + context('On Detections home page', () => { + beforeEach(() => { + loadPageAsPlatformEngineerUser(DETECTIONS_URL); + }); + + it('We show the need admin primary callout', () => { + getCallOut(NEED_ADMIN_FOR_UPDATE_CALLOUT).should('not.exist'); + }); + }); + + context('On Rules Management page', () => { + beforeEach(() => { + loadPageAsPlatformEngineerUser(DETECTIONS_RULE_MANAGEMENT_URL); + }); + + it('We show 1 primary callout of need admin', () => { + getCallOut(NEED_ADMIN_FOR_UPDATE_CALLOUT).should('not.exist'); + }); + }); + + context('On Rule Details page', () => { + beforeEach(() => { + createCustomRule(newRule); + loadPageAsPlatformEngineerUser(DETECTIONS_RULE_MANAGEMENT_URL); + waitForPageTitleToBeShown(); + goToRuleDetails(); + }); + + afterEach(() => { + deleteCustomRule(); + }); + + it('We show 1 primary callout', () => { + getCallOut(NEED_ADMIN_FOR_UPDATE_CALLOUT).should('not.exist'); + }); + }); + } + ); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_detection_callouts_readonly.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_detection_callouts_readonly.spec.ts index 85257f7d9176f5..d807857cd72bde 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_detection_callouts_readonly.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_detection_callouts_readonly.spec.ts @@ -26,6 +26,11 @@ const loadPageAsReadOnlyUser = (url: string) => { waitForPageTitleToBeShown(); }; +const loadPageAsPlatformEngineer = (url: string) => { + waitForPageWithoutDateRange(url, ROLES.platform_engineer); + waitForPageTitleToBeShown(); +}; + const reloadPage = () => { cy.reload(); waitForPageTitleToBeShown(); @@ -35,7 +40,7 @@ const waitForPageTitleToBeShown = () => { cy.get(PAGE_TITLE).should('be.visible'); }; -describe('Detections > Callouts indicating read-only access to resources', () => { +describe('Detections > Callouts', () => { const ALERTS_CALLOUT = 'read-only-access-to-alerts'; const RULES_CALLOUT = 'read-only-access-to-rules'; @@ -50,75 +55,119 @@ describe('Detections > Callouts indicating read-only access to resources', () => login(ROLES.reader); }); - context('On Detections home page', () => { - beforeEach(() => { - loadPageAsReadOnlyUser(DETECTIONS_URL); - }); - - it('We show one primary callout', () => { - waitForCallOutToBeShown(ALERTS_CALLOUT, 'primary'); - }); + context('indicating read-only access to resources', () => { + context('On Detections home page', () => { + beforeEach(() => { + loadPageAsReadOnlyUser(DETECTIONS_URL); + }); - context('When a user clicks Dismiss on the callout', () => { - it('We hide it and persist the dismissal', () => { + it('We show one primary callout', () => { waitForCallOutToBeShown(ALERTS_CALLOUT, 'primary'); - dismissCallOut(ALERTS_CALLOUT); - reloadPage(); - getCallOut(ALERTS_CALLOUT).should('not.exist'); }); - }); - }); - context('On Rules Management page', () => { - beforeEach(() => { - loadPageAsReadOnlyUser(DETECTIONS_RULE_MANAGEMENT_URL); + context('When a user clicks Dismiss on the callout', () => { + it('We hide it and persist the dismissal', () => { + waitForCallOutToBeShown(ALERTS_CALLOUT, 'primary'); + dismissCallOut(ALERTS_CALLOUT); + reloadPage(); + getCallOut(ALERTS_CALLOUT).should('not.exist'); + }); + }); }); - it('We show one primary callout', () => { - waitForCallOutToBeShown(RULES_CALLOUT, 'primary'); - }); + context('On Rules Management page', () => { + beforeEach(() => { + loadPageAsReadOnlyUser(DETECTIONS_RULE_MANAGEMENT_URL); + }); - context('When a user clicks Dismiss on the callout', () => { - it('We hide it and persist the dismissal', () => { + it('We show one primary callout', () => { waitForCallOutToBeShown(RULES_CALLOUT, 'primary'); - dismissCallOut(RULES_CALLOUT); - reloadPage(); - getCallOut(RULES_CALLOUT).should('not.exist'); }); - }); - }); - context('On Rule Details page', () => { - beforeEach(() => { - createCustomRule(newRule); - loadPageAsReadOnlyUser(DETECTIONS_RULE_MANAGEMENT_URL); - waitForPageTitleToBeShown(); - goToRuleDetails(); + context('When a user clicks Dismiss on the callout', () => { + it('We hide it and persist the dismissal', () => { + waitForCallOutToBeShown(RULES_CALLOUT, 'primary'); + dismissCallOut(RULES_CALLOUT); + reloadPage(); + getCallOut(RULES_CALLOUT).should('not.exist'); + }); + }); }); - afterEach(() => { - deleteCustomRule(); - }); + context('On Rule Details page', () => { + beforeEach(() => { + createCustomRule(newRule); + loadPageAsReadOnlyUser(DETECTIONS_RULE_MANAGEMENT_URL); + waitForPageTitleToBeShown(); + goToRuleDetails(); + }); - it('We show two primary callouts', () => { - waitForCallOutToBeShown(ALERTS_CALLOUT, 'primary'); - waitForCallOutToBeShown(RULES_CALLOUT, 'primary'); - }); + afterEach(() => { + deleteCustomRule(); + }); - context('When a user clicks Dismiss on the callouts', () => { - it('We hide them and persist the dismissal', () => { + it('We show two primary callouts', () => { waitForCallOutToBeShown(ALERTS_CALLOUT, 'primary'); waitForCallOutToBeShown(RULES_CALLOUT, 'primary'); + }); - dismissCallOut(ALERTS_CALLOUT); - reloadPage(); + context('When a user clicks Dismiss on the callouts', () => { + it('We hide them and persist the dismissal', () => { + waitForCallOutToBeShown(ALERTS_CALLOUT, 'primary'); + waitForCallOutToBeShown(RULES_CALLOUT, 'primary'); + dismissCallOut(ALERTS_CALLOUT); + reloadPage(); + + getCallOut(ALERTS_CALLOUT).should('not.exist'); + getCallOut(RULES_CALLOUT).should('be.visible'); + + dismissCallOut(RULES_CALLOUT); + reloadPage(); + + getCallOut(ALERTS_CALLOUT).should('not.exist'); + getCallOut(RULES_CALLOUT).should('not.exist'); + }); + }); + }); + }); + + context('indicating read-write access to resources', () => { + context('On Detections home page', () => { + beforeEach(() => { + loadPageAsPlatformEngineer(DETECTIONS_URL); + }); + + it('We show no callout', () => { + getCallOut(ALERTS_CALLOUT).should('not.exist'); + getCallOut(RULES_CALLOUT).should('not.exist'); + }); + }); + + context('On Rules Management page', () => { + beforeEach(() => { + loadPageAsPlatformEngineer(DETECTIONS_RULE_MANAGEMENT_URL); + }); + + it('We show no callout', () => { getCallOut(ALERTS_CALLOUT).should('not.exist'); - getCallOut(RULES_CALLOUT).should('be.visible'); + getCallOut(RULES_CALLOUT).should('not.exist'); + }); + }); - dismissCallOut(RULES_CALLOUT); - reloadPage(); + context('On Rule Details page', () => { + beforeEach(() => { + createCustomRule(newRule); + loadPageAsPlatformEngineer(DETECTIONS_RULE_MANAGEMENT_URL); + waitForPageTitleToBeShown(); + goToRuleDetails(); + }); + + afterEach(() => { + deleteCustomRule(); + }); + it('We show no callouts', () => { getCallOut(ALERTS_CALLOUT).should('not.exist'); getCallOut(RULES_CALLOUT).should('not.exist'); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index a69f808001800d..bc52be678347aa 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -98,359 +98,375 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { DETECTIONS_URL, RULE_CREATION } from '../../urls/navigation'; -describe('Detection rules, Indicator Match', () => { - const expectedUrls = newThreatIndicatorRule.referenceUrls.join(''); - const expectedFalsePositives = newThreatIndicatorRule.falsePositivesExamples.join(''); - const expectedTags = newThreatIndicatorRule.tags.join(''); - const expectedMitre = formatMitreAttackDescription(newThreatIndicatorRule.mitre); - const expectedNumberOfRules = 1; - const expectedNumberOfAlerts = 1; - - before(() => { - cleanKibana(); - esArchiverLoad('threat_indicator'); - esArchiverLoad('threat_data'); - }); - after(() => { - esArchiverUnload('threat_indicator'); - esArchiverUnload('threat_data'); - }); - - describe('Creating new indicator match rules', () => { - beforeEach(() => { - loginAndWaitForPageWithoutDateRange(RULE_CREATION); - selectIndicatorMatchType(); +// Skipped for 7.12 FF - flaky tests +describe.skip('indicator match', () => { + describe('Detection rules, Indicator Match', () => { + const expectedUrls = newThreatIndicatorRule.referenceUrls.join(''); + const expectedFalsePositives = newThreatIndicatorRule.falsePositivesExamples.join(''); + const expectedTags = newThreatIndicatorRule.tags.join(''); + const expectedMitre = formatMitreAttackDescription(newThreatIndicatorRule.mitre); + const expectedNumberOfRules = 1; + const expectedNumberOfAlerts = 1; + + before(() => { + cleanKibana(); + esArchiverLoad('threat_indicator'); + esArchiverLoad('threat_data'); }); - - describe('Index patterns', () => { - it('Contains a predefined index pattern', () => { - getIndicatorIndex().should('have.text', indexPatterns.join('')); - }); - - it('Does NOT show invalidation text on initial page load if indicator index pattern is filled out', () => { - getIndicatorIndicatorIndex().type(`${newThreatIndicatorRule.indicatorIndexPattern}{enter}`); - getDefineContinueButton().click(); - getIndexPatternInvalidationText().should('not.exist'); - }); - - it('Shows invalidation text when you try to continue without filling it out', () => { - getIndexPatternClearButton().click(); - getIndicatorIndicatorIndex().type(`${newThreatIndicatorRule.indicatorIndexPattern}{enter}`); - getDefineContinueButton().click(); - getIndexPatternInvalidationText().should('exist'); - }); + after(() => { + esArchiverUnload('threat_indicator'); + esArchiverUnload('threat_data'); }); - describe('Indicator index patterns', () => { - it('Contains empty index pattern', () => { - getIndicatorIndicatorIndex().should('have.text', ''); - }); - - it('Does NOT show invalidation text on initial page load', () => { - getIndexPatternInvalidationText().should('not.exist'); - }); - - it('Shows invalidation text if you try to continue without filling it out', () => { - getDefineContinueButton().click(); - getIndexPatternInvalidationText().should('exist'); + describe('Creating new indicator match rules', () => { + beforeEach(() => { + loginAndWaitForPageWithoutDateRange(RULE_CREATION); + selectIndicatorMatchType(); }); - }); - describe('custom query input', () => { - it('Has a default set of *:*', () => { - getCustomQueryInput().should('have.text', '*:*'); - }); + describe('Index patterns', () => { + it('Contains a predefined index pattern', () => { + getIndicatorIndex().should('have.text', indexPatterns.join('')); + }); - it('Shows invalidation text if text is removed', () => { - getCustomQueryInput().type('{selectall}{del}'); - getCustomQueryInvalidationText().should('exist'); - }); - }); + it('Does NOT show invalidation text on initial page load if indicator index pattern is filled out', () => { + getIndicatorIndicatorIndex().type( + `${newThreatIndicatorRule.indicatorIndexPattern}{enter}` + ); + getDefineContinueButton().click(); + getIndexPatternInvalidationText().should('not.exist'); + }); - describe('custom indicator query input', () => { - it('Has a default set of *:*', () => { - getCustomIndicatorQueryInput().should('have.text', '*:*'); + it('Shows invalidation text when you try to continue without filling it out', () => { + getIndexPatternClearButton().click(); + getIndicatorIndicatorIndex().type( + `${newThreatIndicatorRule.indicatorIndexPattern}{enter}` + ); + getDefineContinueButton().click(); + getIndexPatternInvalidationText().should('exist'); + }); }); - it('Shows invalidation text if text is removed', () => { - getCustomIndicatorQueryInput().type('{selectall}{del}'); - getCustomQueryInvalidationText().should('exist'); - }); - }); + describe('Indicator index patterns', () => { + it('Contains empty index pattern', () => { + getIndicatorIndicatorIndex().should('have.text', ''); + }); - describe('Indicator mapping', () => { - beforeEach(() => { - fillIndexAndIndicatorIndexPattern( - newThreatIndicatorRule.index, - newThreatIndicatorRule.indicatorIndexPattern - ); - }); + it('Does NOT show invalidation text on initial page load', () => { + getIndexPatternInvalidationText().should('not.exist'); + }); - it('Does NOT show invalidation text on initial page load', () => { - getIndicatorInvalidationText().should('not.exist'); + it('Shows invalidation text if you try to continue without filling it out', () => { + getDefineContinueButton().click(); + getIndexPatternInvalidationText().should('exist'); + }); }); - it('Shows invalidation text when you try to press continue without filling anything out', () => { - getDefineContinueButton().click(); - getIndicatorAtLeastOneInvalidationText().should('exist'); - }); + describe('custom query input', () => { + it('Has a default set of *:*', () => { + getCustomQueryInput().should('have.text', '*:*'); + }); - it('Shows invalidation text when the "AND" button is pressed and both the mappings are blank', () => { - getIndicatorAndButton().click(); - getIndicatorInvalidationText().should('exist'); + it('Shows invalidation text if text is removed', () => { + getCustomQueryInput().type('{selectall}{del}'); + getCustomQueryInvalidationText().should('exist'); + }); }); - it('Shows invalidation text when the "OR" button is pressed and both the mappings are blank', () => { - getIndicatorOrButton().click(); - getIndicatorInvalidationText().should('exist'); - }); + describe('custom indicator query input', () => { + it('Has a default set of *:*', () => { + getCustomIndicatorQueryInput().should('have.text', '*:*'); + }); - it('Does NOT show invalidation text when there is a valid "index field" and a valid "indicator index field"', () => { - fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMapping, - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + it('Shows invalidation text if text is removed', () => { + getCustomIndicatorQueryInput().type('{selectall}{del}'); + getCustomQueryInvalidationText().should('exist'); }); - getDefineContinueButton().click(); - getIndicatorInvalidationText().should('not.exist'); }); - it('Shows invalidation text when there is an invalid "index field" and a valid "indicator index field"', () => { - fillIndicatorMatchRow({ - indexField: 'non-existent-value', - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, - validColumns: 'indicatorField', + describe('Indicator mapping', () => { + beforeEach(() => { + fillIndexAndIndicatorIndexPattern( + newThreatIndicatorRule.index, + newThreatIndicatorRule.indicatorIndexPattern + ); }); - getDefineContinueButton().click(); - getIndicatorInvalidationText().should('exist'); - }); - it('Shows invalidation text when there is a valid "index field" and an invalid "indicator index field"', () => { - fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMapping, - indicatorIndexField: 'non-existent-value', - validColumns: 'indexField', + it('Does NOT show invalidation text on initial page load', () => { + getIndicatorInvalidationText().should('not.exist'); }); - getDefineContinueButton().click(); - getIndicatorInvalidationText().should('exist'); - }); - it('Deletes the first row when you have two rows. Both rows valid rows of "index fields" and valid "indicator index fields". The second row should become the first row', () => { - fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMapping, - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + it('Shows invalidation text when you try to press continue without filling anything out', () => { + getDefineContinueButton().click(); + getIndicatorAtLeastOneInvalidationText().should('exist'); }); - getIndicatorAndButton().click(); - fillIndicatorMatchRow({ - rowNumber: 2, - indexField: 'agent.name', - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, - validColumns: 'indicatorField', + + it('Shows invalidation text when the "AND" button is pressed and both the mappings are blank', () => { + getIndicatorAndButton().click(); + getIndicatorInvalidationText().should('exist'); }); - getIndicatorDeleteButton().click(); - getIndicatorIndexComboField().should('have.text', 'agent.name'); - getIndicatorMappingComboField().should( - 'have.text', - newThreatIndicatorRule.indicatorIndexField - ); - getIndicatorIndexComboField(2).should('not.exist'); - getIndicatorMappingComboField(2).should('not.exist'); - }); - it('Deletes the first row when you have two rows. Both rows have valid "index fields" and invalid "indicator index fields". The second row should become the first row', () => { - fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMapping, - indicatorIndexField: 'non-existent-value', - validColumns: 'indexField', + it('Shows invalidation text when the "OR" button is pressed and both the mappings are blank', () => { + getIndicatorOrButton().click(); + getIndicatorInvalidationText().should('exist'); }); - getIndicatorAndButton().click(); - fillIndicatorMatchRow({ - rowNumber: 2, - indexField: newThreatIndicatorRule.indicatorMapping, - indicatorIndexField: 'second-non-existent-value', - validColumns: 'indexField', + + it('Does NOT show invalidation text when there is a valid "index field" and a valid "indicator index field"', () => { + fillIndicatorMatchRow({ + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + }); + getDefineContinueButton().click(); + getIndicatorInvalidationText().should('not.exist'); }); - getIndicatorDeleteButton().click(); - getIndicatorMappingComboField().should('have.text', 'second-non-existent-value'); - getIndicatorIndexComboField(2).should('not.exist'); - getIndicatorMappingComboField(2).should('not.exist'); - }); - it('Deletes the first row when you have two rows. Both rows have valid "indicator index fields" and invalid "index fields". The second row should become the first row', () => { - fillIndicatorMatchRow({ - indexField: 'non-existent-value', - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, - validColumns: 'indicatorField', + it('Shows invalidation text when there is an invalid "index field" and a valid "indicator index field"', () => { + fillIndicatorMatchRow({ + indexField: 'non-existent-value', + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + validColumns: 'indicatorField', + }); + getDefineContinueButton().click(); + getIndicatorInvalidationText().should('exist'); }); - getIndicatorAndButton().click(); - fillIndicatorMatchRow({ - rowNumber: 2, - indexField: 'second-non-existent-value', - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, - validColumns: 'indicatorField', + + it('Shows invalidation text when there is a valid "index field" and an invalid "indicator index field"', () => { + fillIndicatorMatchRow({ + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: 'non-existent-value', + validColumns: 'indexField', + }); + getDefineContinueButton().click(); + getIndicatorInvalidationText().should('exist'); }); - getIndicatorDeleteButton().click(); - getIndicatorIndexComboField().should('have.text', 'second-non-existent-value'); - getIndicatorIndexComboField(2).should('not.exist'); - getIndicatorMappingComboField(2).should('not.exist'); - }); - it('Deletes the first row of data but not the UI elements and the text defaults back to the placeholder of Search', () => { - fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMapping, - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + it('Deletes the first row when you have two rows. Both rows valid rows of "index fields" and valid "indicator index fields". The second row should become the first row', () => { + fillIndicatorMatchRow({ + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + }); + getIndicatorAndButton().click(); + fillIndicatorMatchRow({ + rowNumber: 2, + indexField: 'agent.name', + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + validColumns: 'indicatorField', + }); + getIndicatorDeleteButton().click(); + getIndicatorIndexComboField().should('have.text', 'agent.name'); + getIndicatorMappingComboField().should( + 'have.text', + newThreatIndicatorRule.indicatorIndexField + ); + getIndicatorIndexComboField(2).should('not.exist'); + getIndicatorMappingComboField(2).should('not.exist'); }); - getIndicatorDeleteButton().click(); - getIndicatorIndexComboField().should('text', 'Search'); - getIndicatorMappingComboField().should('text', 'Search'); - getIndicatorIndexComboField(2).should('not.exist'); - getIndicatorMappingComboField(2).should('not.exist'); - }); - it('Deletes the second row when you have three rows. The first row is valid data, the second row is invalid data, and the third row is valid data. Third row should shift up correctly', () => { - fillIndicatorMatchRow({ - indexField: newThreatIndicatorRule.indicatorMapping, - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + it('Deletes the first row when you have two rows. Both rows have valid "index fields" and invalid "indicator index fields". The second row should become the first row', () => { + fillIndicatorMatchRow({ + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: 'non-existent-value', + validColumns: 'indexField', + }); + getIndicatorAndButton().click(); + fillIndicatorMatchRow({ + rowNumber: 2, + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: 'second-non-existent-value', + validColumns: 'indexField', + }); + getIndicatorDeleteButton().click(); + getIndicatorMappingComboField().should('have.text', 'second-non-existent-value'); + getIndicatorIndexComboField(2).should('not.exist'); + getIndicatorMappingComboField(2).should('not.exist'); }); - getIndicatorAndButton().click(); - fillIndicatorMatchRow({ - rowNumber: 2, - indexField: 'non-existent-value', - indicatorIndexField: 'non-existent-value', - validColumns: 'none', + + it('Deletes the first row when you have two rows. Both rows have valid "indicator index fields" and invalid "index fields". The second row should become the first row', () => { + fillIndicatorMatchRow({ + indexField: 'non-existent-value', + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + validColumns: 'indicatorField', + }); + getIndicatorAndButton().click(); + fillIndicatorMatchRow({ + rowNumber: 2, + indexField: 'second-non-existent-value', + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + validColumns: 'indicatorField', + }); + getIndicatorDeleteButton().click(); + getIndicatorIndexComboField().should('have.text', 'second-non-existent-value'); + getIndicatorIndexComboField(2).should('not.exist'); + getIndicatorMappingComboField(2).should('not.exist'); }); - getIndicatorAndButton().click(); - fillIndicatorMatchRow({ - rowNumber: 3, - indexField: newThreatIndicatorRule.indicatorMapping, - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + + it('Deletes the first row of data but not the UI elements and the text defaults back to the placeholder of Search', () => { + fillIndicatorMatchRow({ + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + }); + getIndicatorDeleteButton().click(); + getIndicatorIndexComboField().should('text', 'Search'); + getIndicatorMappingComboField().should('text', 'Search'); + getIndicatorIndexComboField(2).should('not.exist'); + getIndicatorMappingComboField(2).should('not.exist'); }); - getIndicatorDeleteButton(2).click(); - getIndicatorIndexComboField(1).should('text', newThreatIndicatorRule.indicatorMapping); - getIndicatorMappingComboField(1).should('text', newThreatIndicatorRule.indicatorIndexField); - getIndicatorIndexComboField(2).should('text', newThreatIndicatorRule.indicatorMapping); - getIndicatorMappingComboField(2).should('text', newThreatIndicatorRule.indicatorIndexField); - getIndicatorIndexComboField(3).should('not.exist'); - getIndicatorMappingComboField(3).should('not.exist'); - }); - it('Can add two OR rows and delete the second row. The first row has invalid data and the second row has valid data. The first row is deleted and the second row shifts up correctly.', () => { - fillIndicatorMatchRow({ - indexField: 'non-existent-value-one', - indicatorIndexField: 'non-existent-value-two', - validColumns: 'none', + it('Deletes the second row when you have three rows. The first row is valid data, the second row is invalid data, and the third row is valid data. Third row should shift up correctly', () => { + fillIndicatorMatchRow({ + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + }); + getIndicatorAndButton().click(); + fillIndicatorMatchRow({ + rowNumber: 2, + indexField: 'non-existent-value', + indicatorIndexField: 'non-existent-value', + validColumns: 'none', + }); + getIndicatorAndButton().click(); + fillIndicatorMatchRow({ + rowNumber: 3, + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + }); + getIndicatorDeleteButton(2).click(); + getIndicatorIndexComboField(1).should('text', newThreatIndicatorRule.indicatorMapping); + getIndicatorMappingComboField(1).should( + 'text', + newThreatIndicatorRule.indicatorIndexField + ); + getIndicatorIndexComboField(2).should('text', newThreatIndicatorRule.indicatorMapping); + getIndicatorMappingComboField(2).should( + 'text', + newThreatIndicatorRule.indicatorIndexField + ); + getIndicatorIndexComboField(3).should('not.exist'); + getIndicatorMappingComboField(3).should('not.exist'); }); - getIndicatorOrButton().click(); - fillIndicatorMatchRow({ - rowNumber: 2, - indexField: newThreatIndicatorRule.indicatorMapping, - indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + + it('Can add two OR rows and delete the second row. The first row has invalid data and the second row has valid data. The first row is deleted and the second row shifts up correctly.', () => { + fillIndicatorMatchRow({ + indexField: 'non-existent-value-one', + indicatorIndexField: 'non-existent-value-two', + validColumns: 'none', + }); + getIndicatorOrButton().click(); + fillIndicatorMatchRow({ + rowNumber: 2, + indexField: newThreatIndicatorRule.indicatorMapping, + indicatorIndexField: newThreatIndicatorRule.indicatorIndexField, + }); + getIndicatorDeleteButton().click(); + getIndicatorIndexComboField().should('text', newThreatIndicatorRule.indicatorMapping); + getIndicatorMappingComboField().should( + 'text', + newThreatIndicatorRule.indicatorIndexField + ); + getIndicatorIndexComboField(2).should('not.exist'); + getIndicatorMappingComboField(2).should('not.exist'); }); - getIndicatorDeleteButton().click(); - getIndicatorIndexComboField().should('text', newThreatIndicatorRule.indicatorMapping); - getIndicatorMappingComboField().should('text', newThreatIndicatorRule.indicatorIndexField); - getIndicatorIndexComboField(2).should('not.exist'); - getIndicatorMappingComboField(2).should('not.exist'); }); }); - }); - describe('Generating signals', () => { - beforeEach(() => { - cleanKibana(); - loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); - waitForAlertsPanelToBeLoaded(); - waitForAlertsIndexToBeCreated(); - goToManageAlertsDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); - goToCreateNewRule(); - selectIndicatorMatchType(); - }); + describe('Generating signals', () => { + beforeEach(() => { + cleanKibana(); + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); + waitForAlertsPanelToBeLoaded(); + waitForAlertsIndexToBeCreated(); + goToManageAlertsDetectionRules(); + waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + goToCreateNewRule(); + selectIndicatorMatchType(); + }); - it('Creates and activates a new Indicator Match rule', () => { - fillDefineIndicatorMatchRuleAndContinue(newThreatIndicatorRule); - fillAboutRuleAndContinue(newThreatIndicatorRule); - fillScheduleRuleAndContinue(newThreatIndicatorRule); - createAndActivateRule(); + it('Creates and activates a new Indicator Match rule', () => { + fillDefineIndicatorMatchRuleAndContinue(newThreatIndicatorRule); + fillAboutRuleAndContinue(newThreatIndicatorRule); + fillScheduleRuleAndContinue(newThreatIndicatorRule); + createAndActivateRule(); - cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); + cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); + changeToThreeHundredRowsPerPage(); + waitForRulesToBeLoaded(); - cy.get(RULES_TABLE).then(($table) => { - cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); - }); + cy.get(RULES_TABLE).then(($table) => { + cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); + }); - filterByCustomRules(); + filterByCustomRules(); - cy.get(RULES_TABLE).then(($table) => { - cy.wrap($table.find(RULES_ROW).length).should('eql', 1); - }); - cy.get(RULE_NAME).should('have.text', newThreatIndicatorRule.name); - cy.get(RISK_SCORE).should('have.text', newThreatIndicatorRule.riskScore); - cy.get(SEVERITY).should('have.text', newThreatIndicatorRule.severity); - cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); - - goToRuleDetails(); - - cy.get(RULE_NAME_HEADER).should('have.text', `${newThreatIndicatorRule.name}`); - cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newThreatIndicatorRule.description); - cy.get(ABOUT_DETAILS).within(() => { - getDetails(SEVERITY_DETAILS).should('have.text', newThreatIndicatorRule.severity); - getDetails(RISK_SCORE_DETAILS).should('have.text', newThreatIndicatorRule.riskScore); - getDetails(REFERENCE_URLS_DETAILS).should((details) => { - expect(removeExternalLinkText(details.text())).equal(expectedUrls); + cy.get(RULES_TABLE).then(($table) => { + cy.wrap($table.find(RULES_ROW).length).should('eql', 1); }); - getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); - getDetails(MITRE_ATTACK_DETAILS).should((mitre) => { - expect(removeExternalLinkText(mitre.text())).equal(expectedMitre); + cy.get(RULE_NAME).should('have.text', newThreatIndicatorRule.name); + cy.get(RISK_SCORE).should('have.text', newThreatIndicatorRule.riskScore); + cy.get(SEVERITY).should('have.text', newThreatIndicatorRule.severity); + cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); + + goToRuleDetails(); + + cy.get(RULE_NAME_HEADER).should('have.text', `${newThreatIndicatorRule.name}`); + cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', newThreatIndicatorRule.description); + cy.get(ABOUT_DETAILS).within(() => { + getDetails(SEVERITY_DETAILS).should('have.text', newThreatIndicatorRule.severity); + getDetails(RISK_SCORE_DETAILS).should('have.text', newThreatIndicatorRule.riskScore); + getDetails(REFERENCE_URLS_DETAILS).should((details) => { + expect(removeExternalLinkText(details.text())).equal(expectedUrls); + }); + getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); + getDetails(MITRE_ATTACK_DETAILS).should((mitre) => { + expect(removeExternalLinkText(mitre.text())).equal(expectedMitre); + }); + getDetails(TAGS_DETAILS).should('have.text', expectedTags); + }); + cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); + cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); + + cy.get(DEFINITION_DETAILS).within(() => { + getDetails(INDEX_PATTERNS_DETAILS).should( + 'have.text', + newThreatIndicatorRule.index!.join('') + ); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', '*:*'); + getDetails(RULE_TYPE_DETAILS).should('have.text', 'Indicator Match'); + getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); + getDetails(INDICATOR_INDEX_PATTERNS).should( + 'have.text', + newThreatIndicatorRule.indicatorIndexPattern.join('') + ); + getDetails(INDICATOR_MAPPING).should( + 'have.text', + `${newThreatIndicatorRule.indicatorMapping} MATCHES ${newThreatIndicatorRule.indicatorIndexField}` + ); + getDetails(INDICATOR_INDEX_QUERY).should('have.text', '*:*'); }); - getDetails(TAGS_DETAILS).should('have.text', expectedTags); - }); - cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); - cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); - - cy.get(DEFINITION_DETAILS).within(() => { - getDetails(INDEX_PATTERNS_DETAILS).should( - 'have.text', - newThreatIndicatorRule.index!.join('') - ); - getDetails(CUSTOM_QUERY_DETAILS).should('have.text', '*:*'); - getDetails(RULE_TYPE_DETAILS).should('have.text', 'Indicator Match'); - getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); - getDetails(INDICATOR_INDEX_PATTERNS).should( - 'have.text', - newThreatIndicatorRule.indicatorIndexPattern.join('') - ); - getDetails(INDICATOR_MAPPING).should( - 'have.text', - `${newThreatIndicatorRule.indicatorMapping} MATCHES ${newThreatIndicatorRule.indicatorIndexField}` - ); - getDetails(INDICATOR_INDEX_QUERY).should('have.text', '*:*'); - }); - cy.get(SCHEDULE_DETAILS).within(() => { - getDetails(RUNS_EVERY_DETAILS).should( - 'have.text', - `${newThreatIndicatorRule.runsEvery.interval}${newThreatIndicatorRule.runsEvery.type}` - ); - getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( - 'have.text', - `${newThreatIndicatorRule.lookBack.interval}${newThreatIndicatorRule.lookBack.type}` - ); - }); + cy.get(SCHEDULE_DETAILS).within(() => { + getDetails(RUNS_EVERY_DETAILS).should( + 'have.text', + `${newThreatIndicatorRule.runsEvery.interval}${newThreatIndicatorRule.runsEvery.type}` + ); + getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( + 'have.text', + `${newThreatIndicatorRule.lookBack.interval}${newThreatIndicatorRule.lookBack.type}` + ); + }); - waitForTheRuleToBeExecuted(); - waitForAlertsToPopulate(); - - cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts); - cy.get(ALERT_RULE_NAME).first().should('have.text', newThreatIndicatorRule.name); - cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); - cy.get(ALERT_RULE_METHOD).first().should('have.text', 'threat_match'); - cy.get(ALERT_RULE_SEVERITY) - .first() - .should('have.text', newThreatIndicatorRule.severity.toLowerCase()); - cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', newThreatIndicatorRule.riskScore); + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(); + + cy.get(NUMBER_OF_ALERTS).should('have.text', expectedNumberOfAlerts); + cy.get(ALERT_RULE_NAME).first().should('have.text', newThreatIndicatorRule.name); + cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); + cy.get(ALERT_RULE_METHOD).first().should('have.text', 'threat_match'); + cy.get(ALERT_RULE_SEVERITY) + .first() + .should('have.text', newThreatIndicatorRule.severity.toLowerCase()); + cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', newThreatIndicatorRule.riskScore); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts index d93ed7a0e97a5c..3c188345111c85 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/threshold_rule.spec.ts @@ -80,101 +80,104 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { DETECTIONS_URL } from '../../urls/navigation'; -describe('Detection rules, threshold', () => { - const expectedUrls = newThresholdRule.referenceUrls.join(''); - const expectedFalsePositives = newThresholdRule.falsePositivesExamples.join(''); - const expectedTags = newThresholdRule.tags.join(''); - const expectedMitre = formatMitreAttackDescription(newThresholdRule.mitre); - - const rule = { ...newThresholdRule }; - - beforeEach(() => { - cleanKibana(); - createTimeline(newThresholdRule.timeline).then((response) => { - rule.timeline.id = response.body.data.persistTimeline.timeline.savedObjectId; +// Skipped until post-FF for 7.12 +describe.skip('Threshold Rules', () => { + describe('Detection rules, threshold', () => { + const expectedUrls = newThresholdRule.referenceUrls.join(''); + const expectedFalsePositives = newThresholdRule.falsePositivesExamples.join(''); + const expectedTags = newThresholdRule.tags.join(''); + const expectedMitre = formatMitreAttackDescription(newThresholdRule.mitre); + + const rule = { ...newThresholdRule }; + + beforeEach(() => { + cleanKibana(); + createTimeline(newThresholdRule.timeline).then((response) => { + rule.timeline.id = response.body.data.persistTimeline.timeline.savedObjectId; + }); }); - }); - it('Creates and activates a new threshold rule', () => { - loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); - waitForAlertsPanelToBeLoaded(); - waitForAlertsIndexToBeCreated(); - goToManageAlertsDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); - goToCreateNewRule(); - selectThresholdRuleType(); - fillDefineThresholdRuleAndContinue(rule); - fillAboutRuleAndContinue(rule); - fillScheduleRuleAndContinue(rule); - createAndActivateRule(); - - cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); - - changeToThreeHundredRowsPerPage(); - waitForRulesToBeLoaded(); - - const expectedNumberOfRules = 1; - cy.get(RULES_TABLE).then(($table) => { - cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); - }); + it('Creates and activates a new threshold rule', () => { + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); + waitForAlertsPanelToBeLoaded(); + waitForAlertsIndexToBeCreated(); + goToManageAlertsDetectionRules(); + waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + goToCreateNewRule(); + selectThresholdRuleType(); + fillDefineThresholdRuleAndContinue(rule); + fillAboutRuleAndContinue(rule); + fillScheduleRuleAndContinue(rule); + createAndActivateRule(); + + cy.get(CUSTOM_RULES_BTN).should('have.text', 'Custom rules (1)'); + + changeToThreeHundredRowsPerPage(); + waitForRulesToBeLoaded(); + + const expectedNumberOfRules = 1; + cy.get(RULES_TABLE).then(($table) => { + cy.wrap($table.find(RULES_ROW).length).should('eql', expectedNumberOfRules); + }); - filterByCustomRules(); + filterByCustomRules(); - cy.get(RULES_TABLE).then(($table) => { - cy.wrap($table.find(RULES_ROW).length).should('eql', 1); - }); - cy.get(RULE_NAME).should('have.text', rule.name); - cy.get(RISK_SCORE).should('have.text', rule.riskScore); - cy.get(SEVERITY).should('have.text', rule.severity); - cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); - - goToRuleDetails(); - - cy.get(RULE_NAME_HEADER).should('have.text', `${rule.name}`); - cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', rule.description); - cy.get(ABOUT_DETAILS).within(() => { - getDetails(SEVERITY_DETAILS).should('have.text', rule.severity); - getDetails(RISK_SCORE_DETAILS).should('have.text', rule.riskScore); - getDetails(REFERENCE_URLS_DETAILS).should((details) => { - expect(removeExternalLinkText(details.text())).equal(expectedUrls); + cy.get(RULES_TABLE).then(($table) => { + cy.wrap($table.find(RULES_ROW).length).should('eql', 1); }); - getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); - getDetails(MITRE_ATTACK_DETAILS).should((mitre) => { - expect(removeExternalLinkText(mitre.text())).equal(expectedMitre); + cy.get(RULE_NAME).should('have.text', rule.name); + cy.get(RISK_SCORE).should('have.text', rule.riskScore); + cy.get(SEVERITY).should('have.text', rule.severity); + cy.get(RULE_SWITCH).should('have.attr', 'aria-checked', 'true'); + + goToRuleDetails(); + + cy.get(RULE_NAME_HEADER).should('have.text', `${rule.name}`); + cy.get(ABOUT_RULE_DESCRIPTION).should('have.text', rule.description); + cy.get(ABOUT_DETAILS).within(() => { + getDetails(SEVERITY_DETAILS).should('have.text', rule.severity); + getDetails(RISK_SCORE_DETAILS).should('have.text', rule.riskScore); + getDetails(REFERENCE_URLS_DETAILS).should((details) => { + expect(removeExternalLinkText(details.text())).equal(expectedUrls); + }); + getDetails(FALSE_POSITIVES_DETAILS).should('have.text', expectedFalsePositives); + getDetails(MITRE_ATTACK_DETAILS).should((mitre) => { + expect(removeExternalLinkText(mitre.text())).equal(expectedMitre); + }); + getDetails(TAGS_DETAILS).should('have.text', expectedTags); + }); + cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); + cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); + cy.get(DEFINITION_DETAILS).within(() => { + getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); + getDetails(CUSTOM_QUERY_DETAILS).should('have.text', rule.customQuery); + getDetails(RULE_TYPE_DETAILS).should('have.text', 'Threshold'); + getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); + getDetails(THRESHOLD_DETAILS).should( + 'have.text', + `Results aggregated by ${rule.thresholdField} >= ${rule.threshold}` + ); + }); + cy.get(SCHEDULE_DETAILS).within(() => { + getDetails(RUNS_EVERY_DETAILS).should( + 'have.text', + `${rule.runsEvery.interval}${rule.runsEvery.type}` + ); + getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( + 'have.text', + `${rule.lookBack.interval}${rule.lookBack.type}` + ); }); - getDetails(TAGS_DETAILS).should('have.text', expectedTags); - }); - cy.get(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); - cy.get(ABOUT_INVESTIGATION_NOTES).should('have.text', INVESTIGATION_NOTES_MARKDOWN); - cy.get(DEFINITION_DETAILS).within(() => { - getDetails(INDEX_PATTERNS_DETAILS).should('have.text', indexPatterns.join('')); - getDetails(CUSTOM_QUERY_DETAILS).should('have.text', rule.customQuery); - getDetails(RULE_TYPE_DETAILS).should('have.text', 'Threshold'); - getDetails(TIMELINE_TEMPLATE_DETAILS).should('have.text', 'None'); - getDetails(THRESHOLD_DETAILS).should( - 'have.text', - `Results aggregated by ${rule.thresholdField} >= ${rule.threshold}` - ); - }); - cy.get(SCHEDULE_DETAILS).within(() => { - getDetails(RUNS_EVERY_DETAILS).should( - 'have.text', - `${rule.runsEvery.interval}${rule.runsEvery.type}` - ); - getDetails(ADDITIONAL_LOOK_BACK_DETAILS).should( - 'have.text', - `${rule.lookBack.interval}${rule.lookBack.type}` - ); - }); - waitForTheRuleToBeExecuted(); - waitForAlertsToPopulate(); + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(); - cy.get(NUMBER_OF_ALERTS).should(($count) => expect(+$count.text()).to.be.lt(100)); - cy.get(ALERT_RULE_NAME).first().should('have.text', rule.name); - cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); - cy.get(ALERT_RULE_METHOD).first().should('have.text', 'threshold'); - cy.get(ALERT_RULE_SEVERITY).first().should('have.text', rule.severity.toLowerCase()); - cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', rule.riskScore); + cy.get(NUMBER_OF_ALERTS).should(($count) => expect(+$count.text()).to.be.lt(100)); + cy.get(ALERT_RULE_NAME).first().should('have.text', rule.name); + cy.get(ALERT_RULE_VERSION).first().should('have.text', '1'); + cy.get(ALERT_RULE_METHOD).first().should('have.text', 'threshold'); + cy.get(ALERT_RULE_SEVERITY).first().should('have.text', rule.severity.toLowerCase()); + cy.get(ALERT_RULE_RISK_SCORE).first().should('have.text', rule.riskScore); + }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/all_cases.ts b/x-pack/plugins/security_solution/cypress/screens/all_cases.ts index 06d1a9fca91c67..e9c5ff89dd8c4b 100644 --- a/x-pack/plugins/security_solution/cypress/screens/all_cases.ts +++ b/x-pack/plugins/security_solution/cypress/screens/all_cases.ts @@ -9,8 +9,6 @@ export const ALL_CASES_CASE = (id: string) => { return `[data-test-subj="cases-table-row-${id}"]`; }; -export const ALL_CASES_CLOSE_ACTION = '[data-test-subj="action-close"]'; - export const ALL_CASES_CLOSED_CASES_STATS = '[data-test-subj="closedStatsHeader"]'; export const ALL_CASES_COMMENTS_COUNT = '[data-test-subj="case-table-column-commentCount"]'; @@ -19,10 +17,10 @@ export const ALL_CASES_CREATE_NEW_CASE_BTN = '[data-test-subj="createNewCaseBtn" export const ALL_CASES_CREATE_NEW_CASE_TABLE_BTN = '[data-test-subj="cases-table-add-case"]'; -export const ALL_CASES_DELETE_ACTION = '[data-test-subj="action-delete"]'; - export const ALL_CASES_IN_PROGRESS_CASES_STATS = '[data-test-subj="inProgressStatsHeader"]'; +export const ALL_CASES_ITEM_ACTIONS_BTN = '[data-test-subj="euiCollapsedItemActionsButton"]'; + export const ALL_CASES_NAME = '[data-test-subj="case-details-link"]'; export const ALL_CASES_OPEN_CASES_COUNT = '[data-test-subj="case-status-filter"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts index cbcfed8a4cf95a..a2fb94e462023e 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts @@ -129,6 +129,10 @@ export const RISK_MAPPING_OVERRIDE_OPTION = '#risk_score-mapping-override'; export const RISK_OVERRIDE = '[data-test-subj="detectionEngineStepAboutRuleRiskScore-riskOverride"]'; +export const RULES_CREATION_FORM = '[data-test-subj="stepDefineRule"]'; + +export const RULES_CREATION_PREVIEW = '[data-test-subj="ruleCreationQueryPreview"]'; + export const RULE_DESCRIPTION_INPUT = '[data-test-subj="detectionEngineStepAboutRuleDescription"] [data-test-subj="input"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/common/callouts.ts b/x-pack/plugins/security_solution/cypress/tasks/common/callouts.ts index 4139c911e4063d..8440409f80f38e 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/common/callouts.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/common/callouts.ts @@ -12,13 +12,11 @@ export const getCallOut = (id: string, options?: Cypress.Timeoutable) => { }; export const waitForCallOutToBeShown = (id: string, color: string) => { - getCallOut(id, { timeout: 10000 }) - .should('be.visible') - .should('have.class', `euiCallOut--${color}`); + getCallOut(id).should('be.visible').should('have.class', `euiCallOut--${color}`); }; export const dismissCallOut = (id: string) => { - getCallOut(id, { timeout: 10000 }).within(() => { + getCallOut(id).within(() => { cy.get(CALLOUT_DISMISS_BTN).should('be.visible').click(); cy.root().should('not.exist'); }); diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index 475ce5ecb15727..02ba3937ed542a 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -78,6 +78,8 @@ import { AT_LEAST_ONE_VALID_MATCH, AT_LEAST_ONE_INDEX_PATTERN, CUSTOM_QUERY_REQUIRED, + RULES_CREATION_FORM, + RULES_CREATION_PREVIEW, } from '../screens/create_new_rule'; import { TOAST_ERROR } from '../screens/shared'; import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline'; @@ -271,23 +273,26 @@ export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRule) => { }; export const fillDefineEqlRuleAndContinue = (rule: CustomRule) => { - cy.get(EQL_QUERY_INPUT).should('exist'); - cy.get(EQL_QUERY_INPUT).should('be.visible'); - cy.get(EQL_QUERY_INPUT).type(rule.customQuery!); - cy.get(EQL_QUERY_VALIDATION_SPINNER).should('not.exist'); - cy.get(QUERY_PREVIEW_BUTTON).should('not.be.disabled').click({ force: true }); + cy.get(RULES_CREATION_FORM).find(EQL_QUERY_INPUT).should('exist'); + cy.get(RULES_CREATION_FORM).find(EQL_QUERY_INPUT).should('be.visible'); + cy.get(RULES_CREATION_FORM).find(EQL_QUERY_INPUT).type(rule.customQuery!); + cy.get(RULES_CREATION_FORM).find(EQL_QUERY_VALIDATION_SPINNER).should('not.exist'); + cy.get(RULES_CREATION_PREVIEW) + .find(QUERY_PREVIEW_BUTTON) + .should('not.be.disabled') + .click({ force: true }); cy.get(EQL_QUERY_PREVIEW_HISTOGRAM) .invoke('text') .then((text) => { if (text !== 'Hits') { - cy.get(QUERY_PREVIEW_BUTTON).click({ force: true }); + cy.get(RULES_CREATION_PREVIEW).find(QUERY_PREVIEW_BUTTON).click({ force: true }); cy.get(EQL_QUERY_PREVIEW_HISTOGRAM).should('contain.text', 'Hits'); } }); cy.get(TOAST_ERROR).should('not.exist'); cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); - cy.get(EQL_QUERY_INPUT).should('not.exist'); + cy.get(`${RULES_CREATION_FORM} ${EQL_QUERY_INPUT}`).should('not.exist'); }; /** @@ -480,7 +485,7 @@ export const waitForAlertsToPopulate = async () => { export const waitForTheRuleToBeExecuted = () => { cy.waitUntil(() => { - cy.get(REFRESH_BUTTON).click(); + cy.get(REFRESH_BUTTON).click({ force: true }); return cy .get(RULE_STATUS) .invoke('text') diff --git a/x-pack/plugins/security_solution/cypress/tasks/date_picker.ts b/x-pack/plugins/security_solution/cypress/tasks/date_picker.ts index 018be0ec72f22f..5fef4f2f5569be 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/date_picker.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/date_picker.ts @@ -33,7 +33,7 @@ export const setStartDate = (date: string) => { }; export const setTimelineEndDate = (date: string) => { - cy.get(DATE_PICKER_END_DATE_POPOVER_BUTTON_TIMELINE).click({ force: true }); + cy.get(DATE_PICKER_END_DATE_POPOVER_BUTTON_TIMELINE).first().click({ force: true }); cy.get(DATE_PICKER_ABSOLUTE_TAB).first().click({ force: true }); @@ -47,7 +47,7 @@ export const setTimelineEndDate = (date: string) => { }; export const setTimelineStartDate = (date: string) => { - cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON_TIMELINE).click({ + cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON_TIMELINE).first().click({ force: true, }); @@ -68,6 +68,7 @@ export const updateDates = () => { export const updateTimelineDates = () => { cy.get(DATE_PICKER_APPLY_BUTTON_TIMELINE) + .first() .click({ force: true }) .should('not.have.text', 'Updating'); }; diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index ca4c869e0f2d38..c001f1fc2bc47d 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -99,7 +99,7 @@ export const goToQueryTab = () => { export const addNotesToTimeline = (notes: string) => { goToNotesTab(); cy.get(NOTES_TEXT_AREA).type(notes); - cy.get(ADD_NOTE_BUTTON).click(); + cy.get(ADD_NOTE_BUTTON).click({ force: true }); cy.get(QUERY_TAB_BUTTON).click(); }; diff --git a/x-pack/plugins/security_solution/public/app/search/index.test.ts b/x-pack/plugins/security_solution/public/app/search/index.test.ts new file mode 100644 index 00000000000000..d6c36e89558d02 --- /dev/null +++ b/x-pack/plugins/security_solution/public/app/search/index.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { getSearchDeepLinksAndKeywords } from '.'; +import { SecurityPageName } from '../../../common/constants'; + +describe('public search functions', () => { + it('returns a subset of links for basic license, full set for platinum', () => { + const basicLicense = 'basic'; + const platinumLicense = 'platinum'; + for (const pageName of Object.values(SecurityPageName)) { + expect.assertions(Object.values(SecurityPageName).length * 2); + const basicLinkCount = + getSearchDeepLinksAndKeywords(pageName, basicLicense).searchDeepLinks?.length || 0; + const platinumLinks = getSearchDeepLinksAndKeywords(pageName, platinumLicense); + expect(platinumLinks.searchDeepLinks?.length).toBeGreaterThanOrEqual(basicLinkCount); + expect(platinumLinks.keywords?.length).not.toBe(null); + } + }); +}); diff --git a/x-pack/plugins/security_solution/public/app/search/index.ts b/x-pack/plugins/security_solution/public/app/search/index.ts new file mode 100644 index 00000000000000..110356269e8917 --- /dev/null +++ b/x-pack/plugins/security_solution/public/app/search/index.ts @@ -0,0 +1,242 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { Subject } from 'rxjs'; + +import { AppUpdater } from 'src/core/public'; +import { LicenseType } from '../../../../licensing/common/types'; +import { SecuritySubPluginNames, SecurityDeepLinks } from '../types'; +import { AppMeta } from '../../../../../../src/core/public'; + +const securityDeepLinks: SecurityDeepLinks = { + detections: { + base: [ + { + id: 'siemDetectionRules', + title: i18n.translate('xpack.securitySolution.search.detections.manage', { + defaultMessage: 'Manage Rules', + }), + keywords: ['rules'], + path: '/rules', + }, + ], + }, + hosts: { + base: [ + { + id: 'authentications', + title: i18n.translate('xpack.securitySolution.search.hosts.authentications', { + defaultMessage: 'Authentications', + }), + path: '/authentications', + }, + { + id: 'uncommonProcesses', + title: i18n.translate('xpack.securitySolution.search.hosts.uncommonProcesses', { + defaultMessage: 'Uncommon Processes', + }), + path: '/uncommonProcesses', + }, + { + id: 'events', + title: i18n.translate('xpack.securitySolution.search.hosts.events', { + defaultMessage: 'Events', + }), + path: '/events', + }, + { + id: 'externalAlerts', + title: i18n.translate('xpack.securitySolution.search.hosts.externalAlerts', { + defaultMessage: 'External Alerts', + }), + path: '/alerts', + }, + ], + premium: [ + { + id: 'anomalies', + title: i18n.translate('xpack.securitySolution.search.hosts.anomalies', { + defaultMessage: 'Anomalies', + }), + path: '/anomalies', + }, + ], + }, + network: { + base: [ + { + id: 'dns', + title: i18n.translate('xpack.securitySolution.search.network.dns', { + defaultMessage: 'DNS', + }), + path: '/dns', + }, + { + id: 'http', + title: i18n.translate('xpack.securitySolution.search.network.http', { + defaultMessage: 'HTTP', + }), + path: '/http', + }, + { + id: 'tls', + title: i18n.translate('xpack.securitySolution.search.network.tls', { + defaultMessage: 'TLS', + }), + path: '/tls', + }, + { + id: 'externalAlertsNetwork', + title: i18n.translate('xpack.securitySolution.search.network.externalAlerts', { + defaultMessage: 'External Alerts', + }), + path: '/external-alerts', + }, + ], + premium: [ + { + id: 'anomalies', + title: i18n.translate('xpack.securitySolution.search.hosts.anomalies', { + defaultMessage: 'Anomalies', + }), + path: '/anomalies', + }, + ], + }, + timelines: { + base: [ + { + id: 'timelineTemplates', + title: i18n.translate('xpack.securitySolution.search.timeline.templates', { + defaultMessage: 'Templates', + }), + path: '/template', + }, + ], + }, + overview: { + base: [], + }, + case: { + base: [ + { + id: 'create', + title: i18n.translate('xpack.securitySolution.search.cases.create', { + defaultMessage: 'Create New Case', + }), + path: '/create', + }, + ], + premium: [ + { + id: 'configure', + title: i18n.translate('xpack.securitySolution.search.cases.configure', { + defaultMessage: 'Configure Cases', + }), + path: '/configure', + }, + ], + }, + administration: { + base: [ + { + id: 'trustApplications', + title: i18n.translate('xpack.securitySolution.search.administration.trustedApps', { + defaultMessage: 'Trusted Applications', + }), + path: '/trusted_apps', + }, + ], + }, +}; + +const subpluginKeywords: { [key in SecuritySubPluginNames]: string[] } = { + detections: [ + i18n.translate('xpack.securitySolution.search.detections', { + defaultMessage: 'Detections', + }), + ], + hosts: [ + i18n.translate('xpack.securitySolution.search.hosts', { + defaultMessage: 'Hosts', + }), + ], + network: [ + i18n.translate('xpack.securitySolution.search.network', { + defaultMessage: 'Network', + }), + ], + timelines: [ + i18n.translate('xpack.securitySolution.search.timelines', { + defaultMessage: 'Timelines', + }), + ], + overview: [ + i18n.translate('xpack.securitySolution.search.overview', { + defaultMessage: 'Overview', + }), + ], + case: [ + i18n.translate('xpack.securitySolution.search.cases', { + defaultMessage: 'Cases', + }), + ], + administration: [ + i18n.translate('xpack.securitySolution.search.administration', { + defaultMessage: 'Endpoint Administration', + }), + ], +}; + +/** + * A function that generates a subPlugin's meta tag + * @param subPluginName SubPluginName of the app to retrieve meta information for. + * @param licenseType optional string for license level, if not provided basic is assumed. + */ +export function getSearchDeepLinksAndKeywords( + subPluginName: SecuritySubPluginNames, + licenseType?: LicenseType +): AppMeta { + const baseRoutes = [...securityDeepLinks[subPluginName].base]; + if ( + licenseType === 'gold' || + licenseType === 'platinum' || + licenseType === 'enterprise' || + licenseType === 'trial' + ) { + const premiumRoutes = + securityDeepLinks[subPluginName] && securityDeepLinks[subPluginName].premium; + if (premiumRoutes !== undefined) { + return { + keywords: subpluginKeywords[subPluginName], + searchDeepLinks: [...baseRoutes, ...premiumRoutes], + }; + } + } + return { + keywords: subpluginKeywords[subPluginName], + searchDeepLinks: baseRoutes, + }; +} +/** + * A function that updates a subplugin's meta property as appropriate when license level changes. + * @param subPluginName SubPluginName of the app to register searchDeepLinks for + * @param appUpdater an instance of appUpdater$ observable to update search links when needed. + * @param licenseType A string representing the current license level. + */ +export function registerSearchLinks( + subPluginName: SecuritySubPluginNames, + appUpdater?: Subject, + licenseType?: LicenseType +) { + if (appUpdater !== undefined) { + appUpdater.next(() => ({ + meta: getSearchDeepLinksAndKeywords(subPluginName, licenseType), + })); + } +} diff --git a/x-pack/plugins/security_solution/public/app/types.ts b/x-pack/plugins/security_solution/public/app/types.ts index 4d1c091fdaa5fd..95e64fe37d3331 100644 --- a/x-pack/plugins/security_solution/public/app/types.ts +++ b/x-pack/plugins/security_solution/public/app/types.ts @@ -17,7 +17,7 @@ import { CombinedState, } from 'redux'; -import { AppMountParameters } from '../../../../../src/core/public'; +import { AppMountParameters, AppSearchDeepLink } from '../../../../../src/core/public'; import { StartServices } from '../types'; import { AppFrontendLibs } from '../common/lib/lib'; @@ -34,6 +34,7 @@ import { State, SubPluginsInitReducer } from '../common/store'; import { Immutable } from '../../common/endpoint/types'; import { AppAction } from '../common/store/actions'; import { TimelineState } from '../timelines/store/timeline/types'; +import { SecurityPageName } from '../../common/constants'; export { SecurityPageName } from '../../common/constants'; export interface SecuritySubPluginStore { @@ -55,6 +56,15 @@ export type SecuritySubPluginKeyStore = | 'alertList' | 'management'; +export type SecuritySubPluginNames = keyof typeof SecurityPageName; + +interface SecurityDeepLink { + base: AppSearchDeepLink[]; + premium?: AppSearchDeepLink[]; +} + +export type SecurityDeepLinks = { [key in SecuritySubPluginNames]: SecurityDeepLink }; + /** * Returned by the various 'SecuritySubPlugin' classes from the `start` method. */ diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx index c447e00cbb94fc..d02f7e0ee09615 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx @@ -79,7 +79,12 @@ describe('AddComment ', () => { await waitFor(() => { expect(onCommentSaving).toBeCalled(); - expect(postComment).toBeCalledWith(addCommentProps.caseId, sampleData, onCommentPosted); + expect(postComment).toBeCalledWith({ + caseId: addCommentProps.caseId, + data: sampleData, + subCaseId: undefined, + updateCase: onCommentPosted, + }); expect(wrapper.find(`[data-test-subj="add-comment"] textarea`).text()).toBe(''); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx index 01b86a989e022f..c94ef75523e2cc 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx @@ -39,11 +39,15 @@ interface AddCommentProps { onCommentSaving?: () => void; onCommentPosted: (newCase: Case) => void; showLoading?: boolean; + subCaseId?: string; } export const AddComment = React.memo( forwardRef( - ({ caseId, disabled, showLoading = true, onCommentPosted, onCommentSaving }, ref) => { + ( + { caseId, disabled, onCommentPosted, onCommentSaving, showLoading = true, subCaseId }, + ref + ) => { const { isLoading, postComment } = usePostComment(); const { form } = useForm({ @@ -80,10 +84,15 @@ export const AddComment = React.memo( if (onCommentSaving != null) { onCommentSaving(); } - postComment(caseId, { ...data, type: CommentType.user }, onCommentPosted); + postComment({ + caseId, + data: { ...data, type: CommentType.user }, + updateCase: onCommentPosted, + subCaseId, + }); reset(); } - }, [onCommentPosted, onCommentSaving, postComment, reset, submit, caseId]); + }, [caseId, onCommentPosted, onCommentSaving, postComment, reset, submit, subCaseId]); return ( diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/actions.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/actions.tsx index ae2d9879559931..66563deae54227 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/actions.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/actions.tsx @@ -9,8 +9,9 @@ import { Dispatch } from 'react'; import { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_table/action_types'; import { CaseStatuses } from '../../../../../case/common/api'; -import { Case } from '../../containers/types'; +import { Case, SubCase } from '../../containers/types'; import { UpdateCase } from '../../containers/use_get_cases'; +import { statuses } from '../status'; import * as i18n from './translations'; interface GetActions { @@ -19,46 +20,74 @@ interface GetActions { deleteCaseOnClick: (deleteCase: Case) => void; } +const hasSubCases = (subCases: SubCase[] | null | undefined) => + subCases != null && subCases?.length > 0; + export const getActions = ({ caseStatus, dispatchUpdate, deleteCaseOnClick, -}: GetActions): Array> => [ - { - description: i18n.DELETE_CASE, - icon: 'trash', - name: i18n.DELETE_CASE, - onClick: deleteCaseOnClick, - type: 'icon', - 'data-test-subj': 'action-delete', - }, - caseStatus === CaseStatuses.open - ? { - description: i18n.CLOSE_CASE, - icon: 'folderCheck', - name: i18n.CLOSE_CASE, - onClick: (theCase: Case) => - dispatchUpdate({ - updateKey: 'status', - updateValue: CaseStatuses.closed, - caseId: theCase.id, - version: theCase.version, - }), - type: 'icon', - 'data-test-subj': 'action-close', - } - : { - description: i18n.REOPEN_CASE, - icon: 'folderExclamation', - name: i18n.REOPEN_CASE, - onClick: (theCase: Case) => - dispatchUpdate({ - updateKey: 'status', - updateValue: CaseStatuses.open, - caseId: theCase.id, - version: theCase.version, - }), - type: 'icon', - 'data-test-subj': 'action-open', - }, -]; +}: GetActions): Array> => { + const openCaseAction = { + available: (item: Case) => caseStatus !== CaseStatuses.open && !hasSubCases(item.subCases), + description: statuses[CaseStatuses.open].actions.single.title, + icon: statuses[CaseStatuses.open].icon, + name: statuses[CaseStatuses.open].actions.single.title, + onClick: (theCase: Case) => + dispatchUpdate({ + updateKey: 'status', + updateValue: CaseStatuses.open, + caseId: theCase.id, + version: theCase.version, + }), + type: 'icon' as const, + 'data-test-subj': 'action-open', + }; + + const makeInProgressAction = { + available: (item: Case) => + caseStatus !== CaseStatuses['in-progress'] && !hasSubCases(item.subCases), + description: statuses[CaseStatuses['in-progress']].actions.single.title, + icon: statuses[CaseStatuses['in-progress']].icon, + name: statuses[CaseStatuses['in-progress']].actions.single.title, + onClick: (theCase: Case) => + dispatchUpdate({ + updateKey: 'status', + updateValue: CaseStatuses['in-progress'], + caseId: theCase.id, + version: theCase.version, + }), + type: 'icon' as const, + 'data-test-subj': 'action-in-progress', + }; + + const closeCaseAction = { + available: (item: Case) => caseStatus !== CaseStatuses.closed && !hasSubCases(item.subCases), + description: statuses[CaseStatuses.closed].actions.single.title, + icon: statuses[CaseStatuses.closed].icon, + name: statuses[CaseStatuses.closed].actions.single.title, + onClick: (theCase: Case) => + dispatchUpdate({ + updateKey: 'status', + updateValue: CaseStatuses.closed, + caseId: theCase.id, + version: theCase.version, + }), + type: 'icon' as const, + 'data-test-subj': 'action-close', + }; + + return [ + { + description: i18n.DELETE_CASE, + icon: 'trash', + name: i18n.DELETE_CASE, + onClick: deleteCaseOnClick, + type: 'icon', + 'data-test-subj': 'action-delete', + }, + openCaseAction, + makeInProgressAction, + closeCaseAction, + ]; +}; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.tsx index 38f1b343670c8e..47db362c7b4bfe 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.tsx @@ -14,17 +14,20 @@ import { EuiTableActionsColumnType, EuiTableComputedColumnType, EuiTableFieldDataColumnType, - HorizontalAlignment, } from '@elastic/eui'; +import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; import styled from 'styled-components'; import { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_table/action_types'; import { CaseStatuses } from '../../../../../case/common/api'; import { getEmptyTagValue } from '../../../common/components/empty_value'; -import { Case } from '../../containers/types'; +import { Case, SubCase } from '../../containers/types'; import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date'; import { CaseDetailsLink } from '../../../common/components/links'; import * as i18n from './translations'; +import { Status } from '../status'; +import { getSubCasesStatusCountsBadges, isSubCase } from './helpers'; +import { ALERTS } from '../../../app/home/translations'; export type CasesColumns = | EuiTableFieldDataColumnType @@ -54,10 +57,14 @@ export const getCasesColumns = ( const columns = [ { name: i18n.NAME, - render: (theCase: Case) => { + render: (theCase: Case | SubCase) => { if (theCase.id != null && theCase.title != null) { const caseDetailsLinkComponent = !isModal ? ( - + {theCase.title} ) : ( @@ -122,7 +129,17 @@ export const getCasesColumns = ( truncateText: true, }, { - align: 'right' as HorizontalAlignment, + align: RIGHT_ALIGNMENT, + field: 'totalAlerts', + name: ALERTS, + sortable: true, + render: (totalAlerts: Case['totalAlerts']) => + totalAlerts != null + ? renderStringField(`${totalAlerts}`, `case-table-column-alertsCount`) + : getEmptyTagValue(), + }, + { + align: RIGHT_ALIGNMENT, field: 'totalComment', name: i18n.COMMENTS, sortable: true, @@ -183,6 +200,24 @@ export const getCasesColumns = ( return getEmptyTagValue(); }, }, + { + name: i18n.STATUS, + render: (theCase: Case) => { + if (theCase?.subCases == null || theCase.subCases.length === 0) { + if (theCase.status == null) { + return getEmptyTagValue(); + } + return ; + } + + const badges = getSubCasesStatusCountsBadges(theCase.subCases); + return badges.map(({ color, count }, index) => ( + + {count} + + )); + }, + }, { name: i18n.ACTIONS, actions, diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/expanded_row.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/expanded_row.tsx new file mode 100644 index 00000000000000..bb4bd0f98949da --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/expanded_row.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiBasicTable as _EuiBasicTable } from '@elastic/eui'; +import styled from 'styled-components'; +import { Case } from '../../containers/types'; +import { CasesColumns } from './columns'; +import { AssociationType } from '../../../../../case/common/api'; + +type ExpandedRowMap = Record | {}; + +const EuiBasicTable: any = _EuiBasicTable; // eslint-disable-line @typescript-eslint/no-explicit-any +const BasicTable = styled(EuiBasicTable)` + thead { + display: none; + } + + tbody { + .euiTableCellContent { + padding: 8px !important; + } + .euiTableRowCell { + border: 0; + } + } +`; +BasicTable.displayName = 'BasicTable'; + +export const getExpandedRowMap = ({ + data, + columns, +}: { + data: Case[] | null; + columns: CasesColumns[]; +}): ExpandedRowMap => { + if (data == null) { + return {}; + } + + return data.reduce((acc, curr) => { + if (curr.subCases != null) { + const subCases = curr.subCases.map((subCase, index) => ({ + ...subCase, + caseParentId: curr.id, + title: `${curr.title} ${index + 1}`, + associationType: AssociationType.subCase, + })); + return { + ...acc, + [curr.id]: ( + + ), + }; + } else { + return acc; + } + }, {}); +}; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/helpers.ts b/x-pack/plugins/security_solution/public/cases/components/all_cases/helpers.ts new file mode 100644 index 00000000000000..1ab36d3c672250 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/helpers.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { filter } from 'lodash/fp'; +import { AssociationType, CaseStatuses } from '../../../../../case/common/api'; +import { Case, SubCase } from '../../containers/types'; +import { statuses } from '../status'; + +export const isSubCase = (theCase: Case | SubCase): theCase is SubCase => + (theCase as SubCase).caseParentId !== undefined && + (theCase as SubCase).associationType === AssociationType.subCase; + +export const getSubCasesStatusCountsBadges = ( + subCases: SubCase[] +): Array<{ name: CaseStatuses; color: string; count: number }> => { + return [ + { + name: CaseStatuses.open, + color: statuses[CaseStatuses.open].color, + count: filter({ status: CaseStatuses.open }, subCases).length, + }, + { + name: CaseStatuses['in-progress'], + color: statuses[CaseStatuses['in-progress']].color, + count: filter({ status: CaseStatuses['in-progress'] }, subCases).length, + }, + { + name: CaseStatuses.closed, + color: statuses[CaseStatuses.closed].color, + count: filter({ status: CaseStatuses.closed }, subCases).length, + }, + ]; +}; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx index 318143426af584..a145bdf117813e 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx @@ -210,9 +210,12 @@ describe('AllCases', () => { id: null, createdAt: null, createdBy: null, + status: null, + subCases: null, tags: null, title: null, totalComment: null, + totalAlerts: null, }, ], }, @@ -278,6 +281,7 @@ describe('AllCases', () => { ); await waitFor(() => { + wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click'); wrapper.find('[data-test-subj="action-close"]').first().simulate('click'); const firstCase = useGetCasesMockState.data.cases[0]; expect(dispatchUpdateCaseProperty).toBeCalledWith({ @@ -302,6 +306,7 @@ describe('AllCases', () => { ); await waitFor(() => { + wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click'); wrapper.find('[data-test-subj="action-open"]').first().simulate('click'); const firstCase = useGetCasesMockState.data.cases[0]; expect(dispatchUpdateCaseProperty).toBeCalledWith({ @@ -314,6 +319,26 @@ describe('AllCases', () => { }); }); + it('put case in progress when row action icon clicked', async () => { + const wrapper = mount( + + + + ); + await waitFor(() => { + wrapper.find('[data-test-subj="euiCollapsedItemActionsButton"]').first().simulate('click'); + wrapper.find('[data-test-subj="action-in-progress"]').first().simulate('click'); + const firstCase = useGetCasesMockState.data.cases[0]; + expect(dispatchUpdateCaseProperty).toBeCalledWith({ + caseId: firstCase.id, + updateKey: 'status', + updateValue: CaseStatuses['in-progress'], + refetchCasesStatus: fetchCasesStatus, + version: firstCase.version, + }); + }); + }); + it('Bulk delete', async () => { useGetCasesMock.mockReturnValue({ ...defaultGetCases, @@ -392,6 +417,27 @@ describe('AllCases', () => { }); }); + it('Bulk in-progress status update', async () => { + useGetCasesMock.mockReturnValue({ + ...defaultGetCases, + selectedCases: useGetCasesMockState.data.cases, + }); + + const wrapper = mount( + + + + ); + await waitFor(() => { + wrapper.find('[data-test-subj="case-table-bulk-actions"] button').first().simulate('click'); + wrapper.find('[data-test-subj="cases-bulk-in-progress-button"]').first().simulate('click'); + expect(updateBulkStatus).toBeCalledWith( + useGetCasesMockState.data.cases, + CaseStatuses['in-progress'] + ); + }); + }); + it('isDeleted is true, refetch', async () => { useDeleteCasesMock.mockReturnValue({ ...defaultDeleteCases, @@ -542,6 +588,7 @@ describe('AllCases', () => { }, id: '1', status: 'open', + subCaseIds: [], tags: ['coke', 'pepsi'], title: 'Another horrible breach!!', totalAlerts: 0, diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx index 001251a8a71ae6..ce0fea07bf473c 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { - EuiBasicTable, + EuiBasicTable as _EuiBasicTable, EuiContextMenuPanel, EuiEmptyPrompt, EuiFlexGroup, @@ -53,6 +53,7 @@ import { SecurityPageName } from '../../../app/types'; import { useKibana } from '../../../common/lib/kibana'; import { APP_ID } from '../../../../common/constants'; import { Stats } from '../status'; +import { getExpandedRowMap } from './expanded_row'; const Div = styled.div` margin-top: ${({ theme }) => theme.eui.paddingSizes.m}; @@ -83,6 +84,14 @@ const getSortField = (field: string): SortFieldCase => { return SortFieldCase.createdAt; }; +const EuiBasicTable: any = _EuiBasicTable; // eslint-disable-line @typescript-eslint/no-explicit-any +const BasicTable = styled(EuiBasicTable)` + .euiTableRow-isExpandedRow.euiTableRow-isSelectable .euiTableCellContent { + padding: 8px 0 8px 32px; + } +`; +BasicTable.displayName = 'BasicTable'; + interface AllCasesProps { onRowClick?: (theCase?: Case) => void; isModal?: boolean; @@ -130,7 +139,7 @@ export const AllCases = React.memo( isUpdated, updateBulkStatus, } = useUpdateCases(); - const [deleteThisCase, setDeleteThisCase] = useState({ + const [deleteThisCase, setDeleteThisCase] = useState({ title: '', id: '', }); @@ -190,7 +199,7 @@ export const AllCases = React.memo( const toggleDeleteModal = useCallback( (deleteCase: Case) => { handleToggleModal(); - setDeleteThisCase(deleteCase); + setDeleteThisCase({ id: deleteCase.id, title: deleteCase.title, type: deleteCase.type }); }, [handleToggleModal] ); @@ -201,7 +210,11 @@ export const AllCases = React.memo( if (caseIds.length === 1) { const singleCase = selectedCases.find((theCase) => theCase.id === caseIds[0]); if (singleCase) { - return setDeleteThisCase({ id: singleCase.id, title: singleCase.title }); + return setDeleteThisCase({ + id: singleCase.id, + title: singleCase.title, + type: singleCase.type, + }); } } const convertToDeleteCases: DeleteCase[] = caseIds.map((id) => ({ id })); @@ -315,6 +328,16 @@ export const AllCases = React.memo( () => getCasesColumns(userCanCrud ? actions : [], filterOptions.status, isModal), [actions, filterOptions.status, userCanCrud, isModal] ); + + const itemIdToExpandedRowMap = useMemo( + () => + getExpandedRowMap({ + columns: memoizedGetCasesColumns, + data: data.cases, + }), + [data.cases, memoizedGetCasesColumns] + ); + const memoizedPagination = useMemo( () => ({ pageIndex: queryParams.page - 1, @@ -330,7 +353,10 @@ export const AllCases = React.memo( }; const euiBasicTableSelectionProps = useMemo>( - () => ({ onSelectionChange: setSelectedCases }), + () => ({ + selectable: (theCase) => isEmpty(theCase.subCases), + onSelectionChange: setSelectedCases, + }), [setSelectedCases] ); const isCasesLoading = useMemo( @@ -472,12 +498,13 @@ export const AllCases = React.memo( )} - {i18n.NO_CASES}} diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/translations.ts b/x-pack/plugins/security_solution/public/cases/components/all_cases/translations.ts index bcc5df6c39e5bf..3b27ef25eda146 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/translations.ts @@ -103,3 +103,7 @@ export const SERVICENOW_LINK_ARIA = i18n.translate( defaultMessage: 'click to view the incident on servicenow', } ); + +export const STATUS = i18n.translate('xpack.securitySolution.case.caseTable.status', { + defaultMessage: 'Status', +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/bulk_actions/index.tsx b/x-pack/plugins/security_solution/public/cases/components/bulk_actions/index.tsx index f9722b3903b122..ec3b391cdcbfee 100644 --- a/x-pack/plugins/security_solution/public/cases/components/bulk_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/bulk_actions/index.tsx @@ -9,10 +9,11 @@ import React from 'react'; import { EuiContextMenuItem } from '@elastic/eui'; import { CaseStatuses } from '../../../../../case/common/api'; +import { statuses } from '../status'; import * as i18n from './translations'; interface GetBulkItems { - caseStatus: string; + caseStatus: CaseStatuses; closePopover: () => void; deleteCasesAction: (cases: string[]) => void; selectedCaseIds: string[]; @@ -26,34 +27,72 @@ export const getBulkItems = ({ selectedCaseIds, updateCaseStatus, }: GetBulkItems) => { + let statusMenuItems: JSX.Element[] = []; + + const openMenuItem = ( + { + closePopover(); + updateCaseStatus(CaseStatuses.open); + }} + > + {statuses[CaseStatuses.open].actions.bulk.title} + + ); + + const inProgressMenuItem = ( + { + closePopover(); + updateCaseStatus(CaseStatuses['in-progress']); + }} + > + {statuses[CaseStatuses['in-progress']].actions.bulk.title} + + ); + + const closeMenuItem = ( + { + closePopover(); + updateCaseStatus(CaseStatuses.closed); + }} + > + {statuses[CaseStatuses.closed].actions.bulk.title} + + ); + + switch (caseStatus) { + case CaseStatuses.open: + statusMenuItems = [inProgressMenuItem, closeMenuItem]; + break; + + case CaseStatuses['in-progress']: + statusMenuItems = [openMenuItem, closeMenuItem]; + break; + + case CaseStatuses.closed: + statusMenuItems = [openMenuItem, inProgressMenuItem]; + break; + + default: + break; + } + return [ - caseStatus === CaseStatuses.open ? ( - { - closePopover(); - updateCaseStatus(CaseStatuses.closed); - }} - > - {i18n.BULK_ACTION_CLOSE_SELECTED} - - ) : ( - { - closePopover(); - updateCaseStatus(CaseStatuses.open); - }} - > - {i18n.BULK_ACTION_OPEN_SELECTED} - - ), + ...statusMenuItems, { - describe('getRuleIdsFromComments', () => { - it('it returns the rules ids from the comments', () => { - expect(getRuleIdsFromComments(comments)).toEqual(['alert-id-1', 'alert-id-2']); + describe('getAlertIdsFromComments', () => { + it('it returns the alert id from the comments where rule is not defined', () => { + expect(getManualAlertIdsWithNoRuleId(comments)).toEqual(['alert-id-1']); }); }); @@ -54,13 +62,13 @@ describe('Case view helpers', () => { query: { bool: { filter: { - bool: { - should: [{ match: { _id: 'alert-id-1' } }, { match: { _id: 'alert-id-2' } }], - minimum_should_match: 1, + ids: { + values: ['alert-id-1', 'alert-id-2'], }, }, }, }, + size: 10000, }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/helpers.ts b/x-pack/plugins/security_solution/public/cases/components/case_view/helpers.ts index 6b92e414675e2c..3dece29e64ac5e 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/helpers.ts +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/helpers.ts @@ -5,28 +5,37 @@ * 2.0. */ +import { isEmpty } from 'lodash'; import { CommentType } from '../../../../../case/common/api'; import { Comment } from '../../containers/types'; -export const getRuleIdsFromComments = (comments: Comment[]) => - comments.reduce((ruleIds, comment: Comment) => { - if (comment.type === CommentType.alert) { +export const getManualAlertIdsWithNoRuleId = (comments: Comment[]): string[] => { + const dedupeAlerts = comments.reduce((alertIds, comment: Comment) => { + if (comment.type === CommentType.alert && isEmpty(comment.rule.id)) { const ids = Array.isArray(comment.alertId) ? comment.alertId : [comment.alertId]; - return [...ruleIds, ...ids]; + ids.forEach((id) => alertIds.add(id)); + return alertIds; } + return alertIds; + }, new Set()); + return [...dedupeAlerts]; +}; - return ruleIds; - }, []); - -export const buildAlertsQuery = (ruleIds: string[]) => ({ - query: { - bool: { - filter: { - bool: { - should: ruleIds.map((_id) => ({ match: { _id } })), - minimum_should_match: 1, +// TODO we need to allow -> docValueFields: [{ field: "@timestamp" }], +export const buildAlertsQuery = (alertIds: string[]) => { + if (alertIds.length === 0) { + return {}; + } + return { + query: { + bool: { + filter: { + ids: { + values: alertIds, + }, }, }, }, - }, -}); + size: 10000, + }; +}; diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx index dc0ef9ad026a41..7a5f6647a8dcf6 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx @@ -629,6 +629,14 @@ describe('CaseView ', () => { loading: true, data: { hits: { hits: [] } }, })); + useGetCaseUserActionsMock.mockReturnValue({ + caseServices: {}, + caseUserActions: [], + hasDataToPush: false, + isError: false, + isLoading: true, + participants: [], + }); const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index 0eaa867077a4a7..e42431e55ee290 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -42,8 +42,6 @@ import { normalizeActionConnector, getNoneConnector, } from '../configure_cases/utils'; -import { useQueryAlerts } from '../../../detections/containers/detection_engine/alerts/use_query'; -import { buildAlertsQuery, getRuleIdsFromComments } from './helpers'; import { DetailsPanel } from '../../../timelines/components/side_panel'; import { useSourcererScope } from '../../../common/containers/sourcerer'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; @@ -55,6 +53,7 @@ import * as i18n from './translations'; interface Props { caseId: string; + subCaseId?: string; userCanCrud: boolean; } @@ -87,32 +86,8 @@ export interface CaseProps extends Props { updateCase: (newCase: Case) => void; } -interface Signal { - rule: { - id: string; - name: string; - to: string; - from: string; - }; -} - -interface SignalHit { - _id: string; - _index: string; - _source: { - '@timestamp': string; - signal: Signal; - }; -} - -export type Alert = { - _id: string; - _index: string; - '@timestamp': string; -} & Signal; - export const CaseComponent = React.memo( - ({ caseId, caseData, fetchCase, updateCase, userCanCrud }) => { + ({ caseId, caseData, fetchCase, subCaseId, updateCase, userCanCrud }) => { const dispatch = useDispatch(); const { formatUrl, search } = useFormatUrl(SecurityPageName.case); const allCasesLink = getCaseUrl(search); @@ -127,45 +102,18 @@ export const CaseComponent = React.memo( hasDataToPush, isLoading: isLoadingUserActions, participants, - } = useGetCaseUserActions(caseId, caseData.connector.id); + } = useGetCaseUserActions(caseId, caseData.connector.id, subCaseId); const { isLoading, updateKey, updateCaseProperty } = useUpdateCase({ caseId, + subCaseId, }); - const alertsQuery = useMemo(() => buildAlertsQuery(getRuleIdsFromComments(caseData.comments)), [ - caseData.comments, - ]); - /** * For the future developer: useSourcererScope is security solution dependent. * You can use useSignalIndex as an alternative. */ - const { browserFields, docValueFields, selectedPatterns } = useSourcererScope( - SourcererScopeName.detections - ); - - const { loading: isLoadingAlerts, data: alertsData } = useQueryAlerts( - alertsQuery, - selectedPatterns[0] - ); - - const alerts = useMemo( - () => - alertsData?.hits.hits.reduce>( - (acc, { _id, _index, _source }) => ({ - ...acc, - [_id]: { - _id, - _index, - '@timestamp': _source['@timestamp'], - ..._source.signal, - }, - }), - {} - ) ?? {}, - [alertsData?.hits.hits] - ); + const { browserFields, docValueFields } = useSourcererScope(SourcererScopeName.detections); // Update Fields const onUpdateField = useCallback( @@ -350,10 +298,10 @@ export const CaseComponent = React.memo( ); useEffect(() => { - if (initLoadingData && !isLoadingUserActions && !isLoadingAlerts) { + if (initLoadingData && !isLoadingUserActions) { setInitLoadingData(false); } - }, [initLoadingData, isLoadingAlerts, isLoadingUserActions]); + }, [initLoadingData, isLoadingUserActions]); const backOptions = useMemo( () => ({ @@ -435,18 +383,17 @@ export const CaseComponent = React.memo( {!initLoadingData && ( <> @@ -513,8 +460,8 @@ export const CaseComponent = React.memo( } ); -export const CaseView = React.memo(({ caseId, userCanCrud }: Props) => { - const { data, isLoading, isError, fetchCase, updateCase } = useGetCase(caseId); +export const CaseView = React.memo(({ caseId, subCaseId, userCanCrud }: Props) => { + const { data, isLoading, isError, fetchCase, updateCase } = useGetCase(caseId, subCaseId); if (isError) { return null; } @@ -531,6 +478,7 @@ export const CaseView = React.memo(({ caseId, userCanCrud }: Props) => { return ( void; @@ -28,20 +28,24 @@ const ConfirmDeleteCaseModalComp: React.FC = ({ return null; } return ( - - - {isPlural ? i18n.CONFIRM_QUESTION_PLURAL : i18n.CONFIRM_QUESTION} - - + + {isPlural ? i18n.CONFIRM_QUESTION_PLURAL : i18n.CONFIRM_QUESTION} + ); }; diff --git a/x-pack/plugins/security_solution/public/cases/components/confirm_delete_case/translations.ts b/x-pack/plugins/security_solution/public/cases/components/confirm_delete_case/translations.ts index 2c2ebc53607994..0bd37fa18281a6 100644 --- a/x-pack/plugins/security_solution/public/cases/components/confirm_delete_case/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/confirm_delete_case/translations.ts @@ -14,6 +14,11 @@ export const DELETE_TITLE = (caseTitle: string) => defaultMessage: 'Delete "{caseTitle}"', }); +export const DELETE_THIS_CASE = (caseTitle: string) => + i18n.translate('xpack.securitySolution.case.confirmDeleteCase.deleteThisCase', { + defaultMessage: 'Delete this case', + }); + export const CONFIRM_QUESTION = i18n.translate( 'xpack.securitySolution.case.confirmDeleteCase.confirmQuestion', { diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/case/alert_fields.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/case/alert_fields.tsx index 656257f2b36c43..d5c90bd09a6db2 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/case/alert_fields.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/case/alert_fields.tsx @@ -27,9 +27,7 @@ const Container = styled.div` const defaultAlertComment = { type: CommentType.generatedAlert, - alerts: '{{context.alerts}}', - index: '{{context.rule.output_index}}', - ruleId: '{{context.rule.id}}', + alerts: `[{{#context.alerts}}{"_id": "{{_id}}", "_index": "{{_index}}", "ruleId": "{{rule.id}}", "ruleName": "{{rule.name}}"}__SEPARATOR__{{/context.alerts}}]`, }; const CaseParamsFields: React.FunctionComponent> = ({ diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/case/existing_case.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/case/existing_case.tsx index 348e488a36a8d6..1c786bade97533 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/case/existing_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/case/existing_case.tsx @@ -5,10 +5,22 @@ * 2.0. */ -import React, { memo, useMemo, useCallback } from 'react'; -import { useGetCases } from '../../../containers/use_get_cases'; +import { + EuiButton, + EuiButtonIcon, + EuiCallOut, + EuiTextColor, + EuiLoadingSpinner, +} from '@elastic/eui'; +import { isEmpty } from 'lodash'; +import React, { memo, useEffect, useCallback, useState } from 'react'; +import { CaseType } from '../../../../../../case/common/api'; +import { Case } from '../../../containers/types'; +import { useDeleteCases } from '../../../containers/use_delete_cases'; +import { useGetCase } from '../../../containers/use_get_case'; +import { ConfirmDeleteCaseModal } from '../../confirm_delete_case'; import { useCreateCaseModal } from '../../use_create_case_modal'; -import { CasesDropdown, ADD_CASE_BUTTON_ID } from './cases_dropdown'; +import * as i18n from './translations'; interface ExistingCaseProps { selectedCase: string | null; @@ -16,37 +28,72 @@ interface ExistingCaseProps { } const ExistingCaseComponent: React.FC = ({ onCaseChanged, selectedCase }) => { - const { data: cases, loading: isLoadingCases, refetchCases } = useGetCases(); + const { data, isLoading, isError } = useGetCase(selectedCase ?? ''); + const [createdCase, setCreatedCase] = useState(null); - const onCaseCreated = useCallback(() => refetchCases(), [refetchCases]); + const onCaseCreated = useCallback( + (newCase: Case) => { + onCaseChanged(newCase.id); + setCreatedCase(newCase); + }, + [onCaseChanged] + ); - const { modal, openModal } = useCreateCaseModal({ onCaseCreated }); + const { modal, openModal } = useCreateCaseModal({ caseType: CaseType.collection, onCaseCreated }); - const onChange = useCallback( - (id: string) => { - if (id === ADD_CASE_BUTTON_ID) { - openModal(); - return; - } + // Delete case + const { + dispatchResetIsDeleted, + handleOnDeleteConfirm, + handleToggleModal, + isLoading: isDeleting, + isDeleted, + isDisplayConfirmDeleteModal, + } = useDeleteCases(); - onCaseChanged(id); - }, - [onCaseChanged, openModal] - ); + useEffect(() => { + if (isDeleted) { + setCreatedCase(null); + onCaseChanged(''); + dispatchResetIsDeleted(); + } + }, [isDeleted, dispatchResetIsDeleted, onCaseChanged]); - const isCasesLoading = useMemo( - () => isLoadingCases.includes('cases') || isLoadingCases.includes('caseUpdate'), - [isLoadingCases] - ); + useEffect(() => { + if (!isLoading && !isError && data != null) { + setCreatedCase(data); + onCaseChanged(data.id); + } + }, [data, isLoading, isError, onCaseChanged]); return ( <> - + {createdCase == null && isEmpty(selectedCase) && ( + + {i18n.CREATE_CASE} + + )} + {createdCase == null && isLoading && } + {createdCase != null && !isLoading && ( + <> + + + {createdCase.title}{' '} + {!isDeleting && ( + + )} + {isDeleting && } + + + + + )} {modal} ); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/case/translations.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/case/translations.ts index c9553455f687da..731e94a17d9235 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/case/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/case/translations.ts @@ -85,3 +85,17 @@ export const CASE_CONNECTOR_ADD_NEW_CASE = i18n.translate( defaultMessage: 'Add new case', } ); + +export const CREATE_CASE = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.createCaseLabel', + { + defaultMessage: 'Create case', + } +); + +export const CONNECTED_CASE = i18n.translate( + 'xpack.securitySolution.case.components.connectors.case.connectedCaseLabel', + { + defaultMessage: 'Connected case', + } +); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/flyout.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/flyout.test.tsx new file mode 100644 index 00000000000000..842fe9e00ab390 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/flyout.test.tsx @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* eslint-disable react/display-name */ +import React, { ReactNode } from 'react'; +import { mount } from 'enzyme'; + +import '../../../common/mock/match_media'; +import { CreateCaseFlyout } from './flyout'; +import { TestProviders } from '../../../common/mock'; + +jest.mock('../create/form_context', () => { + return { + FormContext: ({ + children, + onSuccess, + }: { + children: ReactNode; + onSuccess: ({ id }: { id: string }) => void; + }) => { + return ( + <> + + {children} + + ); + }, + }; +}); + +jest.mock('../create/form', () => { + return { + CreateCaseForm: () => { + return <>{'form'}; + }, + }; +}); + +jest.mock('../create/submit_button', () => { + return { + SubmitCaseButton: () => { + return <>{'Submit'}; + }, + }; +}); + +const onCloseFlyout = jest.fn(); +const onCaseCreated = jest.fn(); +const defaultProps = { + onCloseFlyout, + onCaseCreated, +}; + +describe('CreateCaseFlyout', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('renders', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj='create-case-flyout']`).exists()).toBeTruthy(); + }); + + it('Closing modal calls onCloseCaseModal', () => { + const wrapper = mount( + + + + ); + + wrapper.find('.euiFlyout__closeButton').first().simulate('click'); + expect(onCloseFlyout).toBeCalled(); + }); + + it('pass the correct props to FormContext component', () => { + const wrapper = mount( + + + + ); + + const props = wrapper.find('FormContext').props(); + expect(props).toEqual( + expect.objectContaining({ + onSuccess: onCaseCreated, + }) + ); + }); + + it('onSuccess called when creating a case', () => { + const wrapper = mount( + + + + ); + + wrapper.find(`[data-test-subj='form-context-on-success']`).first().simulate('click'); + expect(onCaseCreated).toHaveBeenCalledWith({ id: 'case-id' }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx b/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx new file mode 100644 index 00000000000000..cb3436f6ba3bcb --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/create/flyout.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import styled from 'styled-components'; +import { EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui'; + +import { FormContext } from '../create/form_context'; +import { CreateCaseForm } from '../create/form'; +import { SubmitCaseButton } from '../create/submit_button'; +import { Case } from '../../containers/types'; +import * as i18n from '../../translations'; + +export interface CreateCaseModalProps { + onCloseFlyout: () => void; + onCaseCreated: (theCase: Case) => void; +} + +const Container = styled.div` + ${({ theme }) => ` + margin-top: ${theme.eui.euiSize}; + text-align: right; + `} +`; + +const StyledFlyout = styled(EuiFlyout)` + ${({ theme }) => ` + z-index: ${theme.eui.euiZModal}; + `} +`; + +// Adding bottom padding because timeline's +// bottom bar gonna hide the submit button. +const FormWrapper = styled.div` + padding-bottom: 50px; +`; + +const CreateCaseFlyoutComponent: React.FC = ({ + onCaseCreated, + onCloseFlyout, +}) => { + return ( + + + +

    {i18n.CREATE_TITLE}

    + + + + + + + + + + + + + + ); +}; + +export const CreateCaseFlyout = memo(CreateCaseFlyoutComponent); + +CreateCaseFlyout.displayName = 'CreateCaseFlyout'; diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx index cc38e07cf49e4a..83b8870ab597d3 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx @@ -19,6 +19,7 @@ import { usePostPushToService } from '../../containers/use_post_push_to_service' import { useConnectors } from '../../containers/configure/use_connectors'; import { useCaseConfigure } from '../../containers/configure/use_configure'; import { Case } from '../../containers/types'; +import { CaseType } from '../../../../../case/common/api'; const initialCaseValue: FormProps = { description: '', @@ -30,10 +31,15 @@ const initialCaseValue: FormProps = { }; interface Props { + caseType?: CaseType; onSuccess?: (theCase: Case) => void; } -export const FormContext: React.FC = ({ children, onSuccess }) => { +export const FormContext: React.FC = ({ + caseType = CaseType.individual, + children, + onSuccess, +}) => { const { connectors } = useConnectors(); const { connector: configurationConnector } = useCaseConfigure(); const { postCase } = usePostCase(); @@ -61,6 +67,7 @@ export const FormContext: React.FC = ({ children, onSuccess }) => { const updatedCase = await postCase({ ...dataWithoutConnectorId, + type: caseType, connector: connectorToUpdate, settings: { syncAlerts }, }); @@ -77,7 +84,7 @@ export const FormContext: React.FC = ({ children, onSuccess }) => { } } }, - [connectors, postCase, onSuccess, pushCaseToExternalService] + [caseType, connectors, postCase, onSuccess, pushCaseToExternalService] ); const { form } = useForm({ diff --git a/x-pack/plugins/security_solution/public/cases/components/create/mock.ts b/x-pack/plugins/security_solution/public/cases/components/create/mock.ts index 909b49940e1899..81a7fe9cd9387b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/components/create/mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CasePostRequest } from '../../../../../case/common/api'; +import { CasePostRequest, CaseType } from '../../../../../case/common/api'; import { ConnectorTypes } from '../../../../../case/common/api/connectors'; import { choices } from '../connectors/mock'; @@ -14,6 +14,7 @@ export const sampleData: CasePostRequest = { description: 'what a great description', tags: sampleTags, title: 'what a cool title', + type: CaseType.individual, connector: { fields: null, id: 'none', diff --git a/x-pack/plugins/security_solution/public/cases/components/status/button.test.tsx b/x-pack/plugins/security_solution/public/cases/components/status/button.test.tsx index aa99543b49d2a7..ab30fe2979b9eb 100644 --- a/x-pack/plugins/security_solution/public/cases/components/status/button.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/status/button.test.tsx @@ -42,7 +42,7 @@ describe('StatusActionButton', () => { expect( wrapper.find(`[data-test-subj="case-view-status-action-button"]`).first().prop('iconType') - ).toBe('folderCheck'); + ).toBe('folderClosed'); }); it('it renders the correct button icon: status closed', () => { @@ -50,7 +50,7 @@ describe('StatusActionButton', () => { expect( wrapper.find(`[data-test-subj="case-view-status-action-button"]`).first().prop('iconType') - ).toBe('folderCheck'); + ).toBe('folderOpen'); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/status/button.tsx b/x-pack/plugins/security_solution/public/cases/components/status/button.tsx index 2ec0ccd245b1ef..4ee69766fe1287 100644 --- a/x-pack/plugins/security_solution/public/cases/components/status/button.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/status/button.tsx @@ -40,7 +40,7 @@ const StatusActionButtonComponent: React.FC = ({ return ( { wrapper.find(`[data-test-subj="form-context-on-success"]`).first().simulate('click'); - expect(postComment.mock.calls[0][0]).toBe('new-case'); - expect(postComment.mock.calls[0][1]).toEqual({ + expect(postComment.mock.calls[0][0].caseId).toBe('new-case'); + expect(postComment.mock.calls[0][0].data).toEqual({ alertId: 'test-id', index: 'test-index', + rule: { + id: null, + name: null, + }, type: 'alert', }); }); @@ -196,10 +200,14 @@ describe('AddToCaseAction', () => { wrapper.find(`[data-test-subj="all-cases-modal-button"]`).first().simulate('click'); - expect(postComment.mock.calls[0][0]).toBe('selected-case'); - expect(postComment.mock.calls[0][1]).toEqual({ + expect(postComment.mock.calls[0][0].caseId).toBe('selected-case'); + expect(postComment.mock.calls[0][0].data).toEqual({ alertId: 'test-id', index: 'test-index', + rule: { + id: null, + name: null, + }, type: 'alert', }); }); @@ -208,7 +216,7 @@ describe('AddToCaseAction', () => { usePostCommentMock.mockImplementation(() => { return { ...defaultPostComment, - postComment: jest.fn().mockImplementation((caseId, data, updateCase) => updateCase()), + postComment: jest.fn().mockImplementation(({ caseId, data, updateCase }) => updateCase()), }; }); diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx index 6067f3fd7b15aa..aa9cec2d6b5b14 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx @@ -25,10 +25,11 @@ import { APP_ID } from '../../../../common/constants'; import { useKibana } from '../../../common/lib/kibana'; import { getCaseDetailsUrl } from '../../../common/components/link_to'; import { SecurityPageName } from '../../../app/types'; -import { useCreateCaseModal } from '../use_create_case_modal'; import { useAllCasesModal } from '../use_all_cases_modal'; import { createUpdateSuccessToaster } from './helpers'; import * as i18n from './translations'; +import { useControl } from '../../../common/hooks/use_control'; +import { CreateCaseFlyout } from '../create/flyout'; interface AddToCaseActionProps { ariaLabel?: string; @@ -43,6 +44,7 @@ const AddToCaseActionComponent: React.FC = ({ }) => { const eventId = ecsRowData._id; const eventIndex = ecsRowData._index; + const rule = ecsRowData.signal?.rule; const { navigateToApp } = useKibana().services.application; const [, dispatchToaster] = useStateToaster(); @@ -61,29 +63,36 @@ const AddToCaseActionComponent: React.FC = ({ [navigateToApp] ); + const { + isControlOpen: isCreateCaseFlyoutOpen, + openControl: openCaseFlyoutOpen, + closeControl: closeCaseFlyoutOpen, + } = useControl(); + const attachAlertToCase = useCallback( (theCase: Case) => { - postComment( - theCase.id, - { + closeCaseFlyoutOpen(); + postComment({ + caseId: theCase.id, + data: { type: CommentType.alert, alertId: eventId, index: eventIndex ?? '', + rule: { + id: rule?.id != null ? rule.id[0] : null, + name: rule?.name != null ? rule.name[0] : null, + }, }, - () => + updateCase: () => dispatchToaster({ type: 'addToaster', toast: createUpdateSuccessToaster(theCase, onViewCaseClick), - }) - ); + }), + }); }, - [postComment, eventId, eventIndex, dispatchToaster, onViewCaseClick] + [closeCaseFlyoutOpen, postComment, eventId, eventIndex, rule, dispatchToaster, onViewCaseClick] ); - const { modal: createCaseModal, openModal: openCreateCaseModal } = useCreateCaseModal({ - onCaseCreated: attachAlertToCase, - }); - const onCaseClicked = useCallback( (theCase) => { /** @@ -92,13 +101,13 @@ const AddToCaseActionComponent: React.FC = ({ * We gonna open the create case modal. */ if (theCase == null) { - openCreateCaseModal(); + openCaseFlyoutOpen(); return; } attachAlertToCase(theCase); }, - [attachAlertToCase, openCreateCaseModal] + [attachAlertToCase, openCaseFlyoutOpen] ); const { modal: allCasesModal, openModal: openAllCaseModal } = useAllCasesModal({ @@ -107,8 +116,8 @@ const AddToCaseActionComponent: React.FC = ({ const addNewCaseClick = useCallback(() => { closePopover(); - openCreateCaseModal(); - }, [openCreateCaseModal, closePopover]); + openCaseFlyoutOpen(); + }, [openCaseFlyoutOpen, closePopover]); const addExistingCaseClick = useCallback(() => { closePopover(); @@ -173,7 +182,9 @@ const AddToCaseActionComponent: React.FC = ({ - {createCaseModal} + {isCreateCaseFlyoutOpen && ( + + )} {allCasesModal} ); diff --git a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx index 1dfabda8068f17..eda8ed8cdfbcd5 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_all_cases_modal/all_cases_modal.tsx @@ -6,13 +6,7 @@ */ import React, { memo } from 'react'; -import { - EuiModal, - EuiModalBody, - EuiModalHeader, - EuiModalHeaderTitle, - EuiOverlayMask, -} from '@elastic/eui'; +import { EuiModal, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui'; import { useGetUserSavedObjectPermissions } from '../../../common/lib/kibana'; import { Case } from '../../containers/types'; @@ -34,16 +28,14 @@ const AllCasesModalComponent: React.FC = ({ const userCanCrud = userPermissions?.crud ?? false; return isModalOpen ? ( - - - - {i18n.SELECT_CASE_TITLE} - - - - - - + + + {i18n.SELECT_CASE_TITLE} + + + + + ) : null; }; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx index 3595f2c916af71..2806e358fceee9 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/create_case_modal.tsx @@ -7,24 +7,20 @@ import React, { memo } from 'react'; import styled from 'styled-components'; -import { - EuiModal, - EuiModalBody, - EuiModalHeader, - EuiModalHeaderTitle, - EuiOverlayMask, -} from '@elastic/eui'; +import { EuiModal, EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui'; import { FormContext } from '../create/form_context'; import { CreateCaseForm } from '../create/form'; import { SubmitCaseButton } from '../create/submit_button'; import { Case } from '../../containers/types'; import * as i18n from '../../translations'; +import { CaseType } from '../../../../../case/common/api'; export interface CreateCaseModalProps { isModalOpen: boolean; onCloseCaseModal: () => void; onSuccess: (theCase: Case) => void; + caseType?: CaseType; } const Container = styled.div` @@ -38,23 +34,22 @@ const CreateModalComponent: React.FC = ({ isModalOpen, onCloseCaseModal, onSuccess, + caseType = CaseType.individual, }) => { return isModalOpen ? ( - - - - {i18n.CREATE_TITLE} - - - - - - - - - - - + + + {i18n.CREATE_TITLE} + + + + + + + + + + ) : null; }; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx index 86313ebcb3bfad..3dc852a19e73fc 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_create_case_modal/index.tsx @@ -6,11 +6,13 @@ */ import React, { useState, useCallback, useMemo } from 'react'; +import { CaseType } from '../../../../../case/common/api'; import { Case } from '../../containers/types'; import { CreateCaseModal } from './create_case_modal'; export interface UseCreateCaseModalProps { onCaseCreated: (theCase: Case) => void; + caseType?: CaseType; } export interface UseCreateCaseModalReturnedValues { modal: JSX.Element; @@ -19,7 +21,10 @@ export interface UseCreateCaseModalReturnedValues { openModal: () => void; } -export const useCreateCaseModal = ({ onCaseCreated }: UseCreateCaseModalProps) => { +export const useCreateCaseModal = ({ + caseType = CaseType.individual, + onCaseCreated, +}: UseCreateCaseModalProps) => { const [isModalOpen, setIsModalOpen] = useState(false); const closeModal = useCallback(() => setIsModalOpen(false), []); const openModal = useCallback(() => setIsModalOpen(true), []); @@ -35,6 +40,7 @@ export const useCreateCaseModal = ({ onCaseCreated }: UseCreateCaseModalProps) = () => ({ modal: ( void; + alertId: string; + index: string; + loadingAlertData: boolean; + ruleId: string; + ruleName: string; }): EuiCommentProps => { return { username: ( @@ -212,7 +231,15 @@ export const getAlertComment = ({ ), className: 'comment-alert', type: 'update', - event: , + event: ( + + ), 'data-test-subj': `${action.actionField[0]}-${action.action}-action-${action.actionId}`, timestamp: , timelineIcon: 'bell', @@ -222,23 +249,188 @@ export const getAlertComment = ({ - {alert != null ? ( - - ) : ( - - )} + ), }; }; + +export const toStringArray = (value: unknown): string[] => { + if (Array.isArray(value)) { + return value.reduce((acc, v) => { + if (v != null) { + switch (typeof v) { + case 'number': + case 'boolean': + return [...acc, v.toString()]; + case 'object': + try { + return [...acc, JSON.stringify(v)]; + } catch { + return [...acc, 'Invalid Object']; + } + case 'string': + return [...acc, v]; + default: + return [...acc, `${v}`]; + } + } + return acc; + }, []); + } else if (value == null) { + return []; + } else if (!Array.isArray(value) && typeof value === 'object') { + try { + return [JSON.stringify(value)]; + } catch { + return ['Invalid Object']; + } + } else { + return [`${value}`]; + } +}; + +export const formatAlertToEcsSignal = (alert: {}): Ecs => + Object.keys(alert).reduce((accumulator, key) => { + const item = get(alert, key); + if (item != null && isObject(item)) { + return { ...accumulator, [key]: formatAlertToEcsSignal(item) }; + } else if (Array.isArray(item) || isString(item) || isNumber(item)) { + return { ...accumulator, [key]: toStringArray(item) }; + } + return accumulator; + }, {} as Ecs); + +const EMPTY_ARRAY: TimelineNonEcsData[] = []; +export const getGeneratedAlertsAttachment = ({ + action, + alertIds, + ruleId, + ruleName, +}: { + action: CaseUserActions; + alertIds: string[]; + ruleId: string; + ruleName: string; +}): EuiCommentProps => { + const fetchEcsAlertsData = async (fetchAlertIds?: string[]): Promise => { + if (isEmpty(fetchAlertIds)) { + return []; + } + const alertResponse = await KibanaServices.get().http.fetch< + SearchResponse<{ '@timestamp': string; [key: string]: unknown }> + >(DETECTION_ENGINE_QUERY_SIGNALS_URL, { + method: 'POST', + body: JSON.stringify(buildAlertsQuery(fetchAlertIds ?? [])), + }); + return ( + alertResponse?.hits.hits.reduce( + (acc, { _id, _index, _source }) => [ + ...acc, + { + ...formatAlertToEcsSignal(_source as {}), + _id, + _index, + timestamp: _source['@timestamp'], + }, + ], + [] + ) ?? [] + ); + }; + return { + username: , + className: 'comment-alert', + type: 'update', + event: ( + + ), + 'data-test-subj': `${action.actionField[0]}-${action.action}-action-${action.actionId}`, + timestamp: , + timelineIcon: 'bell', + actions: ( + + + + + + + + + ), + }; +}; + +interface Signal { + rule: { + id: string; + name: string; + to: string; + from: string; + }; +} + +interface SignalHit { + _id: string; + _index: string; + _source: { + '@timestamp': string; + signal: Signal; + }; +} + +export interface Alert { + _id: string; + _index: string; + '@timestamp': string; + signal: Signal; + [key: string]: unknown; +} + +export const useFetchAlertData = (alertIds: string[]): [boolean, Record] => { + const { selectedPatterns } = useSourcererScope(SourcererScopeName.detections); + const alertsQuery = useMemo(() => buildAlertsQuery(alertIds), [alertIds]); + + const { loading: isLoadingAlerts, data: alertsData } = useQueryAlerts( + alertsQuery, + selectedPatterns[0] + ); + + const alerts = useMemo( + () => + alertsData?.hits.hits.reduce>( + (acc, { _id, _index, _source }) => ({ + ...acc, + [_id]: { + ...formatAlertToEcsSignal(_source), + _id, + _index, + timestamp: _source['@timestamp'], + }, + }), + {} + ) ?? {}, + [alertsData?.hits.hits] + ); + + return [isLoadingAlerts, alerts]; +}; diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx index 3b81fc0afccf3b..2a9f99465251b9 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx @@ -5,8 +5,6 @@ * 2.0. */ -import classNames from 'classnames'; - import { EuiFlexGroup, EuiFlexItem, @@ -14,9 +12,12 @@ import { EuiCommentList, EuiCommentProps, } from '@elastic/eui'; +import classNames from 'classnames'; +import { isEmpty } from 'lodash'; import React, { useCallback, useMemo, useRef, useState, useEffect } from 'react'; import { useParams } from 'react-router-dom'; import styled from 'styled-components'; +import { isRight } from 'fp-ts/Either'; import * as i18n from './translations'; @@ -24,23 +25,31 @@ import { Case, CaseUserActions } from '../../containers/types'; import { useUpdateComment } from '../../containers/use_update_comment'; import { useCurrentUser } from '../../../common/lib/kibana'; import { AddComment, AddCommentRefObject } from '../add_comment'; -import { ActionConnector, CommentType } from '../../../../../case/common/api'; +import { + ActionConnector, + AlertCommentRequestRt, + CommentType, + ContextTypeUserRt, +} from '../../../../../case/common/api'; import { CaseServices } from '../../containers/use_get_case_user_actions'; import { parseString } from '../../containers/utils'; -import { Alert, OnUpdateFields } from '../case_view'; +import { OnUpdateFields } from '../case_view'; import { getConnectorLabelTitle, getLabelTitle, getPushedServiceLabelTitle, getPushInfo, getUpdateAction, - getAlertComment, + getAlertAttachment, + getGeneratedAlertsAttachment, + useFetchAlertData, } from './helpers'; import { UserActionAvatar } from './user_action_avatar'; import { UserActionMarkdown } from './user_action_markdown'; import { UserActionTimestamp } from './user_action_timestamp'; import { UserActionUsername } from './user_action_username'; import { UserActionContentToolbar } from './user_action_content_toolbar'; +import { getManualAlertIdsWithNoRuleId } from '../case_view/helpers'; export interface UserActionTreeProps { caseServices: CaseServices; @@ -53,7 +62,6 @@ export interface UserActionTreeProps { onUpdateField: ({ key, value, onSuccess, onError }: OnUpdateFields) => void; updateCase: (newCase: Case) => void; userCanCrud: boolean; - alerts: Record; onShowAlertDetails: (alertId: string, index: string) => void; } @@ -112,10 +120,9 @@ export const UserActionTree = React.memo( onUpdateField, updateCase, userCanCrud, - alerts, onShowAlertDetails, }: UserActionTreeProps) => { - const { commentId } = useParams<{ commentId?: string }>(); + const { commentId, subCaseId } = useParams<{ commentId?: string; subCaseId?: string }>(); const handlerTimeoutId = useRef(0); const addCommentRef = useRef(null); const [initLoading, setInitLoading] = useState(true); @@ -124,6 +131,10 @@ export const UserActionTree = React.memo( const currentUser = useCurrentUser(); const [manageMarkdownEditIds, setManangeMardownEditIds] = useState([]); + const [loadingAlertData, manualAlertsData] = useFetchAlertData( + getManualAlertIdsWithNoRuleId(caseData.comments) + ); + const handleManageMarkdownEditId = useCallback( (id: string) => { if (!manageMarkdownEditIds.includes(id)) { @@ -218,9 +229,10 @@ export const UserActionTree = React.memo( onCommentPosted={handleUpdate} onCommentSaving={handleManageMarkdownEditId.bind(null, NEW_ID)} showLoading={false} + subCaseId={subCaseId} /> ), - [caseData.id, handleUpdate, userCanCrud, handleManageMarkdownEditId] + [caseData.id, handleUpdate, userCanCrud, handleManageMarkdownEditId, subCaseId] ); useEffect(() => { @@ -279,11 +291,16 @@ export const UserActionTree = React.memo( const userActions: EuiCommentProps[] = useMemo( () => caseUserActions.reduce( + // eslint-disable-next-line complexity (comments, action, index) => { // Comment creation if (action.commentId != null && action.action === 'create') { const comment = caseData.comments.find((c) => c.id === action.commentId); - if (comment != null && comment.type === CommentType.user) { + if ( + comment != null && + isRight(ContextTypeUserRt.decode(comment)) && + comment.type === CommentType.user + ) { return [ ...comments, { @@ -335,16 +352,65 @@ export const UserActionTree = React.memo( ), }, ]; - // TODO: need to handle CommentType.generatedAlert here to - } else if (comment != null && comment.type === CommentType.alert) { + } else if ( + comment != null && + isRight(AlertCommentRequestRt.decode(comment)) && + comment.type === CommentType.alert + ) { // TODO: clean this up const alertId = Array.isArray(comment.alertId) ? comment.alertId.length > 0 ? comment.alertId[0] : '' : comment.alertId; - const alert = alerts[alertId]; - return [...comments, getAlertComment({ action, alert, onShowAlertDetails })]; + + const alertIndex = Array.isArray(comment.index) + ? comment.index.length > 0 + ? comment.index[0] + : '' + : comment.index; + + if (isEmpty(alertId)) { + return comments; + } + + const ruleId = comment?.rule?.id ?? manualAlertsData[alertId]?.rule?.id?.[0] ?? ''; + const ruleName = + comment?.rule?.name ?? + manualAlertsData[alertId]?.rule?.name?.[0] ?? + i18n.UNKNOWN_RULE; + + return [ + ...comments, + getAlertAttachment({ + action, + alertId, + index: alertIndex, + loadingAlertData, + ruleId, + ruleName, + onShowAlertDetails, + }), + ]; + } else if (comment != null && comment.type === CommentType.generatedAlert) { + // TODO: clean this up + const alertIds = Array.isArray(comment.alertId) + ? comment.alertId + : [comment.alertId]; + + if (isEmpty(alertIds)) { + return comments; + } + + return [ + ...comments, + getGeneratedAlertsAttachment({ + action, + alertIds, + ruleId: comment.rule?.id ?? '', + ruleName: comment.rule?.name ?? i18n.UNKNOWN_RULE, + }), + ]; } } @@ -438,10 +504,11 @@ export const UserActionTree = React.memo( handleManageQuote, handleSaveComment, isLoadingIds, + loadingAlertData, + manualAlertsData, manageMarkdownEditIds, selectedOutlineCommentId, userCanCrud, - alerts, onShowAlertDetails, ] ); diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/translations.ts b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/translations.ts index ede216d562f117..46f36615b1a4ea 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/translations.ts @@ -42,6 +42,19 @@ export const ALERT_COMMENT_LABEL_TITLE = i18n.translate( } ); +export const GENERATED_ALERT_COMMENT_LABEL_TITLE = i18n.translate( + 'xpack.securitySolution.case.caseView.generatedAlertCommentLabelTitle', + { + defaultMessage: 'were added from', + } +); + +export const GENERATED_ALERT_COUNT_COMMENT_LABEL_TITLE = (totalCount: number) => + i18n.translate('xpack.securitySolution.case.caseView.generatedAlertCountCommentLabelTitle', { + values: { totalCount }, + defaultMessage: `{totalCount} {totalCount, plural, =1 {alert} other {alerts}}`, + }); + export const ALERT_RULE_DELETED_COMMENT_LABEL = i18n.translate( 'xpack.securitySolution.case.caseView.alertRuleDeletedLabelTitle', { @@ -56,9 +69,16 @@ export const SHOW_ALERT_TOOLTIP = i18n.translate( } ); -export const ALERT_NOT_FOUND_TOOLTIP = i18n.translate( - 'xpack.securitySolution.case.caseView.showAlertDeletedTooltip', +export const SEND_ALERT_TO_TIMELINE = i18n.translate( + 'xpack.securitySolution.case.caseView.sendAlertToTimelineTooltip', + { + defaultMessage: 'Investigate in timeline', + } +); + +export const UNKNOWN_RULE = i18n.translate( + 'xpack.securitySolution.case.caseView.unknownRule.label', { - defaultMessage: 'Alert not found', + defaultMessage: 'Unknown rule', } ); diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_alert_comment_event.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_alert_comment_event.test.tsx index f0ff145a269ff4..228945bacf8a49 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_alert_comment_event.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_alert_comment_event.test.tsx @@ -11,19 +11,14 @@ import { mount } from 'enzyme'; import { TestProviders } from '../../../common/mock'; import { useKibana } from '../../../common/lib/kibana'; import { AlertCommentEvent } from './user_action_alert_comment_event'; +import { CommentType } from '../../../../../case/common/api'; const props = { - alert: { - _id: 'alert-id-1', - _index: 'alert-index-1', - '@timestamp': '2021-01-07T13:58:31.487Z', - rule: { - id: 'rule-id-1', - name: 'Awesome rule', - from: '2021-01-07T13:58:31.487Z', - to: '2021-01-07T14:58:31.487Z', - }, - }, + alertId: 'alert-id-1', + ruleId: 'rule-id-1', + ruleName: 'Awesome rule', + alertsCount: 1, + commentType: CommentType.alert, }; jest.mock('../../../common/lib/kibana'); @@ -54,7 +49,8 @@ describe('UserActionAvatar ', () => { it('does NOT render the link when the alert is undefined', async () => { const wrapper = mount( - + {/* @ts-expect-error */} + ); @@ -62,13 +58,13 @@ describe('UserActionAvatar ', () => { wrapper.find(`[data-test-subj="alert-rule-link-alert-id-1"]`).first().exists() ).toBeFalsy(); - expect(wrapper.text()).toBe('added an alert'); + expect(wrapper.text()).toBe('added an alert from '); }); it('does NOT render the link when the rule is undefined', async () => { const alert = { - _id: 'alert-id-1', - _index: 'alert-index-1', + alertId: 'alert-id-1', + commentType: CommentType.alert, }; const wrapper = mount( @@ -82,7 +78,7 @@ describe('UserActionAvatar ', () => { wrapper.find(`[data-test-subj="alert-rule-link-alert-id-1"]`).first().exists() ).toBeFalsy(); - expect(wrapper.text()).toBe('added an alert'); + expect(wrapper.text()).toBe('added an alert from '); }); it('navigate to app on link click', async () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_alert_comment_event.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_alert_comment_event.tsx index 183116167f84a2..2a604b7c54d6b4 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_alert_comment_event.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_alert_comment_event.tsx @@ -6,24 +6,36 @@ */ import React, { memo, useCallback } from 'react'; -import { EuiLink } from '@elastic/eui'; +import { EuiText, EuiLoadingSpinner } from '@elastic/eui'; import { APP_ID } from '../../../../common/constants'; import { useKibana } from '../../../common/lib/kibana'; -import { getRuleDetailsUrl } from '../../../common/components/link_to'; +import { getRuleDetailsUrl, useFormatUrl } from '../../../common/components/link_to'; import { SecurityPageName } from '../../../app/types'; -import { Alert } from '../case_view'; import * as i18n from './translations'; +import { CommentType } from '../../../../../case/common/api'; +import { LinkAnchor } from '../../../common/components/links'; interface Props { - alert: Alert | undefined; + alertId: string; + commentType: CommentType; + ruleId: string; + ruleName: string; + alertsCount?: number; + loadingAlertData?: boolean; } -const AlertCommentEventComponent: React.FC = ({ alert }) => { - const ruleName = alert?.rule?.name ?? null; - const ruleId = alert?.rule?.id ?? null; +const AlertCommentEventComponent: React.FC = ({ + alertId, + loadingAlertData = false, + ruleId, + ruleName, + alertsCount, + commentType, +}) => { const { navigateToApp } = useKibana().services.application; + const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.detections); const onLinkClick = useCallback( (ev: { preventDefault: () => void }) => { @@ -35,15 +47,37 @@ const AlertCommentEventComponent: React.FC = ({ alert }) => { [ruleId, navigateToApp] ); - return ruleId != null && ruleName != null ? ( + return commentType !== CommentType.generatedAlert ? ( <> {`${i18n.ALERT_COMMENT_LABEL_TITLE} `} - - {ruleName} - + {loadingAlertData && } + {!loadingAlertData && ruleId !== '' && ( + + {ruleName} + + )} + {!loadingAlertData && ruleId === '' && {ruleName}} ) : ( - <>{i18n.ALERT_RULE_DELETED_COMMENT_LABEL} + <> + {i18n.GENERATED_ALERT_COUNT_COMMENT_LABEL_TITLE(alertsCount ?? 0)}{' '} + {i18n.GENERATED_ALERT_COMMENT_LABEL_TITLE}{' '} + {loadingAlertData && } + {!loadingAlertData && ruleId !== '' && ( + + {ruleName} + + )} + {!loadingAlertData && ruleId === '' && {ruleName}} + ); }; diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_copy_link.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_copy_link.tsx index 4bd4734e30671e..ff4e151197464c 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_copy_link.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_copy_link.tsx @@ -18,24 +18,26 @@ interface UserActionCopyLinkProps { id: string; } -const UserActionCopyLinkComponent = ({ id }: UserActionCopyLinkProps) => { - const { detailName: caseId } = useParams<{ detailName: string }>(); +const UserActionCopyLinkComponent = ({ id: commentId }: UserActionCopyLinkProps) => { + const { detailName: caseId, subCaseId } = useParams<{ detailName: string; subCaseId?: string }>(); const { formatUrl } = useFormatUrl(SecurityPageName.case); const handleAnchorLink = useCallback(() => { copy( - formatUrl(getCaseDetailsUrlWithCommentId({ id: caseId, commentId: id }), { absolute: true }) + formatUrl(getCaseDetailsUrlWithCommentId({ id: caseId, commentId, subCaseId }), { + absolute: true, + }) ); - }, [caseId, formatUrl, id]); + }, [caseId, commentId, formatUrl, subCaseId]); return ( {i18n.COPY_REFERENCE_LINK}

    }>
    ); diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_show_alert.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_show_alert.test.tsx index 5d619a39d0e79a..789a6eb68e0fc7 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_show_alert.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_show_alert.test.tsx @@ -8,20 +8,24 @@ import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; import { UserActionShowAlert } from './user_action_show_alert'; +import { RuleEcs } from '../../../../common/ecs/rule'; const props = { id: 'action-id', + alertId: 'alert-id', + index: 'alert-index', alert: { _id: 'alert-id', _index: 'alert-index', - '@timestamp': '2021-01-07T13:58:31.487Z', + timestamp: '2021-01-07T13:58:31.487Z', rule: { - id: 'rule-id', - name: 'Awesome Rule', - from: '2021-01-07T13:58:31.487Z', - to: '2021-01-07T14:58:31.487Z', - }, + id: ['rule-id'], + name: ['Awesome Rule'], + from: ['2021-01-07T13:58:31.487Z'], + to: ['2021-01-07T14:58:31.487Z'], + } as RuleEcs, }, + onShowAlertDetails: jest.fn(), }; describe('UserActionShowAlert ', () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_show_alert.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_show_alert.tsx index ea4994d1c80983..4f5ce00806417b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_show_alert.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_show_alert.tsx @@ -7,25 +7,24 @@ import React, { memo, useCallback } from 'react'; import { EuiToolTip, EuiButtonIcon } from '@elastic/eui'; -import deepEqual from 'fast-deep-equal'; - -import { Alert } from '../case_view'; import * as i18n from './translations'; interface UserActionShowAlertProps { id: string; - alert: Alert; + alertId: string; + index: string; onShowAlertDetails: (alertId: string, index: string) => void; } const UserActionShowAlertComponent = ({ id, - alert, + alertId, + index, onShowAlertDetails, }: UserActionShowAlertProps) => { - const onClick = useCallback(() => onShowAlertDetails(alert._id, alert._index), [ - alert._id, - alert._index, + const onClick = useCallback(() => onShowAlertDetails(alertId, index), [ + alertId, + index, onShowAlertDetails, ]); return ( @@ -41,10 +40,4 @@ const UserActionShowAlertComponent = ({ ); }; -export const UserActionShowAlert = memo( - UserActionShowAlertComponent, - (prevProps, nextProps) => - prevProps.id === nextProps.id && - deepEqual(prevProps.alert, nextProps.alert) && - prevProps.onShowAlertDetails === nextProps.onShowAlertDetails -); +export const UserActionShowAlert = memo(UserActionShowAlertComponent); diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.ts b/x-pack/plugins/security_solution/public/cases/containers/api.ts index 00a45aadd2ae08..c87e210b42bc05 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/api.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/api.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { assign } from 'lodash'; + import { CasePatchRequest, CasePostRequest, @@ -16,6 +18,9 @@ import { CaseUserActionsResponse, CommentRequest, CommentType, + SubCasePatchRequest, + SubCaseResponse, + SubCasesResponse, User, } from '../../../../case/common/api'; @@ -25,6 +30,8 @@ import { CASE_STATUS_URL, CASE_TAGS_URL, CASES_URL, + SUB_CASE_DETAILS_URL, + SUB_CASES_PATCH_DEL_URL, } from '../../../../case/common/constants'; import { @@ -32,6 +39,8 @@ import { getCasePushUrl, getCaseDetailsUrl, getCaseUserActionUrl, + getSubCaseDetailsUrl, + getSubCaseUserActionUrl, } from '../../../../case/common/api/helpers'; import { KibanaServices } from '../../common/lib/kibana'; @@ -73,6 +82,34 @@ export const getCase = async ( return convertToCamelCase(decodeCaseResponse(response)); }; +export const getSubCase = async ( + caseId: string, + subCaseId: string, + includeComments: boolean = true, + signal: AbortSignal +): Promise => { + const [caseResponse, subCaseResponse] = await Promise.all([ + KibanaServices.get().http.fetch(getCaseDetailsUrl(caseId), { + method: 'GET', + query: { + includeComments: false, + }, + signal, + }), + KibanaServices.get().http.fetch(getSubCaseDetailsUrl(caseId, subCaseId), { + method: 'GET', + query: { + includeComments, + }, + signal, + }), + ]); + const response = assign(caseResponse, subCaseResponse); + const subCaseIndex = response.subCaseIds?.findIndex((scId) => scId === response.id) ?? -1; + response.title = `${response.title}${subCaseIndex >= 0 ? ` ${subCaseIndex + 1}` : ''}`; + return convertToCamelCase(decodeCaseResponse(response)); +}; + export const getCasesStatus = async (signal: AbortSignal): Promise => { const response = await KibanaServices.get().http.fetch(CASE_STATUS_URL, { method: 'GET', @@ -111,6 +148,21 @@ export const getCaseUserActions = async ( return convertArrayToCamelCase(decodeCaseUserActionsResponse(response)) as CaseUserActions[]; }; +export const getSubCaseUserActions = async ( + caseId: string, + subCaseId: string, + signal: AbortSignal +): Promise => { + const response = await KibanaServices.get().http.fetch( + getSubCaseUserActionUrl(caseId, subCaseId), + { + method: 'GET', + signal, + } + ); + return convertArrayToCamelCase(decodeCaseUserActionsResponse(response)) as CaseUserActions[]; +}; + export const getCases = async ({ filterOptions = { search: '', @@ -167,6 +219,35 @@ export const patchCase = async ( return convertToCamelCase(decodeCasesResponse(response)); }; +export const patchSubCase = async ( + caseId: string, + subCaseId: string, + updatedSubCase: Pick, + version: string, + signal: AbortSignal +): Promise => { + const subCaseResponse = await KibanaServices.get().http.fetch( + SUB_CASE_DETAILS_URL, + { + method: 'PATCH', + body: JSON.stringify({ cases: [{ ...updatedSubCase, id: caseId, version }] }), + signal, + } + ); + const caseResponse = await KibanaServices.get().http.fetch( + getCaseDetailsUrl(caseId), + { + method: 'GET', + query: { + includeComments: false, + }, + signal, + } + ); + const response = subCaseResponse.map((subCaseResp) => assign(caseResponse, subCaseResp)); + return convertToCamelCase(decodeCasesResponse(response)); +}; + export const patchCasesStatus = async ( cases: BulkUpdateStatus[], signal: AbortSignal @@ -182,13 +263,15 @@ export const patchCasesStatus = async ( export const postComment = async ( newComment: CommentRequest, caseId: string, - signal: AbortSignal + signal: AbortSignal, + subCaseId?: string ): Promise => { const response = await KibanaServices.get().http.fetch( `${CASES_URL}/${caseId}/comments`, { method: 'POST', body: JSON.stringify(newComment), + ...(subCaseId ? { query: { subCaseId } } : {}), signal, } ); @@ -200,7 +283,8 @@ export const patchComment = async ( commentId: string, commentUpdate: string, version: string, - signal: AbortSignal + signal: AbortSignal, + subCaseId?: string ): Promise => { const response = await KibanaServices.get().http.fetch(getCaseCommentsUrl(caseId), { method: 'PATCH', @@ -210,6 +294,7 @@ export const patchComment = async ( id: commentId, version, }), + ...(subCaseId ? { query: { subCaseId } } : {}), signal, }); return convertToCamelCase(decodeCaseResponse(response)); @@ -224,6 +309,15 @@ export const deleteCases = async (caseIds: string[], signal: AbortSignal): Promi return response; }; +export const deleteSubCases = async (caseIds: string[], signal: AbortSignal): Promise => { + const response = await KibanaServices.get().http.fetch(SUB_CASES_PATCH_DEL_URL, { + method: 'DELETE', + query: { ids: JSON.stringify(caseIds) }, + signal, + }); + return response; +}; + export const pushCase = async ( caseId: string, connectorId: string, diff --git a/x-pack/plugins/security_solution/public/cases/containers/mock.ts b/x-pack/plugins/security_solution/public/cases/containers/mock.ts index 80d4816bedd53d..d8692da986cbef 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/mock.ts @@ -26,6 +26,7 @@ import { ConnectorTypes } from '../../../../case/common/api/connectors'; export { connectorsMock } from './configure/mock'; export const basicCaseId = 'basic-case-id'; +export const basicSubCaseId = 'basic-sub-case-id'; const basicCommentId = 'basic-comment-id'; const basicCreatedAt = '2020-02-19T23:06:33.798Z'; const basicUpdatedAt = '2020-02-20T15:02:57.995Z'; @@ -63,6 +64,10 @@ export const alertComment: Comment = { createdBy: elasticUser, pushedAt: null, pushedBy: null, + rule: { + id: 'rule-id-1', + name: 'Awesome rule', + }, updatedAt: null, updatedBy: null, version: 'WzQ3LDFc', @@ -95,6 +100,7 @@ export const basicCase: Case = { settings: { syncAlerts: true, }, + subCaseIds: [], }; export const basicCasePost: Case = { @@ -217,7 +223,7 @@ export const basicCaseSnake: CaseResponse = { external_service: null, updated_at: basicUpdatedAt, updated_by: elasticUserSnake, -}; +} as CaseResponse; export const casesStatusSnake: CasesStatusResponse = { count_closed_cases: 130, diff --git a/x-pack/plugins/security_solution/public/cases/containers/translations.ts b/x-pack/plugins/security_solution/public/cases/containers/translations.ts index 75939b46b1f771..c79b897ba43a14 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/translations.ts @@ -53,7 +53,20 @@ export const REOPENED_CASES = ({ }) => i18n.translate('xpack.securitySolution.containers.case.reopenedCases', { values: { caseTitle, totalCases }, - defaultMessage: 'Reopened {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', + defaultMessage: 'Opened {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', + }); + +export const MARK_IN_PROGRESS_CASES = ({ + totalCases, + caseTitle, +}: { + totalCases: number; + caseTitle?: string; +}) => + i18n.translate('xpack.securitySolution.containers.case.markInProgressCases', { + values: { caseTitle, totalCases }, + defaultMessage: + 'Marked {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}} as in progress', }); export const SUCCESS_SEND_TO_EXTERNAL_SERVICE = (serviceName: string) => diff --git a/x-pack/plugins/security_solution/public/cases/containers/types.ts b/x-pack/plugins/security_solution/public/cases/containers/types.ts index 30ea8344434686..d2931a790bd798 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/types.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/types.ts @@ -18,7 +18,9 @@ import { AssociationType, } from '../../../../case/common/api'; -export { CaseConnector, ActionConnector } from '../../../../case/common/api'; +export { CaseConnector, ActionConnector, CaseStatuses } from '../../../../case/common/api'; + +export type AllCaseType = AssociationType & CaseType; export type Comment = CommentRequest & { associationType: AssociationType; @@ -52,26 +54,37 @@ export interface CaseExternalService { externalTitle: string; externalUrl: string; } -export interface Case { + +interface BasicCase { id: string; closedAt: string | null; closedBy: ElasticUser | null; comments: Comment[]; - connector: CaseConnector; createdAt: string; createdBy: ElasticUser; - description: string; - externalService: CaseExternalService | null; status: CaseStatuses; - tags: string[]; title: string; totalAlerts: number; totalComment: number; - type: CaseType; updatedAt: string | null; updatedBy: ElasticUser | null; version: string; +} + +export interface SubCase extends BasicCase { + associationType: AssociationType; + caseParentId: string; +} + +export interface Case extends BasicCase { + connector: CaseConnector; + description: string; + externalService: CaseExternalService | null; + subCases?: SubCase[] | null; + subCaseIds: string[]; settings: CaseAttributes['settings']; + tags: string[]; + type: CaseType; } export interface QueryParams { @@ -138,6 +151,7 @@ export interface ActionLicense { export interface DeleteCase { id: string; title?: string; + type?: CaseType; } export interface FieldMappings { @@ -153,7 +167,7 @@ export type UpdateKey = keyof Pick< export interface UpdateByKey { updateKey: UpdateKey; updateValue: CasePatchRequest[UpdateKey]; - fetchCaseUserActions?: (caseId: string) => void; + fetchCaseUserActions?: (caseId: string, subCaseId?: string) => void; updateCase?: (newCase: Case) => void; caseData: Case; onSuccess?: () => void; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.tsx index 5fd181a4bbd414..0fe45aaab799b9 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_bulk_update_case.tsx @@ -62,6 +62,24 @@ export interface UseUpdateCases extends UpdateState { dispatchResetIsUpdated: () => void; } +const getStatusToasterMessage = ( + status: CaseStatuses, + messageArgs: { + totalCases: number; + caseTitle?: string; + } +): string => { + if (status === CaseStatuses.open) { + return i18n.REOPENED_CASES(messageArgs); + } else if (status === CaseStatuses['in-progress']) { + return i18n.MARK_IN_PROGRESS_CASES(messageArgs); + } else if (status === CaseStatuses.closed) { + return i18n.CLOSED_CASES(messageArgs); + } + + return ''; +}; + export const useUpdateCases = (): UseUpdateCases => { const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, @@ -70,7 +88,7 @@ export const useUpdateCases = (): UseUpdateCases => { }); const [, dispatchToaster] = useStateToaster(); - const dispatchUpdateCases = useCallback((cases: BulkUpdateStatus[]) => { + const dispatchUpdateCases = useCallback((cases: BulkUpdateStatus[], action: string) => { let cancel = false; const abortCtrl = new AbortController(); @@ -83,14 +101,16 @@ export const useUpdateCases = (): UseUpdateCases => { const firstTitle = patchResponse[0].title; dispatch({ type: 'FETCH_SUCCESS', payload: true }); + const messageArgs = { totalCases: resultCount, caseTitle: resultCount === 1 ? firstTitle : '', }; + const message = - resultCount && patchResponse[0].status === CaseStatuses.open - ? i18n.REOPENED_CASES(messageArgs) - : i18n.CLOSED_CASES(messageArgs); + action === 'status' + ? getStatusToasterMessage(patchResponse[0].status, messageArgs) + : ''; displaySuccessToast(message, dispatchToaster); } @@ -123,7 +143,7 @@ export const useUpdateCases = (): UseUpdateCases => { id: theCase.id, version: theCase.version, })); - dispatchUpdateCases(updateCasesStatus); + dispatchUpdateCases(updateCasesStatus, 'status'); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return { ...state, updateBulkStatus, dispatchResetIsUpdated }; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.tsx index b777b16b1c0c1c..923c20dcf8ebd5 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_delete_cases.tsx @@ -12,7 +12,7 @@ import { useStateToaster, } from '../../common/components/toasters'; import * as i18n from './translations'; -import { deleteCases } from './api'; +import { deleteCases, deleteSubCases } from './api'; import { DeleteCase } from './types'; interface DeleteState { @@ -87,7 +87,13 @@ export const useDeleteCases = (): UseDeleteCase => { try { dispatch({ type: 'FETCH_INIT' }); const caseIds = cases.map((theCase) => theCase.id); - await deleteCases(caseIds, abortCtrl.signal); + // We don't allow user batch delete sub cases on UI at the moment. + if (cases[0].type != null || cases.length > 1) { + await deleteCases(caseIds, abortCtrl.signal); + } else { + await deleteSubCases(caseIds, abortCtrl.signal); + } + if (!cancel) { dispatch({ type: 'FETCH_SUCCESS', payload: true }); displaySuccessToast( diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx index 45827a4bebff80..1c4476e3cb2b7c 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_case.tsx @@ -5,13 +5,14 @@ * 2.0. */ -import { useEffect, useReducer, useCallback } from 'react'; +import { isEmpty } from 'lodash'; +import { useEffect, useReducer, useCallback, useRef } from 'react'; import { CaseStatuses, CaseType } from '../../../../case/common/api'; import { Case } from './types'; import * as i18n from './translations'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; -import { getCase } from './api'; +import { getCase, getSubCase } from './api'; import { getNoneConnector } from '../components/configure_cases/utils'; interface CaseState { @@ -77,6 +78,7 @@ export const initialData: Case = { updatedAt: null, updatedBy: null, version: '', + subCaseIds: [], settings: { syncAlerts: true, }, @@ -87,31 +89,32 @@ export interface UseGetCase extends CaseState { updateCase: (newCase: Case) => void; } -export const useGetCase = (caseId: string): UseGetCase => { +export const useGetCase = (caseId: string, subCaseId?: string): UseGetCase => { const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: true, isError: false, data: initialData, }); const [, dispatchToaster] = useStateToaster(); + const abortCtrl = useRef(new AbortController()); + const didCancel = useRef(false); const updateCase = useCallback((newCase: Case) => { dispatch({ type: 'UPDATE_CASE', payload: newCase }); }, []); const callFetch = useCallback(async () => { - let didCancel = false; - const abortCtrl = new AbortController(); - const fetchData = async () => { dispatch({ type: 'FETCH_INIT' }); try { - const response = await getCase(caseId, true, abortCtrl.signal); - if (!didCancel) { + const response = await (subCaseId + ? getSubCase(caseId, subCaseId, true, abortCtrl.current.signal) + : getCase(caseId, true, abortCtrl.current.signal)); + if (!didCancel.current) { dispatch({ type: 'FETCH_SUCCESS', payload: response }); } } catch (error) { - if (!didCancel) { + if (!didCancel.current) { errorToToaster({ title: i18n.ERROR_TITLE, error: error.body && error.body.message ? new Error(error.body.message) : error, @@ -121,17 +124,22 @@ export const useGetCase = (caseId: string): UseGetCase => { } } }; + didCancel.current = false; + abortCtrl.current.abort(); + abortCtrl.current = new AbortController(); fetchData(); - return () => { - didCancel = true; - abortCtrl.abort(); - }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [caseId]); + }, [caseId, subCaseId]); useEffect(() => { - callFetch(); + if (!isEmpty(caseId)) { + callFetch(); + } + return () => { + didCancel.current = true; + abortCtrl.current.abort(); + }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [caseId]); + }, [caseId, subCaseId]); return { ...state, fetchCase: callFetch, updateCase }; }; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.tsx index 8ebd46e64296f6..12e5f6643351ff 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_get_case_user_actions.tsx @@ -6,12 +6,12 @@ */ import { isEmpty, uniqBy } from 'lodash/fp'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import deepEqual from 'fast-deep-equal'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import { CaseFullExternalService } from '../../../../case/common/api/cases'; -import { getCaseUserActions } from './api'; +import { getCaseUserActions, getSubCaseUserActions } from './api'; import * as i18n from './translations'; import { CaseConnector, CaseExternalService, CaseUserActions, ElasticUser } from './types'; import { convertToCamelCase, parseString } from './utils'; @@ -46,7 +46,7 @@ export const initialData: CaseUserActionsState = { }; export interface UseGetCaseUserActions extends CaseUserActionsState { - fetchCaseUserActions: (caseId: string) => void; + fetchCaseUserActions: (caseId: string, subCaseId?: string) => void; } const getExternalService = (value: string): CaseExternalService | null => @@ -238,26 +238,29 @@ export const getPushedInfo = ( export const useGetCaseUserActions = ( caseId: string, - caseConnectorId: string + caseConnectorId: string, + subCaseId?: string ): UseGetCaseUserActions => { const [caseUserActionsState, setCaseUserActionsState] = useState( initialData ); - + const abortCtrl = useRef(new AbortController()); + const didCancel = useRef(false); const [, dispatchToaster] = useStateToaster(); const fetchCaseUserActions = useCallback( - (thisCaseId: string) => { - let didCancel = false; - const abortCtrl = new AbortController(); + (thisCaseId: string, thisSubCaseId?: string) => { const fetchData = async () => { - setCaseUserActionsState({ - ...caseUserActionsState, - isLoading: true, - }); try { - const response = await getCaseUserActions(thisCaseId, abortCtrl.signal); - if (!didCancel) { + setCaseUserActionsState({ + ...caseUserActionsState, + isLoading: true, + }); + + const response = await (thisSubCaseId + ? getSubCaseUserActions(thisCaseId, thisSubCaseId, abortCtrl.current.signal) + : getCaseUserActions(thisCaseId, abortCtrl.current.signal)); + if (!didCancel.current) { // Attention Future developer // We are removing the first item because it will always be the creation of the case // and we do not want it to simplify our life @@ -265,7 +268,11 @@ export const useGetCaseUserActions = ( ? uniqBy('actionBy.username', response).map((cau) => cau.actionBy) : []; - const caseUserActions = !isEmpty(response) ? response.slice(1) : []; + const caseUserActions = !isEmpty(response) + ? thisSubCaseId + ? response + : response.slice(1) + : []; setCaseUserActionsState({ caseUserActions, ...getPushedInfo(caseUserActions, caseConnectorId), @@ -275,7 +282,7 @@ export const useGetCaseUserActions = ( }); } } catch (error) { - if (!didCancel) { + if (!didCancel.current) { errorToToaster({ title: i18n.ERROR_TITLE, error: error.body && error.body.message ? new Error(error.body.message) : error, @@ -292,21 +299,24 @@ export const useGetCaseUserActions = ( } } }; + abortCtrl.current.abort(); + abortCtrl.current = new AbortController(); fetchData(); - return () => { - didCancel = true; - abortCtrl.abort(); - }; }, // eslint-disable-next-line react-hooks/exhaustive-deps - [caseUserActionsState, caseConnectorId] + [caseConnectorId] ); useEffect(() => { if (!isEmpty(caseId)) { - fetchCaseUserActions(caseId); + fetchCaseUserActions(caseId, subCaseId); } + + return () => { + didCancel.current = true; + abortCtrl.current.abort(); + }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [caseId, caseConnectorId]); + }, [caseId, subCaseId]); return { ...caseUserActionsState, fetchCaseUserActions }; }; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_case.tsx index 4ce6948ed8791c..c4fa0304735341 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_case.tsx @@ -6,6 +6,7 @@ */ import { useReducer, useCallback, useRef, useEffect } from 'react'; + import { CasePostRequest } from '../../../../case/common/api'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import { postCase } from './api'; @@ -16,6 +17,7 @@ interface NewCaseState { isError: boolean; } type Action = { type: 'FETCH_INIT' } | { type: 'FETCH_SUCCESS' } | { type: 'FETCH_FAILURE' }; + const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => { switch (action.type) { case 'FETCH_INIT': @@ -76,6 +78,7 @@ export const usePostCase = (): UsePostCase => { }, [dispatchToaster] ); + useEffect(() => { return () => { abortCtrl.current.abort(); diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx index f9d4454f63ffb6..42cd0deafa048f 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.test.tsx @@ -9,7 +9,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { CommentType } from '../../../../case/common/api'; import { usePostComment, UsePostComment } from './use_post_comment'; -import { basicCaseId } from './mock'; +import { basicCaseId, basicSubCaseId } from './mock'; import * as api from './api'; jest.mock('./api'); @@ -40,7 +40,7 @@ describe('usePostComment', () => { }); }); - it('calls postComment with correct arguments', async () => { + it('calls postComment with correct arguments - case', async () => { const spyOnPostCase = jest.spyOn(api, 'postComment'); await act(async () => { @@ -49,9 +49,38 @@ describe('usePostComment', () => { ); await waitForNextUpdate(); - result.current.postComment(basicCaseId, samplePost, updateCaseCallback); + result.current.postComment({ + caseId: basicCaseId, + data: samplePost, + updateCase: updateCaseCallback, + }); + await waitForNextUpdate(); + expect(spyOnPostCase).toBeCalledWith(samplePost, basicCaseId, abortCtrl.signal, undefined); + }); + }); + + it('calls postComment with correct arguments - sub case', async () => { + const spyOnPostCase = jest.spyOn(api, 'postComment'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + usePostComment() + ); + await waitForNextUpdate(); + + result.current.postComment({ + caseId: basicCaseId, + data: samplePost, + updateCase: updateCaseCallback, + subCaseId: basicSubCaseId, + }); await waitForNextUpdate(); - expect(spyOnPostCase).toBeCalledWith(samplePost, basicCaseId, abortCtrl.signal); + expect(spyOnPostCase).toBeCalledWith( + samplePost, + basicCaseId, + abortCtrl.signal, + basicSubCaseId + ); }); }); @@ -61,7 +90,11 @@ describe('usePostComment', () => { usePostComment() ); await waitForNextUpdate(); - result.current.postComment(basicCaseId, samplePost, updateCaseCallback); + result.current.postComment({ + caseId: basicCaseId, + data: samplePost, + updateCase: updateCaseCallback, + }); await waitForNextUpdate(); expect(result.current).toEqual({ isLoading: false, @@ -77,7 +110,11 @@ describe('usePostComment', () => { usePostComment() ); await waitForNextUpdate(); - result.current.postComment(basicCaseId, samplePost, updateCaseCallback); + result.current.postComment({ + caseId: basicCaseId, + data: samplePost, + updateCase: updateCaseCallback, + }); expect(result.current.isLoading).toBe(true); }); @@ -94,7 +131,11 @@ describe('usePostComment', () => { usePostComment() ); await waitForNextUpdate(); - result.current.postComment(basicCaseId, samplePost, updateCaseCallback); + result.current.postComment({ + caseId: basicCaseId, + data: samplePost, + updateCase: updateCaseCallback, + }); expect(result.current).toEqual({ isLoading: false, diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx index f2bd9d3f41f3ce..8fc8053c14f70d 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_comment.tsx @@ -42,8 +42,14 @@ const dataFetchReducer = (state: NewCommentState, action: Action): NewCommentSta } }; +interface PostComment { + caseId: string; + data: CommentRequest; + updateCase?: (newCase: Case) => void; + subCaseId?: string; +} export interface UsePostComment extends NewCommentState { - postComment: (caseId: string, data: CommentRequest, updateCase?: (newCase: Case) => void) => void; + postComment: (args: PostComment) => void; } export const usePostComment = (): UsePostComment => { @@ -54,13 +60,13 @@ export const usePostComment = (): UsePostComment => { const [, dispatchToaster] = useStateToaster(); const postMyComment = useCallback( - async (caseId: string, data: CommentRequest, updateCase?: (newCase: Case) => void) => { + async ({ caseId, data, updateCase, subCaseId }: PostComment) => { let cancel = false; const abortCtrl = new AbortController(); try { dispatch({ type: 'FETCH_INIT' }); - const response = await postComment(data, caseId, abortCtrl.signal); + const response = await postComment(data, caseId, abortCtrl.signal, subCaseId); if (!cancel) { dispatch({ type: 'FETCH_SUCCESS' }); if (updateCase) { diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.test.tsx index 62560244fe9c80..0adf2cc0bf92ab 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.test.tsx @@ -7,7 +7,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useUpdateCase, UseUpdateCase } from './use_update_case'; -import { basicCase } from './mock'; +import { basicCase, basicSubCaseId } from './mock'; import * as api from './api'; import { UpdateKey } from './types'; @@ -84,7 +84,27 @@ describe('useUpdateCase', () => { isError: false, updateCaseProperty: result.current.updateCaseProperty, }); - expect(fetchCaseUserActions).toBeCalledWith(basicCase.id); + expect(fetchCaseUserActions).toBeCalledWith(basicCase.id, undefined); + expect(updateCase).toBeCalledWith(basicCase); + expect(onSuccess).toHaveBeenCalled(); + }); + }); + + it('patch sub case', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useUpdateCase({ caseId: basicCase.id, subCaseId: basicSubCaseId }) + ); + await waitForNextUpdate(); + result.current.updateCaseProperty(sampleUpdate); + await waitForNextUpdate(); + expect(result.current).toEqual({ + updateKey: null, + isLoading: false, + isError: false, + updateCaseProperty: result.current.updateCaseProperty, + }); + expect(fetchCaseUserActions).toBeCalledWith(basicCase.id, basicSubCaseId); expect(updateCase).toBeCalledWith(basicCase); expect(onSuccess).toHaveBeenCalled(); }); diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx index b2b919ae1422b0..23a23caeb71bdf 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_update_case.tsx @@ -5,12 +5,12 @@ * 2.0. */ -import { useReducer, useCallback } from 'react'; +import { useReducer, useCallback, useEffect, useRef } from 'react'; import { errorToToaster, useStateToaster } from '../../common/components/toasters'; -import { patchCase } from './api'; -import { UpdateKey, UpdateByKey } from './types'; +import { patchCase, patchSubCase } from './api'; +import { UpdateKey, UpdateByKey, CaseStatuses } from './types'; import * as i18n from './translations'; import { createUpdateSuccessToaster } from './utils'; @@ -57,13 +57,21 @@ const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => export interface UseUpdateCase extends NewCaseState { updateCaseProperty: (updates: UpdateByKey) => void; } -export const useUpdateCase = ({ caseId }: { caseId: string }): UseUpdateCase => { +export const useUpdateCase = ({ + caseId, + subCaseId, +}: { + caseId: string; + subCaseId?: string; +}): UseUpdateCase => { const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, updateKey: null, }); const [, dispatchToaster] = useStateToaster(); + const abortCtrl = useRef(new AbortController()); + const didCancel = useRef(false); const dispatchUpdateCaseProperty = useCallback( async ({ @@ -75,20 +83,27 @@ export const useUpdateCase = ({ caseId }: { caseId: string }): UseUpdateCase => onSuccess, onError, }: UpdateByKey) => { - let cancel = false; - const abortCtrl = new AbortController(); - try { + didCancel.current = false; + abortCtrl.current = new AbortController(); dispatch({ type: 'FETCH_INIT', payload: updateKey }); - const response = await patchCase( - caseId, - { [updateKey]: updateValue }, - caseData.version, - abortCtrl.signal - ); - if (!cancel) { + const response = await (updateKey === 'status' && subCaseId + ? patchSubCase( + caseId, + subCaseId, + { status: updateValue as CaseStatuses }, + caseData.version, + abortCtrl.current.signal + ) + : patchCase( + caseId, + { [updateKey]: updateValue }, + caseData.version, + abortCtrl.current.signal + )); + if (!didCancel.current) { if (fetchCaseUserActions != null) { - fetchCaseUserActions(caseId); + fetchCaseUserActions(caseId, subCaseId); } if (updateCase != null) { updateCase(response[0]); @@ -104,26 +119,31 @@ export const useUpdateCase = ({ caseId }: { caseId: string }): UseUpdateCase => } } } catch (error) { - if (!cancel) { - errorToToaster({ - title: i18n.ERROR_TITLE, - error: error.body && error.body.message ? new Error(error.body.message) : error, - dispatchToaster, - }); + if (!didCancel.current) { + if (error.name !== 'AbortError') { + errorToToaster({ + title: i18n.ERROR_TITLE, + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + } dispatch({ type: 'FETCH_FAILURE' }); if (onError) { onError(); } } } - return () => { - cancel = true; - abortCtrl.abort(); - }; }, // eslint-disable-next-line react-hooks/exhaustive-deps - [] + [caseId, subCaseId] ); + useEffect(() => { + return () => { + didCancel.current = true; + abortCtrl.current.abort(); + }; + }, []); + return { ...state, updateCaseProperty: dispatchUpdateCaseProperty }; }; diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.test.tsx index d7d98879459fe4..9ff266ad9c9881 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.test.tsx @@ -7,7 +7,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { useUpdateComment, UseUpdateComment } from './use_update_comment'; -import { basicCase, basicCaseCommentPatch } from './mock'; +import { basicCase, basicCaseCommentPatch, basicSubCaseId } from './mock'; import * as api from './api'; jest.mock('./api'); @@ -43,7 +43,7 @@ describe('useUpdateComment', () => { }); }); - it('calls patchComment with correct arguments', async () => { + it('calls patchComment with correct arguments - case', async () => { const spyOnPatchComment = jest.spyOn(api, 'patchComment'); await act(async () => { @@ -59,7 +59,30 @@ describe('useUpdateComment', () => { basicCase.comments[0].id, 'updated comment', basicCase.comments[0].version, - abortCtrl.signal + abortCtrl.signal, + undefined + ); + }); + }); + + it('calls patchComment with correct arguments - sub case', async () => { + const spyOnPatchComment = jest.spyOn(api, 'patchComment'); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useUpdateComment() + ); + await waitForNextUpdate(); + + result.current.patchComment({ ...sampleUpdate, subCaseId: basicSubCaseId }); + await waitForNextUpdate(); + expect(spyOnPatchComment).toBeCalledWith( + basicCase.id, + basicCase.comments[0].id, + 'updated comment', + basicCase.comments[0].version, + abortCtrl.signal, + basicSubCaseId ); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.tsx index 6222d993bb798b..e36b21823310eb 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_update_comment.tsx @@ -57,6 +57,7 @@ interface UpdateComment { commentId: string; commentUpdate: string; fetchUserActions: () => void; + subCaseId?: string; updateCase: (newCase: Case) => void; version: string; } @@ -78,6 +79,7 @@ export const useUpdateComment = (): UseUpdateComment => { commentId, commentUpdate, fetchUserActions, + subCaseId, updateCase, version, }: UpdateComment) => { @@ -90,7 +92,8 @@ export const useUpdateComment = (): UseUpdateComment => { commentId, commentUpdate, version, - abortCtrl.signal + abortCtrl.signal, + subCaseId ); if (!cancel) { updateCase(response); diff --git a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx index 701ecdf8580f00..edb84db89b878a 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/case_details.tsx @@ -21,7 +21,10 @@ import { savedObjectReadOnlyErrorMessage, CaseCallOut } from '../components/call export const CaseDetailsPage = React.memo(() => { const history = useHistory(); const userPermissions = useGetUserSavedObjectPermissions(); - const { detailName: caseId } = useParams<{ detailName?: string }>(); + const { detailName: caseId, subCaseId } = useParams<{ + detailName?: string; + subCaseId?: string; + }>(); const search = useGetUrlSearch(navTabs.case); if (userPermissions != null && !userPermissions.read) { @@ -38,7 +41,11 @@ export const CaseDetailsPage = React.memo(() => { messages={[{ ...savedObjectReadOnlyErrorMessage }]} /> )} - + diff --git a/x-pack/plugins/security_solution/public/cases/pages/index.tsx b/x-pack/plugins/security_solution/public/cases/pages/index.tsx index 32c94e593665f8..314bdc9bfd117f 100644 --- a/x-pack/plugins/security_solution/public/cases/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/pages/index.tsx @@ -15,7 +15,9 @@ import { ConfigureCasesPage } from './configure_cases'; const casesPagePath = ''; const caseDetailsPagePath = `${casesPagePath}/:detailName`; -const caseDetailsPagePathWithCommentId = `${casesPagePath}/:detailName/:commentId`; +const subCaseDetailsPagePath = `${caseDetailsPagePath}/sub-cases/:subCaseId`; +const caseDetailsPagePathWithCommentId = `${caseDetailsPagePath}/:commentId`; +const subCaseDetailsPagePathWithCommentId = `${subCaseDetailsPagePath}/:commentId`; const createCasePagePath = `${casesPagePath}/create`; const configureCasesPagePath = `${casesPagePath}/configure`; @@ -27,7 +29,13 @@ const CaseContainerComponent: React.FC = () => ( - + + + + + + + diff --git a/x-pack/plugins/security_solution/public/cases/translations.ts b/x-pack/plugins/security_solution/public/cases/translations.ts index 156cb91994468e..caaa1f6e248ea6 100644 --- a/x-pack/plugins/security_solution/public/cases/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/translations.ts @@ -131,6 +131,10 @@ export const REOPEN_CASE = i18n.translate('xpack.securitySolution.case.caseView. defaultMessage: 'Reopen case', }); +export const OPEN_CASE = i18n.translate('xpack.securitySolution.case.caseView.openCase', { + defaultMessage: 'Open case', +}); + export const CASE_NAME = i18n.translate('xpack.securitySolution.case.caseView.caseName', { defaultMessage: 'Case name', }); diff --git a/x-pack/plugins/security_solution/public/common/components/callouts/callout.test.tsx b/x-pack/plugins/security_solution/public/common/components/callouts/callout.test.tsx new file mode 100644 index 00000000000000..f908a79361d0a5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/callouts/callout.test.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mount } from 'enzyme'; +import React from 'react'; +import { TestProviders } from '../../mock'; +import { CallOut } from './callout'; +import { CallOutMessage } from './callout_types'; + +describe('callout', () => { + let message: CallOutMessage = { + type: 'primary', + id: 'some-id', + title: 'title', + description: <>{'some description'}, + }; + + beforeEach(() => { + message = { + type: 'primary', + id: 'some-id', + title: 'title', + description: <>{'some description'}, + }; + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + test('renders the callout data-test-subj from the given id', () => { + const wrapper = mount( + + + + ); + expect(wrapper.exists('[data-test-subj="callout-some-id"]')).toEqual(true); + }); + + test('renders the callout dismiss button by default', () => { + const wrapper = mount( + + + + ); + expect(wrapper.exists('[data-test-subj="callout-dismiss-btn"]')).toEqual(true); + }); + + test('renders the callout dismiss button if given an explicit true to enable it', () => { + const wrapper = mount( + + + + ); + expect(wrapper.exists('[data-test-subj="callout-dismiss-btn"]')).toEqual(true); + }); + + test('Does NOT render the callout dismiss button if given an explicit false to disable it', () => { + const wrapper = mount( + + + + ); + expect(wrapper.exists('[data-test-subj="callout-dismiss-btn"]')).toEqual(false); + }); + + test('onDismiss callback operates when dismiss button is clicked', () => { + const onDismiss = jest.fn(); + const wrapper = mount( + + + + ); + wrapper.find('[data-test-subj="callout-dismiss-btn"]').first().simulate('click'); + expect(onDismiss).toBeCalledWith(message); + }); + + test('dismissButtonText can be set', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="callout-dismiss-btn"]').first().text()).toEqual( + 'Some other text' + ); + }); + + test('a default icon type of "iInCircle" will be chosen if no iconType is set and the message type is "primary"', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="callout-some-id"]').first().prop('iconType')).toEqual( + 'iInCircle' + ); + }); + + test('icon type can be changed from the type within the message', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="callout-some-id"]').first().prop('iconType')).toEqual( + 'something_else' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/callouts/callout.tsx b/x-pack/plugins/security_solution/public/common/components/callouts/callout.tsx index f6e0c89cab266c..2077e421c427a1 100644 --- a/x-pack/plugins/security_solution/public/common/components/callouts/callout.tsx +++ b/x-pack/plugins/security_solution/public/common/components/callouts/callout.tsx @@ -8,8 +8,8 @@ import React, { FC, memo } from 'react'; import { EuiCallOut } from '@elastic/eui'; +import { assertUnreachable } from '../../../../common/utility_types'; import { CallOutType, CallOutMessage } from './callout_types'; -import { CallOutDescription } from './callout_description'; import { CallOutDismissButton } from './callout_dismiss_button'; export interface CallOutProps { @@ -17,6 +17,7 @@ export interface CallOutProps { iconType?: string; dismissButtonText?: string; onDismiss?: (message: CallOutMessage) => void; + showDismissButton?: boolean; } const CallOutComponent: FC = ({ @@ -24,8 +25,9 @@ const CallOutComponent: FC = ({ iconType, dismissButtonText, onDismiss, + showDismissButton = true, }) => { - const { type, id, title } = message; + const { type, id, title, description } = message; const finalIconType = iconType ?? getDefaultIconType(type); return ( @@ -36,8 +38,10 @@ const CallOutComponent: FC = ({ data-test-subj={`callout-${id}`} data-test-messages={`[${id}]`} > - - + {description} + {showDismissButton && ( + + )} ); }; @@ -53,7 +57,7 @@ const getDefaultIconType = (type: CallOutType): string => { case 'danger': return 'alert'; default: - return ''; + return assertUnreachable(type); } }; diff --git a/x-pack/plugins/security_solution/public/common/components/callouts/callout_description.tsx b/x-pack/plugins/security_solution/public/common/components/callouts/callout_description.tsx deleted file mode 100644 index dbb1267c73323d..00000000000000 --- a/x-pack/plugins/security_solution/public/common/components/callouts/callout_description.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { FC } from 'react'; -import { EuiDescriptionList } from '@elastic/eui'; -import { CallOutMessage } from './callout_types'; - -export interface CallOutDescriptionProps { - messages: CallOutMessage | CallOutMessage[]; -} - -export const CallOutDescription: FC = ({ messages }) => { - if (!Array.isArray(messages)) { - return messages.description; - } - - if (messages.length < 1) { - return null; - } - - return ; -}; diff --git a/x-pack/plugins/security_solution/public/common/components/callouts/callout_persistent_switcher.tsx b/x-pack/plugins/security_solution/public/common/components/callouts/callout_persistent_switcher.tsx new file mode 100644 index 00000000000000..5b67410bb904a8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/callouts/callout_persistent_switcher.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, memo } from 'react'; + +import { CallOutMessage } from './callout_types'; +import { CallOut } from './callout'; + +export interface CallOutPersistentSwitcherProps { + condition: boolean; + message: CallOutMessage; +} + +const CallOutPersistentSwitcherComponent: FC = ({ + condition, + message, +}): JSX.Element | null => + condition ? : null; + +export const CallOutPersistentSwitcher = memo(CallOutPersistentSwitcherComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/callouts/callout_types.ts b/x-pack/plugins/security_solution/public/common/components/callouts/callout_types.ts index 604f7b3e61c794..e04638a57ad06a 100644 --- a/x-pack/plugins/security_solution/public/common/components/callouts/callout_types.ts +++ b/x-pack/plugins/security_solution/public/common/components/callouts/callout_types.ts @@ -5,7 +5,9 @@ * 2.0. */ -export type CallOutType = 'primary' | 'success' | 'warning' | 'danger'; +import { EuiCallOutProps } from '@elastic/eui'; + +export type CallOutType = NonNullable; export interface CallOutMessage { type: CallOutType; diff --git a/x-pack/plugins/security_solution/public/common/components/callouts/index.ts b/x-pack/plugins/security_solution/public/common/components/callouts/index.ts index 222bf5daee6f57..0b7ec42744a6e4 100644 --- a/x-pack/plugins/security_solution/public/common/components/callouts/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/callouts/index.ts @@ -8,3 +8,4 @@ export * from './callout_switcher'; export * from './callout_types'; export * from './callout'; +export * from './callout_persistent_switcher'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx index e388b485ba89c1..af76a79f0e330a 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.test.tsx @@ -20,7 +20,6 @@ import { stubIndexPattern } from 'src/plugins/data/common/index_patterns/index_p import { useAddOrUpdateException } from '../use_add_exception'; import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_exception_list'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; -import { Ecs } from '../../../../../common/ecs'; import * as builder from '../builder'; import * as helpers from '../helpers'; import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; @@ -31,6 +30,7 @@ import { getRulesSchemaMock, } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async'; +import { AlertData } from '../types'; jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_index'); jest.mock('../../../../common/lib/kibana'); @@ -157,8 +157,11 @@ describe('When the add exception modal is opened', () => { describe('when there is alert data passed to an endpoint list exception', () => { let wrapper: ReactWrapper; beforeEach(async () => { - const alertDataMock: Ecs = { _id: 'test-id', file: { path: ['test/path'] } }; - + const alertDataMock: AlertData = { + '@timestamp': '1234567890', + _id: 'test-id', + file: { path: 'test/path' }, + }; wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { describe('when there is alert data passed to a detection list exception', () => { let wrapper: ReactWrapper; beforeEach(async () => { - const alertDataMock: Ecs = { _id: 'test-id', file: { path: ['test/path'] } }; - + const alertDataMock: AlertData = { + '@timestamp': '1234567890', + _id: 'test-id', + file: { path: 'test/path' }, + }; wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { describe('when there is an exception being created on a sequence eql rule type', () => { let wrapper: ReactWrapper; beforeEach(async () => { - const alertDataMock: Ecs = { _id: 'test-id', file: { path: ['test/path'] } }; (useRuleAsync as jest.Mock).mockImplementation(() => ({ rule: { ...getRulesEqlSchemaMock(), @@ -270,6 +275,11 @@ describe('When the add exception modal is opened', () => { 'sequence [process where process.name = "test.exe"] [process where process.name = "explorer.exe"]', }, })); + const alertDataMock: AlertData = { + '@timestamp': '1234567890', + _id: 'test-id', + file: { path: 'test/path' }, + }; wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { }, }, ]); - const alertDataMock: Ecs = { _id: 'test-id', file: { path: ['test/path'] } }; + const alertDataMock: AlertData = { + '@timestamp': '1234567890', + _id: 'test-id', + file: { path: 'test/path' }, + }; wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> void; onConfirm: (didCloseAlert: boolean, didBulkCloseAlert: boolean) => void; @@ -104,6 +109,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ ruleIndices, exceptionListType, alertData, + isAlertDataLoading, onCancel, onConfirm, onRuleChange, @@ -240,7 +246,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ } else { return []; } - }, [alertData, exceptionListType, ruleExceptionList, ruleName]); + }, [exceptionListType, ruleExceptionList, ruleName, alertData]); useEffect((): void => { if (isSignalIndexPatternLoading === false && isSignalIndexLoading === false) { @@ -348,133 +354,131 @@ export const AddExceptionModal = memo(function AddExceptionModal({ }, [maybeRule]); return ( - - - - {addExceptionMessage} - - {ruleName} - - - - {fetchOrCreateListError != null && ( - - - + + + {addExceptionMessage} + + {ruleName} + + + + {fetchOrCreateListError != null && ( + + + + )} + {fetchOrCreateListError == null && + (isLoadingExceptionList || + isIndexPatternLoading || + isSignalIndexLoading || + isAlertDataLoading || + isSignalIndexPatternLoading) && ( + )} - {fetchOrCreateListError == null && - (isLoadingExceptionList || - isIndexPatternLoading || - isSignalIndexLoading || - isSignalIndexPatternLoading) && ( - - )} - {fetchOrCreateListError == null && - !isSignalIndexLoading && - !isSignalIndexPatternLoading && - !isLoadingExceptionList && - !isIndexPatternLoading && - !isRuleLoading && - !mlJobLoading && - ruleExceptionList && ( - <> - - {isRuleEQLSequenceStatement && ( - <> - - - - )} - {i18n.EXCEPTION_BUILDER_INFO} - - - - - - - - - - {alertData !== undefined && alertStatus !== 'closed' && ( - - - - )} + {fetchOrCreateListError == null && + !isSignalIndexLoading && + !isSignalIndexPatternLoading && + !isLoadingExceptionList && + !isIndexPatternLoading && + !isRuleLoading && + !mlJobLoading && + !isAlertDataLoading && + ruleExceptionList && ( + <> + + {isRuleEQLSequenceStatement && ( + <> + + + + )} + {i18n.EXCEPTION_BUILDER_INFO} + + + + + + + + + + {alertData != null && alertStatus !== 'closed' && ( - {exceptionListType === 'endpoint' && ( - <> - - - {i18n.ENDPOINT_QUARANTINE_TEXT} - - - )} - - - )} - {fetchOrCreateListError == null && ( - - {i18n.CANCEL} - - - {addExceptionMessage} - - + )} + + + + {exceptionListType === 'endpoint' && ( + <> + + + {i18n.ENDPOINT_QUARANTINE_TEXT} + + + )} + + )} - - + {fetchOrCreateListError == null && ( + + {i18n.CANCEL} + + + {addExceptionMessage} + + + )} + ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 75b7bf2aabd7fd..336732016e9369 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -12,7 +12,6 @@ import { EuiModalHeader, EuiModalHeaderTitle, EuiModalFooter, - EuiOverlayMask, EuiButton, EuiButtonEmpty, EuiHorizontalRule, @@ -281,125 +280,121 @@ export const EditExceptionModal = memo(function EditExceptionModal({ }, [maybeRule]); return ( - - - - - {exceptionListType === 'endpoint' - ? i18n.EDIT_ENDPOINT_EXCEPTION_TITLE - : i18n.EDIT_EXCEPTION_TITLE} - - - {ruleName} - - - {(addExceptionIsLoading || isIndexPatternLoading || isSignalIndexLoading) && ( - - )} - {!isSignalIndexLoading && - !addExceptionIsLoading && - !isIndexPatternLoading && - !isRuleLoading && - !mlJobLoading && ( - <> - - {isRuleEQLSequenceStatement && ( - <> - - - - )} - {i18n.EXCEPTION_BUILDER_INFO} - - - - - - - - - - - + + + {exceptionListType === 'endpoint' + ? i18n.EDIT_ENDPOINT_EXCEPTION_TITLE + : i18n.EDIT_EXCEPTION_TITLE} + + + {ruleName} + + + {(addExceptionIsLoading || isIndexPatternLoading || isSignalIndexLoading) && ( + + )} + {!isSignalIndexLoading && + !addExceptionIsLoading && + !isIndexPatternLoading && + !isRuleLoading && + !mlJobLoading && ( + <> + + {isRuleEQLSequenceStatement && ( + <> + - - {exceptionListType === 'endpoint' && ( - <> - - - {i18n.ENDPOINT_QUARANTINE_TEXT} - - - )} - - - )} - {updateError != null && ( - - - - )} - {hasVersionConflict && ( - - -

    {i18n.VERSION_CONFLICT_ERROR_DESCRIPTION}

    -
    -
    - )} - {updateError == null && ( - - {i18n.CANCEL} - - - {i18n.EDIT_EXCEPTION_SAVE_BUTTON} - - + + + )} + {i18n.EXCEPTION_BUILDER_INFO} + + + + + + + + + + + + + {exceptionListType === 'endpoint' && ( + <> + + + {i18n.ENDPOINT_QUARANTINE_TEXT} + + + )} + + )} -
    -
    + {updateError != null && ( + + + + )} + {hasVersionConflict && ( + + +

    {i18n.VERSION_CONFLICT_ERROR_DESCRIPTION}

    +
    +
    + )} + {updateError == null && ( + + {i18n.CANCEL} + + + {i18n.EDIT_EXCEPTION_SAVE_BUTTON} + + + )} + ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json index 2ea200466445b9..a59e440918175d 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_fields.json @@ -127,5 +127,6 @@ "user.domain", "user.email", "user.hash", - "user.id" + "user.id", + "Ransomware.feature" ] \ No newline at end of file diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index f40df27c6ba7f9..b09ad60b239de8 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -25,11 +25,12 @@ import { entryHasNonEcsType, prepareExceptionItemsForBulkClose, lowercaseHashValues, - getPrepopulatedItem, - getCodeSignatureValue, + getPrepopulatedEndpointException, defaultEndpointExceptionItems, + getFileCodeSignature, + getProcessCodeSignature, } from './helpers'; -import { EmptyEntry } from './types'; +import { AlertData, EmptyEntry } from './types'; import { isOperator, isNotOperator, @@ -635,14 +636,18 @@ describe('Exception helpers', () => { }); describe('getPrepopulatedItem', () => { - test('it returns prepopulated items', () => { - const prepopulatedItem = getPrepopulatedItem({ + const alertDataMock: AlertData = { + '@timestamp': '1234567890', + _id: 'test-id', + file: { path: 'some-file-path', hash: { sha256: 'some-hash' } }, + }; + test('it returns prepopulated fields with empty values', () => { + const prepopulatedItem = getPrepopulatedEndpointException({ listId: 'some_id', ruleName: 'my rule', codeSignature: { subjectName: '', trusted: '' }, - filePath: '', - sha256Hash: '', eventCode: '', + alertEcsData: { ...alertDataMock, file: { path: '', hash: { sha256: '' } } }, }); expect(prepopulatedItem.entries).toEqual([ @@ -660,14 +665,13 @@ describe('Exception helpers', () => { ]); }); - test('it returns prepopulated items with values', () => { - const prepopulatedItem = getPrepopulatedItem({ + test('it returns prepopulated items with actual values', () => { + const prepopulatedItem = getPrepopulatedEndpointException({ listId: 'some_id', ruleName: 'my rule', codeSignature: { subjectName: 'someSubjectName', trusted: 'false' }, - filePath: 'some-file-path', - sha256Hash: 'some-hash', eventCode: 'some-event-code', + alertEcsData: alertDataMock, }); expect(prepopulatedItem.entries).toEqual([ @@ -696,15 +700,15 @@ describe('Exception helpers', () => { }); }); - describe('getCodeSignatureValue', () => { + describe('getFileCodeSignature', () => { test('it works when file.Ext.code_signature is an object', () => { - const codeSignatures = getCodeSignatureValue({ + const codeSignatures = getFileCodeSignature({ _id: '123', file: { Ext: { code_signature: { - subject_name: ['some_subject'], - trusted: ['false'], + subject_name: 'some_subject', + trusted: 'false', }, }, }, @@ -714,13 +718,13 @@ describe('Exception helpers', () => { }); test('it works when file.Ext.code_signature is nested type', () => { - const codeSignatures = getCodeSignatureValue({ + const codeSignatures = getFileCodeSignature({ _id: '123', file: { Ext: { code_signature: [ - { subject_name: ['some_subject'], trusted: ['false'] }, - { subject_name: ['some_subject_2'], trusted: ['true'] }, + { subject_name: 'some_subject', trusted: 'false' }, + { subject_name: 'some_subject_2', trusted: 'true' }, ], }, }, @@ -736,11 +740,11 @@ describe('Exception helpers', () => { }); test('it returns default when file.Ext.code_signatures values are empty', () => { - const codeSignatures = getCodeSignatureValue({ + const codeSignatures = getFileCodeSignature({ _id: '123', file: { Ext: { - code_signature: { subject_name: [], trusted: [] }, + code_signature: { subject_name: '', trusted: '' }, }, }, }); @@ -749,7 +753,7 @@ describe('Exception helpers', () => { }); test('it returns default when file.Ext.code_signatures is empty array', () => { - const codeSignatures = getCodeSignatureValue({ + const codeSignatures = getFileCodeSignature({ _id: '123', file: { Ext: { @@ -762,7 +766,81 @@ describe('Exception helpers', () => { }); test('it returns default when file.Ext.code_signatures does not exist', () => { - const codeSignatures = getCodeSignatureValue({ + const codeSignatures = getFileCodeSignature({ + _id: '123', + }); + + expect(codeSignatures).toEqual([{ subjectName: '', trusted: '' }]); + }); + }); + + describe('getProcessCodeSignature', () => { + test('it works when file.Ext.code_signature is an object', () => { + const codeSignatures = getProcessCodeSignature({ + _id: '123', + process: { + Ext: { + code_signature: { + subject_name: 'some_subject', + trusted: 'false', + }, + }, + }, + }); + + expect(codeSignatures).toEqual([{ subjectName: 'some_subject', trusted: 'false' }]); + }); + + test('it works when file.Ext.code_signature is nested type', () => { + const codeSignatures = getProcessCodeSignature({ + _id: '123', + process: { + Ext: { + code_signature: [ + { subject_name: 'some_subject', trusted: 'false' }, + { subject_name: 'some_subject_2', trusted: 'true' }, + ], + }, + }, + }); + + expect(codeSignatures).toEqual([ + { subjectName: 'some_subject', trusted: 'false' }, + { + subjectName: 'some_subject_2', + trusted: 'true', + }, + ]); + }); + + test('it returns default when file.Ext.code_signatures values are empty', () => { + const codeSignatures = getProcessCodeSignature({ + _id: '123', + process: { + Ext: { + code_signature: { subject_name: '', trusted: '' }, + }, + }, + }); + + expect(codeSignatures).toEqual([{ subjectName: '', trusted: '' }]); + }); + + test('it returns default when file.Ext.code_signatures is empty array', () => { + const codeSignatures = getProcessCodeSignature({ + _id: '123', + process: { + Ext: { + code_signature: [], + }, + }, + }); + + expect(codeSignatures).toEqual([{ subjectName: '', trusted: '' }]); + }); + + test('it returns default when file.Ext.code_signatures does not exist', () => { + const codeSignatures = getProcessCodeSignature({ _id: '123', }); @@ -771,23 +849,23 @@ describe('Exception helpers', () => { }); describe('defaultEndpointExceptionItems', () => { - test('it should return pre-populated items', () => { + test('it should return pre-populated Endpoint items for non-specified event code', () => { const defaultItems = defaultEndpointExceptionItems('list_id', 'my_rule', { _id: '123', file: { Ext: { code_signature: [ - { subject_name: ['some_subject'], trusted: ['false'] }, - { subject_name: ['some_subject_2'], trusted: ['true'] }, + { subject_name: 'some_subject', trusted: 'false' }, + { subject_name: 'some_subject_2', trusted: 'true' }, ], }, - path: ['some file path'], + path: 'some file path', hash: { - sha256: ['some hash'], + sha256: 'some hash', }, }, event: { - code: ['some event code'], + code: 'some event code', }, }); @@ -838,5 +916,88 @@ describe('Exception helpers', () => { { field: 'event.code', operator: 'included', type: 'match', value: 'some event code' }, ]); }); + + test('it should return pre-populated ransomware items for event code `ransomware`', () => { + const defaultItems = defaultEndpointExceptionItems('list_id', 'my_rule', { + _id: '123', + process: { + Ext: { + code_signature: [ + { subject_name: 'some_subject', trusted: 'false' }, + { subject_name: 'some_subject_2', trusted: 'true' }, + ], + }, + executable: 'some file path', + hash: { + sha256: 'some hash', + }, + }, + Ransomware: { + feature: 'some ransomware feature', + }, + event: { + code: 'ransomware', + }, + }); + + expect(defaultItems[0].entries).toEqual([ + { + entries: [ + { + field: 'subject_name', + operator: 'included', + type: 'match', + value: 'some_subject', + }, + { field: 'trusted', operator: 'included', type: 'match', value: 'false' }, + ], + field: 'process.Ext.code_signature', + type: 'nested', + }, + { + field: 'process.executable', + operator: 'included', + type: 'match', + value: 'some file path', + }, + { field: 'process.hash.sha256', operator: 'included', type: 'match', value: 'some hash' }, + { + field: 'Ransomware.feature', + operator: 'included', + type: 'match', + value: 'some ransomware feature', + }, + { field: 'event.code', operator: 'included', type: 'match', value: 'ransomware' }, + ]); + expect(defaultItems[1].entries).toEqual([ + { + entries: [ + { + field: 'subject_name', + operator: 'included', + type: 'match', + value: 'some_subject_2', + }, + { field: 'trusted', operator: 'included', type: 'match', value: 'true' }, + ], + field: 'process.Ext.code_signature', + type: 'nested', + }, + { + field: 'process.executable', + operator: 'included', + type: 'match', + value: 'some file path', + }, + { field: 'process.hash.sha256', operator: 'included', type: 'match', value: 'some hash' }, + { + field: 'Ransomware.feature', + operator: 'included', + type: 'match', + value: 'some ransomware feature', + }, + { field: 'event.code', operator: 'included', type: 'match', value: 'ransomware' }, + ]); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index 1a66dd2f27cc2b..507fd51a90486b 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -16,6 +16,7 @@ import { BuilderEntry, CreateExceptionListItemBuilderSchema, ExceptionsBuilderExceptionItem, + Flattened, } from './types'; import { EXCEPTION_OPERATORS, isOperator } from '../autocomplete/operators'; import { OperatorOption } from '../autocomplete/types'; @@ -264,6 +265,16 @@ export const enrichNewExceptionItemsWithComments = ( }); }; +export const buildGetAlertByIdQuery = (id: string | undefined) => ({ + query: { + match: { + _id: { + query: id || '', + }, + }, + }, +}); + /** * Adds new and existing comments to exceptionItem * @param exceptionItem existing ExceptionItem @@ -358,31 +369,50 @@ export const entryHasListType = ( * Returns the value for `file.Ext.code_signature` which * can be an object or array of objects */ -export const getCodeSignatureValue = ( - alertData: Ecs +export const getFileCodeSignature = ( + alertData: Flattened ): Array<{ subjectName: string; trusted: string }> => { const { file } = alertData; const codeSignature = file && file.Ext && file.Ext.code_signature; - // Pre 7.10 file.Ext.code_signature was mistakenly populated as - // a single object with subject_name and trusted. + return getCodeSignatureValue(codeSignature); +}; + +/** + * Returns the value for `process.Ext.code_signature` which + * can be an object or array of objects + */ +export const getProcessCodeSignature = ( + alertData: Flattened +): Array<{ subjectName: string; trusted: string }> => { + const { process } = alertData; + const codeSignature = process && process.Ext && process.Ext.code_signature; + return getCodeSignatureValue(codeSignature); +}; + +/** + * Pre 7.10 `Ext.code_signature` fields were mistakenly populated as + * a single object with subject_name and trusted. + */ +export const getCodeSignatureValue = ( + codeSignature: Flattened | Flattened | undefined +): Array<{ subjectName: string; trusted: string }> => { if (Array.isArray(codeSignature) && codeSignature.length > 0) { - return codeSignature.map((signature) => ({ - subjectName: (signature.subject_name && signature.subject_name[0]) ?? '', - trusted: (signature.trusted && signature.trusted[0]) ?? '', - })); + return codeSignature.map((signature) => { + return { + subjectName: signature.subject_name ?? '', + trusted: signature.trusted ?? '', + }; + }); } else { - const signature: CodeSignature | undefined = !Array.isArray(codeSignature) + const signature: Flattened | undefined = !Array.isArray(codeSignature) ? codeSignature : undefined; - const subjectName: string | undefined = - signature && signature.subject_name && signature.subject_name[0]; - const trusted: string | undefined = signature && signature.trusted && signature.trusted[0]; return [ { - subjectName: subjectName ?? '', - trusted: trusted ?? '', + subjectName: signature?.subject_name ?? '', + trusted: signature?.trusted ?? '', }, ]; } @@ -391,23 +421,24 @@ export const getCodeSignatureValue = ( /** * Returns the default values from the alert data to autofill new endpoint exceptions */ -export const getPrepopulatedItem = ({ +export const getPrepopulatedEndpointException = ({ listId, ruleName, codeSignature, - filePath, - sha256Hash, eventCode, listNamespace = 'agnostic', + alertEcsData, }: { listId: string; listNamespace?: NamespaceType; ruleName: string; codeSignature: { subjectName: string; trusted: string }; - filePath: string; - sha256Hash: string; eventCode: string; + alertEcsData: Flattened; }): ExceptionsBuilderExceptionItem => { + const { file } = alertEcsData; + const filePath = file?.path ?? ''; + const sha256Hash = file?.hash?.sha256 ?? ''; return { ...getNewExceptionItem({ listId, namespaceType: listNamespace, ruleName }), entries: [ @@ -451,6 +482,77 @@ export const getPrepopulatedItem = ({ }; }; +/** + * Returns the default values from the alert data to autofill new endpoint exceptions + */ +export const getPrepopulatedRansomwareException = ({ + listId, + ruleName, + codeSignature, + eventCode, + listNamespace = 'agnostic', + alertEcsData, +}: { + listId: string; + listNamespace?: NamespaceType; + ruleName: string; + codeSignature: { subjectName: string; trusted: string }; + eventCode: string; + alertEcsData: Flattened; +}): ExceptionsBuilderExceptionItem => { + const { process, Ransomware } = alertEcsData; + const sha256Hash = process?.hash?.sha256 ?? ''; + const executable = process?.executable ?? ''; + const ransomwareFeature = Ransomware?.feature ?? ''; + return { + ...getNewExceptionItem({ listId, namespaceType: listNamespace, ruleName }), + entries: [ + { + field: 'process.Ext.code_signature', + type: 'nested', + entries: [ + { + field: 'subject_name', + operator: 'included', + type: 'match', + value: codeSignature != null ? codeSignature.subjectName : '', + }, + { + field: 'trusted', + operator: 'included', + type: 'match', + value: codeSignature != null ? codeSignature.trusted : '', + }, + ], + }, + { + field: 'process.executable', + operator: 'included', + type: 'match', + value: executable ?? '', + }, + { + field: 'process.hash.sha256', + operator: 'included', + type: 'match', + value: sha256Hash ?? '', + }, + { + field: 'Ransomware.feature', + operator: 'included', + type: 'match', + value: ransomwareFeature ?? '', + }, + { + field: 'event.code', + operator: 'included', + type: 'match', + value: eventCode ?? '', + }, + ], + }; +}; + /** * Determines whether or not any entries within the given exceptionItems contain values not in the specified ECS mapping */ @@ -487,18 +589,31 @@ export const entryHasNonEcsType = ( export const defaultEndpointExceptionItems = ( listId: string, ruleName: string, - alertEcsData: Ecs + alertEcsData: Flattened ): ExceptionsBuilderExceptionItem[] => { - const { file, event: alertEvent } = alertEcsData; + const { event: alertEvent } = alertEcsData; + const eventCode = alertEvent?.code ?? ''; + + if (eventCode === 'ransomware') { + return getProcessCodeSignature(alertEcsData).map((codeSignature) => + getPrepopulatedRansomwareException({ + listId, + ruleName, + eventCode, + codeSignature, + alertEcsData, + }) + ); + } - return getCodeSignatureValue(alertEcsData).map((codeSignature) => - getPrepopulatedItem({ + // By default return the standard prepopulated Endpoint Exception fields + return getFileCodeSignature(alertEcsData).map((codeSignature) => + getPrepopulatedEndpointException({ listId, ruleName, - filePath: file && file.path ? file.path[0] : '', - sha256Hash: file && file.hash && file.hash.sha256 ? file.hash.sha256[0] : '', - eventCode: alertEvent && alertEvent.code ? alertEvent.code[0] : '', + eventCode, codeSignature, + alertEcsData, }) ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts index 3593fe3d054910..6108a21ce5624a 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts @@ -6,6 +6,8 @@ */ import { ReactNode } from 'react'; +import { Ecs } from '../../../../common/ecs'; +import { CodeSignature } from '../../../../common/ecs/file'; import { IFieldType } from '../../../../../../../src/plugins/data/common'; import { OperatorOption } from '../autocomplete/types'; import { @@ -104,3 +106,32 @@ export type CreateExceptionListItemBuilderSchema = Omit< export type ExceptionsBuilderExceptionItem = | ExceptionListItemBuilderSchema | CreateExceptionListItemBuilderSchema; + +export interface FlattenedCodeSignature { + subject_name: string; + trusted: string; +} + +export type Flattened = { + [K in keyof T]: T[K] extends infer AliasType + ? AliasType extends CodeSignature[] + ? FlattenedCodeSignature[] + : AliasType extends Array + ? rawType + : AliasType extends object + ? Flattened + : AliasType + : never; +}; + +export type AlertData = { + '@timestamp': string; +} & Flattened; + +export interface EcsHit { + _id: string; + _index: string; + _source: { + '@timestamp': string; + } & Omit, '_id' | '_index'>; +} diff --git a/x-pack/plugins/security_solution/public/common/components/import_data_modal/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/import_data_modal/__snapshots__/index.test.tsx.snap index 6503dd8dfb5086..d1a41b1c32c102 100644 --- a/x-pack/plugins/security_solution/public/common/components/import_data_modal/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/import_data_modal/__snapshots__/index.test.tsx.snap @@ -2,64 +2,62 @@ exports[`ImportDataModal renders correctly against snapshot 1`] = ` - - - - - title - - - - -

    - description -

    -
    - - - - -
    - - - Cancel - - - submitBtnText - - -
    -
    + + + + title + + + + +

    + description +

    +
    + + + + +
    + + + Cancel + + + submitBtnText + + +
    `; diff --git a/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx index 8a29ce3799321f..4c3dc2a249b4ff 100644 --- a/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/import_data_modal/index.tsx @@ -15,7 +15,6 @@ import { EuiModalFooter, EuiModalHeader, EuiModalHeaderTitle, - EuiOverlayMask, EuiSpacer, EuiText, } from '@elastic/eui'; @@ -132,51 +131,49 @@ export const ImportDataModalComponent = ({ return ( <> {showModal && ( - - - - {title} - - - - -

    {description}

    -
    - - - { - setSelectedFiles(files && files.length > 0 ? files : null); - }} - display={'large'} - fullWidth={true} - isLoading={isImporting} + + + {title} + + + + +

    {description}

    +
    + + + { + setSelectedFiles(files && files.length > 0 ? files : null); + }} + display={'large'} + fullWidth={true} + isLoading={isImporting} + /> + + {showCheckBox && ( + setOverwrite(!overwrite)} /> - - {showCheckBox && ( - setOverwrite(!overwrite)} - /> - )} -
    - - - {i18n.CANCEL_BUTTON} - - {submitBtnText} - - -
    -
    + )} + + + + {i18n.CANCEL_BUTTON} + + {submitBtnText} + + + )} ); diff --git a/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx b/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx index ece29cd360ce71..a5c0144531110a 100644 --- a/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx +++ b/x-pack/plugins/security_solution/public/common/components/inspect/modal.tsx @@ -15,7 +15,6 @@ import { EuiModalHeader, EuiModalHeaderTitle, EuiModalFooter, - EuiOverlayMask, EuiSpacer, EuiTabbedContent, } from '@elastic/eui'; @@ -211,24 +210,22 @@ export const ModalInspectQuery = ({ ]; return ( - - - - - {i18n.INSPECT} {title} - - - - - - - - - - {i18n.CLOSE} - - - - + + + + {i18n.INSPECT} {title} + + + + + + + + + + {i18n.CLOSE} + + + ); }; diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_case.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_case.tsx index 7a7da6f89306cf..82d8aac904e9b3 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_case.tsx +++ b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_case.tsx @@ -9,19 +9,43 @@ import { appendSearch } from './helpers'; export const getCaseUrl = (search?: string | null) => `${appendSearch(search ?? undefined)}`; -export const getCaseDetailsUrl = ({ id, search }: { id: string; search?: string | null }) => - `/${encodeURIComponent(id)}${appendSearch(search ?? undefined)}`; +export const getCaseDetailsUrl = ({ + id, + search, + subCaseId, +}: { + id: string; + search?: string | null; + subCaseId?: string; +}) => { + if (subCaseId) { + return `/${encodeURIComponent(id)}/sub-cases/${encodeURIComponent(subCaseId)}${appendSearch( + search ?? undefined + )}`; + } + return `/${encodeURIComponent(id)}${appendSearch(search ?? undefined)}`; +}; export const getCaseDetailsUrlWithCommentId = ({ id, commentId, search, + subCaseId, }: { id: string; commentId: string; search?: string | null; -}) => - `/${encodeURIComponent(id)}/${encodeURIComponent(commentId)}${appendSearch(search ?? undefined)}`; + subCaseId?: string; +}) => { + if (subCaseId) { + return `/${encodeURIComponent(id)}/sub-cases/${encodeURIComponent( + subCaseId + )}/${encodeURIComponent(commentId)}${appendSearch(search ?? undefined)}`; + } + return `/${encodeURIComponent(id)}/${encodeURIComponent(commentId)}${appendSearch( + search ?? undefined + )}`; +}; export const getCreateCaseUrl = (search?: string | null) => `/create${appendSearch(search ?? undefined)}`; diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.tsx index 6b4148db2b1eeb..8e2f57a1a597c8 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.tsx @@ -164,24 +164,25 @@ export const NetworkDetailsLink = React.memo(NetworkDetailsLinkComponent); const CaseDetailsLinkComponent: React.FC<{ children?: React.ReactNode; detailName: string; + subCaseId?: string; title?: string; -}> = ({ children, detailName, title }) => { +}> = ({ children, detailName, subCaseId, title }) => { const { formatUrl, search } = useFormatUrl(SecurityPageName.case); const { navigateToApp } = useKibana().services.application; const goToCaseDetails = useCallback( (ev) => { ev.preventDefault(); navigateToApp(`${APP_ID}:${SecurityPageName.case}`, { - path: getCaseDetailsUrl({ id: detailName, search }), + path: getCaseDetailsUrl({ id: detailName, search, subCaseId }), }); }, - [detailName, navigateToApp, search] + [detailName, navigateToApp, search, subCaseId] ); return ( diff --git a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts index 0da881d2c6ea47..b993bcda56b8ed 100644 --- a/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/matrix_histogram/types.ts @@ -74,7 +74,14 @@ export interface MatrixHistogramQueryProps { stackByField: string; startDate: string; histogramType: MatrixHistogramType; - threshold?: { field: string | undefined; value: number } | undefined; + threshold?: + | { + field: string | string[] | undefined; + value: number; + cardinality_field?: string | undefined; + cardinality_value?: number | undefined; + } + | undefined; skip?: boolean; isPtrIncluded?: boolean; } diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap index 778916ad2d07ac..be5702550a44c8 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap @@ -246,6 +246,12 @@ exports[`Paginated Table Component rendering it renders the default load more ta }, "euiFilePickerTallHeight": "128px", "euiFlyoutBorder": "1px solid #343741", + "euiFlyoutPaddingModifiers": Object { + "paddingLarge": "24px", + "paddingMedium": "16px", + "paddingNone": 0, + "paddingSmall": "8px", + }, "euiFocusBackgroundColor": "#08334a", "euiFocusRingAnimStartColor": "rgba(27, 169, 245, 0)", "euiFocusRingAnimStartSize": "6px", @@ -357,6 +363,7 @@ exports[`Paginated Table Component rendering it renders the default load more ta }, "euiMarkdownEditorMinHeight": "150px", "euiPageBackgroundColor": "#1a1b20", + "euiPageDefaultMaxWidth": "1000px", "euiPaletteColorBlind": Object { "euiColorVis0": Object { "behindText": "#6dccb1", @@ -534,6 +541,7 @@ exports[`Paginated Table Component rendering it renders the default load more ta "euiSwitchWidthCompressed": "28px", "euiSwitchWidthMini": "22px", "euiTabFontSize": "16px", + "euiTabFontSizeL": "18px", "euiTabFontSizeS": "14px", "euiTableActionsAreaWidth": "40px", "euiTableActionsBorderColor": "rgba(83, 89, 102, 0.09999999999999998)", diff --git a/x-pack/plugins/security_solution/public/common/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap index f7924f37d2c173..5e008e28073de1 100644 --- a/x-pack/plugins/security_solution/public/common/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap @@ -1,50 +1,48 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Modal all errors rendering it renders the default all errors modal when isShowing is positive 1`] = ` - - - - - Your visualization has error(s) - - - - - - - - Error 1, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. - - - - - - Close - - - - + + + + Your visualization has error(s) + + + + + + + + Error 1, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + + + + + + Close + + + `; diff --git a/x-pack/plugins/security_solution/public/common/components/toasters/modal_all_errors.tsx b/x-pack/plugins/security_solution/public/common/components/toasters/modal_all_errors.tsx index 873ebe97317f4f..0a78139f5fe3a1 100644 --- a/x-pack/plugins/security_solution/public/common/components/toasters/modal_all_errors.tsx +++ b/x-pack/plugins/security_solution/public/common/components/toasters/modal_all_errors.tsx @@ -7,7 +7,6 @@ import { EuiButton, - EuiOverlayMask, EuiModal, EuiModalHeader, EuiModalHeaderTitle, @@ -36,36 +35,34 @@ const ModalAllErrorsComponent: React.FC = ({ isShowing, toast, t if (!isShowing || toast == null) return null; return ( - - - - {i18n.TITLE_ERROR_MODAL} - + + + {i18n.TITLE_ERROR_MODAL} + - - - - {toast.errors != null && - toast.errors.map((error, index) => ( - 100 ? `${error.substring(0, 100)} ...` : error} - data-test-subj="modal-all-errors-accordion" - > - {error} - - ))} - + + + + {toast.errors != null && + toast.errors.map((error, index) => ( + 100 ? `${error.substring(0, 100)} ...` : error} + data-test-subj="modal-all-errors-accordion" + > + {error} + + ))} + - - - {i18n.CLOSE_ERROR_MODAL} - - - - + + + {i18n.CLOSE_ERROR_MODAL} + + + ); }; diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx index 2c744b228ba212..2fa63205ffe975 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx @@ -66,7 +66,12 @@ export const useInitSourcerer = ( selectedPatterns: [...ConfigIndexPatterns, signalIndexName], }) ); - } else if (signalIndexNameSelector != null && initialTimelineSourcerer.current) { + } else if ( + signalIndexNameSelector != null && + (activeTimeline == null || + (activeTimeline != null && activeTimeline.savedObjectId == null)) && + initialTimelineSourcerer.current + ) { initialTimelineSourcerer.current = false; dispatch( sourcererActions.setSelectedIndexPatterns({ diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_control.test.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_control.test.tsx new file mode 100644 index 00000000000000..953f39fcf23726 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_control.test.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { useControl, UseControlsReturn } from './use_control'; + +describe('useControl', () => { + it('init', async () => { + const { result } = renderHook<{}, UseControlsReturn>(() => useControl()); + expect(result.current.isControlOpen).toBe(false); + }); + + it('should open the control', async () => { + const { result } = renderHook<{}, UseControlsReturn>(() => useControl()); + + act(() => { + result.current.openControl(); + }); + + expect(result.current.isControlOpen).toBe(true); + }); + + it('should close the control', async () => { + const { result } = renderHook<{}, UseControlsReturn>(() => useControl()); + + act(() => { + result.current.openControl(); + }); + + expect(result.current.isControlOpen).toBe(true); + + act(() => { + result.current.closeControl(); + }); + + expect(result.current.isControlOpen).toBe(false); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_control.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_control.tsx new file mode 100644 index 00000000000000..37d7f4ff79986f --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_control.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useState, useCallback } from 'react'; + +export interface UseControlsReturn { + isControlOpen: boolean; + openControl: () => void; + closeControl: () => void; +} + +export const useControl = (): UseControlsReturn => { + const [isControlOpen, setIsControlOpen] = useState(false); + const openControl = useCallback(() => setIsControlOpen(true), []); + const closeControl = useCallback(() => setIsControlOpen(false), []); + + return { isControlOpen, openControl, closeControl }; +}; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_timeline_events_count.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_timeline_events_count.tsx index 714da27908423f..e801db0190799d 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_timeline_events_count.tsx +++ b/x-pack/plugins/security_solution/public/common/hooks/use_timeline_events_count.tsx @@ -12,17 +12,28 @@ import { createPortalNode, OutPortal } from 'react-reverse-portal'; * A singleton portal for rendering content in the global header */ const timelineEventsCountPortalNodeSingleton = createPortalNode(); +const eqlEventsCountPortalNodeSingleton = createPortalNode(); export const useTimelineEventsCountPortal = () => { const [timelineEventsCountPortalNode] = useState(timelineEventsCountPortalNodeSingleton); - - return { timelineEventsCountPortalNode }; + return { portalNode: timelineEventsCountPortalNode }; }; export const TimelineEventsCountBadge = React.memo(() => { - const { timelineEventsCountPortalNode } = useTimelineEventsCountPortal(); - - return ; + const { portalNode } = useTimelineEventsCountPortal(); + return ; }); TimelineEventsCountBadge.displayName = 'TimelineEventsCountBadge'; + +export const useEqlEventsCountPortal = () => { + const [eqlEventsCountPortalNode] = useState(eqlEventsCountPortalNodeSingleton); + return { portalNode: eqlEventsCountPortalNode }; +}; + +export const EqlEventsCountBadge = React.memo(() => { + const { portalNode } = useEqlEventsCountPortal(); + return ; +}); + +EqlEventsCountBadge.displayName = 'EqlEventsCountBadge'; diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index bfd25aa469c931..5eae3a4d729882 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -212,6 +212,11 @@ export const mockGlobalState: State = { itemsPerPage: 5, dataProviders: [], description: '', + eqlOptions: { + eventCategoryField: 'event.category', + tiebreakerField: 'event.sequence', + timestampField: '@timestamp', + }, eventIdToNoteIds: {}, excludedRowRendererIds: [], expandedDetail: {}, diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_endgame_ecs_data.ts b/x-pack/plugins/security_solution/public/common/mock/mock_endgame_ecs_data.ts index 1082b5f9474e53..3400844e671b30 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_endgame_ecs_data.ts +++ b/x-pack/plugins/security_solution/public/common/mock/mock_endgame_ecs_data.ts @@ -343,6 +343,885 @@ export const mockEndpointFileDeletionEvent: Ecs = { _id: 'mnXHO3cBPmkOXwyNlyv_', }; +export const mockEndpointFileCreationMalwarePreventionAlert: Ecs = { + process: { + hash: { + md5: ['efca0a88adab8b92e4a333b56db5fbaa'], + sha256: ['8c177f6129dddbd36cae196ef9d9eb71f50cee44640068f24830e83d6a9dd1d0'], + sha1: ['e55e587058112c60d015994424f70a7a8e78afb1'], + }, + parent: { + name: ['explorer.exe'], + pid: [1008], + }, + entity_id: [ + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTg5NDQtMTMyNDkwNjg0NzIuNzM4OTY4NTAw', + ], + executable: ['C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'], + name: ['chrome.exe'], + pid: [8944], + args: ['C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'], + }, + host: { + os: { + full: ['Windows Server 2019 Datacenter 1809 (10.0.17763.1518)'], + name: ['Windows'], + version: ['1809 (10.0.17763.1518)'], + platform: ['windows'], + family: ['windows'], + kernel: ['1809 (10.0.17763.1518)'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + architecture: ['x86_64'], + ip: ['10.1.2.3'], + id: ['d8ad572e-d224-4044-a57d-f5a84c0dfe5d'], + name: ['win2019-endpoint-1'], + }, + event: { + category: ['malware', 'intrusion_detection', 'file'], + outcome: ['success'], + code: ['malicious_file'], + action: ['creation'], + id: ['LsuMZVr+sdhvehVM++++Ic8J'], + kind: ['alert'], + module: ['endpoint'], + type: ['info', 'creation', 'denied'], + dataset: ['endpoint.alerts'], + }, + file: { + path: ['C:\\Users\\sean\\Downloads\\6a5eabd6-1c79-4962-b411-a5e7d9e967d4.tmp'], + owner: ['sean'], + hash: { + md5: ['c1f8d2b73b4c2488f95e7305f0421bdf'], + sha256: ['7cc42618e580f233fee47e82312cc5c3476cb5de9219ba3f9eb7f99ac0659c30'], + sha1: ['542b2796e9f57a92504f852b6698148bba9ff289'], + }, + name: ['6a5eabd6-1c79-4962-b411-a5e7d9e967d4.tmp'], + extension: ['tmp'], + size: [196608], + }, + agent: { + type: ['endpoint'], + }, + timestamp: '2020-11-05T16:48:19.923Z', + message: ['Malware Prevention Alert'], + _id: 'dGZQmXUB-o9SpDeMqvln', +}; + +export const mockEndpointFileCreationMalwareDetectionAlert: Ecs = { + process: { + hash: { + md5: ['16d6a536bb2115dcbd16011e6991a9fd'], + sha256: ['6637eca55fedbabc510168f0c4696d41971c89e5d1fb440f2f9391e6ab0e8f54'], + sha1: ['05cc6d37603ca9076f3baf4dc421500c5cf69e4c'], + }, + entity_id: [ + 'Yjk3ZWYwODktNzYyZi00ZTljLTg3OWMtNmQ5MDM1ZjBmYTUzLTQ0MDAtMTMyNDM2MTgwMzIuMjA0MzMxMDA=', + ], + executable: ['C:\\Python27\\python.exe'], + parent: { + name: ['pythonservice.exe'], + pid: [2936], + }, + name: ['python.exe'], + args: ['C:\\Python27\\python.exe', 'main.py', '-a,execute', '-p', 'c:\\temp'], + pid: [4400], + }, + host: { + os: { + full: ['Windows 10 Pro 1903 (10.0.18362.1016)'], + name: ['Windows'], + version: ['1903 (10.0.18362.1016)'], + platform: ['windows'], + family: ['windows'], + kernel: ['1903 (10.0.18362.1016)'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + ip: ['10.1.2.3'], + id: ['c85e6c40-d4a1-db21-7458-2565a6b857f3'], + architecture: ['x86_64'], + name: ['DESKTOP-1'], + }, + file: { + path: ['C:\\temp\\mimikatz_write.exe'], + owner: ['Administrators'], + hash: { + md5: ['cc52aebdf82048364119f117f52dbba0'], + sha256: ['263f09eeee80e03aa27a2d19530e2451978e18bf733c5f1c64ff2389c5dc17b0'], + sha1: ['c929f6ff2d6d1085ee69625cd8efb92101a0e906'], + }, + name: ['mimikatz_write.exe'], + extension: ['exe'], + size: [1265456], + }, + event: { + id: ['Lp/73XQ38EF48a6i+++++5Ds'], + module: ['endpoint'], + category: ['malware', 'intrusion_detection', 'file'], + outcome: ['success'], + code: ['malicious_file'], + action: ['creation'], + kind: ['signal'], + type: ['info', 'creation', 'allowed'], + dataset: ['endpoint.alerts'], + }, + agent: { + type: ['endpoint'], + }, + message: ['Malware Detection Alert'], + timestamp: '2020-09-03T15:51:50.209Z', + _id: '51e04f7dad15fe394a3f7ed582ad4528c8ce62948e315571fc3388befd9aa0e6', +}; + +export const mockEndpointFilesEncryptedRansomwarePreventionAlert: Ecs = { + process: { + hash: { + md5: ['85bc517e37fe24f909e4378a46a4b567'], + sha256: ['e9fa973eb5ad446e0be31c7b8ae02d48281319e7f492e1ddaadddfbdd5b480c7'], + sha1: ['10a3671c0fbc2bce14fc94891e87e2f4ba07e0df'], + }, + parent: { + name: ['cmd.exe'], + pid: [10680], + }, + entity_id: [ + 'OTI1MTRiMTYtMWJkNi05NzljLWE2MDMtOTgwY2ZkNzQ4M2IwLTYwNTYtMTMyNTczODEzMzYuNzIxNTIxODAw', + ], + name: ['powershell.exe'], + pid: [6056], + args: ['powershell.exe', '-file', 'mock_ransomware_v3.ps1'], + }, + host: { + os: { + full: ['Windows 7 Enterprise Service Pack 1 (6.1.7601)'], + name: ['Windows'], + version: ['Service Pack 1 (6.1.7601)'], + platform: ['windows'], + family: ['windows'], + kernel: ['Service Pack 1 (6.1.7601)'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + architecture: ['x86_64'], + ip: ['10.1.2.3'], + id: ['c6bb2832-d58c-4c57-9d1f-3b102ea74d46'], + name: ['DESKTOP-1'], + }, + event: { + category: ['malware', 'intrusion_detection', 'process', 'file'], + outcome: ['success'], + code: ['ransomware'], + action: ['files-encrypted'], + id: ['M0A1DXHIg6/kaeku+++++1Gv'], + kind: ['alert'], + module: ['endpoint'], + type: ['info', 'start', 'change', 'denied'], + dataset: ['endpoint.alerts'], + }, + agent: { + type: ['endpoint'], + }, + timestamp: '2021-02-09T21:55:48.941Z', + message: ['Ransomware Prevention Alert'], + _id: 'BfvLiHcBVXUk10dUK1Pk', +}; + +export const mockEndpointFilesEncryptedRansomwareDetectionAlert: Ecs = { + process: { + hash: { + md5: ['85bc517e37fe24f909e4378a46a4b567'], + sha256: ['e9fa973eb5ad446e0be31c7b8ae02d48281319e7f492e1ddaadddfbdd5b480c7'], + sha1: ['10a3671c0fbc2bce14fc94891e87e2f4ba07e0df'], + }, + parent: { + name: ['cmd.exe'], + pid: [8616], + }, + entity_id: [ + 'MDAwODRkOTAtZDRhOC1kOTZhLWVmYWItZDU1ZWFhNDY1N2M2LTQ2ODQtMTMyNTc0NjE2MzEuNDM3NDUzMDA=', + ], + executable: ['C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'], + name: ['powershell.exe'], + pid: [4684], + args: ['powershell.exe', '-file', 'mock_ransomware_v3.ps1'], + }, + host: { + os: { + full: ['Windows 7 Enterprise Service Pack 1 (6.1.7601)'], + name: ['Windows'], + version: ['Service Pack 1 (6.1.7601)'], + platform: ['windows'], + family: ['windows'], + kernel: ['Service Pack 1 (6.1.7601)'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + architecture: ['x86_64'], + ip: ['10.1.2.3'], + id: ['c6bb2832-d58c-4c57-9d1f-3b102ea74d46'], + name: ['DESKTOP-1'], + }, + event: { + category: ['malware', 'intrusion_detection', 'process', 'file'], + code: ['ransomware'], + action: ['files-encrypted'], + id: ['M0ExfR/BggxoHQ1e+++++1Zv'], + kind: ['alert'], + module: ['endpoint'], + type: ['info', 'start', 'change', 'allowed'], + dataset: ['endpoint.alerts'], + }, + agent: { + type: ['endpoint'], + }, + timestamp: '2021-02-10T20:14:03.927Z', + message: ['Ransomware Detection Alert'], + _id: 'enyUjXcBxUk8qlINZEJr', +}; + +export const mockEndpointFileModificationMalwarePreventionAlert: Ecs = { + process: { + hash: { + md5: ['47ea9e07b7dbfbeba368bd95a3a2d25b'], + sha256: ['f45557c0b57dec4c000d8cb7d7068c8a4dccf392de740501b1046994460d77ea'], + sha1: ['da714f84a7bbaee2be9f1ca0262aca649657cf3e'], + }, + parent: { + name: ['C:\\Windows\\System32\\userinit.exe'], + pid: [356], + }, + entity_id: [ + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTEwMDgtMTMyNDc1Njk3ODUuODA0NzQyMDA=', + ], + executable: ['C:\\Windows\\explorer.exe'], + name: ['explorer.exe'], + pid: [1008], + args: ['C:\\Windows\\Explorer.EXE'], + }, + host: { + os: { + full: ['Windows Server 2019 Datacenter 1809 (10.0.17763.1518)'], + name: ['Windows'], + version: ['1809 (10.0.17763.1518)'], + platform: ['windows'], + family: ['windows'], + kernel: ['1809 (10.0.17763.1518)'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + architecture: ['x86_64'], + ip: ['10.1.2.3'], + id: ['d8ad572e-d224-4044-a57d-f5a84c0dfe5d'], + name: ['win2019-endpoint-1'], + }, + file: { + path: ['C:\\Users\\sean\\Downloads\\mimikatz_trunk (1)\\x64\\mimikatz - Copy.exe'], + owner: ['sean'], + hash: { + md5: ['a3cb3b02a683275f7e0a0f8a9a5c9e07'], + sha256: ['31eb1de7e840a342fd468e558e5ab627bcb4c542a8fe01aec4d5ba01d539a0fc'], + sha1: ['d241df7b9d2ec0b8194751cd5ce153e27cc40fa4'], + }, + name: ['mimikatz - Copy.exe'], + extension: ['exe'], + size: [1309448], + }, + event: { + category: ['malware', 'intrusion_detection', 'file'], + outcome: ['success'], + code: ['malicious_file'], + action: ['modification'], + id: ['LsuMZVr+sdhvehVM++++GvWi'], + kind: ['alert'], + created: ['2020-11-04T22:40:51.724Z'], + module: ['endpoint'], + type: ['info', 'change', 'denied'], + dataset: ['endpoint.alerts'], + }, + agent: { + type: ['endpoint'], + }, + timestamp: '2020-11-04T22:40:51.724Z', + message: ['Malware Prevention Alert'], + _id: 'j0RtlXUB-o9SpDeMLdEE', +}; + +export const mockEndpointFileModificationMalwareDetectionAlert: Ecs = { + process: { + hash: { + md5: ['c93876879542fc4710ab1d3b52382d95'], + sha256: ['0ead4d0131ca81aa4820efdcd3c6053eab23179a46c5480c94d7c11eb8451d62'], + sha1: ['def88472b5d92022b6182bfe031c043ddfc5ff0f'], + }, + parent: { + name: ['Python'], + pid: [97], + }, + entity_id: [ + 'ZGQ0NDBhNjMtZjcyNy00NGY4LWI5M2UtNzQzZWEzMDBiYTk2LTU5OTUtMTMyNDM2MTg1MzkuOTUyNjkwMDA=', + ], + executable: [ + '/usr/local/Cellar/python/2.7.14/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python', + ], + name: ['Python'], + args: [ + '/usr/local/Cellar/python/2.7.14/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python', + 'main.py', + '-a', + 'modify', + ], + pid: [5995], + }, + host: { + os: { + full: ['macOS 10.14.1'], + name: ['macOS'], + version: ['10.14.1'], + platform: ['macos'], + family: ['macos'], + kernel: [ + 'Darwin Kernel Version 18.2.0: Fri Oct 5 19:40:55 PDT 2018; root:xnu-4903.221.2~1/RELEASE_X86_64', + ], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + ip: ['10.1.2.3'], + id: ['7d59b1a5-afa1-6531-07ea-691602558230'], + architecture: ['x86_64'], + name: ['mac-1.local'], + }, + file: { + mtime: ['2020-09-03T14:55:42.842Z'], + path: ['/private/var/root/write_malware/modules/write_malware/aircrack'], + owner: ['root'], + hash: { + md5: ['59328cdab10fb4f25a026eb362440422'], + sha256: ['f0954d9673878b2223b00b7ec770c7b438d876a9bb44ec78457e5c618f31f52b'], + sha1: ['f10b043652da8c444e04aede3a9ce4a10ef9028e'], + }, + name: ['aircrack'], + size: [240916], + }, + event: { + id: ['Lp21aufnU2nkG+fO++++++7h'], + module: ['endpoint'], + category: ['malware', 'intrusion_detection', 'file'], + outcome: ['success'], + code: ['malicious_file'], + action: ['modification'], + type: ['info', 'change', 'allowed'], + dataset: ['endpoint.alerts'], + }, + agent: { + type: ['endpoint'], + }, + message: ['Malware Detection Alert'], + timestamp: '2020-09-03T15:01:19.445Z', + _id: '04d309c7e4cf7c4e54b7e3d93c38399e51797eed2484078487f4d6661f94da2c', +}; + +export const mockEndpointFileRenameMalwarePreventionAlert: Ecs = { + process: { + hash: { + md5: ['47ea9e07b7dbfbeba368bd95a3a2d25b'], + sha256: ['f45557c0b57dec4c000d8cb7d7068c8a4dccf392de740501b1046994460d77ea'], + sha1: ['da714f84a7bbaee2be9f1ca0262aca649657cf3e'], + }, + parent: { + name: ['C:\\Windows\\System32\\userinit.exe'], + pid: [356], + }, + entity_id: [ + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTEwMDgtMTMyNDc1Njk3ODUuODA0NzQyMDA=', + ], + executable: ['C:\\Windows\\explorer.exe'], + name: ['explorer.exe'], + pid: [1008], + args: ['C:\\Windows\\Explorer.EXE'], + }, + host: { + os: { + full: ['Windows Server 2019 Datacenter 1809 (10.0.17763.1518)'], + name: ['Windows'], + version: ['1809 (10.0.17763.1518)'], + platform: ['windows'], + family: ['windows'], + kernel: ['1809 (10.0.17763.1518)'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + architecture: ['x86_64'], + ip: ['10.1.2.3'], + id: ['d8ad572e-d224-4044-a57d-f5a84c0dfe5d'], + name: ['win2019-endpoint-1'], + }, + file: { + mtime: ['2020-11-04T21:48:47.559Z'], + path: [ + 'C:\\Users\\sean\\Downloads\\23361f8f413dd9258545030e42056a352fe35f66bac376d49954551c9b4bcf97.exe', + ], + owner: ['sean'], + hash: { + md5: ['9798063a1fe056ef2f1d6f5217e7b82b'], + sha256: ['23361f8f413dd9258545030e42056a352fe35f66bac376d49954551c9b4bcf97'], + sha1: ['ced72fe7fc3835385faea41c657efab7b9f883cd'], + }, + name: ['23361f8f413dd9258545030e42056a352fe35f66bac376d49954551c9b4bcf97.exe'], + extension: ['exe'], + size: [242010], + }, + event: { + category: ['malware', 'intrusion_detection', 'file'], + outcome: ['success'], + code: ['malicious_file'], + action: ['rename'], + id: ['LsuMZVr+sdhvehVM++++GppA'], + kind: ['alert'], + module: ['endpoint'], + type: ['info', 'change', 'denied'], + dataset: ['endpoint.alerts'], + }, + agent: { + type: ['endpoint'], + }, + timestamp: '2020-11-04T21:48:57.847Z', + message: ['Malware Prevention Alert'], + _id: 'qtA9lXUBn9bLIbfPj-Tu', +}; + +export const mockEndpointFileRenameMalwareDetectionAlert: Ecs = { + ...mockEndpointFileRenameMalwarePreventionAlert, + event: { + ...mockEndpointFileRenameMalwarePreventionAlert.event, + type: ['info', 'change', 'allowed'], + }, + message: ['Malware Detection Alert'], + _id: 'CD7B6A22-809C-4502-BB94-BC38901EC942', +}; + +// NOTE: see `mock_timeline_data.ts` for the mockEndpointProcessExecutionMalwarePreventionAlert + +export const mockEndpointProcessExecutionMalwareDetectionAlert: Ecs = { + process: { + hash: { + md5: ['cc52aebdf82048364119f117f52dbba0'], + sha256: ['263f09eeee80e03aa27a2d19530e2451978e18bf733c5f1c64ff2389c5dc17b0'], + sha1: ['c929f6ff2d6d1085ee69625cd8efb92101a0e906'], + }, + entity_id: [ + 'Yjk3ZWYwODktNzYyZi00ZTljLTg3OWMtNmQ5MDM1ZjBmYTUzLTg2NjgtMTMyNDM2MTgwMzQuODU3Njg5MDA=', + ], + executable: ['C:\\temp\\mimikatz_write.exe'], + parent: { + name: ['python.exe'], + }, + name: ['mimikatz_write.exe'], + args: ['c:\\temp\\mimikatz_write.exe'], + pid: [8668], + }, + host: { + os: { + full: ['Windows 10 Pro 1903 (10.0.18362.1016)'], + name: ['Windows'], + version: ['1903 (10.0.18362.1016)'], + platform: ['windows'], + family: ['windows'], + kernel: ['1903 (10.0.18362.1016)'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + ip: ['10.1.2.3'], + id: ['c85e6c40-d4a1-db21-7458-2565a6b857f3'], + architecture: ['x86_64'], + name: ['DESKTOP-1'], + }, + file: { + mtime: ['2020-09-03T14:47:14.647Z'], + path: ['C:\\temp\\mimikatz_write.exe'], + owner: ['Administrators'], + hash: { + md5: ['cc52aebdf82048364119f117f52dbba0'], + sha256: ['263f09eeee80e03aa27a2d19530e2451978e18bf733c5f1c64ff2389c5dc17b0'], + sha1: ['c929f6ff2d6d1085ee69625cd8efb92101a0e906'], + }, + name: ['mimikatz_write.exe'], + extension: ['exe'], + size: [1265456], + }, + event: { + id: ['Lp/73XQ38EF48a6i+++++5Do'], + module: ['endpoint'], + category: ['malware', 'intrusion_detection', 'process'], + outcome: ['success'], + code: ['malicious_file'], + action: ['execution'], + kind: ['signal'], + type: ['info', 'start', 'allowed'], + dataset: ['endpoint.alerts'], + }, + agent: { + type: ['endpoint'], + }, + message: ['Malware Detection Alert'], + timestamp: '2020-09-03T15:51:50.209Z', + _id: '96b3db3079891faaf155f1ada645b7364a03018c65677ce002f18038e7ce1c47', +}; + +export const mockEndpointFileModificationEvent: Ecs = { + file: { + path: ['/Users/admin/Library/Application Support/CrashReporter/.dat.nosync01a5.6hoWv1'], + name: ['.dat.nosync01a5.6hoWv1'], + }, + host: { + os: { + full: ['macOS 10.14.6'], + name: ['macOS'], + version: ['10.14.6'], + family: ['macos'], + kernel: [ + 'Darwin Kernel Version 18.7.0: Mon Aug 31 20:53:32 PDT 2020; root:xnu-4903.278.44~1/RELEASE_X86_64', + ], + platform: ['macos'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + name: ['test-Mac.local'], + architecture: ['x86_64'], + ip: ['10.1.2.3'], + id: ['fce6b9f1-5c09-d8f0-3d99-9ecb30f995df'], + }, + event: { + category: ['file'], + kind: ['event'], + module: ['endpoint'], + action: ['modification'], + type: ['change'], + dataset: ['endpoint.events.file'], + }, + process: { + name: ['diagnostics_agent'], + pid: [421], + entity_id: ['OTA1ZDkzMTctMjIxOS00ZjQ1LTg4NTMtYzNiYzk1NGU1ZGU4LTQyMS0xMzI0OTEwNTIwOC4w'], + executable: ['/System/Library/CoreServices/diagnostics_agent'], + }, + user: { + id: ['501'], + name: ['admin'], + }, + agent: { + type: ['endpoint'], + }, + message: ['Endpoint file event'], + timestamp: '2021-02-02T18:56:12.871Z', + _id: 'ulkWZHcBGrBB52F2vFf_', +}; + +export const mockEndpointFileOverwriteEvent: Ecs = { + file: { + path: ['C:\\Windows\\ServiceState\\EventLog\\Data\\lastalive0.dat'], + extension: ['dat'], + name: ['lastalive0.dat'], + }, + host: { + os: { + full: ['Windows Server 2019 Datacenter 1809 (10.0.17763.1697)'], + name: ['Windows'], + version: ['1809 (10.0.17763.1697)'], + family: ['windows'], + kernel: ['1809 (10.0.17763.1697)'], + platform: ['windows'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + name: ['windows-endpoint-1'], + architecture: ['x86_64'], + ip: ['10.1.2.3'], + id: ['ce6fa3c3-fda1-4984-9bce-f6d602a5bd1a'], + }, + event: { + category: ['file'], + kind: ['event'], + created: ['2021-02-02T21:40:14.400Z'], + module: ['endpoint'], + action: ['overwrite'], + type: ['change'], + id: ['Lzty2lsJxA05IUWg++++Icrn'], + dataset: ['endpoint.events.file'], + }, + process: { + name: ['svchost.exe'], + pid: [1228], + entity_id: [ + 'YjUwNDNiMTMtYTdjNi0xZGFlLTEyZWQtODQ1ZDlhNTRhZmQyLTEyMjgtMTMyNTQ5ODc1MDcuODc1MTIxNjAw', + ], + executable: ['C:\\Windows\\System32\\svchost.exe'], + }, + user: { + id: ['S-1-5-19'], + name: ['LOCAL SERVICE'], + domain: ['NT AUTHORITY'], + }, + agent: { + type: ['endpoint'], + }, + message: ['Endpoint file event'], + timestamp: '2021-02-02T21:40:14.400Z', + _id: 'LBmxZHcBtgfIO53sCImw', +}; + +export const mockEndpointFileRenameEvent: Ecs = { + file: { + path: ['C:\\Windows\\System32\\sru\\SRU.log'], + Ext: { + original: { + path: ['C:\\Windows\\System32\\sru\\SRUtmp.log'], + name: ['SRUtmp.log'], + }, + }, + extension: ['log'], + name: ['SRU.log'], + }, + host: { + os: { + full: ['Windows Server 2019 Datacenter 1809 (10.0.17763.1697)'], + name: ['Windows'], + version: ['1809 (10.0.17763.1697)'], + family: ['windows'], + kernel: ['1809 (10.0.17763.1697)'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + name: ['windows-endpoint-1'], + architecture: ['x86_64'], + ip: ['10.1.2.3'], + id: ['ce6fa3c3-fda1-4984-9bce-f6d602a5bd1a'], + }, + event: { + category: ['file'], + kind: ['event'], + created: ['2021-02-01T16:43:00.373Z'], + module: ['endpoint'], + action: ['rename'], + type: ['change'], + id: ['Lzty2lsJxA05IUWg++++I3jv'], + dataset: ['endpoint.events.file'], + }, + process: { + name: ['svchost.exe'], + pid: [1204], + entity_id: [ + 'YjUwNDNiMTMtYTdjNi0xZGFlLTEyZWQtODQ1ZDlhNTRhZmQyLTEyMDQtMTMyNTQ5ODc2NzQuNzQ5MjUzNzAw', + ], + executable: ['C:\\Windows\\System32\\svchost.exe'], + }, + user: { + id: ['S-1-5-19'], + name: ['LOCAL SERVICE'], + domain: ['NT AUTHORITY'], + }, + agent: { + type: ['endpoint'], + }, + message: ['Endpoint file event'], + timestamp: '2021-02-01T16:43:00.373Z', + _id: 'OlJ8XncBGrBB52F2Oga7', +}; + +// NOTE: see `mock_timeline_data.ts` for the mockEndpointRegistryModificationEvent + +// NOTE: see `mock_timeline_data.ts` for the mockEndpointLibraryLoadEvent + +export const mockEndpointNetworkHttpRequestEvent: Ecs = { + host: { + os: { + full: ['Windows Server 2019 Datacenter 1809 (10.0.17763.1697)'], + name: ['Windows'], + version: ['1809 (10.0.17763.1697)'], + family: ['windows'], + kernel: ['1809 (10.0.17763.1697)'], + platform: ['windows'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + name: ['win2019-endpoint-1'], + architecture: ['x86_64'], + ip: ['10.1.2.3'], + id: ['d8ad572e-d224-4044-a57d-f5a84c0dfe5d'], + }, + event: { + category: ['network'], + kind: ['event'], + module: ['endpoint'], + action: ['http_request'], + type: ['protocol'], + id: ['LzzWB9jjGmCwGMvk++++FD+p'], + dataset: ['endpoint.events.network'], + }, + process: { + name: ['svchost.exe'], + pid: [2232], + entity_id: [ + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTIyMzItMTMyNTUwNzg2ODkuNTA1NzEzMDA=', + ], + executable: ['C:\\Windows\\System32\\svchost.exe'], + }, + destination: { + geo: { + region_name: ['Arizona'], + continent_name: ['North America'], + city_name: ['Phoenix'], + country_name: ['United States'], + region_iso_code: ['US-AZ'], + country_iso_code: ['US'], + }, + port: [80], + ip: ['10.11.12.13'], + }, + source: { + ip: ['10.1.2.3'], + port: [51570], + }, + http: { + request: { + body: { + content: [ + 'GET /msdownload/update/v3/static/trustedr/en/authrootstl.cab?b3d6249cb8dde683 HTTP/1.1\r\nConnection: Keep-Alive\r\nAccept: */*\r\nIf-Modified-Since: Fri, 15 Jan 2021 00:46:38 GMT\r\nIf-None-Match: "0ebbae1d7ead61:0"\r\nUser-Agent: Microsoft-CryptoAPI/10.0\r\nHost: ctldl.windowsupdate.com\r\n\r\n', + ], + bytes: [281], + }, + }, + }, + agent: { + type: ['endpoint'], + }, + user: { + name: ['NETWORK SERVICE'], + domain: ['NT AUTHORITY'], + }, + network: { + protocol: ['http'], + direction: ['outgoing'], + transport: ['tcp'], + }, + message: ['Endpoint network event'], + timestamp: '2021-02-08T19:19:38.241Z', + _id: '5Qwdg3cBX5UUcOOY03W7', +}; + +export const mockEndpointProcessExecEvent: Ecs = { + process: { + hash: { + md5: ['fbc61bd19421211e341e6d9b3f65e334'], + sha256: ['4bc018ac461706496302d1faab0a8bb39aad974eb432758665103165f3a2dd2b'], + sha1: ['1dc525922869533265fbeac8f7d3021489b60129'], + }, + name: ['mdworker_shared'], + parent: { + name: ['launchd'], + pid: [1], + }, + pid: [4454], + entity_id: [ + 'OTA1ZDkzMTctMjIxOS00ZjQ1LTg4NTMtYzNiYzk1NGU1ZGU4LTQ0NTQtMTMyNTY3NjYwMDEuNzIwMjkwMDA=', + ], + executable: [ + '/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/Metadata.framework/Versions/A/Support/mdworker_shared', + ], + args: [ + '/System/Library/Frameworks/CoreServices.framework/Frameworks/Metadata.framework/Versions/A/Support/mdworker_shared', + '-s', + 'mdworker', + '-c', + 'MDSImporterWorker', + '-m', + 'com.apple.mdworker.shared', + ], + }, + host: { + os: { + full: ['macOS 10.14.6'], + name: ['macOS'], + version: ['10.14.6'], + family: ['macos'], + kernel: [ + 'Darwin Kernel Version 18.7.0: Mon Aug 31 20:53:32 PDT 2020; root:xnu-4903.278.44~1/RELEASE_X86_64', + ], + platform: ['macos'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + name: ['test-mac.local'], + architecture: ['x86_64'], + ip: ['10.1.2.3'], + id: ['fce6b9f1-5c09-d8f0-3d99-9ecb30f995df'], + }, + event: { + category: ['process'], + kind: ['event'], + module: ['endpoint'], + action: ['exec'], + type: ['start'], + id: ['LuH/UjERrFf60dea+++++NW7'], + dataset: ['endpoint.events.process'], + }, + user: { + id: ['501'], + name: ['admin'], + }, + agent: { + type: ['endpoint'], + }, + message: ['Endpoint process event'], + timestamp: '2021-02-02T19:00:01.972Z', + _id: '8lkaZHcBGrBB52F2aN8c', +}; + +export const mockEndpointProcessForkEvent: Ecs = { + process: { + hash: { + md5: ['24a77cf54ab89f3d0772c65204074710'], + sha256: ['cbf3d059cc9f9c0adff5ef15bf331b95ab381837fa0adecd965a41b5846f4bd4'], + sha1: ['6cc7c36da55c7af0969539fae73768fbef11aa1a'], + }, + name: ['zoom.us'], + parent: { + name: ['zoom.us'], + pid: [3961], + }, + pid: [4042], + entity_id: [ + 'OTA1ZDkzMTctMjIxOS00ZjQ1LTg4NTMtYzNiYzk1NGU1ZGU4LTQwNDItMTMyNTY2ODI5MjQuNzYxNDAwMA==', + ], + executable: ['/Applications/zoom.us.app/Contents/MacOS/zoom.us'], + args: ['/Applications/zoom.us.app/Contents/MacOS/zoom.us'], + }, + host: { + os: { + full: ['macOS 10.14.6'], + name: ['macOS'], + version: ['10.14.6'], + family: ['macos'], + kernel: [ + 'Darwin Kernel Version 18.7.0: Mon Aug 31 20:53:32 PDT 2020; root:xnu-4903.278.44~1/RELEASE_X86_64', + ], + platform: ['macos'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + name: ['test-mac.local'], + architecture: ['x86_64'], + ip: ['10.1.2.3'], + id: ['fce6b9f1-5c09-d8f0-3d99-9ecb30f995df'], + }, + event: { + category: ['process'], + kind: ['event'], + module: ['endpoint'], + action: ['fork'], + type: ['start'], + id: ['LuH/UjERrFf60dea+++++KYC'], + dataset: ['endpoint.events.process'], + }, + user: { + id: ['501'], + name: ['admin'], + }, + agent: { + type: ['endpoint'], + }, + message: ['Endpoint process event'], + timestamp: '2021-02-01T19:55:24.907Z', + _id: 'KXomX3cBGrBB52F2S9XY', +}; + export const mockEndgameIpv4ConnectionAcceptEvent: Ecs = { _id: 'LsjPcG0BOpWiDweSCNfu', user: { diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts b/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts index cc75518cf2899d..f016b6cc34539c 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts +++ b/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts @@ -1302,3 +1302,186 @@ export const mockDnsEvent: Ecs = { ip: ['10.9.9.9'], }, }; + +export const mockEndpointProcessExecutionMalwarePreventionAlert: Ecs = { + process: { + hash: { + md5: ['177afc1eb0be88eb9983fb74111260c4'], + sha256: ['3be13acde2f4dcded4fd8d518a513bfc9882407a6e384ffb17d12710db7d76fb'], + sha1: ['f573b85e9beb32121f1949217947b2adc6749e3d'], + }, + entity_id: [ + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTY5MjAtMTMyNDg5OTk2OTAuNDgzMzA3NzAw', + ], + executable: [ + 'C:\\Users\\sean\\Downloads\\3be13acde2f4dcded4fd8d518a513bfc9882407a6e384ffb17d12710db7d76fb.exe', + ], + name: [ + 'C:\\Users\\sean\\Downloads\\3be13acde2f4dcded4fd8d518a513bfc9882407a6e384ffb17d12710db7d76fb.exe', + ], + pid: [6920], + args: [ + 'C:\\Users\\sean\\Downloads\\3be13acde2f4dcded4fd8d518a513bfc9882407a6e384ffb17d12710db7d76fb.exe', + ], + }, + host: { + os: { + full: ['Windows Server 2019 Datacenter 1809 (10.0.17763.1518)'], + name: ['Windows'], + version: ['1809 (10.0.17763.1518)'], + platform: ['windows'], + family: ['windows'], + kernel: ['1809 (10.0.17763.1518)'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + architecture: ['x86_64'], + ip: ['10.1.2.3'], + id: ['d8ad572e-d224-4044-a57d-f5a84c0dfe5d'], + name: ['win2019-endpoint-1'], + }, + file: { + mtime: ['2020-11-04T21:40:51.494Z'], + path: [ + 'C:\\Users\\sean\\Downloads\\3be13acde2f4dcded4fd8d518a513bfc9882407a6e384ffb17d12710db7d76fb.exe', + ], + owner: ['sean'], + hash: { + md5: ['177afc1eb0be88eb9983fb74111260c4'], + sha256: ['3be13acde2f4dcded4fd8d518a513bfc9882407a6e384ffb17d12710db7d76fb'], + sha1: ['f573b85e9beb32121f1949217947b2adc6749e3d'], + }, + name: ['3be13acde2f4dcded4fd8d518a513bfc9882407a6e384ffb17d12710db7d76fb.exe'], + extension: ['exe'], + size: [1604112], + }, + event: { + category: ['malware', 'intrusion_detection', 'process'], + outcome: ['success'], + severity: [73], + code: ['malicious_file'], + action: ['execution'], + id: ['LsuMZVr+sdhvehVM++++Gp2Y'], + kind: ['alert'], + created: ['2020-11-04T21:41:30.533Z'], + module: ['endpoint'], + type: ['info', 'start', 'denied'], + dataset: ['endpoint.alerts'], + }, + agent: { + type: ['endpoint'], + }, + timestamp: '2020-11-04T21:41:30.533Z', + message: ['Malware Prevention Alert'], + _id: '0dA2lXUBn9bLIbfPkY7d', +}; + +export const mockEndpointLibraryLoadEvent: Ecs = { + file: { + path: ['C:\\Windows\\System32\\bcrypt.dll'], + hash: { + md5: ['00439016776de367bad087d739a03797'], + sha1: ['2c4ba5c1482987d50a182bad915f52cd6611ee63'], + sha256: ['e70f5d8f87aab14e3160227d38387889befbe37fa4f8f5adc59eff52804b35fd'], + }, + name: ['bcrypt.dll'], + }, + host: { + os: { + full: ['Windows Server 2019 Datacenter 1809 (10.0.17763.1697)'], + name: ['Windows'], + version: ['1809 (10.0.17763.1697)'], + family: ['windows'], + kernel: ['1809 (10.0.17763.1697)'], + platform: ['windows'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + name: ['win2019-endpoint-1'], + architecture: ['x86_64'], + ip: ['10.1.2.3'], + id: ['d8ad572e-d224-4044-a57d-f5a84c0dfe5d'], + }, + event: { + category: ['library'], + kind: ['event'], + created: ['2021-02-05T21:27:23.921Z'], + module: ['endpoint'], + action: ['load'], + type: ['start'], + id: ['LzzWB9jjGmCwGMvk++++Da5H'], + dataset: ['endpoint.events.library'], + }, + process: { + name: ['sshd.exe'], + pid: [9644], + entity_id: [ + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTk2NDQtMTMyNTcwMzQwNDEuNzgyMTczODAw', + ], + executable: ['C:\\Program Files\\OpenSSH-Win64\\sshd.exe'], + }, + agent: { + type: ['endpoint'], + }, + user: { + name: ['SYSTEM'], + domain: ['NT AUTHORITY'], + }, + message: ['Endpoint DLL load event'], + timestamp: '2021-02-05T21:27:23.921Z', + _id: 'IAUYdHcBGrBB52F2zo8Q', +}; + +export const mockEndpointRegistryModificationEvent: Ecs = { + host: { + os: { + full: ['Windows Server 2019 Datacenter 1809 (10.0.17763.1697)'], + name: ['Windows'], + version: ['1809 (10.0.17763.1697)'], + family: ['windows'], + kernel: ['1809 (10.0.17763.1697)'], + platform: ['windows'], + }, + mac: ['aa:bb:cc:dd:ee:ff'], + name: ['win2019-endpoint-1'], + architecture: ['x86_64'], + ip: ['10.1.2.3'], + id: ['d8ad572e-d224-4044-a57d-f5a84c0dfe5d'], + }, + event: { + category: ['registry'], + kind: ['event'], + created: ['2021-02-04T13:44:31.559Z'], + module: ['endpoint'], + action: ['modification'], + type: ['change'], + id: ['LzzWB9jjGmCwGMvk++++CbOn'], + dataset: ['endpoint.events.registry'], + }, + process: { + name: ['GoogleUpdate.exe'], + pid: [7408], + entity_id: [ + 'MWQxNWNmOWUtM2RjNy01Yjk3LWY1ODYtNzQzZjdjMjUxOGIyLTc0MDgtMTMyNTY5MTk4NDguODY4NTI0ODAw', + ], + executable: ['C:\\Program Files (x86)\\Google\\Update\\GoogleUpdate.exe'], + }, + registry: { + hive: ['HKLM'], + key: [ + 'SOFTWARE\\WOW6432Node\\Google\\Update\\ClientState\\{430FD4D0-B729-4F61-AA34-91526481799D}\\CurrentState', + ], + path: [ + 'HKLM\\SOFTWARE\\WOW6432Node\\Google\\Update\\ClientState\\{430FD4D0-B729-4F61-AA34-91526481799D}\\CurrentState\\StateValue', + ], + value: ['StateValue'], + }, + agent: { + type: ['endpoint'], + }, + user: { + name: ['SYSTEM'], + domain: ['NT AUTHORITY'], + }, + message: ['Endpoint registry event'], + timestamp: '2021-02-04T13:44:31.559Z', + _id: '4cxLbXcBGrBB52F2uOfF', +}; diff --git a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts index 351caa2df3e317..70ed497ce0caca 100644 --- a/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts +++ b/x-pack/plugins/security_solution/public/common/mock/timeline_results.ts @@ -2106,6 +2106,11 @@ export const mockTimelineModel: TimelineModel = { }, deletedEventIds: [], description: 'This is a sample rule description', + eqlOptions: { + eventCategoryField: 'event.category', + tiebreakerField: 'event.sequence', + timestampField: '@timestamp', + }, eventIdToNoteIds: {}, eventType: 'all', excludedRowRendererIds: [], @@ -2229,6 +2234,13 @@ export const defaultTimelineProps: CreateTimelineProps = { dateRange: { end: '2018-11-05T19:03:25.937Z', start: '2018-11-05T18:58:25.937Z' }, deletedEventIds: [], description: '', + eqlOptions: { + eventCategoryField: 'event.category', + query: '', + size: 100, + tiebreakerField: 'event.sequence', + timestampField: '@timestamp', + }, eventIdToNoteIds: {}, eventType: 'all', excludedRowRendererIds: [], diff --git a/x-pack/plugins/security_solution/public/common/store/types.ts b/x-pack/plugins/security_solution/public/common/store/types.ts index 3835e708514252..fbf4caad9793dc 100644 --- a/x-pack/plugins/security_solution/public/common/store/types.ts +++ b/x-pack/plugins/security_solution/public/common/store/types.ts @@ -37,7 +37,7 @@ export type StoreState = HostsPluginState & */ export type State = CombinedState; -export type KueryFilterQueryKind = 'kuery' | 'lucene'; +export type KueryFilterQueryKind = 'kuery' | 'lucene' | 'eql'; export interface KueryFilterQuery { kind: KueryFilterQueryKind; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx index 3c3d79c0c518fa..7d577659d66e20 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.test.tsx @@ -153,6 +153,13 @@ describe('alert actions', () => { }, deletedEventIds: [], description: 'This is a sample rule description', + eqlOptions: { + eventCategoryField: 'event.category', + query: '', + size: 100, + tiebreakerField: 'event.sequence', + timestampField: '@timestamp', + }, eventIdToNoteIds: {}, eventType: 'all', excludedRowRendererIds: [], @@ -414,7 +421,7 @@ describe('alert actions', () => { ...mockEcsDataWithAlert, timestamp: '2020-03-20T17:59:46.349Z', }; - const result = determineToAndFrom({ ecsData: ecsDataMock }); + const result = determineToAndFrom({ ecs: ecsDataMock }); expect(result.from).toEqual('2020-03-20T17:54:46.349Z'); expect(result.to).toEqual('2020-03-20T17:59:46.349Z'); @@ -424,7 +431,7 @@ describe('alert actions', () => { const { timestamp, ...ecsDataMock } = { ...mockEcsDataWithAlert, }; - const result = determineToAndFrom({ ecsData: ecsDataMock }); + const result = determineToAndFrom({ ecs: ecsDataMock }); expect(result.from).toEqual('2020-03-01T17:54:46.349Z'); expect(result.to).toEqual('2020-03-01T17:59:46.349Z'); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx index 9d38e2b369fa16..9f5ab7be8a117f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx @@ -8,7 +8,7 @@ /* eslint-disable complexity */ import dateMath from '@elastic/datemath'; -import { get, getOr, isEmpty, find } from 'lodash/fp'; +import { getOr, isEmpty } from 'lodash/fp'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; @@ -38,7 +38,11 @@ import { replaceTemplateFieldFromDataProviders, } from './helpers'; import { KueryFilterQueryKind } from '../../../common/store'; -import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { + DataProvider, + QueryOperator, +} from '../../../timelines/components/timeline/data_providers/data_provider'; +import { esFilters } from '../../../../../../../src/plugins/data/public'; export const getUpdateAlertsQuery = (eventIds: Readonly) => { return { @@ -46,7 +50,7 @@ export const getUpdateAlertsQuery = (eventIds: Readonly) => { bool: { filter: { terms: { - _id: [...eventIds], + _id: eventIds, }, }, }, @@ -102,17 +106,32 @@ export const updateAlertStatusAction = async ({ } }; -export const determineToAndFrom = ({ ecsData }: { ecsData: Ecs }) => { +export const determineToAndFrom = ({ ecs }: { ecs: Ecs[] | Ecs }) => { + if (Array.isArray(ecs)) { + const timestamps = ecs.reduce((acc, item) => { + if (item.timestamp != null) { + const dateTimestamp = new Date(item.timestamp); + if (!acc.includes(dateTimestamp.valueOf())) { + return [...acc, dateTimestamp.valueOf()]; + } + } + return acc; + }, []); + return { + from: new Date(Math.min(...timestamps)).toISOString(), + to: new Date(Math.max(...timestamps)).toISOString(), + }; + } + const ecsData = ecs as Ecs; const ellapsedTimeRule = moment.duration( moment().diff( - dateMath.parse(ecsData.signal?.rule?.from != null ? ecsData.signal?.rule?.from[0] : 'now-0s') + dateMath.parse(ecsData?.signal?.rule?.from != null ? ecsData.signal?.rule?.from[0] : 'now-0s') ) ); - - const from = moment(ecsData.timestamp ?? new Date()) + const from = moment(ecsData?.timestamp ?? new Date()) .subtract(ellapsedTimeRule) .toISOString(); - const to = moment(ecsData.timestamp ?? new Date()).toISOString(); + const to = moment(ecsData?.timestamp ?? new Date()).toISOString(); return { to, from }; }; @@ -128,37 +147,82 @@ const getFiltersFromRule = (filters: string[]): Filter[] => }, [] as Filter[]); export const getThresholdAggregationDataProvider = ( - ecsData: Ecs, + ecsData: Ecs | Ecs[], nonEcsData: TimelineNonEcsData[] ): DataProvider[] => { - const aggregationField = ecsData.signal?.rule?.threshold?.field!; - const aggregationValue = - get(aggregationField, ecsData) ?? find(['field', aggregationField], nonEcsData)?.value; - const dataProviderValue = Array.isArray(aggregationValue) - ? aggregationValue[0] - : aggregationValue; - - if (!dataProviderValue) { - return []; - } + const thresholdEcsData: Ecs[] = Array.isArray(ecsData) ? ecsData : [ecsData]; + return thresholdEcsData.reduce((outerAcc, thresholdData) => { + const threshold = thresholdData.signal?.rule?.threshold as string[]; - const aggregationFieldId = aggregationField.replace('.', '-'); + let aggField: string[] = []; + let thresholdResult: { + terms?: Array<{ + field?: string; + value: string; + }>; + count: number; + }; - return [ - { - and: [], - id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-${aggregationFieldId}-${dataProviderValue}`, - name: aggregationField, - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: aggregationField, - value: dataProviderValue, - operator: ':', - }, - }, - ]; + try { + thresholdResult = JSON.parse((thresholdData.signal?.threshold_result as string[])[0]); + aggField = JSON.parse(threshold[0]).field; + } catch (err) { + thresholdResult = { + terms: [ + { + field: (thresholdData.rule?.threshold as { field: string }).field, + value: (thresholdData.signal?.threshold_result as { value: string }).value, + }, + ], + count: (thresholdData.signal?.threshold_result as { count: number }).count, + }; + } + + const aggregationFields = Array.isArray(aggField) ? aggField : [aggField]; + + return [ + ...outerAcc, + ...aggregationFields.reduce((acc, aggregationField, i) => { + const aggregationValue = (thresholdResult.terms ?? []).filter( + (term: { field?: string | undefined; value: string }) => term.field === aggregationField + )[0].value; + const dataProviderValue = Array.isArray(aggregationValue) + ? aggregationValue[0] + : aggregationValue; + + if (!dataProviderValue) { + return acc; + } + + const aggregationFieldId = aggregationField.replace('.', '-'); + const dataProviderPartial = { + id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-${aggregationFieldId}-${dataProviderValue}`, + name: aggregationField, + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: aggregationField, + value: dataProviderValue, + operator: ':' as QueryOperator, + }, + }; + + if (i === 0) { + return [ + ...acc, + { + ...dataProviderPartial, + and: [], + }, + ]; + } else { + acc[0].and.push(dataProviderPartial); + return acc; + } + }, []), + ]; + }, []); }; export const isEqlRuleWithGroupId = (ecsData: Ecs) => @@ -169,20 +233,134 @@ export const isEqlRuleWithGroupId = (ecsData: Ecs) => export const isThresholdRule = (ecsData: Ecs) => ecsData.signal?.rule?.type?.length && ecsData.signal?.rule?.type[0] === 'threshold'; +export const buildAlertsKqlFilter = ( + key: '_id' | 'signal.group.id', + alertIds: string[] +): Filter[] => { + return [ + { + query: { + bool: { + filter: { + ids: { + values: alertIds, + }, + }, + }, + }, + meta: { + alias: 'Alert Ids', + negate: false, + disabled: false, + type: 'phrases', + key, + value: alertIds.join(), + params: alertIds, + }, + $state: { + store: esFilters.FilterStateStore.APP_STATE, + }, + }, + ]; +}; + +export const buildTimelineDataProviderOrFilter = ( + alertsIds: string[], + _id: string +): { filters: Filter[]; dataProviders: DataProvider[] } => { + if (!isEmpty(alertsIds)) { + return { + dataProviders: [], + filters: buildAlertsKqlFilter('_id', alertsIds), + }; + } + return { + filters: [], + dataProviders: [ + { + and: [], + id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-alert-id-${_id}`, + name: _id, + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: '_id', + value: _id, + operator: ':' as const, + }, + }, + ], + }; +}; + +export const buildEqlDataProviderOrFilter = ( + alertsIds: string[], + ecs: Ecs[] | Ecs +): { filters: Filter[]; dataProviders: DataProvider[] } => { + if (!isEmpty(alertsIds) && Array.isArray(ecs)) { + return { + dataProviders: [], + filters: buildAlertsKqlFilter( + 'signal.group.id', + ecs.reduce((acc, ecsData) => { + const signalGroupId = ecsData.signal?.group?.id?.length + ? ecsData.signal?.group?.id[0] + : 'unknown-signal-group-id'; + if (!acc.includes(signalGroupId)) { + return [...acc, signalGroupId]; + } + return acc; + }, []) + ), + }; + } else if (!Array.isArray(ecs)) { + const signalGroupId = ecs.signal?.group?.id?.length + ? ecs.signal?.group?.id[0] + : 'unknown-signal-group-id'; + return { + dataProviders: [ + { + and: [], + id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-alert-id-${signalGroupId}`, + name: ecs._id, + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'signal.group.id', + value: signalGroupId, + operator: ':' as const, + }, + }, + ], + filters: [], + }; + } + return { filters: [], dataProviders: [] }; +}; + export const sendAlertToTimelineAction = async ({ apolloClient, createTimeline, - ecsData, + ecsData: ecs, nonEcsData, updateTimelineIsLoading, searchStrategyClient, }: SendAlertToTimelineActionProps) => { + /* FUTURE DEVELOPER + * We are making an assumption here that if you have an array of ecs data they are all coming from the same rule + * but we still want to determine the filter for each alerts + */ + const ecsData: Ecs = Array.isArray(ecs) && ecs.length > 0 ? ecs[0] : (ecs as Ecs); + const alertIds = Array.isArray(ecs) ? ecs.map((d) => d._id) : []; const noteContent = ecsData.signal?.rule?.note != null ? ecsData.signal?.rule?.note[0] : ''; const timelineId = ecsData.signal?.rule?.timeline_id != null ? ecsData.signal?.rule?.timeline_id[0] : ''; - const { to, from } = determineToAndFrom({ ecsData }); + const { to, from } = determineToAndFrom({ ecs }); - if (!isEmpty(timelineId) && apolloClient != null) { + // For now we do not want to populate the template timeline if we have alertIds + if (!isEmpty(timelineId) && apolloClient != null && isEmpty(alertIds)) { try { updateTimelineIsLoading({ id: TimelineId.active, isLoading: true }); const [responseTimeline, eventDataResp] = await Promise.all([ @@ -275,7 +453,7 @@ export const sendAlertToTimelineAction = async ({ ...timelineDefaults, description: `_id: ${ecsData._id}`, filters: getFiltersFromRule(ecsData.signal?.rule?.filters as string[]), - dataProviders: [...getThresholdAggregationDataProvider(ecsData, nonEcsData)], + dataProviders: getThresholdAggregationDataProvider(ecsData, nonEcsData), id: TimelineId.active, indexNames: [], dateRange: { @@ -301,36 +479,11 @@ export const sendAlertToTimelineAction = async ({ ruleNote: noteContent, }); } else { - let dataProviders = [ - { - and: [], - id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-alert-id-${ecsData._id}`, - name: ecsData._id, - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: '_id', - value: ecsData._id, - operator: ':' as const, - }, - }, - ]; + let { dataProviders, filters } = buildTimelineDataProviderOrFilter(alertIds ?? [], ecsData._id); if (isEqlRuleWithGroupId(ecsData)) { - const signalGroupId = ecsData.signal?.group?.id?.length - ? ecsData.signal?.group?.id[0] - : 'unknown-signal-group-id'; - dataProviders = [ - { - ...dataProviders[0], - id: `send-alert-to-timeline-action-default-draggable-event-details-value-formatted-field-value-${TimelineId.active}-alert-id-${signalGroupId}`, - queryMatch: { - field: 'signal.group.id', - value: signalGroupId, - operator: ':' as const, - }, - }, - ]; + const tempEql = buildEqlDataProviderOrFilter(alertIds ?? [], ecs); + dataProviders = tempEql.dataProviders; + filters = tempEql.filters; } return createTimeline({ @@ -346,6 +499,7 @@ export const sendAlertToTimelineAction = async ({ end: to, }, eventType: 'all', + filters, kqlQuery: { filterQuery: { kuery: { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index 9236700c2b2e24..b2e5638ff120ee 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -18,6 +18,7 @@ import { import styled from 'styled-components'; import { getOr } from 'lodash/fp'; +import { buildGetAlertByIdQuery } from '../../../../common/components/exceptions/helpers'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; import { TimelineId } from '../../../../../common/types/timeline'; import { DEFAULT_INDEX_PATTERN } from '../../../../../common/constants'; @@ -29,7 +30,10 @@ import { FILTER_OPEN, FILTER_CLOSED, FILTER_IN_PROGRESS } from '../alerts_filter import { updateAlertStatusAction } from '../actions'; import { SetEventsDeletedProps, SetEventsLoadingProps } from '../types'; import { Ecs } from '../../../../../common/ecs'; -import { AddExceptionModal } from '../../../../common/components/exceptions/add_exception_modal'; +import { + AddExceptionModal, + AddExceptionModalProps, +} from '../../../../common/components/exceptions/add_exception_modal'; import * as i18nCommon from '../../../../common/translations'; import * as i18n from '../translations'; import { @@ -40,6 +44,9 @@ import { import { inputsModel } from '../../../../common/store'; import { useUserData } from '../../user_info'; import { ExceptionListType } from '../../../../../common/shared_imports'; +import { AlertData, EcsHit } from '../../../../common/components/exceptions/types'; +import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query'; +import { useSignalIndex } from '../../../containers/detection_engine/alerts/use_signal_index'; interface AlertContextMenuProps { ariaLabel?: string; @@ -386,12 +393,12 @@ const AlertContextMenuComponent: React.FC = ({
    {exceptionModalType != null && ruleId != null && ecsRowData != null && ( - & { + ecsData: Ecs; +}; + +/** + * This component exists to fetch needed data outside of the AddExceptionModal + * Due to the conditional nature of the modal and how we use the `ecsData` field, + * we cannot use the fetch hook within the modal component itself + */ +const AddExceptionModalWrapper: React.FC = ({ + ruleName, + ruleId, + ruleIndices, + exceptionListType, + ecsData, + onCancel, + onConfirm, + alertStatus, + onRuleChange, +}) => { + const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex(); + + const { loading: isLoadingAlertData, data } = useQueryAlerts( + buildGetAlertByIdQuery(ecsData?._id), + signalIndexName + ); + + const enrichedAlert: AlertData | undefined = useMemo(() => { + if (isLoadingAlertData === false) { + const hit = data?.hits.hits[0]; + if (!hit) { + return undefined; + } + const { _id, _index, _source } = hit; + return { ..._source, _id, _index }; + } + }, [data?.hits.hits, isLoadingAlertData]); + + const isLoading = isLoadingAlertData && isSignalIndexLoading; + + return ( + + ); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx index d6813fdef8e54e..2f0fee980c2181 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx @@ -24,14 +24,18 @@ import { } from '../translations'; interface InvestigateInTimelineActionProps { - ariaLabel?: string; - ecsRowData: Ecs; + ecsRowData: Ecs | Ecs[] | null; nonEcsRowData: TimelineNonEcsData[]; + ariaLabel?: string; + alertIds?: string[]; + fetchEcsAlertsData?: (alertIds?: string[]) => Promise; } const InvestigateInTimelineActionComponent: React.FC = ({ ariaLabel = ACTION_INVESTIGATE_IN_TIMELINE_ARIA_LABEL, + alertIds, ecsRowData, + fetchEcsAlertsData, nonEcsRowData, }) => { const { @@ -66,25 +70,42 @@ const InvestigateInTimelineActionComponent: React.FC - sendAlertToTimelineAction({ - apolloClient, - createTimeline, - ecsData: ecsRowData, - nonEcsData: nonEcsRowData, - searchStrategyClient, - updateTimelineIsLoading, - }), - [ - apolloClient, - createTimeline, - ecsRowData, - nonEcsRowData, - searchStrategyClient, - updateTimelineIsLoading, - ] - ); + const investigateInTimelineAlertClick = useCallback(async () => { + try { + if (ecsRowData != null) { + await sendAlertToTimelineAction({ + apolloClient, + createTimeline, + ecsData: ecsRowData, + nonEcsData: nonEcsRowData, + searchStrategyClient, + updateTimelineIsLoading, + }); + } + if (ecsRowData == null && fetchEcsAlertsData) { + const alertsEcsData = await fetchEcsAlertsData(alertIds); + await sendAlertToTimelineAction({ + apolloClient, + createTimeline, + ecsData: alertsEcsData, + nonEcsData: nonEcsRowData, + searchStrategyClient, + updateTimelineIsLoading, + }); + } + } catch { + // TODO show a toaster that something went wrong + } + }, [ + alertIds, + apolloClient, + createTimeline, + ecsRowData, + fetchEcsAlertsData, + nonEcsRowData, + searchStrategyClient, + updateTimelineIsLoading, + ]); return ( ; createTimeline: CreateTimeline; - ecsData: Ecs; + ecsData: Ecs | Ecs[]; nonEcsData: TimelineNonEcsData[]; updateTimelineIsLoading: UpdateTimelineLoading; searchStrategyClient: ISearchStart; diff --git a/x-pack/plugins/security_solution/public/detections/components/callouts/need_admin_for_update_callout/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/callouts/need_admin_for_update_callout/index.test.tsx new file mode 100644 index 00000000000000..66b2bae98c1ae4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/callouts/need_admin_for_update_callout/index.test.tsx @@ -0,0 +1,195 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mount } from 'enzyme'; +import React from 'react'; +import { NeedAdminForUpdateRulesCallOut } from './index'; +import { TestProviders } from '../../../../common/mock'; +import * as userInfo from '../../user_info'; + +describe('need_admin_for_update_callout', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('hasIndexManage is "null"', () => { + const hasIndexManage = null; + test('Does NOT render when "signalIndexMappingOutdated" is true', () => { + jest + .spyOn(userInfo, 'useUserData') + .mockImplementation( + jest.fn().mockReturnValue([{ signalIndexMappingOutdated: true, hasIndexManage }]) + ); + const wrapper = mount( + + + + ); + expect(wrapper.exists('[data-test-subj="callout-need-admin-for-update-rules"]')).toEqual( + false + ); + }); + + test('Does not render a button as this is always persistent', () => { + jest + .spyOn(userInfo, 'useUserData') + .mockImplementation(jest.fn().mockReturnValue([{ signalIndexMappingOutdated: true }])); + const wrapper = mount( + + + + ); + expect(wrapper.exists('[data-test-subj="callout-dismiss-btn"]')).toEqual(false); + }); + + test('Does NOT render when signalIndexMappingOutdated is false', () => { + jest + .spyOn(userInfo, 'useUserData') + .mockImplementation(jest.fn().mockReturnValue([{ signalIndexMappingOutdated: false }])); + const wrapper = mount( + + + + ); + expect(wrapper.exists('[data-test-subj="callout-need-admin-for-update-rules"]')).toEqual( + false + ); + }); + + test('Does NOT render when signalIndexMappingOutdated is null', () => { + jest + .spyOn(userInfo, 'useUserData') + .mockImplementation(jest.fn().mockReturnValue([{ signalIndexMappingOutdated: null }])); + const wrapper = mount( + + + + ); + expect(wrapper.exists('[data-test-subj="callout-need-admin-for-update-rules"]')).toEqual( + false + ); + }); + }); + + describe('hasIndexManage is "false"', () => { + const hasIndexManage = false; + test('renders when "signalIndexMappingOutdated" is true', () => { + jest + .spyOn(userInfo, 'useUserData') + .mockImplementation( + jest.fn().mockReturnValue([{ signalIndexMappingOutdated: true, hasIndexManage }]) + ); + const wrapper = mount( + + + + ); + expect(wrapper.exists('[data-test-subj="callout-need-admin-for-update-rules"]')).toEqual( + true + ); + }); + + test('Does not render a button as this is always persistent', () => { + jest + .spyOn(userInfo, 'useUserData') + .mockImplementation(jest.fn().mockReturnValue([{ signalIndexMappingOutdated: true }])); + const wrapper = mount( + + + + ); + expect(wrapper.exists('[data-test-subj="callout-dismiss-btn"]')).toEqual(false); + }); + + test('Does NOT render when signalIndexMappingOutdated is false', () => { + jest + .spyOn(userInfo, 'useUserData') + .mockImplementation(jest.fn().mockReturnValue([{ signalIndexMappingOutdated: false }])); + const wrapper = mount( + + + + ); + expect(wrapper.exists('[data-test-subj="callout-need-admin-for-update-rules"]')).toEqual( + false + ); + }); + + test('Does NOT render when signalIndexMappingOutdated is null', () => { + jest + .spyOn(userInfo, 'useUserData') + .mockImplementation(jest.fn().mockReturnValue([{ signalIndexMappingOutdated: null }])); + const wrapper = mount( + + + + ); + expect(wrapper.exists('[data-test-subj="callout-need-admin-for-update-rules"]')).toEqual( + false + ); + }); + }); + + describe('hasIndexManage is "true"', () => { + const hasIndexManage = true; + test('Does not render when "signalIndexMappingOutdated" is true', () => { + jest + .spyOn(userInfo, 'useUserData') + .mockImplementation( + jest.fn().mockReturnValue([{ signalIndexMappingOutdated: true, hasIndexManage }]) + ); + const wrapper = mount( + + + + ); + expect(wrapper.exists('[data-test-subj="callout-need-admin-for-update-rules"]')).toEqual( + false + ); + }); + + test('Does not render a button as this is always persistent', () => { + jest + .spyOn(userInfo, 'useUserData') + .mockImplementation(jest.fn().mockReturnValue([{ signalIndexMappingOutdated: true }])); + const wrapper = mount( + + + + ); + expect(wrapper.exists('[data-test-subj="callout-dismiss-btn"]')).toEqual(false); + }); + + test('Does NOT render when signalIndexMappingOutdated is false', () => { + jest + .spyOn(userInfo, 'useUserData') + .mockImplementation(jest.fn().mockReturnValue([{ signalIndexMappingOutdated: false }])); + const wrapper = mount( + + + + ); + expect(wrapper.exists('[data-test-subj="callout-need-admin-for-update-rules"]')).toEqual( + false + ); + }); + + test('Does NOT render when signalIndexMappingOutdated is null', () => { + jest + .spyOn(userInfo, 'useUserData') + .mockImplementation(jest.fn().mockReturnValue([{ signalIndexMappingOutdated: null }])); + const wrapper = mount( + + + + ); + expect(wrapper.exists('[data-test-subj="callout-need-admin-for-update-rules"]')).toEqual( + false + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/callouts/need_admin_for_update_callout/index.tsx b/x-pack/plugins/security_solution/public/detections/components/callouts/need_admin_for_update_callout/index.tsx new file mode 100644 index 00000000000000..fd0be8e0021933 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/callouts/need_admin_for_update_callout/index.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { CallOutMessage, CallOutPersistentSwitcher } from '../../../../common/components/callouts'; +import { useUserData } from '../../user_info'; + +import * as i18n from './translations'; + +const needAdminForUpdateRulesMessage: CallOutMessage = { + type: 'primary', + id: 'need-admin-for-update-rules', + title: i18n.NEED_ADMIN_CALLOUT_TITLE, + description: i18n.needAdminForUpdateCallOutBody(), +}; + +/** + * Callout component that lets the user know that an administrator is needed for performing + * and auto-update of signals or not. For this component to render the user must: + * - Have the permissions to be able to read "signalIndexMappingOutdated" and that condition is "true" + * - Have the permissions to be able to read "hasIndexManage" and that condition is "false" + * + * Some users do not have sufficient privileges to be able to determine if "signalIndexMappingOutdated" + * is outdated or not. Same could apply to "hasIndexManage". When users do not have enough permissions + * to determine if "signalIndexMappingOutdated" is true or false, the permissions system returns a "null" + * instead. + * + * If the user has the permissions to see that signalIndexMappingOutdated is true and that + * hasIndexManage is also true, then the user should be performing the update on the page which is + * why we do not show it for that condition. + */ +const NeedAdminForUpdateCallOutComponent = (): JSX.Element => { + const [{ signalIndexMappingOutdated, hasIndexManage }] = useUserData(); + + const signalIndexMappingIsOutdated = + signalIndexMappingOutdated != null && signalIndexMappingOutdated; + + const userDoesntHaveIndexManage = hasIndexManage != null && !hasIndexManage; + + return ( + + ); +}; + +export const NeedAdminForUpdateRulesCallOut = memo(NeedAdminForUpdateCallOutComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/callouts/need_admin_for_update_callout/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/callouts/need_admin_for_update_callout/translations.tsx new file mode 100644 index 00000000000000..791093788b8e1e --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/callouts/need_admin_for_update_callout/translations.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + SecuritySolutionRequirementsLink, + DetectionsRequirementsLink, +} from '../../../../common/components/links_to_docs'; + +export const NEED_ADMIN_CALLOUT_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.needAdminForUpdateCallOutBody.messageTitle', + { + defaultMessage: 'Administration permissions required for alert migration', + } +); + +/** + * Returns the formatted message of the call out body as a JSX Element with both the message + * and two documentation links. + */ +export const needAdminForUpdateCallOutBody = (): JSX.Element => ( + + +

    + ), + docs: ( +
      +
    • + +
    • +
    • + +
    • +
    + ), + }} + /> +); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/eql_query_bar.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/eql_query_bar.tsx index df09fc1d12e58e..e37fd2eb26ff9b 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/eql_query_bar.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/eql_query_bar/eql_query_bar.tsx @@ -15,6 +15,11 @@ import { DefineStepRule } from '../../../pages/detection_engine/rules/types'; import * as i18n from './translations'; import { EqlQueryBarFooter } from './footer'; import { getValidationResults } from './validators'; +import { + EqlOptionsData, + EqlOptionsSelected, + FieldsEqlOptions, +} from '../../../../../common/search_strategy'; const TextArea = styled(EuiTextArea)` display: block; @@ -28,14 +33,22 @@ export interface EqlQueryBarProps { dataTestSubj: string; field: FieldHook; idAria?: string; + optionsData?: EqlOptionsData; + optionsSelected?: EqlOptionsSelected; + onOptionsChange?: (field: FieldsEqlOptions, newValue: string | null) => void; onValidityChange?: (arg: boolean) => void; + onValiditingChange?: (arg: boolean) => void; } export const EqlQueryBar: FC = ({ dataTestSubj, field, idAria, + optionsData, + optionsSelected, + onOptionsChange, onValidityChange, + onValiditingChange, }) => { const { addError } = useAppToasts(); const [errorMessages, setErrorMessages] = useState([]); @@ -62,10 +75,18 @@ export const EqlQueryBar: FC = ({ } }, [error, addError]); + useEffect(() => { + if (onValiditingChange) { + onValiditingChange(isValidating); + } + }, [isValidating, onValiditingChange]); + const handleChange = useCallback( (e: ChangeEvent) => { const newQuery = e.target.value; - + if (onValiditingChange) { + onValiditingChange(true); + } setErrorMessages([]); setValue({ filters: [], @@ -75,7 +96,7 @@ export const EqlQueryBar: FC = ({ }, }); }, - [setValue] + [setValue, onValiditingChange] ); return ( @@ -84,7 +105,7 @@ export const EqlQueryBar: FC = ({ labelAppend={field.labelAppend} helpText={field.helpText} error={message} - isInvalid={!isValid} + isInvalid={!isValid && !isValidating} fullWidth data-test-subj={dataTestSubj} describedByIds={idAria ? [idAria] : undefined} @@ -93,11 +114,17 @@ export const EqlQueryBar: FC = ({