diff --git a/.ci/es-snapshots/Jenkinsfile_build_es b/.ci/es-snapshots/Jenkinsfile_build_es index aafdf06433c6d..7ace3b441144c 100644 --- a/.ci/es-snapshots/Jenkinsfile_build_es +++ b/.ci/es-snapshots/Jenkinsfile_build_es @@ -25,90 +25,92 @@ def PROMOTE_WITHOUT_VERIFY = !!params.PROMOTE_WITHOUT_VERIFICATION timeout(time: 120, unit: 'MINUTES') { timestamps { ansiColor('xterm') { - node(workers.label('l')) { - catchErrors { - def VERSION - def SNAPSHOT_ID - def DESTINATION - - def scmVars = checkoutEs(ES_BRANCH) - def GIT_COMMIT = scmVars.GIT_COMMIT - def GIT_COMMIT_SHORT = sh(script: "git rev-parse --short ${GIT_COMMIT}", returnStdout: true).trim() - - buildArchives('to-archive') - - dir('to-archive') { - def now = new Date() - def date = now.format("yyyyMMdd-HHmmss") - - def filesRaw = sh(script: "ls -1", returnStdout: true).trim() - def files = filesRaw - .split("\n") - .collect { filename -> - // Filename examples - // elasticsearch-oss-8.0.0-SNAPSHOT-linux-x86_64.tar.gz - // elasticsearch-8.0.0-SNAPSHOT-linux-x86_64.tar.gz - def parts = filename.replace("elasticsearch-oss", "oss").split("-") - - VERSION = VERSION ?: parts[1] - SNAPSHOT_ID = SNAPSHOT_ID ?: "${date}_${GIT_COMMIT_SHORT}" - DESTINATION = DESTINATION ?: "${VERSION}/archives/${SNAPSHOT_ID}" - - return [ - filename: filename, - checksum: filename + '.sha512', - url: "https://storage.googleapis.com/kibana-ci-es-snapshots-daily/${DESTINATION}/${filename}".toString(), - version: parts[1], - platform: parts[3], - architecture: parts[4].split('\\.')[0], - license: parts[0] == 'oss' ? 'oss' : 'default', + slackNotifications.onFailure { + node(workers.label('l')) { + catchErrors { + def VERSION + def SNAPSHOT_ID + def DESTINATION + + def scmVars = checkoutEs(ES_BRANCH) + def GIT_COMMIT = scmVars.GIT_COMMIT + def GIT_COMMIT_SHORT = sh(script: "git rev-parse --short ${GIT_COMMIT}", returnStdout: true).trim() + + buildArchives('to-archive') + + dir('to-archive') { + def now = new Date() + def date = now.format("yyyyMMdd-HHmmss") + + def filesRaw = sh(script: "ls -1", returnStdout: true).trim() + def files = filesRaw + .split("\n") + .collect { filename -> + // Filename examples + // elasticsearch-oss-8.0.0-SNAPSHOT-linux-x86_64.tar.gz + // elasticsearch-8.0.0-SNAPSHOT-linux-x86_64.tar.gz + def parts = filename.replace("elasticsearch-oss", "oss").split("-") + + VERSION = VERSION ?: parts[1] + SNAPSHOT_ID = SNAPSHOT_ID ?: "${date}_${GIT_COMMIT_SHORT}" + DESTINATION = DESTINATION ?: "${VERSION}/archives/${SNAPSHOT_ID}" + + return [ + filename: filename, + checksum: filename + '.sha512', + url: "https://storage.googleapis.com/kibana-ci-es-snapshots-daily/${DESTINATION}/${filename}".toString(), + version: parts[1], + platform: parts[3], + architecture: parts[4].split('\\.')[0], + license: parts[0] == 'oss' ? 'oss' : 'default', + ] + } + + sh 'find * -exec bash -c "shasum -a 512 {} > {}.sha512" \\;' + + def manifest = [ + bucket: "kibana-ci-es-snapshots-daily/${DESTINATION}".toString(), + branch: ES_BRANCH, + sha: GIT_COMMIT, + sha_short: GIT_COMMIT_SHORT, + version: VERSION, + generated: now.format("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone("UTC")), + archives: files, + ] + def manifestJson = toJSON(manifest).toString() + writeFile file: 'manifest.json', text: manifestJson + + upload(DESTINATION, '*.*') + + sh "cp manifest.json manifest-latest.json" + upload(VERSION, 'manifest-latest.json') + } + + if (PROMOTE_WITHOUT_VERIFY) { + esSnapshots.promote(VERSION, SNAPSHOT_ID) + + emailext( + to: 'build-kibana@elastic.co', + subject: "ES snapshot promoted without verification: ${params.ES_BRANCH}", + body: '${SCRIPT,template="groovy-html.template"}', + mimeType: 'text/html', + ) + } else { + build( + propagate: false, + wait: false, + job: 'elasticsearch+snapshots+verify', + parameters: [ + string(name: 'branch_specifier', value: branch_specifier), + string(name: 'SNAPSHOT_VERSION', value: VERSION), + string(name: 'SNAPSHOT_ID', value: SNAPSHOT_ID), ] - } - - sh 'find * -exec bash -c "shasum -a 512 {} > {}.sha512" \\;' - - def manifest = [ - bucket: "kibana-ci-es-snapshots-daily/${DESTINATION}".toString(), - branch: ES_BRANCH, - sha: GIT_COMMIT, - sha_short: GIT_COMMIT_SHORT, - version: VERSION, - generated: now.format("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone("UTC")), - archives: files, - ] - def manifestJson = toJSON(manifest).toString() - writeFile file: 'manifest.json', text: manifestJson - - upload(DESTINATION, '*.*') - - sh "cp manifest.json manifest-latest.json" - upload(VERSION, 'manifest-latest.json') + ) + } } - if (PROMOTE_WITHOUT_VERIFY) { - esSnapshots.promote(VERSION, SNAPSHOT_ID) - - emailext( - to: 'build-kibana@elastic.co', - subject: "ES snapshot promoted without verification: ${params.ES_BRANCH}", - body: '${SCRIPT,template="groovy-html.template"}', - mimeType: 'text/html', - ) - } else { - build( - propagate: false, - wait: false, - job: 'elasticsearch+snapshots+verify', - parameters: [ - string(name: 'branch_specifier', value: branch_specifier), - string(name: 'SNAPSHOT_VERSION', value: VERSION), - string(name: 'SNAPSHOT_ID', value: SNAPSHOT_ID), - ] - ) - } + kibanaPipeline.sendMail() } - - kibanaPipeline.sendMail() } } } diff --git a/.ci/teamcity/setup_ci_stats.js b/.ci/teamcity/setup_ci_stats.js index 6b381530d9bb7..882ad119a3db3 100644 --- a/.ci/teamcity/setup_ci_stats.js +++ b/.ci/teamcity/setup_ci_stats.js @@ -24,7 +24,7 @@ const ciStats = require('./ci_stats'); branch: process.env.GIT_BRANCH.replace(/^(refs\/heads\/|origin\/)/, ''), commit: process.env.GIT_COMMIT, targetBranch: process.env.GITHUB_PR_TARGET_BRANCH || null, - mergeBase: null, // TODO + mergeBase: process.env.GITHUB_PR_MERGE_BASE || null, }); } catch (ex) { console.error(ex); diff --git a/.ci/teamcity/setup_env.sh b/.ci/teamcity/setup_env.sh index 982d129dae2a6..09fcd0357fb7b 100755 --- a/.ci/teamcity/setup_env.sh +++ b/.ci/teamcity/setup_env.sh @@ -33,15 +33,17 @@ fi if is_pr; then tc_set_env ELASTIC_APM_ACTIVE false - tc_set_env CHECKS_REPORTER_ACTIVE true + tc_set_env CHECKS_REPORTER_ACTIVE "${CI_REPORTING_ENABLED-}" # These can be removed once we're not supporting Jenkins and TeamCity at the same time # These are primarily used by github checks reporter and can be configured via /github_checks_api.json tc_set_env ghprbGhRepository "elastic/kibana" # TODO? tc_set_env ghprbActualCommit "$GITHUB_PR_TRIGGERED_SHA" tc_set_env BUILD_URL "$TEAMCITY_BUILD_URL" + + set_git_merge_base else - tc_set_env ELASTIC_APM_ACTIVE true + tc_set_env ELASTIC_APM_ACTIVE "${CI_REPORTING_ENABLED-}" tc_set_env CHECKS_REPORTER_ACTIVE false fi diff --git a/.ci/teamcity/util.sh b/.ci/teamcity/util.sh index fe1afdf04c54c..f43f84059e25f 100755 --- a/.ci/teamcity/util.sh +++ b/.ci/teamcity/util.sh @@ -79,3 +79,10 @@ tc_retry() { } tc_end_block "Retryable Step - Attempt #1" } + +set_git_merge_base() { + if [[ "${GITHUB_PR_TARGET_BRANCH-}" ]]; then + git fetch origin "$GITHUB_PR_TARGET_BRANCH" + tc_set_env GITHUB_PR_MERGE_BASE "$(git merge-base HEAD FETCH_HEAD)" + fi +} diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md index 8502d9be5b56c..ca3a5a68beda0 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -1,6 +1,7 @@ --- name: Bug report about: Things break. Help us identify those things so we can fix them! +labels: bug --- diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c366819c49dde..efba93350b8fb 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -11,6 +11,7 @@ Delete any items that are not applicable to this PR. - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) +- [ ] If a plugin configuration key changed, check if it needs to be whitelisted in the [cloud](https://github.com/elastic/cloud) and added to the [docker list](https://github.com/elastic/kibana/blob/c29adfef29e921cc447d2a5ed06ac2047ceab552/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000000..5c6ff9df9dd25 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +puppeteer_skip_chromium_download=true diff --git a/.teamcity/src/Agents.kt b/.teamcity/src/Agents.kt index 557cce80d0f55..a550fb9e3d375 100644 --- a/.teamcity/src/Agents.kt +++ b/.teamcity/src/Agents.kt @@ -10,14 +10,16 @@ val StandardAgents = sizes.map { size -> size to GoogleCloudAgent { machineType = "n2-standard-$size" diskSizeGb = 75 diskType = GoogleCloudAgentDiskType.SSD + maxInstances = 750 } }.toMap() val BuildAgent = GoogleCloudAgent { - sourceImageFamily = "elastic-kibana-ci-ubuntu-1804-lts" - agentPrefix = "kibana-c2-16-" - machineType = "c2-standard-16" - diskSizeGb = 250 - diskType = GoogleCloudAgentDiskType.SSD + sourceImageFamily = "elastic-kibana-ci-ubuntu-1804-lts" + agentPrefix = "kibana-c2-16-" + machineType = "c2-standard-16" + diskSizeGb = 250 + diskType = GoogleCloudAgentDiskType.SSD + maxInstances = 200 } val CloudProfile = GoogleCloudProfile { diff --git a/.teamcity/src/Common.kt b/.teamcity/src/Common.kt new file mode 100644 index 0000000000000..35bc881b88967 --- /dev/null +++ b/.teamcity/src/Common.kt @@ -0,0 +1,27 @@ +import jetbrains.buildServer.configs.kotlin.v2019_2.DslContext + +// If set to true, github check/commit status will be reported, failed-test-reporter will run, etc. +const val ENABLE_REPORTING = false + +// If set to false, jobs with triggers (scheduled, on commit, etc) will be paused +const val ENABLE_TRIGGERS = false + +fun getProjectBranch(): String { + return DslContext.projectName +} + +fun getCorrespondingESBranch(): String { + return getProjectBranch().replace("_teamcity", "") +} + +fun areTriggersEnabled(): Boolean { + return ENABLE_TRIGGERS; +} + +fun isReportingEnabled(): Boolean { + return ENABLE_REPORTING; +} + +fun makeSafeId(id: String): String { + return id.replace(Regex("[^a-zA-Z0-9_]"), "_") +} diff --git a/.teamcity/src/Extensions.kt b/.teamcity/src/Extensions.kt index 2942a6385f13f..08ab17bc69c6d 100644 --- a/.teamcity/src/Extensions.kt +++ b/.teamcity/src/Extensions.kt @@ -39,7 +39,9 @@ val testArtifactRules = """ fun BuildType.addTestSettings() { artifactRules += "\n" + testArtifactRules steps { - failedTestReporter() + if(isReportingEnabled()) { + failedTestReporter() + } } features { junit() diff --git a/.teamcity/src/builds/BaselineCi.kt b/.teamcity/src/builds/BaselineCi.kt index ae316960acf89..de94e292bd63b 100644 --- a/.teamcity/src/builds/BaselineCi.kt +++ b/.teamcity/src/builds/BaselineCi.kt @@ -1,10 +1,12 @@ package builds import addSlackNotifications +import areTriggersEnabled import builds.default.DefaultBuild import builds.default.DefaultSavedObjectFieldMetrics import builds.oss.OssBuild import dependsOn +import getProjectBranch import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType import jetbrains.buildServer.configs.kotlin.v2019_2.FailureAction import jetbrains.buildServer.configs.kotlin.v2019_2.triggers.vcs @@ -15,14 +17,14 @@ object BaselineCi : BuildType({ name = "Baseline CI" description = "Runs builds, saved object field metrics for every commit" type = Type.COMPOSITE - paused = true + paused = !areTriggersEnabled() templates(KibanaTemplate) triggers { vcs { - branchFilter = "refs/heads/master_teamcity" -// perCheckinTriggering = true // TODO re-enable this later, it wreaks havoc when I merge upstream + branchFilter = "refs/heads/${getProjectBranch()}" + perCheckinTriggering = areTriggersEnabled() } } diff --git a/.teamcity/src/builds/HourlyCi.kt b/.teamcity/src/builds/HourlyCi.kt index 605a22f012976..f50a0e9037758 100644 --- a/.teamcity/src/builds/HourlyCi.kt +++ b/.teamcity/src/builds/HourlyCi.kt @@ -1,7 +1,9 @@ package builds import addSlackNotifications +import areTriggersEnabled import dependsOn +import getProjectBranch import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType import jetbrains.buildServer.configs.kotlin.v2019_2.FailureAction import jetbrains.buildServer.configs.kotlin.v2019_2.triggers.schedule @@ -11,6 +13,7 @@ object HourlyCi : BuildType({ name = "Hourly CI" description = "Runs everything in CI, hourly" type = Type.COMPOSITE + paused = !areTriggersEnabled() triggers { schedule { @@ -18,7 +21,7 @@ object HourlyCi : BuildType({ hours = "*" minutes = "0" } - branchFilter = "refs/heads/master_teamcity" + branchFilter = "refs/heads/${getProjectBranch()}" triggerBuild = always() withPendingChangesOnly = true } diff --git a/.teamcity/src/builds/PullRequestCi.kt b/.teamcity/src/builds/PullRequestCi.kt index d3eb697981ce7..f3fd1c0dfdf75 100644 --- a/.teamcity/src/builds/PullRequestCi.kt +++ b/.teamcity/src/builds/PullRequestCi.kt @@ -1,15 +1,14 @@ package builds +import builds.default.DefaultSavedObjectFieldMetrics import dependsOn -import jetbrains.buildServer.configs.kotlin.v2019_2.AbsoluteId +import getProjectBranch import jetbrains.buildServer.configs.kotlin.v2019_2.BuildType -import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.PullRequests import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.commitStatusPublisher -import jetbrains.buildServer.configs.kotlin.v2019_2.buildFeatures.pullRequests import vcs.Kibana object PullRequestCi : BuildType({ - id = AbsoluteId("Kibana_PullRequest_CI") + id("Pull_Request") name = "Pull Request CI" type = Type.COMPOSITE @@ -36,7 +35,7 @@ object PullRequestCi : BuildType({ params { param("elastic.pull_request.enabled", "true") - param("elastic.pull_request.target_branch", "master_teamcity") + param("elastic.pull_request.target_branch", getProjectBranch()) param("elastic.pull_request.allow_org_users", "true") param("elastic.pull_request.allowed_repo_permissions", "admin,write") param("elastic.pull_request.allowed_list", prAllowedList.joinToString(",")) @@ -74,5 +73,8 @@ object PullRequestCi : BuildType({ } } - dependsOn(FullCi) + dependsOn( + FullCi, + DefaultSavedObjectFieldMetrics + ) }) diff --git a/.teamcity/src/projects/Kibana.kt b/.teamcity/src/projects/Kibana.kt index 1878f49debe8c..fe04d4f5ab36e 100644 --- a/.teamcity/src/projects/Kibana.kt +++ b/.teamcity/src/projects/Kibana.kt @@ -32,7 +32,7 @@ fun Kibana(config: KibanaConfiguration = KibanaConfiguration()) : Project { param("teamcity.ui.settings.readOnly", "true") // https://github.com/JetBrains/teamcity-webhooks - param("teamcity.internal.webhooks.enable", "false") + param("teamcity.internal.webhooks.enable", "true") param("teamcity.internal.webhooks.events", "BUILD_STARTED;BUILD_FINISHED;BUILD_INTERRUPTED;CHANGES_LOADED;BUILD_TYPE_ADDED_TO_QUEUE;BUILD_PROBLEMS_CHANGED") param("teamcity.internal.webhooks.url", "https://ci-stats.kibana.dev/_teamcity_webhook") param("teamcity.internal.webhooks.username", "automation") diff --git a/.teamcity/src/templates/KibanaTemplate.kt b/.teamcity/src/templates/KibanaTemplate.kt index 83fe4fdaa1edd..2e3c151950dbe 100644 --- a/.teamcity/src/templates/KibanaTemplate.kt +++ b/.teamcity/src/templates/KibanaTemplate.kt @@ -2,6 +2,8 @@ package templates import StandardAgents import co.elastic.teamcity.common.requireAgent +import getProjectBranch +import isReportingEnabled import vcs.Kibana import jetbrains.buildServer.configs.kotlin.v2019_2.BuildStep import jetbrains.buildServer.configs.kotlin.v2019_2.ParameterDisplay @@ -33,7 +35,7 @@ object KibanaTemplate : Template({ authType = token { token = "credentialsJSON:07d22002-12de-4627-91c3-672bdb23b55b" } - filterTargetBranch = "refs/heads/master_teamcity" + filterTargetBranch = "refs/heads/${getProjectBranch()}" filterAuthorRole = PullRequests.GitHubRoleFilter.MEMBER } } @@ -63,6 +65,8 @@ object KibanaTemplate : Template({ param("env.GIT_COMMIT", "%build.vcs.number%") param("env.branch_specifier", "%vcsroot.branch%") + param("env.CI_REPORTING_ENABLED", isReportingEnabled().toString()) + password("env.KIBANA_CI_STATS_CONFIG", "", display = ParameterDisplay.HIDDEN) password("env.CI_STATS_TOKEN", "credentialsJSON:ea975068-ca68-4da5-8189-ce90f4286bc0", display = ParameterDisplay.HIDDEN) password("env.CI_STATS_HOST", "credentialsJSON:933ba93e-4b06-44c1-8724-8c536651f2b6", display = ParameterDisplay.HIDDEN) @@ -72,10 +76,19 @@ object KibanaTemplate : Template({ // password("env.CI_STATS_HOST", "%vault:kibana-issues:secret/kibana-issues/dev/kibana_ci_stats!/api_host%", display = ParameterDisplay.HIDDEN) // TODO remove this once we are able to pull it out of vault and put it closer to the things that require it - password("env.GITHUB_TOKEN", "credentialsJSON:07d22002-12de-4627-91c3-672bdb23b55b", display = ParameterDisplay.HIDDEN) - password("env.KIBANA_CI_REPORTER_KEY", "", display = ParameterDisplay.HIDDEN) - password("env.KIBANA_CI_REPORTER_KEY_BASE64", "credentialsJSON:86878779-4cf7-4434-82af-5164a1b992fb", display = ParameterDisplay.HIDDEN) - + if(isReportingEnabled()) { + password( + "env.GITHUB_TOKEN", + "credentialsJSON:07d22002-12de-4627-91c3-672bdb23b55b", + display = ParameterDisplay.HIDDEN + ) + password("env.KIBANA_CI_REPORTER_KEY", "", display = ParameterDisplay.HIDDEN) + password( + "env.KIBANA_CI_REPORTER_KEY_BASE64", + "credentialsJSON:86878779-4cf7-4434-82af-5164a1b992fb", + display = ParameterDisplay.HIDDEN + ) + } } steps { diff --git a/.teamcity/src/vcs/Elasticsearch.kt b/.teamcity/src/vcs/Elasticsearch.kt index ab7120b854446..96982b38fb015 100644 --- a/.teamcity/src/vcs/Elasticsearch.kt +++ b/.teamcity/src/vcs/Elasticsearch.kt @@ -1,11 +1,13 @@ package vcs +import getCorrespondingESBranch import jetbrains.buildServer.configs.kotlin.v2019_2.vcs.GitVcsRoot +import makeSafeId object Elasticsearch : GitVcsRoot({ - id("elasticsearch_master") + id("elasticsearch_${makeSafeId(getCorrespondingESBranch())}") - name = "elasticsearch / master" + name = "elasticsearch / ${getCorrespondingESBranch()}" url = "https://github.com/elastic/elasticsearch.git" - branch = "refs/heads/master" + branch = "refs/heads/${getCorrespondingESBranch()}" }) diff --git a/.teamcity/src/vcs/Kibana.kt b/.teamcity/src/vcs/Kibana.kt index d847a1565e6e0..d094cabab86b8 100644 --- a/.teamcity/src/vcs/Kibana.kt +++ b/.teamcity/src/vcs/Kibana.kt @@ -1,11 +1,13 @@ package vcs +import getProjectBranch import jetbrains.buildServer.configs.kotlin.v2019_2.vcs.GitVcsRoot +import makeSafeId object Kibana : GitVcsRoot({ - id("kibana_master") + id("kibana_${makeSafeId(getProjectBranch())}") - name = "kibana / master" + name = "kibana / ${getProjectBranch()}" url = "https://github.com/elastic/kibana.git" - branch = "refs/heads/master_teamcity" + branch = "refs/heads/${getProjectBranch()}" }) diff --git a/.teamcity/tests/projects/KibanaTest.kt b/.teamcity/tests/projects/KibanaTest.kt index 311c15a1da7cb..6a1b5a4e9c0f8 100644 --- a/.teamcity/tests/projects/KibanaTest.kt +++ b/.teamcity/tests/projects/KibanaTest.kt @@ -2,6 +2,7 @@ package projects import jetbrains.buildServer.configs.kotlin.v2019_2.AbsoluteId import jetbrains.buildServer.configs.kotlin.v2019_2.DslContext +import makeSafeId import org.junit.Assert.* import org.junit.Test diff --git a/docs/api/dashboard/export-dashboard.asciidoc b/docs/api/dashboard/export-dashboard.asciidoc index d33b9603fae73..6d239d755eb0d 100644 --- a/docs/api/dashboard/export-dashboard.asciidoc +++ b/docs/api/dashboard/export-dashboard.asciidoc @@ -18,6 +18,7 @@ experimental[] Export dashboards and corresponding saved objects. `dashboard`:: (Required, array|string) The IDs of the dashboards that you want to export. + To export multiple dashboards, repeat the query parameter. [[dashboard-api-export-response-body]] ==== Response body diff --git a/docs/apm/service-overview.asciidoc b/docs/apm/service-overview.asciidoc index 1afe00806474f..088791e6098e6 100644 --- a/docs/apm/service-overview.asciidoc +++ b/docs/apm/service-overview.asciidoc @@ -20,7 +20,7 @@ image::apm/images/latency.png[Service latency] [[service-traffic-transactions]] === Traffic and transactions -The *Traffic* chart visualizes the average number of transactions per minute for the selected service. +The *Throughput* chart visualizes the average number of transactions per minute for the selected service. The *Transactions* table displays a list of _transaction groups_ for the selected service and includes the latency, traffic, error rate, and the impact for each transaction. @@ -71,7 +71,7 @@ image::apm/images/spans-dependencies.png[Span type duration and dependencies] The *All instances* table displays a list of all the available service instances within the selected time range. Depending on how the service runs, the instance could be a host or a container. The table displays latency, traffic, -errors, CPU usage, and memory usage for each instance. By default, instances are sorted by _Traffic_. +errors, CPU usage, and memory usage for each instance. By default, instances are sorted by _Throughput_. [role="screenshot"] image::apm/images/all-instances.png[All instances] diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index ba2ea98cad5e6..642713e48e833 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -168,10 +168,6 @@ Content is fetched from the remote (https://feeds.elastic.co and https://feeds-s |Create choropleth maps. Display the results of a term-aggregation as e.g. countries, zip-codes, states. -|{kib-repo}blob/{branch}/src/plugins/runtime_fields/README.mdx[runtimeFields] -|The runtime fields plugin provides types and constants for OSS and xpack runtime field related code. - - |{kib-repo}blob/{branch}/src/plugins/saved_objects/README.md[savedObjects] |The savedObjects plugin exposes utilities to manipulate saved objects on the client side. @@ -491,8 +487,8 @@ Elastic. |Welcome to the Kibana rollup plugin! This plugin provides Kibana support for Elasticsearch's rollup feature. Please refer to the Elasticsearch documentation to understand rollup indices and how to create rollup jobs. -|{kib-repo}blob/{branch}/x-pack/plugins/runtime_field_editor/README.md[runtimeFieldEditor] -|Welcome to the home of the runtime field editor! +|{kib-repo}blob/{branch}/x-pack/plugins/runtime_fields/README.md[runtimeFields] +|Welcome to the home of the runtime field editor and everything related to runtime fields! |{kib-repo}blob/{branch}/x-pack/plugins/saved_objects_tagging/README.md[savedObjectsTagging] 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 0e23064385a63..ff2a8a2b5f75f 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 @@ -108,12 +108,38 @@ readonly links: { readonly ml: Record; readonly transforms: Record; readonly visualize: Record; - readonly apis: Record; + readonly apis: Readonly<{ + createIndex: string; + createSnapshotLifecyclePolicy: string; + createRoleMapping: string; + createRoleMappingTemplates: string; + createApiKey: string; + createPipeline: string; + createTransformRequest: string; + executeWatchActionModes: string; + openIndex: string; + putComponentTemplate: string; + painlessExecute: string; + putComponentTemplateMetadata: string; + putWatch: string; + updateTransform: string; + }>; readonly observability: Record; readonly alerting: Record; readonly maps: Record; readonly monitoring: Record; - readonly security: Record; + 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; readonly ccs: Record; }; 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 3ad747a42f84e..8404326f773e6 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +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 filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: 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: Record<string, string>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Record<string, 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 startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: 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;
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>;
} | | diff --git a/docs/development/core/server/kibana-plugin-core-server.capabilitiesstart.md b/docs/development/core/server/kibana-plugin-core-server.capabilitiesstart.md index 1af0bea4067aa..217a782be9d8b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.capabilitiesstart.md +++ b/docs/development/core/server/kibana-plugin-core-server.capabilitiesstart.md @@ -16,5 +16,5 @@ export interface CapabilitiesStart | Method | Description | | --- | --- | -| [resolveCapabilities(request)](./kibana-plugin-core-server.capabilitiesstart.resolvecapabilities.md) | Resolve the [Capabilities](./kibana-plugin-core-server.capabilities.md) to be used for given request | +| [resolveCapabilities(request, options)](./kibana-plugin-core-server.capabilitiesstart.resolvecapabilities.md) | Resolve the [Capabilities](./kibana-plugin-core-server.capabilities.md) to be used for given request | diff --git a/docs/development/core/server/kibana-plugin-core-server.capabilitiesstart.resolvecapabilities.md b/docs/development/core/server/kibana-plugin-core-server.capabilitiesstart.resolvecapabilities.md index 63736a38c2b28..d0e02499c580e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.capabilitiesstart.resolvecapabilities.md +++ b/docs/development/core/server/kibana-plugin-core-server.capabilitiesstart.resolvecapabilities.md @@ -9,7 +9,7 @@ Resolve the [Capabilities](./kibana-plugin-core-server.capabilities.md) to be us Signature: ```typescript -resolveCapabilities(request: KibanaRequest): Promise; +resolveCapabilities(request: KibanaRequest, options?: ResolveCapabilitiesOptions): Promise; ``` ## Parameters @@ -17,6 +17,7 @@ resolveCapabilities(request: KibanaRequest): Promise; | Parameter | Type | Description | | --- | --- | --- | | request | KibanaRequest | | +| options | ResolveCapabilitiesOptions | | Returns: diff --git a/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.getprotocol.md b/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.getprotocol.md new file mode 100644 index 0000000000000..720091174629a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.getprotocol.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IKibanaSocket](./kibana-plugin-core-server.ikibanasocket.md) > [getProtocol](./kibana-plugin-core-server.ikibanasocket.getprotocol.md) + +## IKibanaSocket.getProtocol() method + +Returns a string containing the negotiated SSL/TLS protocol version of the current connection. The value 'unknown' will be returned for connected sockets that have not completed the handshaking process. The value null will be returned for server sockets or disconnected client sockets. See https://www.openssl.org/docs/man1.0.2/ssl/SSL\_get\_version.html for more information. + +Signature: + +```typescript +getProtocol(): string | null; +``` +Returns: + +`string | null` + diff --git a/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.md b/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.md index afcabd834a1aa..99923aecef8df 100644 --- a/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.md +++ b/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.md @@ -26,4 +26,6 @@ export interface IKibanaSocket | [getPeerCertificate(detailed)](./kibana-plugin-core-server.ikibanasocket.getpeercertificate.md) | | | [getPeerCertificate(detailed)](./kibana-plugin-core-server.ikibanasocket.getpeercertificate_1.md) | | | [getPeerCertificate(detailed)](./kibana-plugin-core-server.ikibanasocket.getpeercertificate_2.md) | Returns an object representing the peer's certificate. The returned object has some properties corresponding to the field of the certificate. If detailed argument is true the full chain with issuer property will be returned, if false only the top certificate without issuer property. If the peer does not provide a certificate, it returns null. | +| [getProtocol()](./kibana-plugin-core-server.ikibanasocket.getprotocol.md) | Returns a string containing the negotiated SSL/TLS protocol version of the current connection. The value 'unknown' will be returned for connected sockets that have not completed the handshaking process. The value null will be returned for server sockets or disconnected client sockets. See https://www.openssl.org/docs/man1.0.2/ssl/SSL\_get\_version.html for more information. | +| [renegotiate(options)](./kibana-plugin-core-server.ikibanasocket.renegotiate.md) | Renegotiates a connection to obtain the peer's certificate. This cannot be used when the protocol version is TLSv1.3. | diff --git a/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.renegotiate.md b/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.renegotiate.md new file mode 100644 index 0000000000000..f39d3c08d9f0b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.ikibanasocket.renegotiate.md @@ -0,0 +1,29 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [IKibanaSocket](./kibana-plugin-core-server.ikibanasocket.md) > [renegotiate](./kibana-plugin-core-server.ikibanasocket.renegotiate.md) + +## IKibanaSocket.renegotiate() method + +Renegotiates a connection to obtain the peer's certificate. This cannot be used when the protocol version is TLSv1.3. + +Signature: + +```typescript +renegotiate(options: { + rejectUnauthorized?: boolean; + requestCert?: boolean; + }): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| options | {
rejectUnauthorized?: boolean;
requestCert?: boolean;
} | The options may contain the following fields: rejectUnauthorized, requestCert (See tls.createServer() for details). | + +Returns: + +`Promise` + +A Promise that will be resolved if renegotiation succeeded, or will be rejected if renegotiation failed. + diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 36da1b51ee7b0..06c7983f89a78 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -128,6 +128,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [PluginInitializerContext](./kibana-plugin-core-server.plugininitializercontext.md) | Context that's available to plugins during initialization stage. | | [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) | Describes the set of required and optional properties plugin can define in its mandatory JSON manifest file. | | [RequestHandlerContext](./kibana-plugin-core-server.requesthandlercontext.md) | Plugin specific context passed to a route handler.Provides the following clients and services: - [savedObjects.client](./kibana-plugin-core-server.savedobjectsclient.md) - Saved Objects client which uses the credentials of the incoming request - [savedObjects.typeRegistry](./kibana-plugin-core-server.isavedobjecttyperegistry.md) - Type registry containing all the registered types. - [elasticsearch.client](./kibana-plugin-core-server.iscopedclusterclient.md) - Elasticsearch data client which uses the credentials of the incoming request - [elasticsearch.legacy.client](./kibana-plugin-core-server.legacyscopedclusterclient.md) - The legacy Elasticsearch data client which uses the credentials of the incoming request - [uiSettings.client](./kibana-plugin-core-server.iuisettingsclient.md) - uiSettings client which uses the credentials of the incoming request | +| [ResolveCapabilitiesOptions](./kibana-plugin-core-server.resolvecapabilitiesoptions.md) | Defines a set of additional options for the resolveCapabilities method of [CapabilitiesStart](./kibana-plugin-core-server.capabilitiesstart.md). | | [RouteConfig](./kibana-plugin-core-server.routeconfig.md) | Route specific configuration. | | [RouteConfigOptions](./kibana-plugin-core-server.routeconfigoptions.md) | Additional route options. | | [RouteConfigOptionsBody](./kibana-plugin-core-server.routeconfigoptionsbody.md) | Additional body options for a route | diff --git a/docs/development/core/server/kibana-plugin-core-server.resolvecapabilitiesoptions.md b/docs/development/core/server/kibana-plugin-core-server.resolvecapabilitiesoptions.md new file mode 100644 index 0000000000000..f118c34c9be0f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.resolvecapabilitiesoptions.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ResolveCapabilitiesOptions](./kibana-plugin-core-server.resolvecapabilitiesoptions.md) + +## ResolveCapabilitiesOptions interface + +Defines a set of additional options for the `resolveCapabilities` method of [CapabilitiesStart](./kibana-plugin-core-server.capabilitiesstart.md). + +Signature: + +```typescript +export interface ResolveCapabilitiesOptions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [useDefaultCapabilities](./kibana-plugin-core-server.resolvecapabilitiesoptions.usedefaultcapabilities.md) | boolean | Indicates if capability switchers are supposed to return a default set of capabilities. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.resolvecapabilitiesoptions.usedefaultcapabilities.md b/docs/development/core/server/kibana-plugin-core-server.resolvecapabilitiesoptions.usedefaultcapabilities.md new file mode 100644 index 0000000000000..792893a3fc096 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.resolvecapabilitiesoptions.usedefaultcapabilities.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ResolveCapabilitiesOptions](./kibana-plugin-core-server.resolvecapabilitiesoptions.md) > [useDefaultCapabilities](./kibana-plugin-core-server.resolvecapabilitiesoptions.usedefaultcapabilities.md) + +## ResolveCapabilitiesOptions.useDefaultCapabilities property + +Indicates if capability switchers are supposed to return a default set of capabilities. + +Signature: + +```typescript +useDefaultCapabilities: boolean; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.issynccolorsenabled.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.issynccolorsenabled.md new file mode 100644 index 0000000000000..4a439a1e91316 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.issynccolorsenabled.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [ExecutionContext](./kibana-plugin-plugins-expressions-public.executioncontext.md) > [isSyncColorsEnabled](./kibana-plugin-plugins-expressions-public.executioncontext.issynccolorsenabled.md) + +## ExecutionContext.isSyncColorsEnabled property + +Returns the state (true\|false) of the sync colors across panels switch. + +Signature: + +```typescript +isSyncColorsEnabled?: () => boolean; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md index 1c0d10a382abf..901b46f0888d4 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.executioncontext.md @@ -22,6 +22,7 @@ export interface ExecutionContext() => ExecutionContextSearch | Get search context of the expression. | | [getSearchSessionId](./kibana-plugin-plugins-expressions-public.executioncontext.getsearchsessionid.md) | () => string | undefined | Search context in which expression should operate. | | [inspectorAdapters](./kibana-plugin-plugins-expressions-public.executioncontext.inspectoradapters.md) | InspectorAdapters | Adapters for inspector plugin. | +| [isSyncColorsEnabled](./kibana-plugin-plugins-expressions-public.executioncontext.issynccolorsenabled.md) | () => boolean | Returns the state (true\|false) of the sync colors across panels switch. | | [types](./kibana-plugin-plugins-expressions-public.executioncontext.types.md) | Record<string, ExpressionType> | A map of available expression types. | | [variables](./kibana-plugin-plugins-expressions-public.executioncontext.variables.md) | Record<string, unknown> | Context variables that can be consumed using var and var_set functions. | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.issynccolorsenabled.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.issynccolorsenabled.md new file mode 100644 index 0000000000000..24f7bb618deb8 --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.issynccolorsenabled.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [ExecutionContext](./kibana-plugin-plugins-expressions-server.executioncontext.md) > [isSyncColorsEnabled](./kibana-plugin-plugins-expressions-server.executioncontext.issynccolorsenabled.md) + +## ExecutionContext.isSyncColorsEnabled property + +Returns the state (true\|false) of the sync colors across panels switch. + +Signature: + +```typescript +isSyncColorsEnabled?: () => boolean; +``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md index fbf9dc634d563..39018599a2c92 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.executioncontext.md @@ -22,6 +22,7 @@ export interface ExecutionContext() => ExecutionContextSearch | Get search context of the expression. | | [getSearchSessionId](./kibana-plugin-plugins-expressions-server.executioncontext.getsearchsessionid.md) | () => string | undefined | Search context in which expression should operate. | | [inspectorAdapters](./kibana-plugin-plugins-expressions-server.executioncontext.inspectoradapters.md) | InspectorAdapters | Adapters for inspector plugin. | +| [isSyncColorsEnabled](./kibana-plugin-plugins-expressions-server.executioncontext.issynccolorsenabled.md) | () => boolean | Returns the state (true\|false) of the sync colors across panels switch. | | [types](./kibana-plugin-plugins-expressions-server.executioncontext.types.md) | Record<string, ExpressionType> | A map of available expression types. | | [variables](./kibana-plugin-plugins-expressions-server.executioncontext.variables.md) | Record<string, unknown> | Context variables that can be consumed using var and var_set functions. | diff --git a/docs/discover/images/Discover-Start.png b/docs/discover/images/Discover-Start.png old mode 100755 new mode 100644 index 437684fdbcd79..28c30865844b4 Binary files a/docs/discover/images/Discover-Start.png and b/docs/discover/images/Discover-Start.png differ diff --git a/docs/discover/images/document-table-expanded.png b/docs/discover/images/document-table-expanded.png old mode 100755 new mode 100644 index 052ffbc9cbcc4..f92fdd8747fa4 Binary files a/docs/discover/images/document-table-expanded.png and b/docs/discover/images/document-table-expanded.png differ diff --git a/docs/discover/images/document-table.png b/docs/discover/images/document-table.png index 7aca8b14ae1f7..5b0383b522cea 100644 Binary files a/docs/discover/images/document-table.png and b/docs/discover/images/document-table.png differ diff --git a/docs/discover/images/visualize-from-discover.png b/docs/discover/images/visualize-from-discover.png index 17ef6002b19bd..9543112eb88ca 100644 Binary files a/docs/discover/images/visualize-from-discover.png and b/docs/discover/images/visualize-from-discover.png differ diff --git a/docs/discover/kuery.asciidoc b/docs/discover/kuery.asciidoc index feb16190cb34b..8c0012fb6c6bf 100644 --- a/docs/discover/kuery.asciidoc +++ b/docs/discover/kuery.asciidoc @@ -111,13 +111,35 @@ tags:(success and info and security) [discrete] === Range queries -KQL supports `>`, `>=`, `<`, and `<=` on numeric and date types. For example: +KQL supports `>`, `>=`, `<`, and `<=` on numeric and date types. [source,yaml] ------------------- -account_number >= 100 and items_sold <= 200 and @timestamp >= now-5m +account_number >= 100 and items_sold <= 200 ------------------- +[discrete] +=== Date range queries + +Typically, Kibana's <> is sufficient for setting a time range, +but in some cases you might need to search on dates. Include the date range in quotes. + +[source,yaml] +------------------- +@timestamp < "2021-01-02T21:55:59" +------------------- + +[source,yaml] +------------------- +@timestamp < "2021-01" +------------------- + +[source,yaml] +------------------- +@timestamp < "2021" +------------------- + + [discrete] === Exist queries diff --git a/docs/management/index-patterns.asciidoc b/docs/management/index-patterns.asciidoc index c1ad859f0cb69..d83f2571ad26f 100644 --- a/docs/management/index-patterns.asciidoc +++ b/docs/management/index-patterns.asciidoc @@ -19,8 +19,9 @@ You’ll learn how to: [[index-patterns-read-only-access]] === Before you begin -* To access *Index Patterns*, you must have the {kib} privilege -`Index Pattern Management`. To add the privilege, open the main menu, then click *Stack Management > Roles*. +* To access the *Index Patterns* view, you must have the {kib} privilege +`Index Pattern Management`. To create an index pattern, you must have the {es} privilege +`view_index_metadata`. To add the privileges, open the main menu, then click *Stack Management > Roles*. * If a read-only indicator appears in {kib}, you have insufficient privileges to create or save index patterns. The buttons to create new index patterns or diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index febdf707dce9a..6dd76f782d668 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -231,6 +231,16 @@ This value must be a whole number greater than zero. *Default: `"1000"`* suggestions. This value must be a whole number greater than zero. *Default: `"100000"`* +|=== + +[NOTE] +============ +To reload the logging settings, send a SIGHUP signal to {kib}. +============ + +[cols="2*<"] +|=== + |[[logging-dest]] `logging.dest:` | Enables you to specify a file where {kib} stores log output. *Default: `stdout`* diff --git a/package.json b/package.json index edeeebdddabf1..07984e5a237b0 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,6 @@ "number": 8467, "sha": "6cb7fec4e154faa0a4a3fee4b33dfef91b9870d9" }, - "config": { - "puppeteer_skip_chromium_download": true - }, "homepage": "https://www.elastic.co/products/kibana", "bugs": { "url": "http://github.com/elastic/kibana/issues" @@ -138,7 +135,7 @@ "@kbn/utils": "link:packages/kbn-utils", "@loaders.gl/core": "^2.3.1", "@loaders.gl/json": "^2.3.1", - "@slack/webhook": "^5.0.0", + "@slack/webhook": "^5.0.4", "@storybook/addons": "^6.0.16", "@turf/along": "6.0.1", "@turf/area": "6.0.1", @@ -170,7 +167,7 @@ "apollo-server-errors": "^2.0.2", "apollo-server-hapi": "^1.3.6", "archiver": "^3.1.1", - "axios": "^0.19.2", + "axios": "^0.21.1", "bluebird": "3.5.5", "brace": "0.11.1", "chalk": "^4.1.0", @@ -181,7 +178,6 @@ "classnames": "2.2.6", "color": "1.0.3", "commander": "^3.0.2", - "compression-webpack-plugin": "^4.0.0", "concat-stream": "1.6.2", "content-disposition": "0.5.3", "core-js": "^3.6.5", @@ -328,7 +324,6 @@ "vinyl": "^2.2.0", "vscode-languageserver": "^5.2.1", "vt-pbf": "^3.1.1", - "webpack": "^4.41.5", "wellknown": "^0.5.0", "whatwg-fetch": "^3.0.0", "xml2js": "^0.4.22", @@ -391,7 +386,7 @@ "@microsoft/api-documenter": "7.7.2", "@microsoft/api-extractor": "7.7.0", "@octokit/rest": "^16.35.0", - "@percy/agent": "^0.26.0", + "@percy/agent": "^0.28.6", "@scant/router": "^0.1.0", "@storybook/addon-a11y": "^6.0.26", "@storybook/addon-actions": "^6.0.26", @@ -490,6 +485,7 @@ "@types/md5": "^2.2.0", "@types/memoize-one": "^4.1.0", "@types/mime": "^2.0.1", + "@types/mime-types": "^2.1.0", "@types/minimatch": "^2.0.29", "@types/mocha": "^7.0.2", "@types/mock-fs": "^4.10.0", @@ -593,16 +589,17 @@ "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.0", + "backport": "^5.6.4", "base64-js": "^1.3.1", "base64url": "^3.0.1", "broadcast-channel": "^3.0.3", "chai": "3.5.0", "chance": "1.0.18", - "chromedriver": "^87.0.0", + "chromedriver": "^87.0.3", "clean-webpack-plugin": "^3.0.0", "cmd-shim": "^2.1.0", "compare-versions": "3.5.1", + "compression-webpack-plugin": "^4.0.0", "constate": "^1.3.2", "copy-to-clipboard": "^3.0.8", "copy-webpack-plugin": "^6.0.2", @@ -836,8 +833,9 @@ "vega-tooltip": "^0.25.0", "venn.js": "0.2.20", "vinyl-fs": "^3.0.3", - "wait-on": "^5.0.1", + "wait-on": "^5.2.1", "watchpack": "^1.6.0", + "webpack": "^4.41.5", "webpack-cli": "^3.3.12", "webpack-dev-server": "^3.11.0", "webpack-merge": "^4.2.2", diff --git a/packages/kbn-es/src/cluster.js b/packages/kbn-es/src/cluster.js index eaf353b3e55d0..176c4304055a6 100644 --- a/packages/kbn-es/src/cluster.js +++ b/packages/kbn-es/src/cluster.js @@ -257,8 +257,9 @@ exports.Cluster = class Cluster { this._log.info(chalk.bold('Starting')); this._log.indent(4); + const esArgs = ['indices.query.bool.max_nested_depth=100'].concat(options.esArgs || []); + // Add to esArgs if ssl is enabled - const esArgs = [].concat(options.esArgs || []); if (this._ssl) { esArgs.push('xpack.security.http.ssl.enabled=true'); esArgs.push(`xpack.security.http.ssl.keystore.path=${ES_P12_PATH}`); diff --git a/packages/kbn-es/src/integration_tests/cluster.test.js b/packages/kbn-es/src/integration_tests/cluster.test.js index 6229a8add0d24..33bce2932fe2f 100644 --- a/packages/kbn-es/src/integration_tests/cluster.test.js +++ b/packages/kbn-es/src/integration_tests/cluster.test.js @@ -17,12 +17,19 @@ * under the License. */ -const { ToolingLog, ES_P12_PATH, ES_P12_PASSWORD } = require('@kbn/dev-utils'); +const { + ToolingLog, + ES_P12_PATH, + ES_P12_PASSWORD, + createAnyInstanceSerializer, +} = require('@kbn/dev-utils'); const execa = require('execa'); const { Cluster } = require('../cluster'); const { installSource, installSnapshot, installArchive } = require('../install'); const { extractConfigFiles } = require('../utils/extract_config_files'); +expect.addSnapshotSerializer(createAnyInstanceSerializer(ToolingLog)); + jest.mock('../install', () => ({ installSource: jest.fn(), installSnapshot: jest.fn(), @@ -265,8 +272,19 @@ describe('#start(installPath)', () => { const cluster = new Cluster({ log, ssl: false }); await cluster.start(); - const config = extractConfigFiles.mock.calls[0][0]; - expect(config).toHaveLength(0); + expect(extractConfigFiles.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Array [ + "indices.query.bool.max_nested_depth=100", + ], + undefined, + Object { + "log": , + }, + ], + ] + `); }); }); @@ -332,8 +350,19 @@ describe('#run()', () => { const cluster = new Cluster({ log, ssl: false }); await cluster.run(); - const config = extractConfigFiles.mock.calls[0][0]; - expect(config).toHaveLength(0); + expect(extractConfigFiles.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Array [ + "indices.query.bool.max_nested_depth=100", + ], + undefined, + Object { + "log": , + }, + ], + ] + `); }); }); diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 485481e2a7f14..44cc4fdabb25e 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -102,8 +102,7 @@ pageLoadAssetSize: visualizations: 295025 visualize: 57431 watcher: 43598 - runtimeFields: 10000 + runtimeFields: 41752 stackAlerts: 29684 presentationUtil: 28545 - runtimeFieldEditor: 46986 spacesOss: 18817 diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 922159ab555c8..955a375228d55 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -94,7 +94,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _cli__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "run", function() { return _cli__WEBPACK_IMPORTED_MODULE_0__["run"]; }); -/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(511); +/* harmony import */ var _production__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(510); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _production__WEBPACK_IMPORTED_MODULE_1__["buildProductionProjects"]; }); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(248); @@ -106,7 +106,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(251); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "transformDependencies", function() { return _utils_package_json__WEBPACK_IMPORTED_MODULE_4__["transformDependencies"]; }); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(510); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(509); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "getProjectPaths", function() { return _config__WEBPACK_IMPORTED_MODULE_5__["getProjectPaths"]; }); /* @@ -150,7 +150,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(5); /* harmony import */ var _kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_kbn_dev_utils_tooling_log__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var _commands__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(128); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(504); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(503); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(246); /* * Licensed to Elasticsearch B.V. under one or more contributor @@ -2981,16 +2981,20 @@ var ReplaySubject = /*@__PURE__*/ (function (_super) { return _this; } ReplaySubject.prototype.nextInfiniteTimeWindow = function (value) { - var _events = this._events; - _events.push(value); - if (_events.length > this._bufferSize) { - _events.shift(); + if (!this.isStopped) { + var _events = this._events; + _events.push(value); + if (_events.length > this._bufferSize) { + _events.shift(); + } } _super.prototype.next.call(this, value); }; ReplaySubject.prototype.nextTimeWindow = function (value) { - this._events.push(new ReplayEvent(this._getNow(), value)); - this._trimBufferThenGetEvents(); + if (!this.isStopped) { + this._events.push(new ReplayEvent(this._getNow(), value)); + this._trimBufferThenGetEvents(); + } _super.prototype.next.call(this, value); }; ReplaySubject.prototype._subscribe = function (subscriber) { @@ -8897,9 +8901,9 @@ exports.ToolingLogCollectingWriter = ToolingLogCollectingWriter; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "commands", function() { return commands; }); /* harmony import */ var _bootstrap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(129); -/* harmony import */ var _clean__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(372); -/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(403); -/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(404); +/* harmony import */ var _clean__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(371); +/* harmony import */ var _run__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(402); +/* harmony import */ var _watch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(403); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -8942,10 +8946,10 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(246); /* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(247); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(248); -/* harmony import */ var _utils_project_checksums__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(364); -/* harmony import */ var _utils_bootstrap_cache_file__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(369); -/* harmony import */ var _utils_yarn_lock__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(366); -/* harmony import */ var _utils_validate_dependencies__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(370); +/* harmony import */ var _utils_project_checksums__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(363); +/* harmony import */ var _utils_bootstrap_cache_file__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(368); +/* harmony import */ var _utils_yarn_lock__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(365); +/* harmony import */ var _utils_validate_dependencies__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(369); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -33849,7 +33853,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(320); /* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(execa__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var strong_log_transformer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(356); +/* harmony import */ var strong_log_transformer__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(355); /* harmony import */ var strong_log_transformer__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(strong_log_transformer__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(246); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } @@ -33947,13 +33951,13 @@ const childProcess = __webpack_require__(321); const crossSpawn = __webpack_require__(322); const stripFinalNewline = __webpack_require__(335); const npmRunPath = __webpack_require__(336); -const onetime = __webpack_require__(338); -const makeError = __webpack_require__(340); -const normalizeStdio = __webpack_require__(345); -const {spawnedKill, spawnedCancel, setupTimeout, setExitHandler} = __webpack_require__(346); -const {handleInput, getSpawnedResult, makeAllStream, validateInputSync} = __webpack_require__(347); -const {mergePromise, getSpawnedPromise} = __webpack_require__(354); -const {joinCommand, parseCommand} = __webpack_require__(355); +const onetime = __webpack_require__(337); +const makeError = __webpack_require__(339); +const normalizeStdio = __webpack_require__(344); +const {spawnedKill, spawnedCancel, setupTimeout, setExitHandler} = __webpack_require__(345); +const {handleInput, getSpawnedResult, makeAllStream, validateInputSync} = __webpack_require__(346); +const {mergePromise, getSpawnedPromise} = __webpack_require__(353); +const {joinCommand, parseCommand} = __webpack_require__(354); const DEFAULT_MAX_BUFFER = 1000 * 1000 * 100; @@ -34358,13 +34362,14 @@ module.exports = parse; const path = __webpack_require__(4); const which = __webpack_require__(325); -const pathKey = __webpack_require__(329)(); +const getPathKey = __webpack_require__(329); function resolveCommandAttempt(parsed, withoutPathExt) { + const env = parsed.options.env || process.env; const cwd = process.cwd(); const hasCustomCwd = parsed.options.cwd != null; // Worker threads do not have process.chdir() - const shouldSwitchCwd = hasCustomCwd && process.chdir !== undefined; + const shouldSwitchCwd = hasCustomCwd && process.chdir !== undefined && !process.chdir.disabled; // If a custom `cwd` was specified, we need to change the process cwd // because `which` will do stat calls but does not support a custom cwd @@ -34380,7 +34385,7 @@ function resolveCommandAttempt(parsed, withoutPathExt) { try { resolved = which.sync(parsed.command, { - path: (parsed.options.env || process.env)[pathKey], + path: env[getPathKey({ env })], pathExt: withoutPathExt ? path.delimiter : undefined, }); } catch (e) { @@ -34932,7 +34937,7 @@ module.exports = input => { "use strict"; const path = __webpack_require__(4); -const pathKey = __webpack_require__(337); +const pathKey = __webpack_require__(329); const npmRunPath = options => { options = { @@ -34985,30 +34990,7 @@ module.exports.env = options => { "use strict"; - -const pathKey = (options = {}) => { - const environment = options.env || process.env; - const platform = options.platform || process.platform; - - if (platform !== 'win32') { - return 'PATH'; - } - - return Object.keys(environment).find(key => key.toUpperCase() === 'PATH') || 'Path'; -}; - -module.exports = pathKey; -// TODO: Remove this for the next major release -module.exports.default = pathKey; - - -/***/ }), -/* 338 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - -const mimicFn = __webpack_require__(339); +const mimicFn = __webpack_require__(338); const calledFunctions = new WeakMap(); @@ -35060,7 +35042,7 @@ module.exports.callCount = fn => { /***/ }), -/* 339 */ +/* 338 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -35080,12 +35062,12 @@ module.exports.default = mimicFn; /***/ }), -/* 340 */ +/* 339 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const {signalsByName} = __webpack_require__(341); +const {signalsByName} = __webpack_require__(340); const getErrorPrefix = ({timedOut, timeout, errorCode, signal, signalDescription, exitCode, isCanceled}) => { if (timedOut) { @@ -35173,14 +35155,14 @@ module.exports = makeError; /***/ }), -/* 341 */ +/* 340 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports,"__esModule",{value:true});exports.signalsByNumber=exports.signalsByName=void 0;var _os=__webpack_require__(121); -var _signals=__webpack_require__(342); -var _realtime=__webpack_require__(344); +var _signals=__webpack_require__(341); +var _realtime=__webpack_require__(343); @@ -35250,14 +35232,14 @@ const signalsByNumber=getSignalsByNumber();exports.signalsByNumber=signalsByNumb //# sourceMappingURL=main.js.map /***/ }), -/* 342 */ +/* 341 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports,"__esModule",{value:true});exports.getSignals=void 0;var _os=__webpack_require__(121); -var _core=__webpack_require__(343); -var _realtime=__webpack_require__(344); +var _core=__webpack_require__(342); +var _realtime=__webpack_require__(343); @@ -35291,7 +35273,7 @@ return{name,number,description,supported,action,forced,standard}; //# sourceMappingURL=signals.js.map /***/ }), -/* 343 */ +/* 342 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -35570,7 +35552,7 @@ standard:"other"}];exports.SIGNALS=SIGNALS; //# sourceMappingURL=core.js.map /***/ }), -/* 344 */ +/* 343 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -35595,7 +35577,7 @@ const SIGRTMAX=64;exports.SIGRTMAX=SIGRTMAX; //# sourceMappingURL=realtime.js.map /***/ }), -/* 345 */ +/* 344 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -35654,7 +35636,7 @@ module.exports.node = opts => { /***/ }), -/* 346 */ +/* 345 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -35773,14 +35755,14 @@ module.exports = { /***/ }), -/* 347 */ +/* 346 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const isStream = __webpack_require__(348); -const getStream = __webpack_require__(349); -const mergeStream = __webpack_require__(353); +const isStream = __webpack_require__(347); +const getStream = __webpack_require__(348); +const mergeStream = __webpack_require__(352); // `input` option const handleInput = (spawned, input) => { @@ -35877,7 +35859,7 @@ module.exports = { /***/ }), -/* 348 */ +/* 347 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -35913,13 +35895,13 @@ module.exports = isStream; /***/ }), -/* 349 */ +/* 348 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pump = __webpack_require__(350); -const bufferStream = __webpack_require__(352); +const pump = __webpack_require__(349); +const bufferStream = __webpack_require__(351); class MaxBufferError extends Error { constructor() { @@ -35978,11 +35960,11 @@ module.exports.MaxBufferError = MaxBufferError; /***/ }), -/* 350 */ +/* 349 */ /***/ (function(module, exports, __webpack_require__) { var once = __webpack_require__(162) -var eos = __webpack_require__(351) +var eos = __webpack_require__(350) var fs = __webpack_require__(134) // we only need fs to get the ReadStream and WriteStream prototypes var noop = function () {} @@ -36066,7 +36048,7 @@ module.exports = pump /***/ }), -/* 351 */ +/* 350 */ /***/ (function(module, exports, __webpack_require__) { var once = __webpack_require__(162); @@ -36166,7 +36148,7 @@ module.exports = eos; /***/ }), -/* 352 */ +/* 351 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36225,7 +36207,7 @@ module.exports = options => { /***/ }), -/* 353 */ +/* 352 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36273,7 +36255,7 @@ module.exports = function (/*streams...*/) { /***/ }), -/* 354 */ +/* 353 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36326,7 +36308,7 @@ module.exports = { /***/ }), -/* 355 */ +/* 354 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36371,7 +36353,7 @@ module.exports = { /***/ }), -/* 356 */ +/* 355 */ /***/ (function(module, exports, __webpack_require__) { // Copyright IBM Corp. 2014,2018. All Rights Reserved. @@ -36379,12 +36361,12 @@ module.exports = { // This file is licensed under the Apache License 2.0. // License text available at https://opensource.org/licenses/Apache-2.0 -module.exports = __webpack_require__(357); -module.exports.cli = __webpack_require__(361); +module.exports = __webpack_require__(356); +module.exports.cli = __webpack_require__(360); /***/ }), -/* 357 */ +/* 356 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36399,9 +36381,9 @@ var stream = __webpack_require__(138); var util = __webpack_require__(112); var fs = __webpack_require__(134); -var through = __webpack_require__(358); -var duplexer = __webpack_require__(359); -var StringDecoder = __webpack_require__(360).StringDecoder; +var through = __webpack_require__(357); +var duplexer = __webpack_require__(358); +var StringDecoder = __webpack_require__(359).StringDecoder; module.exports = Logger; @@ -36590,7 +36572,7 @@ function lineMerger(host) { /***/ }), -/* 358 */ +/* 357 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(138) @@ -36704,7 +36686,7 @@ function through (write, end, opts) { /***/ }), -/* 359 */ +/* 358 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(138) @@ -36797,13 +36779,13 @@ function duplex(writer, reader) { /***/ }), -/* 360 */ +/* 359 */ /***/ (function(module, exports) { module.exports = require("string_decoder"); /***/ }), -/* 361 */ +/* 360 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -36814,11 +36796,11 @@ module.exports = require("string_decoder"); -var minimist = __webpack_require__(362); +var minimist = __webpack_require__(361); var path = __webpack_require__(4); -var Logger = __webpack_require__(357); -var pkg = __webpack_require__(363); +var Logger = __webpack_require__(356); +var pkg = __webpack_require__(362); module.exports = cli; @@ -36872,7 +36854,7 @@ function usage($0, p) { /***/ }), -/* 362 */ +/* 361 */ /***/ (function(module, exports) { module.exports = function (args, opts) { @@ -37123,13 +37105,13 @@ function isNumber (x) { /***/ }), -/* 363 */ +/* 362 */ /***/ (function(module) { module.exports = JSON.parse("{\"name\":\"strong-log-transformer\",\"version\":\"2.1.0\",\"description\":\"Stream transformer that prefixes lines with timestamps and other things.\",\"author\":\"Ryan Graham \",\"license\":\"Apache-2.0\",\"repository\":{\"type\":\"git\",\"url\":\"git://github.com/strongloop/strong-log-transformer\"},\"keywords\":[\"logging\",\"streams\"],\"bugs\":{\"url\":\"https://github.com/strongloop/strong-log-transformer/issues\"},\"homepage\":\"https://github.com/strongloop/strong-log-transformer\",\"directories\":{\"test\":\"test\"},\"bin\":{\"sl-log-transformer\":\"bin/sl-log-transformer.js\"},\"main\":\"index.js\",\"scripts\":{\"test\":\"tap --100 test/test-*\"},\"dependencies\":{\"duplexer\":\"^0.1.1\",\"minimist\":\"^1.2.0\",\"through\":\"^2.3.4\"},\"devDependencies\":{\"tap\":\"^12.0.1\"},\"engines\":{\"node\":\">=4\"}}"); /***/ }), -/* 364 */ +/* 363 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -37137,13 +37119,13 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "getAllChecksums", function() { return getAllChecksums; }); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(134); /* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(365); +/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(364); /* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(crypto__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(112); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(util__WEBPACK_IMPORTED_MODULE_2__); /* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(320); /* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(execa__WEBPACK_IMPORTED_MODULE_3__); -/* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(366); +/* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(365); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -37342,20 +37324,20 @@ async function getAllChecksums(kbn, log, yarnLock) { } /***/ }), -/* 365 */ +/* 364 */ /***/ (function(module, exports) { module.exports = require("crypto"); /***/ }), -/* 366 */ +/* 365 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "readYarnLock", function() { return readYarnLock; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "resolveDepsForProject", function() { return resolveDepsForProject; }); -/* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(367); +/* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(366); /* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(131); /* @@ -37468,7 +37450,7 @@ function resolveDepsForProject({ } /***/ }), -/* 367 */ +/* 366 */ /***/ (function(module, exports, __webpack_require__) { module.exports = @@ -39027,7 +39009,7 @@ module.exports = invariant; /* 9 */ /***/ (function(module, exports) { -module.exports = __webpack_require__(365); +module.exports = __webpack_require__(364); /***/ }), /* 10 */, @@ -41351,7 +41333,7 @@ function onceStrict (fn) { /* 63 */ /***/ (function(module, exports) { -module.exports = __webpack_require__(368); +module.exports = __webpack_require__(367); /***/ }), /* 64 */, @@ -47746,13 +47728,13 @@ module.exports = process && support(supportLevel); /******/ ]); /***/ }), -/* 368 */ +/* 367 */ /***/ (function(module, exports) { module.exports = require("buffer"); /***/ }), -/* 369 */ +/* 368 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -47849,13 +47831,13 @@ class BootstrapCacheFile { } /***/ }), -/* 370 */ +/* 369 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "validateDependencies", function() { return validateDependencies; }); -/* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(367); +/* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(366); /* harmony import */ var _yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_yarnpkg_lockfile__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var dedent__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2); /* harmony import */ var dedent__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(dedent__WEBPACK_IMPORTED_MODULE_1__); @@ -47866,7 +47848,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _fs__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(131); /* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(246); /* harmony import */ var _package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(251); -/* harmony import */ var _projects_tree__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(371); +/* harmony import */ var _projects_tree__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(370); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -48058,7 +48040,7 @@ function getDevOnlyProductionDepsTree(kbn, projectName) { } /***/ }), -/* 371 */ +/* 370 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -48211,7 +48193,7 @@ function addProjectToTree(tree, pathParts, project) { } /***/ }), -/* 372 */ +/* 371 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -48219,7 +48201,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "CleanCommand", function() { return CleanCommand; }); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(143); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(373); +/* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(372); /* harmony import */ var ora__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(ora__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); @@ -48319,20 +48301,20 @@ const CleanCommand = { }; /***/ }), -/* 373 */ +/* 372 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readline = __webpack_require__(374); -const chalk = __webpack_require__(375); -const cliCursor = __webpack_require__(382); -const cliSpinners = __webpack_require__(384); -const logSymbols = __webpack_require__(386); -const stripAnsi = __webpack_require__(395); -const wcwidth = __webpack_require__(397); -const isInteractive = __webpack_require__(401); -const MuteStream = __webpack_require__(402); +const readline = __webpack_require__(373); +const chalk = __webpack_require__(374); +const cliCursor = __webpack_require__(381); +const cliSpinners = __webpack_require__(383); +const logSymbols = __webpack_require__(385); +const stripAnsi = __webpack_require__(394); +const wcwidth = __webpack_require__(396); +const isInteractive = __webpack_require__(400); +const MuteStream = __webpack_require__(401); const TEXT = Symbol('text'); const PREFIX_TEXT = Symbol('prefixText'); @@ -48685,23 +48667,23 @@ module.exports.promise = (action, options) => { /***/ }), -/* 374 */ +/* 373 */ /***/ (function(module, exports) { module.exports = require("readline"); /***/ }), -/* 375 */ +/* 374 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const ansiStyles = __webpack_require__(376); +const ansiStyles = __webpack_require__(375); const {stdout: stdoutColor, stderr: stderrColor} = __webpack_require__(120); const { stringReplaceAll, stringEncaseCRLFWithFirstIndex -} = __webpack_require__(380); +} = __webpack_require__(379); // `supportsColor.level` → `ansiStyles.color[name]` mapping const levelMapping = [ @@ -48902,7 +48884,7 @@ const chalkTag = (chalk, ...strings) => { } if (template === undefined) { - template = __webpack_require__(381); + template = __webpack_require__(380); } return template(chalk, parts.join('')); @@ -48931,7 +48913,7 @@ module.exports = chalk; /***/ }), -/* 376 */ +/* 375 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -48977,7 +48959,7 @@ const setLazyProperty = (object, property, get) => { let colorConvert; const makeDynamicStyles = (wrap, targetSpace, identity, isBackground) => { if (colorConvert === undefined) { - colorConvert = __webpack_require__(377); + colorConvert = __webpack_require__(376); } const offset = isBackground ? 10 : 0; @@ -49102,11 +49084,11 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(115)(module))) /***/ }), -/* 377 */ +/* 376 */ /***/ (function(module, exports, __webpack_require__) { -const conversions = __webpack_require__(378); -const route = __webpack_require__(379); +const conversions = __webpack_require__(377); +const route = __webpack_require__(378); const convert = {}; @@ -49189,7 +49171,7 @@ module.exports = convert; /***/ }), -/* 378 */ +/* 377 */ /***/ (function(module, exports, __webpack_require__) { /* MIT license */ @@ -50034,10 +50016,10 @@ convert.rgb.gray = function (rgb) { /***/ }), -/* 379 */ +/* 378 */ /***/ (function(module, exports, __webpack_require__) { -const conversions = __webpack_require__(378); +const conversions = __webpack_require__(377); /* This function routes a model to all other models. @@ -50137,7 +50119,7 @@ module.exports = function (fromModel) { /***/ }), -/* 380 */ +/* 379 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -50183,7 +50165,7 @@ module.exports = { /***/ }), -/* 381 */ +/* 380 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -50324,12 +50306,12 @@ module.exports = (chalk, temporary) => { /***/ }), -/* 382 */ +/* 381 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const restoreCursor = __webpack_require__(383); +const restoreCursor = __webpack_require__(382); let isHidden = false; @@ -50366,12 +50348,12 @@ exports.toggle = (force, writableStream) => { /***/ }), -/* 383 */ +/* 382 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const onetime = __webpack_require__(338); +const onetime = __webpack_require__(337); const signalExit = __webpack_require__(309); module.exports = onetime(() => { @@ -50382,13 +50364,13 @@ module.exports = onetime(() => { /***/ }), -/* 384 */ +/* 383 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const spinners = Object.assign({}, __webpack_require__(385)); +const spinners = Object.assign({}, __webpack_require__(384)); const spinnersList = Object.keys(spinners); @@ -50406,18 +50388,18 @@ module.exports.default = spinners; /***/ }), -/* 385 */ +/* 384 */ /***/ (function(module) { -module.exports = JSON.parse("{\"dots\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠹\",\"⠸\",\"⠼\",\"⠴\",\"⠦\",\"⠧\",\"⠇\",\"⠏\"]},\"dots2\":{\"interval\":80,\"frames\":[\"⣾\",\"⣽\",\"⣻\",\"⢿\",\"⡿\",\"⣟\",\"⣯\",\"⣷\"]},\"dots3\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠚\",\"⠞\",\"⠖\",\"⠦\",\"⠴\",\"⠲\",\"⠳\",\"⠓\"]},\"dots4\":{\"interval\":80,\"frames\":[\"⠄\",\"⠆\",\"⠇\",\"⠋\",\"⠙\",\"⠸\",\"⠰\",\"⠠\",\"⠰\",\"⠸\",\"⠙\",\"⠋\",\"⠇\",\"⠆\"]},\"dots5\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\"]},\"dots6\":{\"interval\":80,\"frames\":[\"⠁\",\"⠉\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠤\",\"⠄\",\"⠄\",\"⠤\",\"⠴\",\"⠲\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠚\",\"⠙\",\"⠉\",\"⠁\"]},\"dots7\":{\"interval\":80,\"frames\":[\"⠈\",\"⠉\",\"⠋\",\"⠓\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠖\",\"⠦\",\"⠤\",\"⠠\",\"⠠\",\"⠤\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\",\"⠉\",\"⠈\"]},\"dots8\":{\"interval\":80,\"frames\":[\"⠁\",\"⠁\",\"⠉\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠤\",\"⠄\",\"⠄\",\"⠤\",\"⠠\",\"⠠\",\"⠤\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\",\"⠉\",\"⠈\",\"⠈\"]},\"dots9\":{\"interval\":80,\"frames\":[\"⢹\",\"⢺\",\"⢼\",\"⣸\",\"⣇\",\"⡧\",\"⡗\",\"⡏\"]},\"dots10\":{\"interval\":80,\"frames\":[\"⢄\",\"⢂\",\"⢁\",\"⡁\",\"⡈\",\"⡐\",\"⡠\"]},\"dots11\":{\"interval\":100,\"frames\":[\"⠁\",\"⠂\",\"⠄\",\"⡀\",\"⢀\",\"⠠\",\"⠐\",\"⠈\"]},\"dots12\":{\"interval\":80,\"frames\":[\"⢀⠀\",\"⡀⠀\",\"⠄⠀\",\"⢂⠀\",\"⡂⠀\",\"⠅⠀\",\"⢃⠀\",\"⡃⠀\",\"⠍⠀\",\"⢋⠀\",\"⡋⠀\",\"⠍⠁\",\"⢋⠁\",\"⡋⠁\",\"⠍⠉\",\"⠋⠉\",\"⠋⠉\",\"⠉⠙\",\"⠉⠙\",\"⠉⠩\",\"⠈⢙\",\"⠈⡙\",\"⢈⠩\",\"⡀⢙\",\"⠄⡙\",\"⢂⠩\",\"⡂⢘\",\"⠅⡘\",\"⢃⠨\",\"⡃⢐\",\"⠍⡐\",\"⢋⠠\",\"⡋⢀\",\"⠍⡁\",\"⢋⠁\",\"⡋⠁\",\"⠍⠉\",\"⠋⠉\",\"⠋⠉\",\"⠉⠙\",\"⠉⠙\",\"⠉⠩\",\"⠈⢙\",\"⠈⡙\",\"⠈⠩\",\"⠀⢙\",\"⠀⡙\",\"⠀⠩\",\"⠀⢘\",\"⠀⡘\",\"⠀⠨\",\"⠀⢐\",\"⠀⡐\",\"⠀⠠\",\"⠀⢀\",\"⠀⡀\"]},\"dots8Bit\":{\"interval\":80,\"frames\":[\"⠀\",\"⠁\",\"⠂\",\"⠃\",\"⠄\",\"⠅\",\"⠆\",\"⠇\",\"⡀\",\"⡁\",\"⡂\",\"⡃\",\"⡄\",\"⡅\",\"⡆\",\"⡇\",\"⠈\",\"⠉\",\"⠊\",\"⠋\",\"⠌\",\"⠍\",\"⠎\",\"⠏\",\"⡈\",\"⡉\",\"⡊\",\"⡋\",\"⡌\",\"⡍\",\"⡎\",\"⡏\",\"⠐\",\"⠑\",\"⠒\",\"⠓\",\"⠔\",\"⠕\",\"⠖\",\"⠗\",\"⡐\",\"⡑\",\"⡒\",\"⡓\",\"⡔\",\"⡕\",\"⡖\",\"⡗\",\"⠘\",\"⠙\",\"⠚\",\"⠛\",\"⠜\",\"⠝\",\"⠞\",\"⠟\",\"⡘\",\"⡙\",\"⡚\",\"⡛\",\"⡜\",\"⡝\",\"⡞\",\"⡟\",\"⠠\",\"⠡\",\"⠢\",\"⠣\",\"⠤\",\"⠥\",\"⠦\",\"⠧\",\"⡠\",\"⡡\",\"⡢\",\"⡣\",\"⡤\",\"⡥\",\"⡦\",\"⡧\",\"⠨\",\"⠩\",\"⠪\",\"⠫\",\"⠬\",\"⠭\",\"⠮\",\"⠯\",\"⡨\",\"⡩\",\"⡪\",\"⡫\",\"⡬\",\"⡭\",\"⡮\",\"⡯\",\"⠰\",\"⠱\",\"⠲\",\"⠳\",\"⠴\",\"⠵\",\"⠶\",\"⠷\",\"⡰\",\"⡱\",\"⡲\",\"⡳\",\"⡴\",\"⡵\",\"⡶\",\"⡷\",\"⠸\",\"⠹\",\"⠺\",\"⠻\",\"⠼\",\"⠽\",\"⠾\",\"⠿\",\"⡸\",\"⡹\",\"⡺\",\"⡻\",\"⡼\",\"⡽\",\"⡾\",\"⡿\",\"⢀\",\"⢁\",\"⢂\",\"⢃\",\"⢄\",\"⢅\",\"⢆\",\"⢇\",\"⣀\",\"⣁\",\"⣂\",\"⣃\",\"⣄\",\"⣅\",\"⣆\",\"⣇\",\"⢈\",\"⢉\",\"⢊\",\"⢋\",\"⢌\",\"⢍\",\"⢎\",\"⢏\",\"⣈\",\"⣉\",\"⣊\",\"⣋\",\"⣌\",\"⣍\",\"⣎\",\"⣏\",\"⢐\",\"⢑\",\"⢒\",\"⢓\",\"⢔\",\"⢕\",\"⢖\",\"⢗\",\"⣐\",\"⣑\",\"⣒\",\"⣓\",\"⣔\",\"⣕\",\"⣖\",\"⣗\",\"⢘\",\"⢙\",\"⢚\",\"⢛\",\"⢜\",\"⢝\",\"⢞\",\"⢟\",\"⣘\",\"⣙\",\"⣚\",\"⣛\",\"⣜\",\"⣝\",\"⣞\",\"⣟\",\"⢠\",\"⢡\",\"⢢\",\"⢣\",\"⢤\",\"⢥\",\"⢦\",\"⢧\",\"⣠\",\"⣡\",\"⣢\",\"⣣\",\"⣤\",\"⣥\",\"⣦\",\"⣧\",\"⢨\",\"⢩\",\"⢪\",\"⢫\",\"⢬\",\"⢭\",\"⢮\",\"⢯\",\"⣨\",\"⣩\",\"⣪\",\"⣫\",\"⣬\",\"⣭\",\"⣮\",\"⣯\",\"⢰\",\"⢱\",\"⢲\",\"⢳\",\"⢴\",\"⢵\",\"⢶\",\"⢷\",\"⣰\",\"⣱\",\"⣲\",\"⣳\",\"⣴\",\"⣵\",\"⣶\",\"⣷\",\"⢸\",\"⢹\",\"⢺\",\"⢻\",\"⢼\",\"⢽\",\"⢾\",\"⢿\",\"⣸\",\"⣹\",\"⣺\",\"⣻\",\"⣼\",\"⣽\",\"⣾\",\"⣿\"]},\"line\":{\"interval\":130,\"frames\":[\"-\",\"\\\\\",\"|\",\"/\"]},\"line2\":{\"interval\":100,\"frames\":[\"⠂\",\"-\",\"–\",\"—\",\"–\",\"-\"]},\"pipe\":{\"interval\":100,\"frames\":[\"┤\",\"┘\",\"┴\",\"└\",\"├\",\"┌\",\"┬\",\"┐\"]},\"simpleDots\":{\"interval\":400,\"frames\":[\". \",\".. \",\"...\",\" \"]},\"simpleDotsScrolling\":{\"interval\":200,\"frames\":[\". \",\".. \",\"...\",\" ..\",\" .\",\" \"]},\"star\":{\"interval\":70,\"frames\":[\"✶\",\"✸\",\"✹\",\"✺\",\"✹\",\"✷\"]},\"star2\":{\"interval\":80,\"frames\":[\"+\",\"x\",\"*\"]},\"flip\":{\"interval\":70,\"frames\":[\"_\",\"_\",\"_\",\"-\",\"`\",\"`\",\"'\",\"´\",\"-\",\"_\",\"_\",\"_\"]},\"hamburger\":{\"interval\":100,\"frames\":[\"☱\",\"☲\",\"☴\"]},\"growVertical\":{\"interval\":120,\"frames\":[\"▁\",\"▃\",\"▄\",\"▅\",\"▆\",\"▇\",\"▆\",\"▅\",\"▄\",\"▃\"]},\"growHorizontal\":{\"interval\":120,\"frames\":[\"▏\",\"▎\",\"▍\",\"▌\",\"▋\",\"▊\",\"▉\",\"▊\",\"▋\",\"▌\",\"▍\",\"▎\"]},\"balloon\":{\"interval\":140,\"frames\":[\" \",\".\",\"o\",\"O\",\"@\",\"*\",\" \"]},\"balloon2\":{\"interval\":120,\"frames\":[\".\",\"o\",\"O\",\"°\",\"O\",\"o\",\".\"]},\"noise\":{\"interval\":100,\"frames\":[\"▓\",\"▒\",\"░\"]},\"bounce\":{\"interval\":120,\"frames\":[\"⠁\",\"⠂\",\"⠄\",\"⠂\"]},\"boxBounce\":{\"interval\":120,\"frames\":[\"▖\",\"▘\",\"▝\",\"▗\"]},\"boxBounce2\":{\"interval\":100,\"frames\":[\"▌\",\"▀\",\"▐\",\"▄\"]},\"triangle\":{\"interval\":50,\"frames\":[\"◢\",\"◣\",\"◤\",\"◥\"]},\"arc\":{\"interval\":100,\"frames\":[\"◜\",\"◠\",\"◝\",\"◞\",\"◡\",\"◟\"]},\"circle\":{\"interval\":120,\"frames\":[\"◡\",\"⊙\",\"◠\"]},\"squareCorners\":{\"interval\":180,\"frames\":[\"◰\",\"◳\",\"◲\",\"◱\"]},\"circleQuarters\":{\"interval\":120,\"frames\":[\"◴\",\"◷\",\"◶\",\"◵\"]},\"circleHalves\":{\"interval\":50,\"frames\":[\"◐\",\"◓\",\"◑\",\"◒\"]},\"squish\":{\"interval\":100,\"frames\":[\"╫\",\"╪\"]},\"toggle\":{\"interval\":250,\"frames\":[\"⊶\",\"⊷\"]},\"toggle2\":{\"interval\":80,\"frames\":[\"▫\",\"▪\"]},\"toggle3\":{\"interval\":120,\"frames\":[\"□\",\"■\"]},\"toggle4\":{\"interval\":100,\"frames\":[\"■\",\"□\",\"▪\",\"▫\"]},\"toggle5\":{\"interval\":100,\"frames\":[\"▮\",\"▯\"]},\"toggle6\":{\"interval\":300,\"frames\":[\"ဝ\",\"၀\"]},\"toggle7\":{\"interval\":80,\"frames\":[\"⦾\",\"⦿\"]},\"toggle8\":{\"interval\":100,\"frames\":[\"◍\",\"◌\"]},\"toggle9\":{\"interval\":100,\"frames\":[\"◉\",\"◎\"]},\"toggle10\":{\"interval\":100,\"frames\":[\"㊂\",\"㊀\",\"㊁\"]},\"toggle11\":{\"interval\":50,\"frames\":[\"⧇\",\"⧆\"]},\"toggle12\":{\"interval\":120,\"frames\":[\"☗\",\"☖\"]},\"toggle13\":{\"interval\":80,\"frames\":[\"=\",\"*\",\"-\"]},\"arrow\":{\"interval\":100,\"frames\":[\"←\",\"↖\",\"↑\",\"↗\",\"→\",\"↘\",\"↓\",\"↙\"]},\"arrow2\":{\"interval\":80,\"frames\":[\"⬆️ \",\"↗️ \",\"➡️ \",\"↘️ \",\"⬇️ \",\"↙️ \",\"⬅️ \",\"↖️ \"]},\"arrow3\":{\"interval\":120,\"frames\":[\"▹▹▹▹▹\",\"▸▹▹▹▹\",\"▹▸▹▹▹\",\"▹▹▸▹▹\",\"▹▹▹▸▹\",\"▹▹▹▹▸\"]},\"bouncingBar\":{\"interval\":80,\"frames\":[\"[ ]\",\"[= ]\",\"[== ]\",\"[=== ]\",\"[ ===]\",\"[ ==]\",\"[ =]\",\"[ ]\",\"[ =]\",\"[ ==]\",\"[ ===]\",\"[====]\",\"[=== ]\",\"[== ]\",\"[= ]\"]},\"bouncingBall\":{\"interval\":80,\"frames\":[\"( ● )\",\"( ● )\",\"( ● )\",\"( ● )\",\"( ●)\",\"( ● )\",\"( ● )\",\"( ● )\",\"( ● )\",\"(● )\"]},\"smiley\":{\"interval\":200,\"frames\":[\"😄 \",\"😝 \"]},\"monkey\":{\"interval\":300,\"frames\":[\"🙈 \",\"🙈 \",\"🙉 \",\"🙊 \"]},\"hearts\":{\"interval\":100,\"frames\":[\"💛 \",\"💙 \",\"💜 \",\"💚 \",\"❤️ \"]},\"clock\":{\"interval\":100,\"frames\":[\"🕛 \",\"🕐 \",\"🕑 \",\"🕒 \",\"🕓 \",\"🕔 \",\"🕕 \",\"🕖 \",\"🕗 \",\"🕘 \",\"🕙 \",\"🕚 \"]},\"earth\":{\"interval\":180,\"frames\":[\"🌍 \",\"🌎 \",\"🌏 \"]},\"material\":{\"interval\":17,\"frames\":[\"█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"███▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"████▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"███████▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"████████▁▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"██████████▁▁▁▁▁▁▁▁▁▁\",\"███████████▁▁▁▁▁▁▁▁▁\",\"█████████████▁▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁▁██████████████▁▁▁▁\",\"▁▁▁██████████████▁▁▁\",\"▁▁▁▁█████████████▁▁▁\",\"▁▁▁▁██████████████▁▁\",\"▁▁▁▁██████████████▁▁\",\"▁▁▁▁▁██████████████▁\",\"▁▁▁▁▁██████████████▁\",\"▁▁▁▁▁██████████████▁\",\"▁▁▁▁▁▁██████████████\",\"▁▁▁▁▁▁██████████████\",\"▁▁▁▁▁▁▁█████████████\",\"▁▁▁▁▁▁▁█████████████\",\"▁▁▁▁▁▁▁▁████████████\",\"▁▁▁▁▁▁▁▁████████████\",\"▁▁▁▁▁▁▁▁▁███████████\",\"▁▁▁▁▁▁▁▁▁███████████\",\"▁▁▁▁▁▁▁▁▁▁██████████\",\"▁▁▁▁▁▁▁▁▁▁██████████\",\"▁▁▁▁▁▁▁▁▁▁▁▁████████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁██████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\"█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"███▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"████▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"██████▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"████████▁▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"███████████▁▁▁▁▁▁▁▁▁\",\"████████████▁▁▁▁▁▁▁▁\",\"████████████▁▁▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁▁▁█████████████▁▁▁▁\",\"▁▁▁▁▁████████████▁▁▁\",\"▁▁▁▁▁████████████▁▁▁\",\"▁▁▁▁▁▁███████████▁▁▁\",\"▁▁▁▁▁▁▁▁█████████▁▁▁\",\"▁▁▁▁▁▁▁▁█████████▁▁▁\",\"▁▁▁▁▁▁▁▁▁█████████▁▁\",\"▁▁▁▁▁▁▁▁▁█████████▁▁\",\"▁▁▁▁▁▁▁▁▁▁█████████▁\",\"▁▁▁▁▁▁▁▁▁▁▁████████▁\",\"▁▁▁▁▁▁▁▁▁▁▁████████▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁███████▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁███████▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\"]},\"moon\":{\"interval\":80,\"frames\":[\"🌑 \",\"🌒 \",\"🌓 \",\"🌔 \",\"🌕 \",\"🌖 \",\"🌗 \",\"🌘 \"]},\"runner\":{\"interval\":140,\"frames\":[\"🚶 \",\"🏃 \"]},\"pong\":{\"interval\":80,\"frames\":[\"▐⠂ ▌\",\"▐⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂▌\",\"▐ ⠠▌\",\"▐ ⡀▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐⠠ ▌\"]},\"shark\":{\"interval\":120,\"frames\":[\"▐|\\\\____________▌\",\"▐_|\\\\___________▌\",\"▐__|\\\\__________▌\",\"▐___|\\\\_________▌\",\"▐____|\\\\________▌\",\"▐_____|\\\\_______▌\",\"▐______|\\\\______▌\",\"▐_______|\\\\_____▌\",\"▐________|\\\\____▌\",\"▐_________|\\\\___▌\",\"▐__________|\\\\__▌\",\"▐___________|\\\\_▌\",\"▐____________|\\\\▌\",\"▐____________/|▌\",\"▐___________/|_▌\",\"▐__________/|__▌\",\"▐_________/|___▌\",\"▐________/|____▌\",\"▐_______/|_____▌\",\"▐______/|______▌\",\"▐_____/|_______▌\",\"▐____/|________▌\",\"▐___/|_________▌\",\"▐__/|__________▌\",\"▐_/|___________▌\",\"▐/|____________▌\"]},\"dqpb\":{\"interval\":100,\"frames\":[\"d\",\"q\",\"p\",\"b\"]},\"weather\":{\"interval\":100,\"frames\":[\"☀️ \",\"☀️ \",\"☀️ \",\"🌤 \",\"⛅️ \",\"🌥 \",\"☁️ \",\"🌧 \",\"🌨 \",\"🌧 \",\"🌨 \",\"🌧 \",\"🌨 \",\"⛈ \",\"🌨 \",\"🌧 \",\"🌨 \",\"☁️ \",\"🌥 \",\"⛅️ \",\"🌤 \",\"☀️ \",\"☀️ \"]},\"christmas\":{\"interval\":400,\"frames\":[\"🌲\",\"🎄\"]},\"grenade\":{\"interval\":80,\"frames\":[\"، \",\"′ \",\" ´ \",\" ‾ \",\" ⸌\",\" ⸊\",\" |\",\" ⁎\",\" ⁕\",\" ෴ \",\" ⁓\",\" \",\" \",\" \"]},\"point\":{\"interval\":125,\"frames\":[\"∙∙∙\",\"●∙∙\",\"∙●∙\",\"∙∙●\",\"∙∙∙\"]},\"layer\":{\"interval\":150,\"frames\":[\"-\",\"=\",\"≡\"]},\"betaWave\":{\"interval\":80,\"frames\":[\"ρββββββ\",\"βρβββββ\",\"ββρββββ\",\"βββρβββ\",\"ββββρββ\",\"βββββρβ\",\"ββββββρ\"]}}"); +module.exports = JSON.parse("{\"dots\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠹\",\"⠸\",\"⠼\",\"⠴\",\"⠦\",\"⠧\",\"⠇\",\"⠏\"]},\"dots2\":{\"interval\":80,\"frames\":[\"⣾\",\"⣽\",\"⣻\",\"⢿\",\"⡿\",\"⣟\",\"⣯\",\"⣷\"]},\"dots3\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠚\",\"⠞\",\"⠖\",\"⠦\",\"⠴\",\"⠲\",\"⠳\",\"⠓\"]},\"dots4\":{\"interval\":80,\"frames\":[\"⠄\",\"⠆\",\"⠇\",\"⠋\",\"⠙\",\"⠸\",\"⠰\",\"⠠\",\"⠰\",\"⠸\",\"⠙\",\"⠋\",\"⠇\",\"⠆\"]},\"dots5\":{\"interval\":80,\"frames\":[\"⠋\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\"]},\"dots6\":{\"interval\":80,\"frames\":[\"⠁\",\"⠉\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠤\",\"⠄\",\"⠄\",\"⠤\",\"⠴\",\"⠲\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠚\",\"⠙\",\"⠉\",\"⠁\"]},\"dots7\":{\"interval\":80,\"frames\":[\"⠈\",\"⠉\",\"⠋\",\"⠓\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠖\",\"⠦\",\"⠤\",\"⠠\",\"⠠\",\"⠤\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\",\"⠉\",\"⠈\"]},\"dots8\":{\"interval\":80,\"frames\":[\"⠁\",\"⠁\",\"⠉\",\"⠙\",\"⠚\",\"⠒\",\"⠂\",\"⠂\",\"⠒\",\"⠲\",\"⠴\",\"⠤\",\"⠄\",\"⠄\",\"⠤\",\"⠠\",\"⠠\",\"⠤\",\"⠦\",\"⠖\",\"⠒\",\"⠐\",\"⠐\",\"⠒\",\"⠓\",\"⠋\",\"⠉\",\"⠈\",\"⠈\"]},\"dots9\":{\"interval\":80,\"frames\":[\"⢹\",\"⢺\",\"⢼\",\"⣸\",\"⣇\",\"⡧\",\"⡗\",\"⡏\"]},\"dots10\":{\"interval\":80,\"frames\":[\"⢄\",\"⢂\",\"⢁\",\"⡁\",\"⡈\",\"⡐\",\"⡠\"]},\"dots11\":{\"interval\":100,\"frames\":[\"⠁\",\"⠂\",\"⠄\",\"⡀\",\"⢀\",\"⠠\",\"⠐\",\"⠈\"]},\"dots12\":{\"interval\":80,\"frames\":[\"⢀⠀\",\"⡀⠀\",\"⠄⠀\",\"⢂⠀\",\"⡂⠀\",\"⠅⠀\",\"⢃⠀\",\"⡃⠀\",\"⠍⠀\",\"⢋⠀\",\"⡋⠀\",\"⠍⠁\",\"⢋⠁\",\"⡋⠁\",\"⠍⠉\",\"⠋⠉\",\"⠋⠉\",\"⠉⠙\",\"⠉⠙\",\"⠉⠩\",\"⠈⢙\",\"⠈⡙\",\"⢈⠩\",\"⡀⢙\",\"⠄⡙\",\"⢂⠩\",\"⡂⢘\",\"⠅⡘\",\"⢃⠨\",\"⡃⢐\",\"⠍⡐\",\"⢋⠠\",\"⡋⢀\",\"⠍⡁\",\"⢋⠁\",\"⡋⠁\",\"⠍⠉\",\"⠋⠉\",\"⠋⠉\",\"⠉⠙\",\"⠉⠙\",\"⠉⠩\",\"⠈⢙\",\"⠈⡙\",\"⠈⠩\",\"⠀⢙\",\"⠀⡙\",\"⠀⠩\",\"⠀⢘\",\"⠀⡘\",\"⠀⠨\",\"⠀⢐\",\"⠀⡐\",\"⠀⠠\",\"⠀⢀\",\"⠀⡀\"]},\"dots8Bit\":{\"interval\":80,\"frames\":[\"⠀\",\"⠁\",\"⠂\",\"⠃\",\"⠄\",\"⠅\",\"⠆\",\"⠇\",\"⡀\",\"⡁\",\"⡂\",\"⡃\",\"⡄\",\"⡅\",\"⡆\",\"⡇\",\"⠈\",\"⠉\",\"⠊\",\"⠋\",\"⠌\",\"⠍\",\"⠎\",\"⠏\",\"⡈\",\"⡉\",\"⡊\",\"⡋\",\"⡌\",\"⡍\",\"⡎\",\"⡏\",\"⠐\",\"⠑\",\"⠒\",\"⠓\",\"⠔\",\"⠕\",\"⠖\",\"⠗\",\"⡐\",\"⡑\",\"⡒\",\"⡓\",\"⡔\",\"⡕\",\"⡖\",\"⡗\",\"⠘\",\"⠙\",\"⠚\",\"⠛\",\"⠜\",\"⠝\",\"⠞\",\"⠟\",\"⡘\",\"⡙\",\"⡚\",\"⡛\",\"⡜\",\"⡝\",\"⡞\",\"⡟\",\"⠠\",\"⠡\",\"⠢\",\"⠣\",\"⠤\",\"⠥\",\"⠦\",\"⠧\",\"⡠\",\"⡡\",\"⡢\",\"⡣\",\"⡤\",\"⡥\",\"⡦\",\"⡧\",\"⠨\",\"⠩\",\"⠪\",\"⠫\",\"⠬\",\"⠭\",\"⠮\",\"⠯\",\"⡨\",\"⡩\",\"⡪\",\"⡫\",\"⡬\",\"⡭\",\"⡮\",\"⡯\",\"⠰\",\"⠱\",\"⠲\",\"⠳\",\"⠴\",\"⠵\",\"⠶\",\"⠷\",\"⡰\",\"⡱\",\"⡲\",\"⡳\",\"⡴\",\"⡵\",\"⡶\",\"⡷\",\"⠸\",\"⠹\",\"⠺\",\"⠻\",\"⠼\",\"⠽\",\"⠾\",\"⠿\",\"⡸\",\"⡹\",\"⡺\",\"⡻\",\"⡼\",\"⡽\",\"⡾\",\"⡿\",\"⢀\",\"⢁\",\"⢂\",\"⢃\",\"⢄\",\"⢅\",\"⢆\",\"⢇\",\"⣀\",\"⣁\",\"⣂\",\"⣃\",\"⣄\",\"⣅\",\"⣆\",\"⣇\",\"⢈\",\"⢉\",\"⢊\",\"⢋\",\"⢌\",\"⢍\",\"⢎\",\"⢏\",\"⣈\",\"⣉\",\"⣊\",\"⣋\",\"⣌\",\"⣍\",\"⣎\",\"⣏\",\"⢐\",\"⢑\",\"⢒\",\"⢓\",\"⢔\",\"⢕\",\"⢖\",\"⢗\",\"⣐\",\"⣑\",\"⣒\",\"⣓\",\"⣔\",\"⣕\",\"⣖\",\"⣗\",\"⢘\",\"⢙\",\"⢚\",\"⢛\",\"⢜\",\"⢝\",\"⢞\",\"⢟\",\"⣘\",\"⣙\",\"⣚\",\"⣛\",\"⣜\",\"⣝\",\"⣞\",\"⣟\",\"⢠\",\"⢡\",\"⢢\",\"⢣\",\"⢤\",\"⢥\",\"⢦\",\"⢧\",\"⣠\",\"⣡\",\"⣢\",\"⣣\",\"⣤\",\"⣥\",\"⣦\",\"⣧\",\"⢨\",\"⢩\",\"⢪\",\"⢫\",\"⢬\",\"⢭\",\"⢮\",\"⢯\",\"⣨\",\"⣩\",\"⣪\",\"⣫\",\"⣬\",\"⣭\",\"⣮\",\"⣯\",\"⢰\",\"⢱\",\"⢲\",\"⢳\",\"⢴\",\"⢵\",\"⢶\",\"⢷\",\"⣰\",\"⣱\",\"⣲\",\"⣳\",\"⣴\",\"⣵\",\"⣶\",\"⣷\",\"⢸\",\"⢹\",\"⢺\",\"⢻\",\"⢼\",\"⢽\",\"⢾\",\"⢿\",\"⣸\",\"⣹\",\"⣺\",\"⣻\",\"⣼\",\"⣽\",\"⣾\",\"⣿\"]},\"line\":{\"interval\":130,\"frames\":[\"-\",\"\\\\\",\"|\",\"/\"]},\"line2\":{\"interval\":100,\"frames\":[\"⠂\",\"-\",\"–\",\"—\",\"–\",\"-\"]},\"pipe\":{\"interval\":100,\"frames\":[\"┤\",\"┘\",\"┴\",\"└\",\"├\",\"┌\",\"┬\",\"┐\"]},\"simpleDots\":{\"interval\":400,\"frames\":[\". \",\".. \",\"...\",\" \"]},\"simpleDotsScrolling\":{\"interval\":200,\"frames\":[\". \",\".. \",\"...\",\" ..\",\" .\",\" \"]},\"star\":{\"interval\":70,\"frames\":[\"✶\",\"✸\",\"✹\",\"✺\",\"✹\",\"✷\"]},\"star2\":{\"interval\":80,\"frames\":[\"+\",\"x\",\"*\"]},\"flip\":{\"interval\":70,\"frames\":[\"_\",\"_\",\"_\",\"-\",\"`\",\"`\",\"'\",\"´\",\"-\",\"_\",\"_\",\"_\"]},\"hamburger\":{\"interval\":100,\"frames\":[\"☱\",\"☲\",\"☴\"]},\"growVertical\":{\"interval\":120,\"frames\":[\"▁\",\"▃\",\"▄\",\"▅\",\"▆\",\"▇\",\"▆\",\"▅\",\"▄\",\"▃\"]},\"growHorizontal\":{\"interval\":120,\"frames\":[\"▏\",\"▎\",\"▍\",\"▌\",\"▋\",\"▊\",\"▉\",\"▊\",\"▋\",\"▌\",\"▍\",\"▎\"]},\"balloon\":{\"interval\":140,\"frames\":[\" \",\".\",\"o\",\"O\",\"@\",\"*\",\" \"]},\"balloon2\":{\"interval\":120,\"frames\":[\".\",\"o\",\"O\",\"°\",\"O\",\"o\",\".\"]},\"noise\":{\"interval\":100,\"frames\":[\"▓\",\"▒\",\"░\"]},\"bounce\":{\"interval\":120,\"frames\":[\"⠁\",\"⠂\",\"⠄\",\"⠂\"]},\"boxBounce\":{\"interval\":120,\"frames\":[\"▖\",\"▘\",\"▝\",\"▗\"]},\"boxBounce2\":{\"interval\":100,\"frames\":[\"▌\",\"▀\",\"▐\",\"▄\"]},\"triangle\":{\"interval\":50,\"frames\":[\"◢\",\"◣\",\"◤\",\"◥\"]},\"arc\":{\"interval\":100,\"frames\":[\"◜\",\"◠\",\"◝\",\"◞\",\"◡\",\"◟\"]},\"circle\":{\"interval\":120,\"frames\":[\"◡\",\"⊙\",\"◠\"]},\"squareCorners\":{\"interval\":180,\"frames\":[\"◰\",\"◳\",\"◲\",\"◱\"]},\"circleQuarters\":{\"interval\":120,\"frames\":[\"◴\",\"◷\",\"◶\",\"◵\"]},\"circleHalves\":{\"interval\":50,\"frames\":[\"◐\",\"◓\",\"◑\",\"◒\"]},\"squish\":{\"interval\":100,\"frames\":[\"╫\",\"╪\"]},\"toggle\":{\"interval\":250,\"frames\":[\"⊶\",\"⊷\"]},\"toggle2\":{\"interval\":80,\"frames\":[\"▫\",\"▪\"]},\"toggle3\":{\"interval\":120,\"frames\":[\"□\",\"■\"]},\"toggle4\":{\"interval\":100,\"frames\":[\"■\",\"□\",\"▪\",\"▫\"]},\"toggle5\":{\"interval\":100,\"frames\":[\"▮\",\"▯\"]},\"toggle6\":{\"interval\":300,\"frames\":[\"ဝ\",\"၀\"]},\"toggle7\":{\"interval\":80,\"frames\":[\"⦾\",\"⦿\"]},\"toggle8\":{\"interval\":100,\"frames\":[\"◍\",\"◌\"]},\"toggle9\":{\"interval\":100,\"frames\":[\"◉\",\"◎\"]},\"toggle10\":{\"interval\":100,\"frames\":[\"㊂\",\"㊀\",\"㊁\"]},\"toggle11\":{\"interval\":50,\"frames\":[\"⧇\",\"⧆\"]},\"toggle12\":{\"interval\":120,\"frames\":[\"☗\",\"☖\"]},\"toggle13\":{\"interval\":80,\"frames\":[\"=\",\"*\",\"-\"]},\"arrow\":{\"interval\":100,\"frames\":[\"←\",\"↖\",\"↑\",\"↗\",\"→\",\"↘\",\"↓\",\"↙\"]},\"arrow2\":{\"interval\":80,\"frames\":[\"⬆️ \",\"↗️ \",\"➡️ \",\"↘️ \",\"⬇️ \",\"↙️ \",\"⬅️ \",\"↖️ \"]},\"arrow3\":{\"interval\":120,\"frames\":[\"▹▹▹▹▹\",\"▸▹▹▹▹\",\"▹▸▹▹▹\",\"▹▹▸▹▹\",\"▹▹▹▸▹\",\"▹▹▹▹▸\"]},\"bouncingBar\":{\"interval\":80,\"frames\":[\"[ ]\",\"[= ]\",\"[== ]\",\"[=== ]\",\"[ ===]\",\"[ ==]\",\"[ =]\",\"[ ]\",\"[ =]\",\"[ ==]\",\"[ ===]\",\"[====]\",\"[=== ]\",\"[== ]\",\"[= ]\"]},\"bouncingBall\":{\"interval\":80,\"frames\":[\"( ● )\",\"( ● )\",\"( ● )\",\"( ● )\",\"( ●)\",\"( ● )\",\"( ● )\",\"( ● )\",\"( ● )\",\"(● )\"]},\"smiley\":{\"interval\":200,\"frames\":[\"😄 \",\"😝 \"]},\"monkey\":{\"interval\":300,\"frames\":[\"🙈 \",\"🙈 \",\"🙉 \",\"🙊 \"]},\"hearts\":{\"interval\":100,\"frames\":[\"💛 \",\"💙 \",\"💜 \",\"💚 \",\"❤️ \"]},\"clock\":{\"interval\":100,\"frames\":[\"🕛 \",\"🕐 \",\"🕑 \",\"🕒 \",\"🕓 \",\"🕔 \",\"🕕 \",\"🕖 \",\"🕗 \",\"🕘 \",\"🕙 \",\"🕚 \"]},\"earth\":{\"interval\":180,\"frames\":[\"🌍 \",\"🌎 \",\"🌏 \"]},\"material\":{\"interval\":17,\"frames\":[\"█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"███▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"████▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"███████▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"████████▁▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"██████████▁▁▁▁▁▁▁▁▁▁\",\"███████████▁▁▁▁▁▁▁▁▁\",\"█████████████▁▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁▁██████████████▁▁▁▁\",\"▁▁▁██████████████▁▁▁\",\"▁▁▁▁█████████████▁▁▁\",\"▁▁▁▁██████████████▁▁\",\"▁▁▁▁██████████████▁▁\",\"▁▁▁▁▁██████████████▁\",\"▁▁▁▁▁██████████████▁\",\"▁▁▁▁▁██████████████▁\",\"▁▁▁▁▁▁██████████████\",\"▁▁▁▁▁▁██████████████\",\"▁▁▁▁▁▁▁█████████████\",\"▁▁▁▁▁▁▁█████████████\",\"▁▁▁▁▁▁▁▁████████████\",\"▁▁▁▁▁▁▁▁████████████\",\"▁▁▁▁▁▁▁▁▁███████████\",\"▁▁▁▁▁▁▁▁▁███████████\",\"▁▁▁▁▁▁▁▁▁▁██████████\",\"▁▁▁▁▁▁▁▁▁▁██████████\",\"▁▁▁▁▁▁▁▁▁▁▁▁████████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁██████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\"█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"███▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"████▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"██████▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"████████▁▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"█████████▁▁▁▁▁▁▁▁▁▁▁\",\"███████████▁▁▁▁▁▁▁▁▁\",\"████████████▁▁▁▁▁▁▁▁\",\"████████████▁▁▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"██████████████▁▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁██████████████▁▁▁▁▁\",\"▁▁▁█████████████▁▁▁▁\",\"▁▁▁▁▁████████████▁▁▁\",\"▁▁▁▁▁████████████▁▁▁\",\"▁▁▁▁▁▁███████████▁▁▁\",\"▁▁▁▁▁▁▁▁█████████▁▁▁\",\"▁▁▁▁▁▁▁▁█████████▁▁▁\",\"▁▁▁▁▁▁▁▁▁█████████▁▁\",\"▁▁▁▁▁▁▁▁▁█████████▁▁\",\"▁▁▁▁▁▁▁▁▁▁█████████▁\",\"▁▁▁▁▁▁▁▁▁▁▁████████▁\",\"▁▁▁▁▁▁▁▁▁▁▁████████▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁███████▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁███████▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁███████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\",\"▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁\"]},\"moon\":{\"interval\":80,\"frames\":[\"🌑 \",\"🌒 \",\"🌓 \",\"🌔 \",\"🌕 \",\"🌖 \",\"🌗 \",\"🌘 \"]},\"runner\":{\"interval\":140,\"frames\":[\"🚶 \",\"🏃 \"]},\"pong\":{\"interval\":80,\"frames\":[\"▐⠂ ▌\",\"▐⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂▌\",\"▐ ⠠▌\",\"▐ ⡀▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐ ⠠ ▌\",\"▐ ⠂ ▌\",\"▐ ⠈ ▌\",\"▐ ⠂ ▌\",\"▐ ⠠ ▌\",\"▐ ⡀ ▌\",\"▐⠠ ▌\"]},\"shark\":{\"interval\":120,\"frames\":[\"▐|\\\\____________▌\",\"▐_|\\\\___________▌\",\"▐__|\\\\__________▌\",\"▐___|\\\\_________▌\",\"▐____|\\\\________▌\",\"▐_____|\\\\_______▌\",\"▐______|\\\\______▌\",\"▐_______|\\\\_____▌\",\"▐________|\\\\____▌\",\"▐_________|\\\\___▌\",\"▐__________|\\\\__▌\",\"▐___________|\\\\_▌\",\"▐____________|\\\\▌\",\"▐____________/|▌\",\"▐___________/|_▌\",\"▐__________/|__▌\",\"▐_________/|___▌\",\"▐________/|____▌\",\"▐_______/|_____▌\",\"▐______/|______▌\",\"▐_____/|_______▌\",\"▐____/|________▌\",\"▐___/|_________▌\",\"▐__/|__________▌\",\"▐_/|___________▌\",\"▐/|____________▌\"]},\"dqpb\":{\"interval\":100,\"frames\":[\"d\",\"q\",\"p\",\"b\"]},\"weather\":{\"interval\":100,\"frames\":[\"☀️ \",\"☀️ \",\"☀️ \",\"🌤 \",\"⛅️ \",\"🌥 \",\"☁️ \",\"🌧 \",\"🌨 \",\"🌧 \",\"🌨 \",\"🌧 \",\"🌨 \",\"⛈ \",\"🌨 \",\"🌧 \",\"🌨 \",\"☁️ \",\"🌥 \",\"⛅️ \",\"🌤 \",\"☀️ \",\"☀️ \"]},\"christmas\":{\"interval\":400,\"frames\":[\"🌲\",\"🎄\"]},\"grenade\":{\"interval\":80,\"frames\":[\"، \",\"′ \",\" ´ \",\" ‾ \",\" ⸌\",\" ⸊\",\" |\",\" ⁎\",\" ⁕\",\" ෴ \",\" ⁓\",\" \",\" \",\" \"]},\"point\":{\"interval\":125,\"frames\":[\"∙∙∙\",\"●∙∙\",\"∙●∙\",\"∙∙●\",\"∙∙∙\"]},\"layer\":{\"interval\":150,\"frames\":[\"-\",\"=\",\"≡\"]},\"betaWave\":{\"interval\":80,\"frames\":[\"ρββββββ\",\"βρβββββ\",\"ββρββββ\",\"βββρβββ\",\"ββββρββ\",\"βββββρβ\",\"ββββββρ\"]},\"aesthetic\":{\"interval\":80,\"frames\":[\"▰▱▱▱▱▱▱\",\"▰▰▱▱▱▱▱\",\"▰▰▰▱▱▱▱\",\"▰▰▰▰▱▱▱\",\"▰▰▰▰▰▱▱\",\"▰▰▰▰▰▰▱\",\"▰▰▰▰▰▰▰\",\"▰▱▱▱▱▱▱\"]}}"); /***/ }), -/* 386 */ +/* 385 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const chalk = __webpack_require__(387); +const chalk = __webpack_require__(386); const isSupported = process.platform !== 'win32' || process.env.CI || process.env.TERM === 'xterm-256color'; @@ -50439,16 +50421,16 @@ module.exports = isSupported ? main : fallbacks; /***/ }), -/* 387 */ +/* 386 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const escapeStringRegexp = __webpack_require__(265); -const ansiStyles = __webpack_require__(388); -const stdoutColor = __webpack_require__(393).stdout; +const ansiStyles = __webpack_require__(387); +const stdoutColor = __webpack_require__(392).stdout; -const template = __webpack_require__(394); +const template = __webpack_require__(393); const isSimpleWindowsTerm = process.platform === 'win32' && !(process.env.TERM || '').toLowerCase().startsWith('xterm'); @@ -50674,12 +50656,12 @@ module.exports.default = module.exports; // For TypeScript /***/ }), -/* 388 */ +/* 387 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; /* WEBPACK VAR INJECTION */(function(module) { -const colorConvert = __webpack_require__(389); +const colorConvert = __webpack_require__(388); const wrapAnsi16 = (fn, offset) => function () { const code = fn.apply(colorConvert, arguments); @@ -50847,11 +50829,11 @@ Object.defineProperty(module, 'exports', { /* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(115)(module))) /***/ }), -/* 389 */ +/* 388 */ /***/ (function(module, exports, __webpack_require__) { -var conversions = __webpack_require__(390); -var route = __webpack_require__(392); +var conversions = __webpack_require__(389); +var route = __webpack_require__(391); var convert = {}; @@ -50931,11 +50913,11 @@ module.exports = convert; /***/ }), -/* 390 */ +/* 389 */ /***/ (function(module, exports, __webpack_require__) { /* MIT license */ -var cssKeywords = __webpack_require__(391); +var cssKeywords = __webpack_require__(390); // NOTE: conversions should only return primitive values (i.e. arrays, or // values that give correct `typeof` results). @@ -51805,7 +51787,7 @@ convert.rgb.gray = function (rgb) { /***/ }), -/* 391 */ +/* 390 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -51964,10 +51946,10 @@ module.exports = { /***/ }), -/* 392 */ +/* 391 */ /***/ (function(module, exports, __webpack_require__) { -var conversions = __webpack_require__(390); +var conversions = __webpack_require__(389); /* this function routes a model to all other models. @@ -52067,7 +52049,7 @@ module.exports = function (fromModel) { /***/ }), -/* 393 */ +/* 392 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -52205,7 +52187,7 @@ module.exports = { /***/ }), -/* 394 */ +/* 393 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -52340,18 +52322,18 @@ module.exports = (chalk, tmp) => { /***/ }), -/* 395 */ +/* 394 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const ansiRegex = __webpack_require__(396); +const ansiRegex = __webpack_require__(395); module.exports = string => typeof string === 'string' ? string.replace(ansiRegex(), '') : string; /***/ }), -/* 396 */ +/* 395 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -52368,14 +52350,14 @@ module.exports = ({onlyFirst = false} = {}) => { /***/ }), -/* 397 */ +/* 396 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var defaults = __webpack_require__(398) -var combining = __webpack_require__(400) +var defaults = __webpack_require__(397) +var combining = __webpack_require__(399) var DEFAULTS = { nul: 0, @@ -52474,10 +52456,10 @@ function bisearch(ucs) { /***/ }), -/* 398 */ +/* 397 */ /***/ (function(module, exports, __webpack_require__) { -var clone = __webpack_require__(399); +var clone = __webpack_require__(398); module.exports = function(options, defaults) { options = options || {}; @@ -52492,7 +52474,7 @@ module.exports = function(options, defaults) { }; /***/ }), -/* 399 */ +/* 398 */ /***/ (function(module, exports, __webpack_require__) { var clone = (function() { @@ -52664,7 +52646,7 @@ if ( true && module.exports) { /***/ }), -/* 400 */ +/* 399 */ /***/ (function(module, exports) { module.exports = [ @@ -52720,7 +52702,7 @@ module.exports = [ /***/ }), -/* 401 */ +/* 400 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -52736,7 +52718,7 @@ module.exports = ({stream = process.stdout} = {}) => { /***/ }), -/* 402 */ +/* 401 */ /***/ (function(module, exports, __webpack_require__) { var Stream = __webpack_require__(138) @@ -52887,7 +52869,7 @@ MuteStream.prototype.close = proxy('close') /***/ }), -/* 403 */ +/* 402 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52948,7 +52930,7 @@ const RunCommand = { }; /***/ }), -/* 404 */ +/* 403 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -52958,7 +52940,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(246); /* harmony import */ var _utils_parallelize__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(247); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(248); -/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(405); +/* harmony import */ var _utils_watch__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(404); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -53044,14 +53026,14 @@ const WatchCommand = { }; /***/ }), -/* 405 */ +/* 404 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "waitUntilWatchIsReady", function() { return waitUntilWatchIsReady; }); /* harmony import */ var rxjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(8); -/* harmony import */ var rxjs_operators__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(406); +/* harmony import */ var rxjs_operators__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(405); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -53118,141 +53100,141 @@ function waitUntilWatchIsReady(stream, opts = {}) { } /***/ }), -/* 406 */ +/* 405 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(407); +/* harmony import */ var _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(406); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "audit", function() { return _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__["audit"]; }); -/* harmony import */ var _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(408); +/* harmony import */ var _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(407); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "auditTime", function() { return _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__["auditTime"]; }); -/* harmony import */ var _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(409); +/* harmony import */ var _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(408); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buffer", function() { return _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__["buffer"]; }); -/* harmony import */ var _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(410); +/* harmony import */ var _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(409); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferCount", function() { return _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__["bufferCount"]; }); -/* harmony import */ var _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(411); +/* harmony import */ var _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(410); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferTime", function() { return _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__["bufferTime"]; }); -/* harmony import */ var _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(412); +/* harmony import */ var _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(411); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferToggle", function() { return _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__["bufferToggle"]; }); -/* harmony import */ var _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(413); +/* harmony import */ var _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(412); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferWhen", function() { return _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__["bufferWhen"]; }); -/* harmony import */ var _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(414); +/* harmony import */ var _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(413); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "catchError", function() { return _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__["catchError"]; }); -/* harmony import */ var _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(415); +/* harmony import */ var _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(414); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "combineAll", function() { return _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__["combineAll"]; }); -/* harmony import */ var _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(416); +/* harmony import */ var _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(415); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "combineLatest", function() { return _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__["combineLatest"]; }); -/* harmony import */ var _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(417); +/* harmony import */ var _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(416); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concat", function() { return _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__["concat"]; }); /* harmony import */ var _internal_operators_concatAll__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(80); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatAll", function() { return _internal_operators_concatAll__WEBPACK_IMPORTED_MODULE_11__["concatAll"]; }); -/* harmony import */ var _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(418); +/* harmony import */ var _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(417); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatMap", function() { return _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__["concatMap"]; }); -/* harmony import */ var _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(419); +/* harmony import */ var _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(418); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatMapTo", function() { return _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__["concatMapTo"]; }); -/* harmony import */ var _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(420); +/* harmony import */ var _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(419); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "count", function() { return _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__["count"]; }); -/* harmony import */ var _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(421); +/* harmony import */ var _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(420); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "debounce", function() { return _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__["debounce"]; }); -/* harmony import */ var _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(422); +/* harmony import */ var _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(421); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "debounceTime", function() { return _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__["debounceTime"]; }); -/* harmony import */ var _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(423); +/* harmony import */ var _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(422); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "defaultIfEmpty", function() { return _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__["defaultIfEmpty"]; }); -/* harmony import */ var _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(424); +/* harmony import */ var _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(423); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "delay", function() { return _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__["delay"]; }); -/* harmony import */ var _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(426); +/* harmony import */ var _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(425); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "delayWhen", function() { return _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__["delayWhen"]; }); -/* harmony import */ var _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(427); +/* harmony import */ var _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(426); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "dematerialize", function() { return _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__["dematerialize"]; }); -/* harmony import */ var _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(428); +/* harmony import */ var _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(427); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinct", function() { return _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__["distinct"]; }); -/* harmony import */ var _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(429); +/* harmony import */ var _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(428); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinctUntilChanged", function() { return _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__["distinctUntilChanged"]; }); -/* harmony import */ var _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(430); +/* harmony import */ var _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(429); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinctUntilKeyChanged", function() { return _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__["distinctUntilKeyChanged"]; }); -/* harmony import */ var _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(431); +/* harmony import */ var _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(430); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "elementAt", function() { return _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__["elementAt"]; }); -/* harmony import */ var _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(434); +/* harmony import */ var _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(433); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "endWith", function() { return _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__["endWith"]; }); -/* harmony import */ var _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(435); +/* harmony import */ var _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(434); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "every", function() { return _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__["every"]; }); -/* harmony import */ var _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(436); +/* harmony import */ var _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(435); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "exhaust", function() { return _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__["exhaust"]; }); -/* harmony import */ var _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(437); +/* harmony import */ var _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(436); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "exhaustMap", function() { return _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__["exhaustMap"]; }); -/* harmony import */ var _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(438); +/* harmony import */ var _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(437); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "expand", function() { return _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__["expand"]; }); /* harmony import */ var _internal_operators_filter__WEBPACK_IMPORTED_MODULE_30__ = __webpack_require__(105); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "filter", function() { return _internal_operators_filter__WEBPACK_IMPORTED_MODULE_30__["filter"]; }); -/* harmony import */ var _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(439); +/* harmony import */ var _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(438); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "finalize", function() { return _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__["finalize"]; }); -/* harmony import */ var _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(440); +/* harmony import */ var _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(439); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "find", function() { return _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__["find"]; }); -/* harmony import */ var _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(441); +/* harmony import */ var _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(440); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "findIndex", function() { return _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__["findIndex"]; }); -/* harmony import */ var _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(442); +/* harmony import */ var _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(441); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "first", function() { return _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__["first"]; }); /* harmony import */ var _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_35__ = __webpack_require__(31); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "groupBy", function() { return _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_35__["groupBy"]; }); -/* harmony import */ var _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(443); +/* harmony import */ var _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(442); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "ignoreElements", function() { return _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__["ignoreElements"]; }); -/* harmony import */ var _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(444); +/* harmony import */ var _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(443); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "isEmpty", function() { return _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__["isEmpty"]; }); -/* harmony import */ var _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(445); +/* harmony import */ var _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(444); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "last", function() { return _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__["last"]; }); /* harmony import */ var _internal_operators_map__WEBPACK_IMPORTED_MODULE_39__ = __webpack_require__(66); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "map", function() { return _internal_operators_map__WEBPACK_IMPORTED_MODULE_39__["map"]; }); -/* harmony import */ var _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(447); +/* harmony import */ var _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(446); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mapTo", function() { return _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__["mapTo"]; }); -/* harmony import */ var _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(448); +/* harmony import */ var _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(447); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "materialize", function() { return _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__["materialize"]; }); -/* harmony import */ var _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(449); +/* harmony import */ var _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(448); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "max", function() { return _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__["max"]; }); -/* harmony import */ var _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(452); +/* harmony import */ var _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(451); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "merge", function() { return _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__["merge"]; }); /* harmony import */ var _internal_operators_mergeAll__WEBPACK_IMPORTED_MODULE_44__ = __webpack_require__(81); @@ -53263,175 +53245,175 @@ __webpack_require__.r(__webpack_exports__); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "flatMap", function() { return _internal_operators_mergeMap__WEBPACK_IMPORTED_MODULE_45__["flatMap"]; }); -/* harmony import */ var _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(453); +/* harmony import */ var _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(452); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mergeMapTo", function() { return _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__["mergeMapTo"]; }); -/* harmony import */ var _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(454); +/* harmony import */ var _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(453); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mergeScan", function() { return _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__["mergeScan"]; }); -/* harmony import */ var _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(455); +/* harmony import */ var _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(454); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "min", function() { return _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__["min"]; }); -/* harmony import */ var _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(456); +/* harmony import */ var _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(455); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "multicast", function() { return _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__["multicast"]; }); /* harmony import */ var _internal_operators_observeOn__WEBPACK_IMPORTED_MODULE_50__ = __webpack_require__(41); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "observeOn", function() { return _internal_operators_observeOn__WEBPACK_IMPORTED_MODULE_50__["observeOn"]; }); -/* harmony import */ var _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(457); +/* harmony import */ var _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(456); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "onErrorResumeNext", function() { return _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__["onErrorResumeNext"]; }); -/* harmony import */ var _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__ = __webpack_require__(458); +/* harmony import */ var _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__ = __webpack_require__(457); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pairwise", function() { return _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__["pairwise"]; }); -/* harmony import */ var _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__ = __webpack_require__(459); +/* harmony import */ var _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__ = __webpack_require__(458); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "partition", function() { return _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__["partition"]; }); -/* harmony import */ var _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__ = __webpack_require__(460); +/* harmony import */ var _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__ = __webpack_require__(459); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pluck", function() { return _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__["pluck"]; }); -/* harmony import */ var _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__ = __webpack_require__(461); +/* harmony import */ var _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__ = __webpack_require__(460); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publish", function() { return _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__["publish"]; }); -/* harmony import */ var _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__ = __webpack_require__(462); +/* harmony import */ var _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__ = __webpack_require__(461); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishBehavior", function() { return _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__["publishBehavior"]; }); -/* harmony import */ var _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__ = __webpack_require__(463); +/* harmony import */ var _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__ = __webpack_require__(462); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishLast", function() { return _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__["publishLast"]; }); -/* harmony import */ var _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__ = __webpack_require__(464); +/* harmony import */ var _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__ = __webpack_require__(463); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishReplay", function() { return _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__["publishReplay"]; }); -/* harmony import */ var _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__ = __webpack_require__(465); +/* harmony import */ var _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__ = __webpack_require__(464); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "race", function() { return _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__["race"]; }); -/* harmony import */ var _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__ = __webpack_require__(450); +/* harmony import */ var _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__ = __webpack_require__(449); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "reduce", function() { return _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__["reduce"]; }); -/* harmony import */ var _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__ = __webpack_require__(466); +/* harmony import */ var _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__ = __webpack_require__(465); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "repeat", function() { return _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__["repeat"]; }); -/* harmony import */ var _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__ = __webpack_require__(467); +/* harmony import */ var _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__ = __webpack_require__(466); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "repeatWhen", function() { return _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__["repeatWhen"]; }); -/* harmony import */ var _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__ = __webpack_require__(468); +/* harmony import */ var _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__ = __webpack_require__(467); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "retry", function() { return _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__["retry"]; }); -/* harmony import */ var _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__ = __webpack_require__(469); +/* harmony import */ var _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__ = __webpack_require__(468); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "retryWhen", function() { return _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__["retryWhen"]; }); /* harmony import */ var _internal_operators_refCount__WEBPACK_IMPORTED_MODULE_65__ = __webpack_require__(30); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "refCount", function() { return _internal_operators_refCount__WEBPACK_IMPORTED_MODULE_65__["refCount"]; }); -/* harmony import */ var _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__ = __webpack_require__(470); +/* harmony import */ var _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__ = __webpack_require__(469); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sample", function() { return _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__["sample"]; }); -/* harmony import */ var _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__ = __webpack_require__(471); +/* harmony import */ var _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__ = __webpack_require__(470); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sampleTime", function() { return _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__["sampleTime"]; }); -/* harmony import */ var _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__ = __webpack_require__(451); +/* harmony import */ var _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__ = __webpack_require__(450); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "scan", function() { return _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__["scan"]; }); -/* harmony import */ var _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__ = __webpack_require__(472); +/* harmony import */ var _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__ = __webpack_require__(471); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sequenceEqual", function() { return _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__["sequenceEqual"]; }); -/* harmony import */ var _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__ = __webpack_require__(473); +/* harmony import */ var _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__ = __webpack_require__(472); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "share", function() { return _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__["share"]; }); -/* harmony import */ var _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__ = __webpack_require__(474); +/* harmony import */ var _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__ = __webpack_require__(473); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "shareReplay", function() { return _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__["shareReplay"]; }); -/* harmony import */ var _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__ = __webpack_require__(475); +/* harmony import */ var _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__ = __webpack_require__(474); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "single", function() { return _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__["single"]; }); -/* harmony import */ var _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__ = __webpack_require__(476); +/* harmony import */ var _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__ = __webpack_require__(475); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skip", function() { return _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__["skip"]; }); -/* harmony import */ var _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__ = __webpack_require__(477); +/* harmony import */ var _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__ = __webpack_require__(476); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipLast", function() { return _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__["skipLast"]; }); -/* harmony import */ var _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__ = __webpack_require__(478); +/* harmony import */ var _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__ = __webpack_require__(477); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipUntil", function() { return _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__["skipUntil"]; }); -/* harmony import */ var _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__ = __webpack_require__(479); +/* harmony import */ var _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__ = __webpack_require__(478); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipWhile", function() { return _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__["skipWhile"]; }); -/* harmony import */ var _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__ = __webpack_require__(480); +/* harmony import */ var _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__ = __webpack_require__(479); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "startWith", function() { return _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__["startWith"]; }); -/* harmony import */ var _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__ = __webpack_require__(481); +/* harmony import */ var _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__ = __webpack_require__(480); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "subscribeOn", function() { return _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__["subscribeOn"]; }); -/* harmony import */ var _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__ = __webpack_require__(483); +/* harmony import */ var _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__ = __webpack_require__(482); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchAll", function() { return _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__["switchAll"]; }); -/* harmony import */ var _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__ = __webpack_require__(484); +/* harmony import */ var _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__ = __webpack_require__(483); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchMap", function() { return _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__["switchMap"]; }); -/* harmony import */ var _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__ = __webpack_require__(485); +/* harmony import */ var _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__ = __webpack_require__(484); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchMapTo", function() { return _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__["switchMapTo"]; }); -/* harmony import */ var _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__ = __webpack_require__(433); +/* harmony import */ var _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__ = __webpack_require__(432); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "take", function() { return _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__["take"]; }); -/* harmony import */ var _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__ = __webpack_require__(446); +/* harmony import */ var _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__ = __webpack_require__(445); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeLast", function() { return _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__["takeLast"]; }); -/* harmony import */ var _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__ = __webpack_require__(486); +/* harmony import */ var _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__ = __webpack_require__(485); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeUntil", function() { return _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__["takeUntil"]; }); -/* harmony import */ var _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__ = __webpack_require__(487); +/* harmony import */ var _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__ = __webpack_require__(486); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeWhile", function() { return _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__["takeWhile"]; }); -/* harmony import */ var _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__ = __webpack_require__(488); +/* harmony import */ var _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__ = __webpack_require__(487); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "tap", function() { return _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__["tap"]; }); -/* harmony import */ var _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__ = __webpack_require__(489); +/* harmony import */ var _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__ = __webpack_require__(488); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throttle", function() { return _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__["throttle"]; }); -/* harmony import */ var _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__ = __webpack_require__(490); +/* harmony import */ var _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__ = __webpack_require__(489); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throttleTime", function() { return _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__["throttleTime"]; }); -/* harmony import */ var _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__ = __webpack_require__(432); +/* harmony import */ var _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__ = __webpack_require__(431); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throwIfEmpty", function() { return _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__["throwIfEmpty"]; }); -/* harmony import */ var _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__ = __webpack_require__(491); +/* harmony import */ var _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__ = __webpack_require__(490); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeInterval", function() { return _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__["timeInterval"]; }); -/* harmony import */ var _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__ = __webpack_require__(492); +/* harmony import */ var _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__ = __webpack_require__(491); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeout", function() { return _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__["timeout"]; }); -/* harmony import */ var _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__ = __webpack_require__(493); +/* harmony import */ var _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__ = __webpack_require__(492); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeoutWith", function() { return _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__["timeoutWith"]; }); -/* harmony import */ var _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__ = __webpack_require__(494); +/* harmony import */ var _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__ = __webpack_require__(493); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timestamp", function() { return _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__["timestamp"]; }); -/* harmony import */ var _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__ = __webpack_require__(495); +/* harmony import */ var _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__ = __webpack_require__(494); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "toArray", function() { return _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__["toArray"]; }); -/* harmony import */ var _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__ = __webpack_require__(496); +/* harmony import */ var _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__ = __webpack_require__(495); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "window", function() { return _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__["window"]; }); -/* harmony import */ var _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__ = __webpack_require__(497); +/* harmony import */ var _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__ = __webpack_require__(496); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowCount", function() { return _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__["windowCount"]; }); -/* harmony import */ var _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__ = __webpack_require__(498); +/* harmony import */ var _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__ = __webpack_require__(497); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowTime", function() { return _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__["windowTime"]; }); -/* harmony import */ var _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__ = __webpack_require__(499); +/* harmony import */ var _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__ = __webpack_require__(498); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowToggle", function() { return _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__["windowToggle"]; }); -/* harmony import */ var _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__ = __webpack_require__(500); +/* harmony import */ var _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__ = __webpack_require__(499); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowWhen", function() { return _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__["windowWhen"]; }); -/* harmony import */ var _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__ = __webpack_require__(501); +/* harmony import */ var _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__ = __webpack_require__(500); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "withLatestFrom", function() { return _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__["withLatestFrom"]; }); -/* harmony import */ var _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__ = __webpack_require__(502); +/* harmony import */ var _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__ = __webpack_require__(501); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "zip", function() { return _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__["zip"]; }); -/* harmony import */ var _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__ = __webpack_require__(503); +/* harmony import */ var _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__ = __webpack_require__(502); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "zipAll", function() { return _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__["zipAll"]; }); /** PURE_IMPORTS_START PURE_IMPORTS_END */ @@ -53542,7 +53524,7 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 407 */ +/* 406 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53621,14 +53603,14 @@ var AuditSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 408 */ +/* 407 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "auditTime", function() { return auditTime; }); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(55); -/* harmony import */ var _audit__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(407); +/* harmony import */ var _audit__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(406); /* harmony import */ var _observable_timer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(108); /** PURE_IMPORTS_START _scheduler_async,_audit,_observable_timer PURE_IMPORTS_END */ @@ -53644,7 +53626,7 @@ function auditTime(duration, scheduler) { /***/ }), -/* 409 */ +/* 408 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53691,7 +53673,7 @@ var BufferSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 410 */ +/* 409 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53792,7 +53774,7 @@ var BufferSkipCountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 411 */ +/* 410 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -53953,7 +53935,7 @@ function dispatchBufferClose(arg) { /***/ }), -/* 412 */ +/* 411 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54072,7 +54054,7 @@ var BufferToggleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 413 */ +/* 412 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54165,7 +54147,7 @@ var BufferWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 414 */ +/* 413 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54225,7 +54207,7 @@ var CatchSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 415 */ +/* 414 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54241,7 +54223,7 @@ function combineAll(project) { /***/ }), -/* 416 */ +/* 415 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54273,7 +54255,7 @@ function combineLatest() { /***/ }), -/* 417 */ +/* 416 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54293,7 +54275,7 @@ function concat() { /***/ }), -/* 418 */ +/* 417 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54309,13 +54291,13 @@ function concatMap(project, resultSelector) { /***/ }), -/* 419 */ +/* 418 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "concatMapTo", function() { return concatMapTo; }); -/* harmony import */ var _concatMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(418); +/* harmony import */ var _concatMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(417); /** PURE_IMPORTS_START _concatMap PURE_IMPORTS_END */ function concatMapTo(innerObservable, resultSelector) { @@ -54325,7 +54307,7 @@ function concatMapTo(innerObservable, resultSelector) { /***/ }), -/* 420 */ +/* 419 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54390,7 +54372,7 @@ var CountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 421 */ +/* 420 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54475,7 +54457,7 @@ var DebounceSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 422 */ +/* 421 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54551,7 +54533,7 @@ function dispatchNext(subscriber) { /***/ }), -/* 423 */ +/* 422 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54601,7 +54583,7 @@ var DefaultIfEmptySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 424 */ +/* 423 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54609,7 +54591,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "delay", function() { return delay; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(12); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(55); -/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(425); +/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(424); /* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(11); /* harmony import */ var _Notification__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(42); /** PURE_IMPORTS_START tslib,_scheduler_async,_util_isDate,_Subscriber,_Notification PURE_IMPORTS_END */ @@ -54708,7 +54690,7 @@ var DelayMessage = /*@__PURE__*/ (function () { /***/ }), -/* 425 */ +/* 424 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54722,7 +54704,7 @@ function isDate(value) { /***/ }), -/* 426 */ +/* 425 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54868,7 +54850,7 @@ var SubscriptionDelaySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 427 */ +/* 426 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54906,7 +54888,7 @@ var DeMaterializeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 428 */ +/* 427 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -54982,7 +54964,7 @@ var DistinctSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 429 */ +/* 428 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55053,13 +55035,13 @@ var DistinctUntilChangedSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 430 */ +/* 429 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "distinctUntilKeyChanged", function() { return distinctUntilKeyChanged; }); -/* harmony import */ var _distinctUntilChanged__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(429); +/* harmony import */ var _distinctUntilChanged__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(428); /** PURE_IMPORTS_START _distinctUntilChanged PURE_IMPORTS_END */ function distinctUntilKeyChanged(key, compare) { @@ -55069,7 +55051,7 @@ function distinctUntilKeyChanged(key, compare) { /***/ }), -/* 431 */ +/* 430 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55077,9 +55059,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "elementAt", function() { return elementAt; }); /* harmony import */ var _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(62); /* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(105); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(432); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(423); -/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(433); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(431); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(422); +/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(432); /** PURE_IMPORTS_START _util_ArgumentOutOfRangeError,_filter,_throwIfEmpty,_defaultIfEmpty,_take PURE_IMPORTS_END */ @@ -55101,7 +55083,7 @@ function elementAt(index, defaultValue) { /***/ }), -/* 432 */ +/* 431 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55167,7 +55149,7 @@ function defaultErrorFactory() { /***/ }), -/* 433 */ +/* 432 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55229,7 +55211,7 @@ var TakeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 434 */ +/* 433 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55251,7 +55233,7 @@ function endWith() { /***/ }), -/* 435 */ +/* 434 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55313,7 +55295,7 @@ var EverySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 436 */ +/* 435 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55367,7 +55349,7 @@ var SwitchFirstSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 437 */ +/* 436 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55461,7 +55443,7 @@ var ExhaustMapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 438 */ +/* 437 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55573,7 +55555,7 @@ var ExpandSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 439 */ +/* 438 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55611,7 +55593,7 @@ var FinallySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 440 */ +/* 439 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55683,13 +55665,13 @@ var FindValueSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 441 */ +/* 440 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "findIndex", function() { return findIndex; }); -/* harmony import */ var _operators_find__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(440); +/* harmony import */ var _operators_find__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(439); /** PURE_IMPORTS_START _operators_find PURE_IMPORTS_END */ function findIndex(predicate, thisArg) { @@ -55699,7 +55681,7 @@ function findIndex(predicate, thisArg) { /***/ }), -/* 442 */ +/* 441 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55707,9 +55689,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "first", function() { return first; }); /* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(63); /* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(105); -/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(433); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(423); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(432); +/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(432); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(422); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(431); /* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(25); /** PURE_IMPORTS_START _util_EmptyError,_filter,_take,_defaultIfEmpty,_throwIfEmpty,_util_identity PURE_IMPORTS_END */ @@ -55726,7 +55708,7 @@ function first(predicate, defaultValue) { /***/ }), -/* 443 */ +/* 442 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55763,7 +55745,7 @@ var IgnoreElementsSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 444 */ +/* 443 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55807,7 +55789,7 @@ var IsEmptySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 445 */ +/* 444 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55815,9 +55797,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "last", function() { return last; }); /* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(63); /* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(105); -/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(446); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(432); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(423); +/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(445); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(431); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(422); /* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(25); /** PURE_IMPORTS_START _util_EmptyError,_filter,_takeLast,_throwIfEmpty,_defaultIfEmpty,_util_identity PURE_IMPORTS_END */ @@ -55834,7 +55816,7 @@ function last(predicate, defaultValue) { /***/ }), -/* 446 */ +/* 445 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55911,7 +55893,7 @@ var TakeLastSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 447 */ +/* 446 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -55950,7 +55932,7 @@ var MapToSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 448 */ +/* 447 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56000,13 +55982,13 @@ var MaterializeSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 449 */ +/* 448 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "max", function() { return max; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(450); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(449); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function max(comparer) { @@ -56019,15 +56001,15 @@ function max(comparer) { /***/ }), -/* 450 */ +/* 449 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "reduce", function() { return reduce; }); -/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(451); -/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(446); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(423); +/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(450); +/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(445); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(422); /* harmony import */ var _util_pipe__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(24); /** PURE_IMPORTS_START _scan,_takeLast,_defaultIfEmpty,_util_pipe PURE_IMPORTS_END */ @@ -56048,7 +56030,7 @@ function reduce(accumulator, seed) { /***/ }), -/* 451 */ +/* 450 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56130,7 +56112,7 @@ var ScanSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 452 */ +/* 451 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56150,7 +56132,7 @@ function merge() { /***/ }), -/* 453 */ +/* 452 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56175,7 +56157,7 @@ function mergeMapTo(innerObservable, resultSelector, concurrent) { /***/ }), -/* 454 */ +/* 453 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56284,13 +56266,13 @@ var MergeScanSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 455 */ +/* 454 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "min", function() { return min; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(450); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(449); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function min(comparer) { @@ -56303,7 +56285,7 @@ function min(comparer) { /***/ }), -/* 456 */ +/* 455 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56352,7 +56334,7 @@ var MulticastOperator = /*@__PURE__*/ (function () { /***/ }), -/* 457 */ +/* 456 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56442,7 +56424,7 @@ var OnErrorResumeNextSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 458 */ +/* 457 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56490,7 +56472,7 @@ var PairwiseSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 459 */ +/* 458 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56513,7 +56495,7 @@ function partition(predicate, thisArg) { /***/ }), -/* 460 */ +/* 459 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56553,14 +56535,14 @@ function plucker(props, length) { /***/ }), -/* 461 */ +/* 460 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publish", function() { return publish; }); /* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(27); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(456); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(455); /** PURE_IMPORTS_START _Subject,_multicast PURE_IMPORTS_END */ @@ -56573,14 +56555,14 @@ function publish(selector) { /***/ }), -/* 462 */ +/* 461 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishBehavior", function() { return publishBehavior; }); /* harmony import */ var _BehaviorSubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(32); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(456); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(455); /** PURE_IMPORTS_START _BehaviorSubject,_multicast PURE_IMPORTS_END */ @@ -56591,14 +56573,14 @@ function publishBehavior(value) { /***/ }), -/* 463 */ +/* 462 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishLast", function() { return publishLast; }); /* harmony import */ var _AsyncSubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(50); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(456); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(455); /** PURE_IMPORTS_START _AsyncSubject,_multicast PURE_IMPORTS_END */ @@ -56609,14 +56591,14 @@ function publishLast() { /***/ }), -/* 464 */ +/* 463 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishReplay", function() { return publishReplay; }); /* harmony import */ var _ReplaySubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(33); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(456); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(455); /** PURE_IMPORTS_START _ReplaySubject,_multicast PURE_IMPORTS_END */ @@ -56632,7 +56614,7 @@ function publishReplay(bufferSize, windowTime, selectorOrScheduler, scheduler) { /***/ }), -/* 465 */ +/* 464 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56659,7 +56641,7 @@ function race() { /***/ }), -/* 466 */ +/* 465 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56724,7 +56706,7 @@ var RepeatSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 467 */ +/* 466 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56818,7 +56800,7 @@ var RepeatWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 468 */ +/* 467 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56871,7 +56853,7 @@ var RetrySubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 469 */ +/* 468 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -56957,7 +56939,7 @@ var RetryWhenSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 470 */ +/* 469 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57012,7 +56994,7 @@ var SampleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 471 */ +/* 470 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57072,7 +57054,7 @@ function dispatchNotification(state) { /***/ }), -/* 472 */ +/* 471 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57195,13 +57177,13 @@ var SequenceEqualCompareToSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 473 */ +/* 472 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "share", function() { return share; }); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(456); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(455); /* harmony import */ var _refCount__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(30); /* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(27); /** PURE_IMPORTS_START _multicast,_refCount,_Subject PURE_IMPORTS_END */ @@ -57218,7 +57200,7 @@ function share() { /***/ }), -/* 474 */ +/* 473 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57287,7 +57269,7 @@ function shareReplayOperator(_a) { /***/ }), -/* 475 */ +/* 474 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57367,7 +57349,7 @@ var SingleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 476 */ +/* 475 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57409,7 +57391,7 @@ var SkipSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 477 */ +/* 476 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57471,7 +57453,7 @@ var SkipLastSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 478 */ +/* 477 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57528,7 +57510,7 @@ var SkipUntilSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 479 */ +/* 478 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57584,7 +57566,7 @@ var SkipWhileSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 480 */ +/* 479 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57613,13 +57595,13 @@ function startWith() { /***/ }), -/* 481 */ +/* 480 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeOn", function() { return subscribeOn; }); -/* harmony import */ var _observable_SubscribeOnObservable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(482); +/* harmony import */ var _observable_SubscribeOnObservable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(481); /** PURE_IMPORTS_START _observable_SubscribeOnObservable PURE_IMPORTS_END */ function subscribeOn(scheduler, delay) { @@ -57644,7 +57626,7 @@ var SubscribeOnOperator = /*@__PURE__*/ (function () { /***/ }), -/* 482 */ +/* 481 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57708,13 +57690,13 @@ var SubscribeOnObservable = /*@__PURE__*/ (function (_super) { /***/ }), -/* 483 */ +/* 482 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "switchAll", function() { return switchAll; }); -/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(484); +/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(483); /* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(25); /** PURE_IMPORTS_START _switchMap,_util_identity PURE_IMPORTS_END */ @@ -57726,7 +57708,7 @@ function switchAll() { /***/ }), -/* 484 */ +/* 483 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57814,13 +57796,13 @@ var SwitchMapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 485 */ +/* 484 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "switchMapTo", function() { return switchMapTo; }); -/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(484); +/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(483); /** PURE_IMPORTS_START _switchMap PURE_IMPORTS_END */ function switchMapTo(innerObservable, resultSelector) { @@ -57830,7 +57812,7 @@ function switchMapTo(innerObservable, resultSelector) { /***/ }), -/* 486 */ +/* 485 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57878,7 +57860,7 @@ var TakeUntilSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 487 */ +/* 486 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -57946,7 +57928,7 @@ var TakeWhileSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 488 */ +/* 487 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58034,7 +58016,7 @@ var TapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 489 */ +/* 488 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58136,7 +58118,7 @@ var ThrottleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 490 */ +/* 489 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58145,7 +58127,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(12); /* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(11); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(55); -/* harmony import */ var _throttle__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(489); +/* harmony import */ var _throttle__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(488); /** PURE_IMPORTS_START tslib,_Subscriber,_scheduler_async,_throttle PURE_IMPORTS_END */ @@ -58234,7 +58216,7 @@ function dispatchNext(arg) { /***/ }), -/* 491 */ +/* 490 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58242,7 +58224,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeInterval", function() { return timeInterval; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TimeInterval", function() { return TimeInterval; }); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(55); -/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(451); +/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(450); /* harmony import */ var _observable_defer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(91); /* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(66); /** PURE_IMPORTS_START _scheduler_async,_scan,_observable_defer,_map PURE_IMPORTS_END */ @@ -58278,7 +58260,7 @@ var TimeInterval = /*@__PURE__*/ (function () { /***/ }), -/* 492 */ +/* 491 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58286,7 +58268,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeout", function() { return timeout; }); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(55); /* harmony import */ var _util_TimeoutError__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(64); -/* harmony import */ var _timeoutWith__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(493); +/* harmony import */ var _timeoutWith__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(492); /* harmony import */ var _observable_throwError__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(49); /** PURE_IMPORTS_START _scheduler_async,_util_TimeoutError,_timeoutWith,_observable_throwError PURE_IMPORTS_END */ @@ -58303,7 +58285,7 @@ function timeout(due, scheduler) { /***/ }), -/* 493 */ +/* 492 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58311,7 +58293,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeoutWith", function() { return timeoutWith; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(12); /* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(55); -/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(425); +/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(424); /* harmony import */ var _innerSubscribe__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(90); /** PURE_IMPORTS_START tslib,_scheduler_async,_util_isDate,_innerSubscribe PURE_IMPORTS_END */ @@ -58382,7 +58364,7 @@ var TimeoutWithSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 494 */ +/* 493 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58412,13 +58394,13 @@ var Timestamp = /*@__PURE__*/ (function () { /***/ }), -/* 495 */ +/* 494 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "toArray", function() { return toArray; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(450); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(449); /** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ function toArrayReducer(arr, item, index) { @@ -58435,7 +58417,7 @@ function toArray() { /***/ }), -/* 496 */ +/* 495 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58513,7 +58495,7 @@ var WindowSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 497 */ +/* 496 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58603,7 +58585,7 @@ var WindowCountSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 498 */ +/* 497 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58773,7 +58755,7 @@ function dispatchWindowClose(state) { /***/ }), -/* 499 */ +/* 498 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -58916,7 +58898,7 @@ var WindowToggleSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 500 */ +/* 499 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -59013,7 +58995,7 @@ var WindowSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 501 */ +/* 500 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -59108,7 +59090,7 @@ var WithLatestFromSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 502 */ +/* 501 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -59130,7 +59112,7 @@ function zip() { /***/ }), -/* 503 */ +/* 502 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -59146,7 +59128,7 @@ function zipAll(project) { /***/ }), -/* 504 */ +/* 503 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -59155,8 +59137,8 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _utils_errors__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(249); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(246); /* harmony import */ var _utils_projects__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(248); -/* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(371); -/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(505); +/* harmony import */ var _utils_projects_tree__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(370); +/* harmony import */ var _utils_kibana__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(504); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -59238,7 +59220,7 @@ function toArray(value) { } /***/ }), -/* 505 */ +/* 504 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -59246,13 +59228,13 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Kibana", function() { return Kibana; }); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(506); +/* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(505); /* harmony import */ var multimatch__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(multimatch__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(239); /* harmony import */ var is_path_inside__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(is_path_inside__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(366); +/* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(365); /* harmony import */ var _projects__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(248); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(510); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(509); function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } @@ -59414,15 +59396,15 @@ class Kibana { } /***/ }), -/* 506 */ +/* 505 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const minimatch = __webpack_require__(150); -const arrayUnion = __webpack_require__(507); -const arrayDiffer = __webpack_require__(508); -const arrify = __webpack_require__(509); +const arrayUnion = __webpack_require__(506); +const arrayDiffer = __webpack_require__(507); +const arrify = __webpack_require__(508); module.exports = (list, patterns, options = {}) => { list = arrify(list); @@ -59446,7 +59428,7 @@ module.exports = (list, patterns, options = {}) => { /***/ }), -/* 507 */ +/* 506 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -59458,7 +59440,7 @@ module.exports = (...arguments_) => { /***/ }), -/* 508 */ +/* 507 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -59473,7 +59455,7 @@ module.exports = arrayDiffer; /***/ }), -/* 509 */ +/* 508 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -59503,7 +59485,7 @@ module.exports = arrify; /***/ }), -/* 510 */ +/* 509 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; @@ -59573,12 +59555,12 @@ function getProjectPaths({ } /***/ }), -/* 511 */ +/* 510 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(512); +/* harmony import */ var _build_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(511); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return _build_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildProductionProjects"]; }); /* @@ -59602,19 +59584,19 @@ __webpack_require__.r(__webpack_exports__); /***/ }), -/* 512 */ +/* 511 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildProductionProjects", function() { return buildProductionProjects; }); -/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(513); +/* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(512); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(143); /* harmony import */ var del__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(del__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(510); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(509); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(131); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(246); /* harmony import */ var _utils_package_json__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(251); @@ -59751,7 +59733,7 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { } /***/ }), -/* 513 */ +/* 512 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -59759,14 +59741,14 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { const EventEmitter = __webpack_require__(156); const path = __webpack_require__(4); const os = __webpack_require__(121); -const pMap = __webpack_require__(514); -const arrify = __webpack_require__(509); -const globby = __webpack_require__(515); -const hasGlob = __webpack_require__(715); -const cpFile = __webpack_require__(717); -const junk = __webpack_require__(727); -const pFilter = __webpack_require__(728); -const CpyError = __webpack_require__(730); +const pMap = __webpack_require__(513); +const arrify = __webpack_require__(508); +const globby = __webpack_require__(514); +const hasGlob = __webpack_require__(714); +const cpFile = __webpack_require__(716); +const junk = __webpack_require__(726); +const pFilter = __webpack_require__(727); +const CpyError = __webpack_require__(729); const defaultOptions = { ignoreJunk: true @@ -59917,7 +59899,7 @@ module.exports = (source, destination, { /***/ }), -/* 514 */ +/* 513 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -60005,17 +59987,17 @@ module.exports = async ( /***/ }), -/* 515 */ +/* 514 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const arrayUnion = __webpack_require__(516); +const arrayUnion = __webpack_require__(515); const glob = __webpack_require__(147); -const fastGlob = __webpack_require__(518); -const dirGlob = __webpack_require__(708); -const gitignore = __webpack_require__(711); +const fastGlob = __webpack_require__(517); +const dirGlob = __webpack_require__(707); +const gitignore = __webpack_require__(710); const DEFAULT_FILTER = () => false; @@ -60160,12 +60142,12 @@ module.exports.gitignore = gitignore; /***/ }), -/* 516 */ +/* 515 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var arrayUniq = __webpack_require__(517); +var arrayUniq = __webpack_require__(516); module.exports = function () { return arrayUniq([].concat.apply([], arguments)); @@ -60173,7 +60155,7 @@ module.exports = function () { /***/ }), -/* 517 */ +/* 516 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -60242,10 +60224,10 @@ if ('Set' in global) { /***/ }), -/* 518 */ +/* 517 */ /***/ (function(module, exports, __webpack_require__) { -const pkg = __webpack_require__(519); +const pkg = __webpack_require__(518); module.exports = pkg.async; module.exports.default = pkg.async; @@ -60258,19 +60240,19 @@ module.exports.generateTasks = pkg.generateTasks; /***/ }), -/* 519 */ +/* 518 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var optionsManager = __webpack_require__(520); -var taskManager = __webpack_require__(521); -var reader_async_1 = __webpack_require__(679); -var reader_stream_1 = __webpack_require__(703); -var reader_sync_1 = __webpack_require__(704); -var arrayUtils = __webpack_require__(706); -var streamUtils = __webpack_require__(707); +var optionsManager = __webpack_require__(519); +var taskManager = __webpack_require__(520); +var reader_async_1 = __webpack_require__(678); +var reader_stream_1 = __webpack_require__(702); +var reader_sync_1 = __webpack_require__(703); +var arrayUtils = __webpack_require__(705); +var streamUtils = __webpack_require__(706); /** * Synchronous API. */ @@ -60336,7 +60318,7 @@ function isString(source) { /***/ }), -/* 520 */ +/* 519 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -60374,13 +60356,13 @@ exports.prepare = prepare; /***/ }), -/* 521 */ +/* 520 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var patternUtils = __webpack_require__(522); +var patternUtils = __webpack_require__(521); /** * Generate tasks based on parent directory of each pattern. */ @@ -60471,16 +60453,16 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 522 */ +/* 521 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); -var globParent = __webpack_require__(523); +var globParent = __webpack_require__(522); var isGlob = __webpack_require__(172); -var micromatch = __webpack_require__(526); +var micromatch = __webpack_require__(525); var GLOBSTAR = '**'; /** * Return true for static pattern. @@ -60626,15 +60608,15 @@ exports.matchAny = matchAny; /***/ }), -/* 523 */ +/* 522 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var path = __webpack_require__(4); -var isglob = __webpack_require__(524); -var pathDirname = __webpack_require__(525); +var isglob = __webpack_require__(523); +var pathDirname = __webpack_require__(524); var isWin32 = __webpack_require__(121).platform() === 'win32'; module.exports = function globParent(str) { @@ -60657,7 +60639,7 @@ module.exports = function globParent(str) { /***/ }), -/* 524 */ +/* 523 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -60688,7 +60670,7 @@ module.exports = function isGlob(str) { /***/ }), -/* 525 */ +/* 524 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -60838,7 +60820,7 @@ module.exports.win32 = win32; /***/ }), -/* 526 */ +/* 525 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -60849,18 +60831,18 @@ module.exports.win32 = win32; */ var util = __webpack_require__(112); -var braces = __webpack_require__(527); -var toRegex = __webpack_require__(528); -var extend = __webpack_require__(645); +var braces = __webpack_require__(526); +var toRegex = __webpack_require__(527); +var extend = __webpack_require__(644); /** * Local dependencies */ -var compilers = __webpack_require__(647); -var parsers = __webpack_require__(674); -var cache = __webpack_require__(675); -var utils = __webpack_require__(676); +var compilers = __webpack_require__(646); +var parsers = __webpack_require__(673); +var cache = __webpack_require__(674); +var utils = __webpack_require__(675); var MAX_LENGTH = 1024 * 64; /** @@ -61722,7 +61704,7 @@ module.exports = micromatch; /***/ }), -/* 527 */ +/* 526 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -61732,18 +61714,18 @@ module.exports = micromatch; * Module dependencies */ -var toRegex = __webpack_require__(528); -var unique = __webpack_require__(550); -var extend = __webpack_require__(551); +var toRegex = __webpack_require__(527); +var unique = __webpack_require__(549); +var extend = __webpack_require__(550); /** * Local dependencies */ -var compilers = __webpack_require__(553); -var parsers = __webpack_require__(568); -var Braces = __webpack_require__(572); -var utils = __webpack_require__(554); +var compilers = __webpack_require__(552); +var parsers = __webpack_require__(567); +var Braces = __webpack_require__(571); +var utils = __webpack_require__(553); var MAX_LENGTH = 1024 * 64; var cache = {}; @@ -62047,16 +62029,16 @@ module.exports = braces; /***/ }), -/* 528 */ +/* 527 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var safe = __webpack_require__(529); -var define = __webpack_require__(535); -var extend = __webpack_require__(543); -var not = __webpack_require__(547); +var safe = __webpack_require__(528); +var define = __webpack_require__(534); +var extend = __webpack_require__(542); +var not = __webpack_require__(546); var MAX_LENGTH = 1024 * 64; /** @@ -62209,10 +62191,10 @@ module.exports.makeRe = makeRe; /***/ }), -/* 529 */ +/* 528 */ /***/ (function(module, exports, __webpack_require__) { -var parse = __webpack_require__(530); +var parse = __webpack_require__(529); var types = parse.types; module.exports = function (re, opts) { @@ -62258,13 +62240,13 @@ function isRegExp (x) { /***/ }), -/* 530 */ +/* 529 */ /***/ (function(module, exports, __webpack_require__) { -var util = __webpack_require__(531); -var types = __webpack_require__(532); -var sets = __webpack_require__(533); -var positions = __webpack_require__(534); +var util = __webpack_require__(530); +var types = __webpack_require__(531); +var sets = __webpack_require__(532); +var positions = __webpack_require__(533); module.exports = function(regexpStr) { @@ -62546,11 +62528,11 @@ module.exports.types = types; /***/ }), -/* 531 */ +/* 530 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(532); -var sets = __webpack_require__(533); +var types = __webpack_require__(531); +var sets = __webpack_require__(532); // All of these are private and only used by randexp. @@ -62663,7 +62645,7 @@ exports.error = function(regexp, msg) { /***/ }), -/* 532 */ +/* 531 */ /***/ (function(module, exports) { module.exports = { @@ -62679,10 +62661,10 @@ module.exports = { /***/ }), -/* 533 */ +/* 532 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(532); +var types = __webpack_require__(531); var INTS = function() { return [{ type: types.RANGE , from: 48, to: 57 }]; @@ -62767,10 +62749,10 @@ exports.anyChar = function() { /***/ }), -/* 534 */ +/* 533 */ /***/ (function(module, exports, __webpack_require__) { -var types = __webpack_require__(532); +var types = __webpack_require__(531); exports.wordBoundary = function() { return { type: types.POSITION, value: 'b' }; @@ -62790,7 +62772,7 @@ exports.end = function() { /***/ }), -/* 535 */ +/* 534 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62803,8 +62785,8 @@ exports.end = function() { -var isobject = __webpack_require__(536); -var isDescriptor = __webpack_require__(537); +var isobject = __webpack_require__(535); +var isDescriptor = __webpack_require__(536); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -62835,7 +62817,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 536 */ +/* 535 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62854,7 +62836,7 @@ module.exports = function isObject(val) { /***/ }), -/* 537 */ +/* 536 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -62867,9 +62849,9 @@ module.exports = function isObject(val) { -var typeOf = __webpack_require__(538); -var isAccessor = __webpack_require__(539); -var isData = __webpack_require__(541); +var typeOf = __webpack_require__(537); +var isAccessor = __webpack_require__(538); +var isData = __webpack_require__(540); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -62883,7 +62865,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 538 */ +/* 537 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -63018,7 +63000,7 @@ function isBuffer(val) { /***/ }), -/* 539 */ +/* 538 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63031,7 +63013,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(540); +var typeOf = __webpack_require__(539); // accessor descriptor properties var accessor = { @@ -63094,7 +63076,7 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 540 */ +/* 539 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -63229,7 +63211,7 @@ function isBuffer(val) { /***/ }), -/* 541 */ +/* 540 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63242,7 +63224,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(542); +var typeOf = __webpack_require__(541); module.exports = function isDataDescriptor(obj, prop) { // data descriptor properties @@ -63285,7 +63267,7 @@ module.exports = function isDataDescriptor(obj, prop) { /***/ }), -/* 542 */ +/* 541 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -63420,14 +63402,14 @@ function isBuffer(val) { /***/ }), -/* 543 */ +/* 542 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(544); -var assignSymbols = __webpack_require__(546); +var isExtendable = __webpack_require__(543); +var assignSymbols = __webpack_require__(545); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -63487,7 +63469,7 @@ function isEnum(obj, key) { /***/ }), -/* 544 */ +/* 543 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63500,7 +63482,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(545); +var isPlainObject = __webpack_require__(544); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -63508,7 +63490,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 545 */ +/* 544 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63521,7 +63503,7 @@ module.exports = function isExtendable(val) { -var isObject = __webpack_require__(536); +var isObject = __webpack_require__(535); function isObjectObject(o) { return isObject(o) === true @@ -63552,7 +63534,7 @@ module.exports = function isPlainObject(o) { /***/ }), -/* 546 */ +/* 545 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63599,14 +63581,14 @@ module.exports = function(receiver, objects) { /***/ }), -/* 547 */ +/* 546 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(548); -var safe = __webpack_require__(529); +var extend = __webpack_require__(547); +var safe = __webpack_require__(528); /** * The main export is a function that takes a `pattern` string and an `options` object. @@ -63678,14 +63660,14 @@ module.exports = toRegex; /***/ }), -/* 548 */ +/* 547 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(549); -var assignSymbols = __webpack_require__(546); +var isExtendable = __webpack_require__(548); +var assignSymbols = __webpack_require__(545); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -63745,7 +63727,7 @@ function isEnum(obj, key) { /***/ }), -/* 549 */ +/* 548 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63758,7 +63740,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(545); +var isPlainObject = __webpack_require__(544); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -63766,7 +63748,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 550 */ +/* 549 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63816,13 +63798,13 @@ module.exports.immutable = function uniqueImmutable(arr) { /***/ }), -/* 551 */ +/* 550 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(552); +var isObject = __webpack_require__(551); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -63856,7 +63838,7 @@ function hasOwn(obj, key) { /***/ }), -/* 552 */ +/* 551 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -63876,13 +63858,13 @@ module.exports = function isExtendable(val) { /***/ }), -/* 553 */ +/* 552 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(554); +var utils = __webpack_require__(553); module.exports = function(braces, options) { braces.compiler @@ -64165,25 +64147,25 @@ function hasQueue(node) { /***/ }), -/* 554 */ +/* 553 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var splitString = __webpack_require__(555); +var splitString = __webpack_require__(554); var utils = module.exports; /** * Module dependencies */ -utils.extend = __webpack_require__(551); -utils.flatten = __webpack_require__(558); -utils.isObject = __webpack_require__(536); -utils.fillRange = __webpack_require__(559); -utils.repeat = __webpack_require__(567); -utils.unique = __webpack_require__(550); +utils.extend = __webpack_require__(550); +utils.flatten = __webpack_require__(557); +utils.isObject = __webpack_require__(535); +utils.fillRange = __webpack_require__(558); +utils.repeat = __webpack_require__(566); +utils.unique = __webpack_require__(549); utils.define = function(obj, key, val) { Object.defineProperty(obj, key, { @@ -64515,7 +64497,7 @@ utils.escapeRegex = function(str) { /***/ }), -/* 555 */ +/* 554 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64528,7 +64510,7 @@ utils.escapeRegex = function(str) { -var extend = __webpack_require__(556); +var extend = __webpack_require__(555); module.exports = function(str, options, fn) { if (typeof str !== 'string') { @@ -64693,14 +64675,14 @@ function keepEscaping(opts, str, idx) { /***/ }), -/* 556 */ +/* 555 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(557); -var assignSymbols = __webpack_require__(546); +var isExtendable = __webpack_require__(556); +var assignSymbols = __webpack_require__(545); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -64760,7 +64742,7 @@ function isEnum(obj, key) { /***/ }), -/* 557 */ +/* 556 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64773,7 +64755,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(545); +var isPlainObject = __webpack_require__(544); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -64781,7 +64763,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 558 */ +/* 557 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64810,7 +64792,7 @@ function flat(arr, res) { /***/ }), -/* 559 */ +/* 558 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -64824,10 +64806,10 @@ function flat(arr, res) { var util = __webpack_require__(112); -var isNumber = __webpack_require__(560); -var extend = __webpack_require__(563); -var repeat = __webpack_require__(565); -var toRegex = __webpack_require__(566); +var isNumber = __webpack_require__(559); +var extend = __webpack_require__(562); +var repeat = __webpack_require__(564); +var toRegex = __webpack_require__(565); /** * Return a range of numbers or letters. @@ -65025,7 +65007,7 @@ module.exports = fillRange; /***/ }), -/* 560 */ +/* 559 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65038,7 +65020,7 @@ module.exports = fillRange; -var typeOf = __webpack_require__(561); +var typeOf = __webpack_require__(560); module.exports = function isNumber(num) { var type = typeOf(num); @@ -65054,10 +65036,10 @@ module.exports = function isNumber(num) { /***/ }), -/* 561 */ +/* 560 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(562); +var isBuffer = __webpack_require__(561); var toString = Object.prototype.toString; /** @@ -65176,7 +65158,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 562 */ +/* 561 */ /***/ (function(module, exports) { /*! @@ -65203,13 +65185,13 @@ function isSlowBuffer (obj) { /***/ }), -/* 563 */ +/* 562 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(564); +var isObject = __webpack_require__(563); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -65243,7 +65225,7 @@ function hasOwn(obj, key) { /***/ }), -/* 564 */ +/* 563 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65263,7 +65245,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 565 */ +/* 564 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65340,7 +65322,7 @@ function repeat(str, num) { /***/ }), -/* 566 */ +/* 565 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65353,8 +65335,8 @@ function repeat(str, num) { -var repeat = __webpack_require__(565); -var isNumber = __webpack_require__(560); +var repeat = __webpack_require__(564); +var isNumber = __webpack_require__(559); var cache = {}; function toRegexRange(min, max, options) { @@ -65641,7 +65623,7 @@ module.exports = toRegexRange; /***/ }), -/* 567 */ +/* 566 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -65666,14 +65648,14 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 568 */ +/* 567 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(569); -var utils = __webpack_require__(554); +var Node = __webpack_require__(568); +var utils = __webpack_require__(553); /** * Braces parsers @@ -66033,15 +66015,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 569 */ +/* 568 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(536); -var define = __webpack_require__(570); -var utils = __webpack_require__(571); +var isObject = __webpack_require__(535); +var define = __webpack_require__(569); +var utils = __webpack_require__(570); var ownNames; /** @@ -66532,7 +66514,7 @@ exports = module.exports = Node; /***/ }), -/* 570 */ +/* 569 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -66545,7 +66527,7 @@ exports = module.exports = Node; -var isDescriptor = __webpack_require__(537); +var isDescriptor = __webpack_require__(536); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -66570,13 +66552,13 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 571 */ +/* 570 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(561); +var typeOf = __webpack_require__(560); var utils = module.exports; /** @@ -67596,17 +67578,17 @@ function assert(val, message) { /***/ }), -/* 572 */ +/* 571 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(551); -var Snapdragon = __webpack_require__(573); -var compilers = __webpack_require__(553); -var parsers = __webpack_require__(568); -var utils = __webpack_require__(554); +var extend = __webpack_require__(550); +var Snapdragon = __webpack_require__(572); +var compilers = __webpack_require__(552); +var parsers = __webpack_require__(567); +var utils = __webpack_require__(553); /** * Customize Snapdragon parser and renderer @@ -67707,17 +67689,17 @@ module.exports = Braces; /***/ }), -/* 573 */ +/* 572 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Base = __webpack_require__(574); -var define = __webpack_require__(603); -var Compiler = __webpack_require__(613); -var Parser = __webpack_require__(642); -var utils = __webpack_require__(622); +var Base = __webpack_require__(573); +var define = __webpack_require__(602); +var Compiler = __webpack_require__(612); +var Parser = __webpack_require__(641); +var utils = __webpack_require__(621); var regexCache = {}; var cache = {}; @@ -67888,20 +67870,20 @@ module.exports.Parser = Parser; /***/ }), -/* 574 */ +/* 573 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var define = __webpack_require__(575); -var CacheBase = __webpack_require__(576); -var Emitter = __webpack_require__(577); -var isObject = __webpack_require__(536); -var merge = __webpack_require__(597); -var pascal = __webpack_require__(600); -var cu = __webpack_require__(601); +var define = __webpack_require__(574); +var CacheBase = __webpack_require__(575); +var Emitter = __webpack_require__(576); +var isObject = __webpack_require__(535); +var merge = __webpack_require__(596); +var pascal = __webpack_require__(599); +var cu = __webpack_require__(600); /** * Optionally define a custom `cache` namespace to use. @@ -68330,7 +68312,7 @@ module.exports.namespace = namespace; /***/ }), -/* 575 */ +/* 574 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68343,7 +68325,7 @@ module.exports.namespace = namespace; -var isDescriptor = __webpack_require__(537); +var isDescriptor = __webpack_require__(536); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -68368,21 +68350,21 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 576 */ +/* 575 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(536); -var Emitter = __webpack_require__(577); -var visit = __webpack_require__(578); -var toPath = __webpack_require__(581); -var union = __webpack_require__(582); -var del = __webpack_require__(588); -var get = __webpack_require__(585); -var has = __webpack_require__(593); -var set = __webpack_require__(596); +var isObject = __webpack_require__(535); +var Emitter = __webpack_require__(576); +var visit = __webpack_require__(577); +var toPath = __webpack_require__(580); +var union = __webpack_require__(581); +var del = __webpack_require__(587); +var get = __webpack_require__(584); +var has = __webpack_require__(592); +var set = __webpack_require__(595); /** * Create a `Cache` constructor that when instantiated will @@ -68636,7 +68618,7 @@ module.exports.namespace = namespace; /***/ }), -/* 577 */ +/* 576 */ /***/ (function(module, exports, __webpack_require__) { @@ -68805,7 +68787,7 @@ Emitter.prototype.hasListeners = function(event){ /***/ }), -/* 578 */ +/* 577 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68818,8 +68800,8 @@ Emitter.prototype.hasListeners = function(event){ -var visit = __webpack_require__(579); -var mapVisit = __webpack_require__(580); +var visit = __webpack_require__(578); +var mapVisit = __webpack_require__(579); module.exports = function(collection, method, val) { var result; @@ -68842,7 +68824,7 @@ module.exports = function(collection, method, val) { /***/ }), -/* 579 */ +/* 578 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68855,7 +68837,7 @@ module.exports = function(collection, method, val) { -var isObject = __webpack_require__(536); +var isObject = __webpack_require__(535); module.exports = function visit(thisArg, method, target, val) { if (!isObject(thisArg) && typeof thisArg !== 'function') { @@ -68882,14 +68864,14 @@ module.exports = function visit(thisArg, method, target, val) { /***/ }), -/* 580 */ +/* 579 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var visit = __webpack_require__(579); +var visit = __webpack_require__(578); /** * Map `visit` over an array of objects. @@ -68926,7 +68908,7 @@ function isObject(val) { /***/ }), -/* 581 */ +/* 580 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68939,7 +68921,7 @@ function isObject(val) { -var typeOf = __webpack_require__(561); +var typeOf = __webpack_require__(560); module.exports = function toPath(args) { if (typeOf(args) !== 'arguments') { @@ -68966,16 +68948,16 @@ function filter(arr) { /***/ }), -/* 582 */ +/* 581 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(583); -var union = __webpack_require__(584); -var get = __webpack_require__(585); -var set = __webpack_require__(586); +var isObject = __webpack_require__(582); +var union = __webpack_require__(583); +var get = __webpack_require__(584); +var set = __webpack_require__(585); module.exports = function unionValue(obj, prop, value) { if (!isObject(obj)) { @@ -69003,7 +68985,7 @@ function arrayify(val) { /***/ }), -/* 583 */ +/* 582 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69023,7 +69005,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 584 */ +/* 583 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69059,7 +69041,7 @@ module.exports = function union(init) { /***/ }), -/* 585 */ +/* 584 */ /***/ (function(module, exports) { /*! @@ -69115,7 +69097,7 @@ function toString(val) { /***/ }), -/* 586 */ +/* 585 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69128,10 +69110,10 @@ function toString(val) { -var split = __webpack_require__(555); -var extend = __webpack_require__(587); -var isPlainObject = __webpack_require__(545); -var isObject = __webpack_require__(583); +var split = __webpack_require__(554); +var extend = __webpack_require__(586); +var isPlainObject = __webpack_require__(544); +var isObject = __webpack_require__(582); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -69177,13 +69159,13 @@ function isValidKey(key) { /***/ }), -/* 587 */ +/* 586 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(583); +var isObject = __webpack_require__(582); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -69217,7 +69199,7 @@ function hasOwn(obj, key) { /***/ }), -/* 588 */ +/* 587 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69230,8 +69212,8 @@ function hasOwn(obj, key) { -var isObject = __webpack_require__(536); -var has = __webpack_require__(589); +var isObject = __webpack_require__(535); +var has = __webpack_require__(588); module.exports = function unset(obj, prop) { if (!isObject(obj)) { @@ -69256,7 +69238,7 @@ module.exports = function unset(obj, prop) { /***/ }), -/* 589 */ +/* 588 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69269,9 +69251,9 @@ module.exports = function unset(obj, prop) { -var isObject = __webpack_require__(590); -var hasValues = __webpack_require__(592); -var get = __webpack_require__(585); +var isObject = __webpack_require__(589); +var hasValues = __webpack_require__(591); +var get = __webpack_require__(584); module.exports = function(obj, prop, noZero) { if (isObject(obj)) { @@ -69282,7 +69264,7 @@ module.exports = function(obj, prop, noZero) { /***/ }), -/* 590 */ +/* 589 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69295,7 +69277,7 @@ module.exports = function(obj, prop, noZero) { -var isArray = __webpack_require__(591); +var isArray = __webpack_require__(590); module.exports = function isObject(val) { return val != null && typeof val === 'object' && isArray(val) === false; @@ -69303,7 +69285,7 @@ module.exports = function isObject(val) { /***/ }), -/* 591 */ +/* 590 */ /***/ (function(module, exports) { var toString = {}.toString; @@ -69314,7 +69296,7 @@ module.exports = Array.isArray || function (arr) { /***/ }), -/* 592 */ +/* 591 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69357,7 +69339,7 @@ module.exports = function hasValue(o, noZero) { /***/ }), -/* 593 */ +/* 592 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69370,9 +69352,9 @@ module.exports = function hasValue(o, noZero) { -var isObject = __webpack_require__(536); -var hasValues = __webpack_require__(594); -var get = __webpack_require__(585); +var isObject = __webpack_require__(535); +var hasValues = __webpack_require__(593); +var get = __webpack_require__(584); module.exports = function(val, prop) { return hasValues(isObject(val) && prop ? get(val, prop) : val); @@ -69380,7 +69362,7 @@ module.exports = function(val, prop) { /***/ }), -/* 594 */ +/* 593 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69393,8 +69375,8 @@ module.exports = function(val, prop) { -var typeOf = __webpack_require__(595); -var isNumber = __webpack_require__(560); +var typeOf = __webpack_require__(594); +var isNumber = __webpack_require__(559); module.exports = function hasValue(val) { // is-number checks for NaN and other edge cases @@ -69447,10 +69429,10 @@ module.exports = function hasValue(val) { /***/ }), -/* 595 */ +/* 594 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(562); +var isBuffer = __webpack_require__(561); var toString = Object.prototype.toString; /** @@ -69572,7 +69554,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 596 */ +/* 595 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69585,10 +69567,10 @@ module.exports = function kindOf(val) { -var split = __webpack_require__(555); -var extend = __webpack_require__(587); -var isPlainObject = __webpack_require__(545); -var isObject = __webpack_require__(583); +var split = __webpack_require__(554); +var extend = __webpack_require__(586); +var isPlainObject = __webpack_require__(544); +var isObject = __webpack_require__(582); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -69634,14 +69616,14 @@ function isValidKey(key) { /***/ }), -/* 597 */ +/* 596 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(598); -var forIn = __webpack_require__(599); +var isExtendable = __webpack_require__(597); +var forIn = __webpack_require__(598); function mixinDeep(target, objects) { var len = arguments.length, i = 0; @@ -69705,7 +69687,7 @@ module.exports = mixinDeep; /***/ }), -/* 598 */ +/* 597 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69718,7 +69700,7 @@ module.exports = mixinDeep; -var isPlainObject = __webpack_require__(545); +var isPlainObject = __webpack_require__(544); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -69726,7 +69708,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 599 */ +/* 598 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69749,7 +69731,7 @@ module.exports = function forIn(obj, fn, thisArg) { /***/ }), -/* 600 */ +/* 599 */ /***/ (function(module, exports) { /*! @@ -69776,14 +69758,14 @@ module.exports = pascalcase; /***/ }), -/* 601 */ +/* 600 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(112); -var utils = __webpack_require__(602); +var utils = __webpack_require__(601); /** * Expose class utils @@ -70148,7 +70130,7 @@ cu.bubble = function(Parent, events) { /***/ }), -/* 602 */ +/* 601 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70162,10 +70144,10 @@ var utils = {}; * Lazily required module dependencies */ -utils.union = __webpack_require__(584); -utils.define = __webpack_require__(603); -utils.isObj = __webpack_require__(536); -utils.staticExtend = __webpack_require__(610); +utils.union = __webpack_require__(583); +utils.define = __webpack_require__(602); +utils.isObj = __webpack_require__(535); +utils.staticExtend = __webpack_require__(609); /** @@ -70176,7 +70158,7 @@ module.exports = utils; /***/ }), -/* 603 */ +/* 602 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70189,7 +70171,7 @@ module.exports = utils; -var isDescriptor = __webpack_require__(604); +var isDescriptor = __webpack_require__(603); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -70214,7 +70196,7 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 604 */ +/* 603 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70227,9 +70209,9 @@ module.exports = function defineProperty(obj, prop, val) { -var typeOf = __webpack_require__(605); -var isAccessor = __webpack_require__(606); -var isData = __webpack_require__(608); +var typeOf = __webpack_require__(604); +var isAccessor = __webpack_require__(605); +var isData = __webpack_require__(607); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -70243,7 +70225,7 @@ module.exports = function isDescriptor(obj, key) { /***/ }), -/* 605 */ +/* 604 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -70396,7 +70378,7 @@ function isBuffer(val) { /***/ }), -/* 606 */ +/* 605 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70409,7 +70391,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(607); +var typeOf = __webpack_require__(606); // accessor descriptor properties var accessor = { @@ -70472,10 +70454,10 @@ module.exports = isAccessorDescriptor; /***/ }), -/* 607 */ +/* 606 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(562); +var isBuffer = __webpack_require__(561); var toString = Object.prototype.toString; /** @@ -70594,7 +70576,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 608 */ +/* 607 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70607,7 +70589,7 @@ module.exports = function kindOf(val) { -var typeOf = __webpack_require__(609); +var typeOf = __webpack_require__(608); // data descriptor properties var data = { @@ -70656,10 +70638,10 @@ module.exports = isDataDescriptor; /***/ }), -/* 609 */ +/* 608 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(562); +var isBuffer = __webpack_require__(561); var toString = Object.prototype.toString; /** @@ -70778,7 +70760,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 610 */ +/* 609 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70791,8 +70773,8 @@ module.exports = function kindOf(val) { -var copy = __webpack_require__(611); -var define = __webpack_require__(603); +var copy = __webpack_require__(610); +var define = __webpack_require__(602); var util = __webpack_require__(112); /** @@ -70875,15 +70857,15 @@ module.exports = extend; /***/ }), -/* 611 */ +/* 610 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(561); -var copyDescriptor = __webpack_require__(612); -var define = __webpack_require__(603); +var typeOf = __webpack_require__(560); +var copyDescriptor = __webpack_require__(611); +var define = __webpack_require__(602); /** * Copy static properties, prototype properties, and descriptors from one object to another. @@ -71056,7 +71038,7 @@ module.exports.has = has; /***/ }), -/* 612 */ +/* 611 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71144,16 +71126,16 @@ function isObject(val) { /***/ }), -/* 613 */ +/* 612 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(614); -var define = __webpack_require__(603); -var debug = __webpack_require__(616)('snapdragon:compiler'); -var utils = __webpack_require__(622); +var use = __webpack_require__(613); +var define = __webpack_require__(602); +var debug = __webpack_require__(615)('snapdragon:compiler'); +var utils = __webpack_require__(621); /** * Create a new `Compiler` with the given `options`. @@ -71307,7 +71289,7 @@ Compiler.prototype = { // source map support if (opts.sourcemap) { - var sourcemaps = __webpack_require__(641); + var sourcemaps = __webpack_require__(640); sourcemaps(this); this.mapVisit(this.ast.nodes); this.applySourceMaps(); @@ -71328,7 +71310,7 @@ module.exports = Compiler; /***/ }), -/* 614 */ +/* 613 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71341,7 +71323,7 @@ module.exports = Compiler; -var utils = __webpack_require__(615); +var utils = __webpack_require__(614); module.exports = function base(app, opts) { if (!utils.isObject(app) && typeof app !== 'function') { @@ -71456,7 +71438,7 @@ module.exports = function base(app, opts) { /***/ }), -/* 615 */ +/* 614 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -71470,8 +71452,8 @@ var utils = {}; * Lazily required module dependencies */ -utils.define = __webpack_require__(603); -utils.isObject = __webpack_require__(536); +utils.define = __webpack_require__(602); +utils.isObject = __webpack_require__(535); utils.isString = function(val) { @@ -71486,7 +71468,7 @@ module.exports = utils; /***/ }), -/* 616 */ +/* 615 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -71495,14 +71477,14 @@ module.exports = utils; */ if (typeof process !== 'undefined' && process.type === 'renderer') { - module.exports = __webpack_require__(617); + module.exports = __webpack_require__(616); } else { - module.exports = __webpack_require__(620); + module.exports = __webpack_require__(619); } /***/ }), -/* 617 */ +/* 616 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -71511,7 +71493,7 @@ if (typeof process !== 'undefined' && process.type === 'renderer') { * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(618); +exports = module.exports = __webpack_require__(617); exports.log = log; exports.formatArgs = formatArgs; exports.save = save; @@ -71693,7 +71675,7 @@ function localstorage() { /***/ }), -/* 618 */ +/* 617 */ /***/ (function(module, exports, __webpack_require__) { @@ -71709,7 +71691,7 @@ exports.coerce = coerce; exports.disable = disable; exports.enable = enable; exports.enabled = enabled; -exports.humanize = __webpack_require__(619); +exports.humanize = __webpack_require__(618); /** * The currently active debug mode names, and names to skip. @@ -71901,7 +71883,7 @@ function coerce(val) { /***/ }), -/* 619 */ +/* 618 */ /***/ (function(module, exports) { /** @@ -72059,7 +72041,7 @@ function plural(ms, n, name) { /***/ }), -/* 620 */ +/* 619 */ /***/ (function(module, exports, __webpack_require__) { /** @@ -72075,7 +72057,7 @@ var util = __webpack_require__(112); * Expose `debug()` as the module. */ -exports = module.exports = __webpack_require__(618); +exports = module.exports = __webpack_require__(617); exports.init = init; exports.log = log; exports.formatArgs = formatArgs; @@ -72254,7 +72236,7 @@ function createWritableStdioStream (fd) { case 'PIPE': case 'TCP': - var net = __webpack_require__(621); + var net = __webpack_require__(620); stream = new net.Socket({ fd: fd, readable: false, @@ -72313,13 +72295,13 @@ exports.enable(load()); /***/ }), -/* 621 */ +/* 620 */ /***/ (function(module, exports) { module.exports = require("net"); /***/ }), -/* 622 */ +/* 621 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72329,9 +72311,9 @@ module.exports = require("net"); * Module dependencies */ -exports.extend = __webpack_require__(587); -exports.SourceMap = __webpack_require__(623); -exports.sourceMapResolve = __webpack_require__(634); +exports.extend = __webpack_require__(586); +exports.SourceMap = __webpack_require__(622); +exports.sourceMapResolve = __webpack_require__(633); /** * Convert backslash in the given string to forward slashes @@ -72374,7 +72356,7 @@ exports.last = function(arr, n) { /***/ }), -/* 623 */ +/* 622 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -72382,13 +72364,13 @@ exports.last = function(arr, n) { * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ -exports.SourceMapGenerator = __webpack_require__(624).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(630).SourceMapConsumer; -exports.SourceNode = __webpack_require__(633).SourceNode; +exports.SourceMapGenerator = __webpack_require__(623).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(629).SourceMapConsumer; +exports.SourceNode = __webpack_require__(632).SourceNode; /***/ }), -/* 624 */ +/* 623 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -72398,10 +72380,10 @@ exports.SourceNode = __webpack_require__(633).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(625); -var util = __webpack_require__(627); -var ArraySet = __webpack_require__(628).ArraySet; -var MappingList = __webpack_require__(629).MappingList; +var base64VLQ = __webpack_require__(624); +var util = __webpack_require__(626); +var ArraySet = __webpack_require__(627).ArraySet; +var MappingList = __webpack_require__(628).MappingList; /** * An instance of the SourceMapGenerator represents a source map which is @@ -72810,7 +72792,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 625 */ +/* 624 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -72850,7 +72832,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(626); +var base64 = __webpack_require__(625); // A single base 64 digit can contain 6 bits of data. For the base 64 variable // length quantities we use in the source map spec, the first bit is the sign, @@ -72956,7 +72938,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 626 */ +/* 625 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73029,7 +73011,7 @@ exports.decode = function (charCode) { /***/ }), -/* 627 */ +/* 626 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73452,7 +73434,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 628 */ +/* 627 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73462,7 +73444,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(627); +var util = __webpack_require__(626); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -73579,7 +73561,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 629 */ +/* 628 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73589,7 +73571,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(627); +var util = __webpack_require__(626); /** * Determine whether mappingB is after mappingA with respect to generated @@ -73664,7 +73646,7 @@ exports.MappingList = MappingList; /***/ }), -/* 630 */ +/* 629 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -73674,11 +73656,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(627); -var binarySearch = __webpack_require__(631); -var ArraySet = __webpack_require__(628).ArraySet; -var base64VLQ = __webpack_require__(625); -var quickSort = __webpack_require__(632).quickSort; +var util = __webpack_require__(626); +var binarySearch = __webpack_require__(630); +var ArraySet = __webpack_require__(627).ArraySet; +var base64VLQ = __webpack_require__(624); +var quickSort = __webpack_require__(631).quickSort; function SourceMapConsumer(aSourceMap) { var sourceMap = aSourceMap; @@ -74752,7 +74734,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 631 */ +/* 630 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -74869,7 +74851,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 632 */ +/* 631 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -74989,7 +74971,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 633 */ +/* 632 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -74999,8 +74981,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(624).SourceMapGenerator; -var util = __webpack_require__(627); +var SourceMapGenerator = __webpack_require__(623).SourceMapGenerator; +var util = __webpack_require__(626); // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). @@ -75408,17 +75390,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 634 */ +/* 633 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var sourceMappingURL = __webpack_require__(635) -var resolveUrl = __webpack_require__(636) -var decodeUriComponent = __webpack_require__(637) -var urix = __webpack_require__(639) -var atob = __webpack_require__(640) +var sourceMappingURL = __webpack_require__(634) +var resolveUrl = __webpack_require__(635) +var decodeUriComponent = __webpack_require__(636) +var urix = __webpack_require__(638) +var atob = __webpack_require__(639) @@ -75716,7 +75698,7 @@ module.exports = { /***/ }), -/* 635 */ +/* 634 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -75779,7 +75761,7 @@ void (function(root, factory) { /***/ }), -/* 636 */ +/* 635 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -75797,13 +75779,13 @@ module.exports = resolveUrl /***/ }), -/* 637 */ +/* 636 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(638) +var decodeUriComponent = __webpack_require__(637) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -75814,7 +75796,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 638 */ +/* 637 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75915,7 +75897,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 639 */ +/* 638 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -75938,7 +75920,7 @@ module.exports = urix /***/ }), -/* 640 */ +/* 639 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75952,7 +75934,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 641 */ +/* 640 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75960,8 +75942,8 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(134); var path = __webpack_require__(4); -var define = __webpack_require__(603); -var utils = __webpack_require__(622); +var define = __webpack_require__(602); +var utils = __webpack_require__(621); /** * Expose `mixin()`. @@ -76104,19 +76086,19 @@ exports.comment = function(node) { /***/ }), -/* 642 */ +/* 641 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(614); +var use = __webpack_require__(613); var util = __webpack_require__(112); -var Cache = __webpack_require__(643); -var define = __webpack_require__(603); -var debug = __webpack_require__(616)('snapdragon:parser'); -var Position = __webpack_require__(644); -var utils = __webpack_require__(622); +var Cache = __webpack_require__(642); +var define = __webpack_require__(602); +var debug = __webpack_require__(615)('snapdragon:parser'); +var Position = __webpack_require__(643); +var utils = __webpack_require__(621); /** * Create a new `Parser` with the given `input` and `options`. @@ -76644,7 +76626,7 @@ module.exports = Parser; /***/ }), -/* 643 */ +/* 642 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76751,13 +76733,13 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 644 */ +/* 643 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var define = __webpack_require__(603); +var define = __webpack_require__(602); /** * Store position for a node @@ -76772,14 +76754,14 @@ module.exports = function Position(start, parser) { /***/ }), -/* 645 */ +/* 644 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(646); -var assignSymbols = __webpack_require__(546); +var isExtendable = __webpack_require__(645); +var assignSymbols = __webpack_require__(545); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -76839,7 +76821,7 @@ function isEnum(obj, key) { /***/ }), -/* 646 */ +/* 645 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76852,7 +76834,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(545); +var isPlainObject = __webpack_require__(544); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -76860,14 +76842,14 @@ module.exports = function isExtendable(val) { /***/ }), -/* 647 */ +/* 646 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(648); -var extglob = __webpack_require__(663); +var nanomatch = __webpack_require__(647); +var extglob = __webpack_require__(662); module.exports = function(snapdragon) { var compilers = snapdragon.compiler.compilers; @@ -76944,7 +76926,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 648 */ +/* 647 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -76955,17 +76937,17 @@ function escapeExtglobs(compiler) { */ var util = __webpack_require__(112); -var toRegex = __webpack_require__(528); -var extend = __webpack_require__(649); +var toRegex = __webpack_require__(527); +var extend = __webpack_require__(648); /** * Local dependencies */ -var compilers = __webpack_require__(651); -var parsers = __webpack_require__(652); -var cache = __webpack_require__(655); -var utils = __webpack_require__(657); +var compilers = __webpack_require__(650); +var parsers = __webpack_require__(651); +var cache = __webpack_require__(654); +var utils = __webpack_require__(656); var MAX_LENGTH = 1024 * 64; /** @@ -77789,14 +77771,14 @@ module.exports = nanomatch; /***/ }), -/* 649 */ +/* 648 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(650); -var assignSymbols = __webpack_require__(546); +var isExtendable = __webpack_require__(649); +var assignSymbols = __webpack_require__(545); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -77856,7 +77838,7 @@ function isEnum(obj, key) { /***/ }), -/* 650 */ +/* 649 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -77869,7 +77851,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(545); +var isPlainObject = __webpack_require__(544); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -77877,7 +77859,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 651 */ +/* 650 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78223,15 +78205,15 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 652 */ +/* 651 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regexNot = __webpack_require__(547); -var toRegex = __webpack_require__(528); -var isOdd = __webpack_require__(653); +var regexNot = __webpack_require__(546); +var toRegex = __webpack_require__(527); +var isOdd = __webpack_require__(652); /** * Characters to use in negation regex (we want to "not" match @@ -78617,7 +78599,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 653 */ +/* 652 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78630,7 +78612,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(654); +var isNumber = __webpack_require__(653); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -78644,7 +78626,7 @@ module.exports = function isOdd(i) { /***/ }), -/* 654 */ +/* 653 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78672,14 +78654,14 @@ module.exports = function isNumber(num) { /***/ }), -/* 655 */ +/* 654 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(656))(); +module.exports = new (__webpack_require__(655))(); /***/ }), -/* 656 */ +/* 655 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78692,7 +78674,7 @@ module.exports = new (__webpack_require__(656))(); -var MapCache = __webpack_require__(643); +var MapCache = __webpack_require__(642); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -78814,7 +78796,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 657 */ +/* 656 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78827,14 +78809,14 @@ var path = __webpack_require__(4); * Module dependencies */ -var isWindows = __webpack_require__(658)(); -var Snapdragon = __webpack_require__(573); -utils.define = __webpack_require__(659); -utils.diff = __webpack_require__(660); -utils.extend = __webpack_require__(649); -utils.pick = __webpack_require__(661); -utils.typeOf = __webpack_require__(662); -utils.unique = __webpack_require__(550); +var isWindows = __webpack_require__(657)(); +var Snapdragon = __webpack_require__(572); +utils.define = __webpack_require__(658); +utils.diff = __webpack_require__(659); +utils.extend = __webpack_require__(648); +utils.pick = __webpack_require__(660); +utils.typeOf = __webpack_require__(661); +utils.unique = __webpack_require__(549); /** * Returns true if the given value is effectively an empty string @@ -79200,7 +79182,7 @@ utils.unixify = function(options) { /***/ }), -/* 658 */ +/* 657 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @@ -79228,7 +79210,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 659 */ +/* 658 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79241,8 +79223,8 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ -var isobject = __webpack_require__(536); -var isDescriptor = __webpack_require__(537); +var isobject = __webpack_require__(535); +var isDescriptor = __webpack_require__(536); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -79273,7 +79255,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 660 */ +/* 659 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79327,7 +79309,7 @@ function diffArray(one, two) { /***/ }), -/* 661 */ +/* 660 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79340,7 +79322,7 @@ function diffArray(one, two) { -var isObject = __webpack_require__(536); +var isObject = __webpack_require__(535); module.exports = function pick(obj, keys) { if (!isObject(obj) && typeof obj !== 'function') { @@ -79369,7 +79351,7 @@ module.exports = function pick(obj, keys) { /***/ }), -/* 662 */ +/* 661 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -79504,7 +79486,7 @@ function isBuffer(val) { /***/ }), -/* 663 */ +/* 662 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79514,18 +79496,18 @@ function isBuffer(val) { * Module dependencies */ -var extend = __webpack_require__(587); -var unique = __webpack_require__(550); -var toRegex = __webpack_require__(528); +var extend = __webpack_require__(586); +var unique = __webpack_require__(549); +var toRegex = __webpack_require__(527); /** * Local dependencies */ -var compilers = __webpack_require__(664); -var parsers = __webpack_require__(670); -var Extglob = __webpack_require__(673); -var utils = __webpack_require__(672); +var compilers = __webpack_require__(663); +var parsers = __webpack_require__(669); +var Extglob = __webpack_require__(672); +var utils = __webpack_require__(671); var MAX_LENGTH = 1024 * 64; /** @@ -79842,13 +79824,13 @@ module.exports = extglob; /***/ }), -/* 664 */ +/* 663 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(665); +var brackets = __webpack_require__(664); /** * Extglob compilers @@ -80018,7 +80000,7 @@ module.exports = function(extglob) { /***/ }), -/* 665 */ +/* 664 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80028,17 +80010,17 @@ module.exports = function(extglob) { * Local dependencies */ -var compilers = __webpack_require__(666); -var parsers = __webpack_require__(668); +var compilers = __webpack_require__(665); +var parsers = __webpack_require__(667); /** * Module dependencies */ -var debug = __webpack_require__(616)('expand-brackets'); -var extend = __webpack_require__(587); -var Snapdragon = __webpack_require__(573); -var toRegex = __webpack_require__(528); +var debug = __webpack_require__(615)('expand-brackets'); +var extend = __webpack_require__(586); +var Snapdragon = __webpack_require__(572); +var toRegex = __webpack_require__(527); /** * Parses the given POSIX character class `pattern` and returns a @@ -80236,13 +80218,13 @@ module.exports = brackets; /***/ }), -/* 666 */ +/* 665 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var posix = __webpack_require__(667); +var posix = __webpack_require__(666); module.exports = function(brackets) { brackets.compiler @@ -80330,7 +80312,7 @@ module.exports = function(brackets) { /***/ }), -/* 667 */ +/* 666 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80359,14 +80341,14 @@ module.exports = { /***/ }), -/* 668 */ +/* 667 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(669); -var define = __webpack_require__(603); +var utils = __webpack_require__(668); +var define = __webpack_require__(602); /** * Text regex @@ -80585,14 +80567,14 @@ module.exports.TEXT_REGEX = TEXT_REGEX; /***/ }), -/* 669 */ +/* 668 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var toRegex = __webpack_require__(528); -var regexNot = __webpack_require__(547); +var toRegex = __webpack_require__(527); +var regexNot = __webpack_require__(546); var cached; /** @@ -80626,15 +80608,15 @@ exports.createRegex = function(pattern, include) { /***/ }), -/* 670 */ +/* 669 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var brackets = __webpack_require__(665); -var define = __webpack_require__(671); -var utils = __webpack_require__(672); +var brackets = __webpack_require__(664); +var define = __webpack_require__(670); +var utils = __webpack_require__(671); /** * Characters to use in text regex (we want to "not" match @@ -80789,7 +80771,7 @@ module.exports = parsers; /***/ }), -/* 671 */ +/* 670 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80802,7 +80784,7 @@ module.exports = parsers; -var isDescriptor = __webpack_require__(537); +var isDescriptor = __webpack_require__(536); module.exports = function defineProperty(obj, prop, val) { if (typeof obj !== 'object' && typeof obj !== 'function') { @@ -80827,14 +80809,14 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 672 */ +/* 671 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regex = __webpack_require__(547); -var Cache = __webpack_require__(656); +var regex = __webpack_require__(546); +var Cache = __webpack_require__(655); /** * Utils @@ -80903,7 +80885,7 @@ utils.createRegex = function(str) { /***/ }), -/* 673 */ +/* 672 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80913,16 +80895,16 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(573); -var define = __webpack_require__(671); -var extend = __webpack_require__(587); +var Snapdragon = __webpack_require__(572); +var define = __webpack_require__(670); +var extend = __webpack_require__(586); /** * Local dependencies */ -var compilers = __webpack_require__(664); -var parsers = __webpack_require__(670); +var compilers = __webpack_require__(663); +var parsers = __webpack_require__(669); /** * Customize Snapdragon parser and renderer @@ -80988,16 +80970,16 @@ module.exports = Extglob; /***/ }), -/* 674 */ +/* 673 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extglob = __webpack_require__(663); -var nanomatch = __webpack_require__(648); -var regexNot = __webpack_require__(547); -var toRegex = __webpack_require__(528); +var extglob = __webpack_require__(662); +var nanomatch = __webpack_require__(647); +var regexNot = __webpack_require__(546); +var toRegex = __webpack_require__(527); var not; /** @@ -81078,14 +81060,14 @@ function textRegex(pattern) { /***/ }), -/* 675 */ +/* 674 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(656))(); +module.exports = new (__webpack_require__(655))(); /***/ }), -/* 676 */ +/* 675 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81098,13 +81080,13 @@ var path = __webpack_require__(4); * Module dependencies */ -var Snapdragon = __webpack_require__(573); -utils.define = __webpack_require__(677); -utils.diff = __webpack_require__(660); -utils.extend = __webpack_require__(645); -utils.pick = __webpack_require__(661); -utils.typeOf = __webpack_require__(678); -utils.unique = __webpack_require__(550); +var Snapdragon = __webpack_require__(572); +utils.define = __webpack_require__(676); +utils.diff = __webpack_require__(659); +utils.extend = __webpack_require__(644); +utils.pick = __webpack_require__(660); +utils.typeOf = __webpack_require__(677); +utils.unique = __webpack_require__(549); /** * Returns true if the platform is windows, or `path.sep` is `\\`. @@ -81401,7 +81383,7 @@ utils.unixify = function(options) { /***/ }), -/* 677 */ +/* 676 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81414,8 +81396,8 @@ utils.unixify = function(options) { -var isobject = __webpack_require__(536); -var isDescriptor = __webpack_require__(537); +var isobject = __webpack_require__(535); +var isDescriptor = __webpack_require__(536); var define = (typeof Reflect !== 'undefined' && Reflect.defineProperty) ? Reflect.defineProperty : Object.defineProperty; @@ -81446,7 +81428,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 678 */ +/* 677 */ /***/ (function(module, exports) { var toString = Object.prototype.toString; @@ -81581,7 +81563,7 @@ function isBuffer(val) { /***/ }), -/* 679 */ +/* 678 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81600,9 +81582,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(680); -var reader_1 = __webpack_require__(693); -var fs_stream_1 = __webpack_require__(697); +var readdir = __webpack_require__(679); +var reader_1 = __webpack_require__(692); +var fs_stream_1 = __webpack_require__(696); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -81663,15 +81645,15 @@ exports.default = ReaderAsync; /***/ }), -/* 680 */ +/* 679 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(681); -const readdirAsync = __webpack_require__(689); -const readdirStream = __webpack_require__(692); +const readdirSync = __webpack_require__(680); +const readdirAsync = __webpack_require__(688); +const readdirStream = __webpack_require__(691); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -81755,7 +81737,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 681 */ +/* 680 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81763,11 +81745,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(682); +const DirectoryReader = __webpack_require__(681); let syncFacade = { - fs: __webpack_require__(687), - forEach: __webpack_require__(688), + fs: __webpack_require__(686), + forEach: __webpack_require__(687), sync: true }; @@ -81796,7 +81778,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 682 */ +/* 681 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81805,9 +81787,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(138).Readable; const EventEmitter = __webpack_require__(156).EventEmitter; const path = __webpack_require__(4); -const normalizeOptions = __webpack_require__(683); -const stat = __webpack_require__(685); -const call = __webpack_require__(686); +const normalizeOptions = __webpack_require__(682); +const stat = __webpack_require__(684); +const call = __webpack_require__(685); /** * Asynchronously reads the contents of a directory and streams the results @@ -82183,14 +82165,14 @@ module.exports = DirectoryReader; /***/ }), -/* 683 */ +/* 682 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const globToRegExp = __webpack_require__(684); +const globToRegExp = __webpack_require__(683); module.exports = normalizeOptions; @@ -82367,7 +82349,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 684 */ +/* 683 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -82504,13 +82486,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 685 */ +/* 684 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(686); +const call = __webpack_require__(685); module.exports = stat; @@ -82585,7 +82567,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 686 */ +/* 685 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82646,14 +82628,14 @@ function callOnce (fn) { /***/ }), -/* 687 */ +/* 686 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const call = __webpack_require__(686); +const call = __webpack_require__(685); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -82717,7 +82699,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 688 */ +/* 687 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82746,7 +82728,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 689 */ +/* 688 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82754,12 +82736,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(690); -const DirectoryReader = __webpack_require__(682); +const maybe = __webpack_require__(689); +const DirectoryReader = __webpack_require__(681); let asyncFacade = { fs: __webpack_require__(134), - forEach: __webpack_require__(691), + forEach: __webpack_require__(690), async: true }; @@ -82801,7 +82783,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 690 */ +/* 689 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82828,7 +82810,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 691 */ +/* 690 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82864,7 +82846,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 692 */ +/* 691 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82872,11 +82854,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(682); +const DirectoryReader = __webpack_require__(681); let streamFacade = { fs: __webpack_require__(134), - forEach: __webpack_require__(691), + forEach: __webpack_require__(690), async: true }; @@ -82896,16 +82878,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 693 */ +/* 692 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); -var deep_1 = __webpack_require__(694); -var entry_1 = __webpack_require__(696); -var pathUtil = __webpack_require__(695); +var deep_1 = __webpack_require__(693); +var entry_1 = __webpack_require__(695); +var pathUtil = __webpack_require__(694); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -82971,14 +82953,14 @@ exports.default = Reader; /***/ }), -/* 694 */ +/* 693 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(695); -var patternUtils = __webpack_require__(522); +var pathUtils = __webpack_require__(694); +var patternUtils = __webpack_require__(521); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { this.options = options; @@ -83061,7 +83043,7 @@ exports.default = DeepFilter; /***/ }), -/* 695 */ +/* 694 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83092,14 +83074,14 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 696 */ +/* 695 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(695); -var patternUtils = __webpack_require__(522); +var pathUtils = __webpack_require__(694); +var patternUtils = __webpack_require__(521); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { this.options = options; @@ -83184,7 +83166,7 @@ exports.default = EntryFilter; /***/ }), -/* 697 */ +/* 696 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83204,8 +83186,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(138); -var fsStat = __webpack_require__(698); -var fs_1 = __webpack_require__(702); +var fsStat = __webpack_require__(697); +var fs_1 = __webpack_require__(701); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -83255,14 +83237,14 @@ exports.default = FileSystemStream; /***/ }), -/* 698 */ +/* 697 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(699); -const statProvider = __webpack_require__(701); +const optionsManager = __webpack_require__(698); +const statProvider = __webpack_require__(700); /** * Asynchronous API. */ @@ -83293,13 +83275,13 @@ exports.statSync = statSync; /***/ }), -/* 699 */ +/* 698 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(700); +const fsAdapter = __webpack_require__(699); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -83312,7 +83294,7 @@ exports.prepare = prepare; /***/ }), -/* 700 */ +/* 699 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83335,7 +83317,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 701 */ +/* 700 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83387,7 +83369,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 702 */ +/* 701 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83418,7 +83400,7 @@ exports.default = FileSystem; /***/ }), -/* 703 */ +/* 702 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83438,9 +83420,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(138); -var readdir = __webpack_require__(680); -var reader_1 = __webpack_require__(693); -var fs_stream_1 = __webpack_require__(697); +var readdir = __webpack_require__(679); +var reader_1 = __webpack_require__(692); +var fs_stream_1 = __webpack_require__(696); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -83508,7 +83490,7 @@ exports.default = ReaderStream; /***/ }), -/* 704 */ +/* 703 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83527,9 +83509,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(680); -var reader_1 = __webpack_require__(693); -var fs_sync_1 = __webpack_require__(705); +var readdir = __webpack_require__(679); +var reader_1 = __webpack_require__(692); +var fs_sync_1 = __webpack_require__(704); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -83589,7 +83571,7 @@ exports.default = ReaderSync; /***/ }), -/* 705 */ +/* 704 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83608,8 +83590,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(698); -var fs_1 = __webpack_require__(702); +var fsStat = __webpack_require__(697); +var fs_1 = __webpack_require__(701); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -83655,7 +83637,7 @@ exports.default = FileSystemSync; /***/ }), -/* 706 */ +/* 705 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83671,7 +83653,7 @@ exports.flatten = flatten; /***/ }), -/* 707 */ +/* 706 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83692,13 +83674,13 @@ exports.merge = merge; /***/ }), -/* 708 */ +/* 707 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const pathType = __webpack_require__(709); +const pathType = __webpack_require__(708); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -83764,13 +83746,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 709 */ +/* 708 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); -const pify = __webpack_require__(710); +const pify = __webpack_require__(709); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -83813,7 +83795,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 710 */ +/* 709 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -83904,17 +83886,17 @@ module.exports = (obj, opts) => { /***/ }), -/* 711 */ +/* 710 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(134); const path = __webpack_require__(4); -const fastGlob = __webpack_require__(518); -const gitIgnore = __webpack_require__(712); -const pify = __webpack_require__(713); -const slash = __webpack_require__(714); +const fastGlob = __webpack_require__(517); +const gitIgnore = __webpack_require__(711); +const pify = __webpack_require__(712); +const slash = __webpack_require__(713); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -84012,7 +83994,7 @@ module.exports.sync = options => { /***/ }), -/* 712 */ +/* 711 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -84481,7 +84463,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 713 */ +/* 712 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84556,7 +84538,7 @@ module.exports = (input, options) => { /***/ }), -/* 714 */ +/* 713 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84574,7 +84556,7 @@ module.exports = input => { /***/ }), -/* 715 */ +/* 714 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84587,7 +84569,7 @@ module.exports = input => { -var isGlob = __webpack_require__(716); +var isGlob = __webpack_require__(715); module.exports = function hasGlob(val) { if (val == null) return false; @@ -84607,7 +84589,7 @@ module.exports = function hasGlob(val) { /***/ }), -/* 716 */ +/* 715 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -84638,17 +84620,17 @@ module.exports = function isGlob(str) { /***/ }), -/* 717 */ +/* 716 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); const {constants: fsConstants} = __webpack_require__(134); -const pEvent = __webpack_require__(718); -const CpFileError = __webpack_require__(721); -const fs = __webpack_require__(723); -const ProgressEmitter = __webpack_require__(726); +const pEvent = __webpack_require__(717); +const CpFileError = __webpack_require__(720); +const fs = __webpack_require__(722); +const ProgressEmitter = __webpack_require__(725); const cpFileAsync = async (source, destination, options, progressEmitter) => { let readError; @@ -84762,12 +84744,12 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 718 */ +/* 717 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pTimeout = __webpack_require__(719); +const pTimeout = __webpack_require__(718); const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; @@ -85058,12 +85040,12 @@ module.exports.iterator = (emitter, event, options) => { /***/ }), -/* 719 */ +/* 718 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pFinally = __webpack_require__(720); +const pFinally = __webpack_require__(719); class TimeoutError extends Error { constructor(message) { @@ -85109,7 +85091,7 @@ module.exports.TimeoutError = TimeoutError; /***/ }), -/* 720 */ +/* 719 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85131,12 +85113,12 @@ module.exports = (promise, onFinally) => { /***/ }), -/* 721 */ +/* 720 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(722); +const NestedError = __webpack_require__(721); class CpFileError extends NestedError { constructor(message, nested) { @@ -85150,7 +85132,7 @@ module.exports = CpFileError; /***/ }), -/* 722 */ +/* 721 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(112).inherits; @@ -85206,16 +85188,16 @@ module.exports = NestedError; /***/ }), -/* 723 */ +/* 722 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(112); const fs = __webpack_require__(133); -const makeDir = __webpack_require__(724); -const pEvent = __webpack_require__(718); -const CpFileError = __webpack_require__(721); +const makeDir = __webpack_require__(723); +const pEvent = __webpack_require__(717); +const CpFileError = __webpack_require__(720); const stat = promisify(fs.stat); const lstat = promisify(fs.lstat); @@ -85312,7 +85294,7 @@ exports.copyFileSync = (source, destination, flags) => { /***/ }), -/* 724 */ +/* 723 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85320,7 +85302,7 @@ exports.copyFileSync = (source, destination, flags) => { const fs = __webpack_require__(134); const path = __webpack_require__(4); const {promisify} = __webpack_require__(112); -const semver = __webpack_require__(725); +const semver = __webpack_require__(724); const useNativeRecursiveOption = semver.satisfies(process.version, '>=10.12.0'); @@ -85475,7 +85457,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 725 */ +/* 724 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -87077,7 +87059,7 @@ function coerce (version, options) { /***/ }), -/* 726 */ +/* 725 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87118,7 +87100,7 @@ module.exports = ProgressEmitter; /***/ }), -/* 727 */ +/* 726 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87164,12 +87146,12 @@ exports.default = module.exports; /***/ }), -/* 728 */ +/* 727 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pMap = __webpack_require__(729); +const pMap = __webpack_require__(728); const pFilter = async (iterable, filterer, options) => { const values = await pMap( @@ -87186,7 +87168,7 @@ module.exports.default = pFilter; /***/ }), -/* 729 */ +/* 728 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87265,12 +87247,12 @@ module.exports.default = pMap; /***/ }), -/* 730 */ +/* 729 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(722); +const NestedError = __webpack_require__(721); class CpyError extends NestedError { constructor(message, nested) { diff --git a/rfcs/text/0013_search_sessions.md b/rfcs/text/0013_search_sessions.md index 659f1933a86f9..4de9617455e56 100644 --- a/rfcs/text/0013_search_sessions.md +++ b/rfcs/text/0013_search_sessions.md @@ -285,7 +285,6 @@ interface ISessionService { * @param url TODO: is the URL provided here? How? * @returns The stored `SearchSessionAttributes` object * @throws Throws an error in OSS. - * @internal (Consumers should use searchInterceptor.sendToBackground()) */ store: ( request: KibanaRequest, diff --git a/src/plugins/vis_type_timeseries/server/saved_objects/index.ts b/scripts/licenses_csv_report.js similarity index 90% rename from src/plugins/vis_type_timeseries/server/saved_objects/index.ts rename to scripts/licenses_csv_report.js index 5f7f5767f423d..6fe813a472745 100644 --- a/src/plugins/vis_type_timeseries/server/saved_objects/index.ts +++ b/scripts/licenses_csv_report.js @@ -17,4 +17,5 @@ * under the License. */ -export { tsvbTelemetrySavedObjectType } from './tsvb_telemetry'; +require('../src/setup_node_env'); +require('../src/dev/run_licenses_csv_report'); diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 43960ce7db467..6c0ced7022cb2 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -220,7 +220,7 @@ export class DocLinksService { }, apis: { createIndex: `${ELASTICSEARCH_DOCS}indices-create-index.html`, - createSnapshotLifecylePolicy: `${ELASTICSEARCH_DOCS}slm-api-put-policy.html`, + createSnapshotLifecyclePolicy: `${ELASTICSEARCH_DOCS}slm-api-put-policy.html`, createRoleMapping: `${ELASTICSEARCH_DOCS}security-api-put-role-mapping.html`, createRoleMappingTemplates: `${ELASTICSEARCH_DOCS}security-api-put-role-mapping.html#_role_templates`, createApiKey: `${ELASTICSEARCH_DOCS}security-api-create-api-key.html`, @@ -344,12 +344,38 @@ export interface DocLinksStart { readonly ml: Record; readonly transforms: Record; readonly visualize: Record; - readonly apis: Record; + readonly apis: Readonly<{ + createIndex: string; + createSnapshotLifecyclePolicy: string; + createRoleMapping: string; + createRoleMappingTemplates: string; + createApiKey: string; + createPipeline: string; + createTransformRequest: string; + executeWatchActionModes: string; + openIndex: string; + putComponentTemplate: string; + painlessExecute: string; + putComponentTemplateMetadata: string; + putWatch: string; + updateTransform: string; + }>; readonly observability: Record; readonly alerting: Record; readonly maps: Record; readonly monitoring: Record; - readonly security: Record; + 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; readonly ccs: Record; }; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index c5b49519ef7b2..2f4c871c33431 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -587,12 +587,38 @@ export interface DocLinksStart { readonly ml: Record; readonly transforms: Record; readonly visualize: Record; - readonly apis: Record; + readonly apis: Readonly<{ + createIndex: string; + createSnapshotLifecyclePolicy: string; + createRoleMapping: string; + createRoleMappingTemplates: string; + createApiKey: string; + createPipeline: string; + createTransformRequest: string; + executeWatchActionModes: string; + openIndex: string; + putComponentTemplate: string; + painlessExecute: string; + putComponentTemplateMetadata: string; + putWatch: string; + updateTransform: string; + }>; readonly observability: Record; readonly alerting: Record; readonly maps: Record; readonly monitoring: Record; - readonly security: Record; + 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; readonly ccs: Record; }; diff --git a/src/core/server/capabilities/capabilities_service.test.ts b/src/core/server/capabilities/capabilities_service.test.ts index 42dc1604281b8..efa51066d4417 100644 --- a/src/core/server/capabilities/capabilities_service.test.ts +++ b/src/core/server/capabilities/capabilities_service.test.ts @@ -184,5 +184,46 @@ describe('CapabilitiesService', () => { } `); }); + + it('allows to indicate that default capabilities should be returned', async () => { + setup.registerProvider(() => ({ customSection: { isDefault: true } })); + setup.registerSwitcher((req, capabilities, useDefaultCapabilities) => + useDefaultCapabilities ? capabilities : { customSection: { isDefault: false } } + ); + + const start = service.start(); + expect(await start.resolveCapabilities({} as any)).toMatchInlineSnapshot(` + Object { + "catalogue": Object {}, + "customSection": Object { + "isDefault": false, + }, + "management": Object {}, + "navLinks": Object {}, + } + `); + expect(await start.resolveCapabilities({} as any, { useDefaultCapabilities: false })) + .toMatchInlineSnapshot(` + Object { + "catalogue": Object {}, + "customSection": Object { + "isDefault": false, + }, + "management": Object {}, + "navLinks": Object {}, + } + `); + expect(await start.resolveCapabilities({} as any, { useDefaultCapabilities: true })) + .toMatchInlineSnapshot(` + Object { + "catalogue": Object {}, + "customSection": Object { + "isDefault": true, + }, + "management": Object {}, + "navLinks": Object {}, + } + `); + }); }); }); diff --git a/src/core/server/capabilities/capabilities_service.ts b/src/core/server/capabilities/capabilities_service.ts index 9af945d17b2ad..f18848e04f547 100644 --- a/src/core/server/capabilities/capabilities_service.ts +++ b/src/core/server/capabilities/capabilities_service.ts @@ -104,6 +104,18 @@ export interface CapabilitiesSetup { registerSwitcher(switcher: CapabilitiesSwitcher): void; } +/** + * Defines a set of additional options for the `resolveCapabilities` method of {@link CapabilitiesStart}. + * + * @public + */ +export interface ResolveCapabilitiesOptions { + /** + * Indicates if capability switchers are supposed to return a default set of capabilities. + */ + useDefaultCapabilities: boolean; +} + /** * APIs to access the application {@link Capabilities}. * @@ -113,7 +125,10 @@ export interface CapabilitiesStart { /** * Resolve the {@link Capabilities} to be used for given request */ - resolveCapabilities(request: KibanaRequest): Promise; + resolveCapabilities( + request: KibanaRequest, + options?: ResolveCapabilitiesOptions + ): Promise; } interface SetupDeps { @@ -162,7 +177,8 @@ export class CapabilitiesService { public start(): CapabilitiesStart { return { - resolveCapabilities: (request) => this.resolveCapabilities(request, [], false), + resolveCapabilities: (request, options) => + this.resolveCapabilities(request, [], options?.useDefaultCapabilities ?? false), }; } } diff --git a/src/core/server/capabilities/index.ts b/src/core/server/capabilities/index.ts index ac9454f01391c..cd14ee949a5d2 100644 --- a/src/core/server/capabilities/index.ts +++ b/src/core/server/capabilities/index.ts @@ -17,5 +17,10 @@ * under the License. */ -export { CapabilitiesService, CapabilitiesSetup, CapabilitiesStart } from './capabilities_service'; +export { + CapabilitiesService, + CapabilitiesSetup, + CapabilitiesStart, + ResolveCapabilitiesOptions, +} from './capabilities_service'; export { Capabilities, CapabilitiesSwitcher, CapabilitiesProvider } from './types'; diff --git a/src/core/server/http/router/socket.test.ts b/src/core/server/http/router/socket.test.ts index c813dcf3fc806..5a78edde2e0db 100644 --- a/src/core/server/http/router/socket.test.ts +++ b/src/core/server/http/router/socket.test.ts @@ -22,10 +22,10 @@ import { KibanaSocket } from './socket'; describe('KibanaSocket', () => { describe('getPeerCertificate', () => { - it('returns null for net.Socket instance', () => { + it('returns `null` for net.Socket instance', () => { const socket = new KibanaSocket(new Socket()); - expect(socket.getPeerCertificate()).toBe(null); + expect(socket.getPeerCertificate()).toBeNull(); }); it('delegates a call to tls.Socket instance', () => { @@ -40,20 +40,82 @@ describe('KibanaSocket', () => { expect(result).toBe(cert); }); - it('returns null if tls.Socket getPeerCertificate returns null', () => { + it('returns `null` if tls.Socket getPeerCertificate returns null', () => { const tlsSocket = new TLSSocket(new Socket()); jest.spyOn(tlsSocket, 'getPeerCertificate').mockImplementation(() => null as any); const socket = new KibanaSocket(tlsSocket); - expect(socket.getPeerCertificate()).toBe(null); + expect(socket.getPeerCertificate()).toBeNull(); }); - it('returns null if tls.Socket getPeerCertificate returns empty object', () => { + it('returns `null` if tls.Socket getPeerCertificate returns empty object', () => { const tlsSocket = new TLSSocket(new Socket()); jest.spyOn(tlsSocket, 'getPeerCertificate').mockImplementation(() => ({} as any)); const socket = new KibanaSocket(tlsSocket); - expect(socket.getPeerCertificate()).toBe(null); + expect(socket.getPeerCertificate()).toBeNull(); + }); + }); + + describe('getProtocol', () => { + it('returns `null` for net.Socket instance', () => { + const socket = new KibanaSocket(new Socket()); + + expect(socket.getProtocol()).toBeNull(); + }); + + it('delegates a call to tls.Socket instance', () => { + const tlsSocket = new TLSSocket(new Socket()); + const protocol = 'TLSv1.2'; + const spy = jest.spyOn(tlsSocket, 'getProtocol').mockImplementation(() => protocol); + const socket = new KibanaSocket(tlsSocket); + const result = socket.getProtocol(); + + expect(spy).toBeCalledTimes(1); + expect(result).toBe(protocol); + }); + + it('returns `null` if tls.Socket getProtocol returns null', () => { + const tlsSocket = new TLSSocket(new Socket()); + jest.spyOn(tlsSocket, 'getProtocol').mockImplementation(() => null as any); + const socket = new KibanaSocket(tlsSocket); + + expect(socket.getProtocol()).toBeNull(); + }); + }); + + describe('renegotiate', () => { + it('throws error for net.Socket instance', async () => { + const socket = new KibanaSocket(new Socket()); + + expect(() => socket.renegotiate({})).rejects.toThrowErrorMatchingInlineSnapshot( + `"Cannot renegotiate a connection when TLS is not enabled."` + ); + }); + + it('delegates a call to tls.Socket instance', async () => { + const tlsSocket = new TLSSocket(new Socket()); + const result = Symbol(); + const spy = jest.spyOn(tlsSocket, 'renegotiate').mockImplementation((_, callback) => { + callback(result as any); + return undefined; + }); + const socket = new KibanaSocket(tlsSocket); + + expect(socket.renegotiate({})).resolves.toBe(result); + expect(spy).toBeCalledTimes(1); + }); + + it('throws error if tls.Socket renegotiate returns error', async () => { + const tlsSocket = new TLSSocket(new Socket()); + const error = new Error('Oh no!'); + jest.spyOn(tlsSocket, 'renegotiate').mockImplementation((_, callback) => { + callback(error); + return undefined; + }); + const socket = new KibanaSocket(tlsSocket); + + expect(() => socket.renegotiate({})).rejects.toThrow(error); }); }); @@ -68,12 +130,11 @@ describe('KibanaSocket', () => { const tlsSocket = new TLSSocket(new Socket()); tlsSocket.authorized = true; - let socket = new KibanaSocket(tlsSocket); + const socket = new KibanaSocket(tlsSocket); expect(tlsSocket.authorized).toBe(true); expect(socket.authorized).toBe(true); tlsSocket.authorized = false; - socket = new KibanaSocket(tlsSocket); expect(tlsSocket.authorized).toBe(false); expect(socket.authorized).toBe(false); }); @@ -90,13 +151,12 @@ describe('KibanaSocket', () => { const tlsSocket = new TLSSocket(new Socket()); tlsSocket.authorizationError = undefined as any; - let socket = new KibanaSocket(tlsSocket); + const socket = new KibanaSocket(tlsSocket); expect(tlsSocket.authorizationError).toBeUndefined(); expect(socket.authorizationError).toBeUndefined(); const authorizationError = new Error('some error'); tlsSocket.authorizationError = authorizationError; - socket = new KibanaSocket(tlsSocket); expect(tlsSocket.authorizationError).toBe(authorizationError); expect(socket.authorizationError).toBe(authorizationError); diff --git a/src/core/server/http/router/socket.ts b/src/core/server/http/router/socket.ts index 83bf65a288c4b..dcf583140e521 100644 --- a/src/core/server/http/router/socket.ts +++ b/src/core/server/http/router/socket.ts @@ -19,6 +19,7 @@ import { Socket } from 'net'; import { DetailedPeerCertificate, PeerCertificate, TLSSocket } from 'tls'; +import { promisify } from 'util'; /** * A tiny abstraction for TCP socket. @@ -38,6 +39,20 @@ export interface IKibanaSocket { */ getPeerCertificate(detailed?: boolean): PeerCertificate | DetailedPeerCertificate | null; + /** + * Returns a string containing the negotiated SSL/TLS protocol version of the current connection. The value 'unknown' will be returned for + * connected sockets that have not completed the handshaking process. The value null will be returned for server sockets or disconnected + * client sockets. See https://www.openssl.org/docs/man1.0.2/ssl/SSL_get_version.html for more information. + */ + getProtocol(): string | null; + + /** + * Renegotiates a connection to obtain the peer's certificate. This cannot be used when the protocol version is TLSv1.3. + * @param options - The options may contain the following fields: rejectUnauthorized, requestCert (See tls.createServer() for details). + * @returns A Promise that will be resolved if renegotiation succeeded, or will be rejected if renegotiation failed. + */ + renegotiate(options: { rejectUnauthorized?: boolean; requestCert?: boolean }): Promise; + /** * Indicates whether or not the peer certificate was signed by one of the specified CAs. When TLS * isn't used the value is `undefined`. @@ -52,15 +67,14 @@ export interface IKibanaSocket { } export class KibanaSocket implements IKibanaSocket { - readonly authorized?: boolean; - readonly authorizationError?: Error; - - constructor(private readonly socket: Socket) { - if (this.socket instanceof TLSSocket) { - this.authorized = this.socket.authorized; - this.authorizationError = this.socket.authorizationError; - } + public get authorized() { + return this.socket instanceof TLSSocket ? this.socket.authorized : undefined; } + public get authorizationError() { + return this.socket instanceof TLSSocket ? this.socket.authorizationError : undefined; + } + + constructor(private readonly socket: Socket) {} getPeerCertificate(detailed: true): DetailedPeerCertificate | null; getPeerCertificate(detailed: false): PeerCertificate | null; @@ -76,4 +90,18 @@ export class KibanaSocket implements IKibanaSocket { } return null; } + + public getProtocol() { + if (this.socket instanceof TLSSocket) { + return this.socket.getProtocol(); + } + return null; + } + + public async renegotiate(options: { rejectUnauthorized?: boolean; requestCert?: boolean }) { + if (this.socket instanceof TLSSocket) { + return promisify(this.socket.renegotiate.bind(this.socket))(options); + } + return Promise.reject(new Error('Cannot renegotiate a connection when TLS is not enabled.')); + } } diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 0dae17b4c211e..72b224cb68df2 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -87,7 +87,12 @@ export { }; export { bootstrap } from './bootstrap'; -export { Capabilities, CapabilitiesProvider, CapabilitiesSwitcher } from './capabilities'; +export { + Capabilities, + CapabilitiesProvider, + CapabilitiesSwitcher, + ResolveCapabilitiesOptions, +} from './capabilities'; export { ConfigPath, ConfigService, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 8c284facb442e..75f1580ceba8e 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -307,7 +307,7 @@ export interface CapabilitiesSetup { // @public export interface CapabilitiesStart { - resolveCapabilities(request: KibanaRequest): Promise; + resolveCapabilities(request: KibanaRequest, options?: ResolveCapabilitiesOptions): Promise; } // @public @@ -1101,6 +1101,11 @@ export interface IKibanaSocket { // (undocumented) getPeerCertificate(detailed: false): PeerCertificate | null; getPeerCertificate(detailed?: boolean): PeerCertificate | DetailedPeerCertificate | null; + getProtocol(): string | null; + renegotiate(options: { + rejectUnauthorized?: boolean; + requestCert?: boolean; + }): Promise; } // @public @deprecated @@ -1919,6 +1924,11 @@ export type RequestHandlerContextProvider(handler: RequestHandler) => RequestHandler; +// @public +export interface ResolveCapabilitiesOptions { + useDefaultCapabilities: boolean; +} + // @public export type ResponseError = string | Error | { message: string | Error; diff --git a/src/dev/build/tasks/clean_tasks.ts b/src/dev/build/tasks/clean_tasks.ts index eb6d68efd8a66..daae4e238df08 100644 --- a/src/dev/build/tasks/clean_tasks.ts +++ b/src/dev/build/tasks/clean_tasks.ts @@ -41,7 +41,10 @@ export const CleanPackages: Task = { description: 'Cleaning source for packages that are now installed in node_modules', async run(config, log, build) { - await deleteAll([build.resolvePath('packages'), build.resolvePath('yarn.lock')], log); + await deleteAll( + [build.resolvePath('packages'), build.resolvePath('yarn.lock'), build.resolvePath('.npmrc')], + log + ); }, }; diff --git a/src/dev/build/tasks/copy_source_task.ts b/src/dev/build/tasks/copy_source_task.ts index 038ccba5ed17e..2a66d3d394066 100644 --- a/src/dev/build/tasks/copy_source_task.ts +++ b/src/dev/build/tasks/copy_source_task.ts @@ -27,6 +27,7 @@ export const CopySource: Task = { dot: false, select: [ 'yarn.lock', + '.npmrc', 'src/**', '!src/**/*.{test,test.mocks,mock}.{js,ts,tsx}', '!src/**/mocks.ts', // special file who imports .mock files diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index 6822fcddc3ac5..30e3b60dcee83 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -138,7 +138,17 @@ kibana_vars=( tilemap.url timelion.enabled vega.enableExternalUrls + xpack.actions.allowedHosts + xpack.actions.enabled + xpack.actions.enabledActionTypes + xpack.actions.preconfigured xpack.actions.proxyUrl + xpack.actions.proxyHeaders + xpack.actions.proxyRejectUnauthorizedCertificates + xpack.actions.rejectUnauthorized + xpack.alerts.healthCheck.interval + xpack.alerts.invalidateApiKeysTask.interval + xpack.alerts.invalidateApiKeysTask.removalDelay xpack.apm.enabled xpack.apm.serviceMapEnabled xpack.apm.ui.enabled @@ -166,6 +176,9 @@ kibana_vars=( xpack.code.security.gitProtocolWhitelist xpack.encryptedSavedObjects.encryptionKey xpack.encryptedSavedObjects.keyRotation.decryptionOnlyKeys + xpack.event_log.enabled + xpack.event_log.logEntries + xpack.event_log.indexEntries xpack.fleet.agents.elasticsearch.host xpack.fleet.agents.kibana.host xpack.fleet.agents.tlsCheckDisabled @@ -255,6 +268,18 @@ kibana_vars=( xpack.security.loginHelp xpack.spaces.enabled xpack.spaces.maxSpaces + xpack.task_manager.enabled + xpack.task_manager.max_attempts + xpack.task_manager.poll_interval + xpack.task_manager.max_poll_inactivity_cycles + xpack.task_manager.request_capacity + xpack.task_manager.index + xpack.task_manager.max_workers + xpack.task_manager.monitored_stats_required_freshness + xpack.task_manager.monitored_aggregated_stats_refresh_rate + xpack.task_manager.monitored_stats_running_average_window + xpack.task_manager.monitored_task_execution_thresholds + xpack.task_manager.version_conflict_threshold telemetry.allowChangingOptInStatus telemetry.enabled telemetry.optIn diff --git a/src/dev/run_licenses_csv_report.js b/src/dev/run_licenses_csv_report.js new file mode 100644 index 0000000000000..791f296085deb --- /dev/null +++ b/src/dev/run_licenses_csv_report.js @@ -0,0 +1,128 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { writeFileSync } from 'fs'; +import { resolve } from 'path'; +import { isNull, isUndefined } from 'lodash'; + +import { run } from '@kbn/dev-utils'; + +import { getInstalledPackages } from './npm'; +import { engines } from '../../package'; +import { LICENSE_OVERRIDES } from './license_checker'; + +const allDoubleQuoteRE = /"/g; + +function escapeValue(value) { + if (isNull(value)) { + return; + } + + return `"${value.replace(allDoubleQuoteRE, '""')}"`; +} + +function formatCsvValues(fields, values) { + return fields + .map((field) => { + const value = values[field]; + + if (isNull(value) || isUndefined(value)) { + return null; + } + + return value.toString(); + }) + .map(escapeValue) + .join(','); +} + +run( + async ({ log, flags }) => { + const fields = ['name', 'version', 'url', 'license', 'sourceURL']; + + const file = flags.csv; + const directory = flags.directory; + const dev = flags.dev; + + const root = resolve(__dirname, '..', '..'); + const packages = await getInstalledPackages({ + directory: directory ? resolve(directory) : root, + licenseOverrides: LICENSE_OVERRIDES, + dev, + }); + + packages.unshift( + { + name: 'Node.js', + version: engines.node, + repository: 'https://nodejs.org', + licenses: ['MIT'], + }, + { + name: 'Red Hat Universal Base Image minimal', + version: '8', + repository: + 'https://catalog.redhat.com/software/containers/ubi8/ubi-minimal/5c359a62bed8bd75a2c3fba8', + licenses: [ + 'Custom;https://www.redhat.com/licenses/EULA_Red_Hat_Universal_Base_Image_English_20190422.pdf', + ], + sourceURL: 'https://oss-dependencies.elastic.co/redhat/ubi/ubi-minimal-8-source.tar.gz', + } + ); + + const csv = packages + .map((pkg) => { + const data = { + name: pkg.name, + version: pkg.version, + url: pkg.repository || `https://www.npmjs.com/package/${pkg.name}`, + license: pkg.licenses.join(','), + sourceURL: pkg.sourceURL, + }; + + return formatCsvValues(fields, data); + }) + .join('\n'); + + if (file) { + writeFileSync(file, `${fields.join(',')}\n${csv}`); + log.success(`wrote to ${file}`); + } else { + log.success(csv); + log.debug('\nspecify "--csv [filepath]" to write the data to a specific file'); + } + }, + { + description: ` + Report of 3rd party dependencies + `, + flags: { + boolean: ['dev'], + string: ['csv', 'directory'], + default: { + dev: false, + }, + help: ` + --dev Include development dependencies + --csv Write csv report to file + --directory Directory to check for licenses + `, + }, + } +); diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx index abef8afcc3985..4abc37a455d05 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx @@ -97,6 +97,7 @@ function EditorUI({ initialTextValue }: EditorProps) { if (textareaElement) { textareaElement.setAttribute('id', inputId); + textareaElement.setAttribute('data-test-subj', 'console-textarea'); } const readQueryParams = () => { @@ -204,7 +205,7 @@ function EditorUI({ initialTextValue }: EditorProps) { }, [sendCurrentRequestToES, openDocumentation]); return ( -
+
    , + }, }); dashboardState.handleDashboardContainerChanges(dashboardContainer); @@ -149,15 +154,39 @@ describe('DashboardState', function () { const dashboardContainer = initDashboardContainer({ expandedPanelId: 'theCoolestPanelOnThisDashboard', + panels: { + theCoolestPanelOnThisDashboard: { + explicitInput: { id: 'theCoolestPanelOnThisDashboard' }, + } as DashboardPanelState, + }, }); dashboardState.handleDashboardContainerChanges(dashboardContainer); dashboardState.handleDashboardContainerChanges(dashboardContainer); expect(dashboardState.setExpandedPanelId).toHaveBeenCalledTimes(1); + }); + + test('expandedPanelId is set to undefined if panel does not exist in input', () => { + dashboardState.setExpandedPanelId = jest + .fn() + .mockImplementation(dashboardState.setExpandedPanelId); + const dashboardContainer = initDashboardContainer({ + expandedPanelId: 'theCoolestPanelOnThisDashboard', + panels: { + theCoolestPanelOnThisDashboard: { + explicitInput: { id: 'theCoolestPanelOnThisDashboard' }, + } as DashboardPanelState, + }, + }); + + dashboardState.handleDashboardContainerChanges(dashboardContainer); + expect(dashboardState.setExpandedPanelId).toHaveBeenCalledWith( + 'theCoolestPanelOnThisDashboard' + ); - dashboardContainer.updateInput({ expandedPanelId: 'woah it changed' }); + dashboardContainer.updateInput({ expandedPanelId: 'theLeastCoolPanelOnThisDashboard' }); dashboardState.handleDashboardContainerChanges(dashboardContainer); - expect(dashboardState.setExpandedPanelId).toHaveBeenCalledTimes(2); + expect(dashboardState.setExpandedPanelId).toHaveBeenCalledWith(undefined); }); }); diff --git a/src/plugins/dashboard/public/application/dashboard_state_manager.ts b/src/plugins/dashboard/public/application/dashboard_state_manager.ts index dfcbfcafd3db1..891add08547db 100644 --- a/src/plugins/dashboard/public/application/dashboard_state_manager.ts +++ b/src/plugins/dashboard/public/application/dashboard_state_manager.ts @@ -223,6 +223,7 @@ export class DashboardStateManager { const savedDashboardPanelMap: { [key: string]: SavedDashboardPanel } = {}; const input = dashboardContainer.getInput(); + this.getPanels().forEach((savedDashboardPanel) => { if (input.panels[savedDashboardPanel.panelIndex] !== undefined) { savedDashboardPanelMap[savedDashboardPanel.panelIndex] = savedDashboardPanel; @@ -234,11 +235,16 @@ export class DashboardStateManager { const convertedPanelStateMap: { [key: string]: SavedDashboardPanel } = {}; + let expandedPanelValid = false; Object.values(input.panels).forEach((panelState) => { if (savedDashboardPanelMap[panelState.explicitInput.id] === undefined) { dirty = true; } + if (panelState.explicitInput.id === input.expandedPanelId) { + expandedPanelValid = true; + } + convertedPanelStateMap[panelState.explicitInput.id] = convertPanelStateToSavedDashboardPanel( panelState, this.kibanaVersion @@ -272,8 +278,10 @@ export class DashboardStateManager { this.setFullScreenMode(input.isFullScreenMode); } - if (input.expandedPanelId !== this.getExpandedPanelId()) { + if (expandedPanelValid && input.expandedPanelId !== this.getExpandedPanelId()) { this.setExpandedPanelId(input.expandedPanelId); + } else if (!expandedPanelValid && this.getExpandedPanelId()) { + this.setExpandedPanelId(undefined); } if (!_.isEqual(input.query, this.getQuery())) { diff --git a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx index c5929c5d85dbb..3d7b9312a5127 100644 --- a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx +++ b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx @@ -34,7 +34,6 @@ import { ViewMode, EmbeddableChildPanel } from '../../../services/embeddable'; import { DASHBOARD_GRID_COLUMN_COUNT, DASHBOARD_GRID_HEIGHT } from '../dashboard_constants'; import { DashboardPanelState } from '../types'; import { withKibana } from '../../../services/kibana_react'; -import { DashboardContainerInput } from '../dashboard_container'; import { DashboardContainer, DashboardReactContextValue } from '../dashboard_container'; let lastValidGridSize = 0; @@ -177,18 +176,17 @@ class DashboardGridUi extends React.Component { isLayoutInvalid, }); - this.subscription = this.props.container - .getInput$() - .subscribe((input: DashboardContainerInput) => { - if (this.mounted) { - this.setState({ - panels: input.panels, - viewMode: input.viewMode, - useMargins: input.useMargins, - expandedPanelId: input.expandedPanelId, - }); - } - }); + this.subscription = this.props.container.getInput$().subscribe(() => { + const { panels, viewMode, useMargins, expandedPanelId } = this.props.container.getInput(); + if (this.mounted) { + this.setState({ + panels, + viewMode, + useMargins, + expandedPanelId, + }); + } + }); } public componentWillUnmount() { diff --git a/src/plugins/data/common/search/aggs/buckets/date_histogram_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/date_histogram_fn.test.ts index bd3c4f8dd58cf..5edbf6fc4eac2 100644 --- a/src/plugins/data/common/search/aggs/buckets/date_histogram_fn.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/date_histogram_fn.test.ts @@ -108,13 +108,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - - expect(() => { - fn({ - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/buckets/date_histogram_fn.ts b/src/plugins/data/common/search/aggs/buckets/date_histogram_fn.ts index 3e3895b7b50db..55a43864879c4 100644 --- a/src/plugins/data/common/search/aggs/buckets/date_histogram_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/date_histogram_fn.ts @@ -152,7 +152,6 @@ export const aggDateHistogram = (): FunctionDefinition => ({ ...rest, timeRange: getParsedValue(args, 'timeRange'), extended_bounds: getParsedValue(args, 'extended_bounds'), - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/buckets/date_range_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/date_range_fn.test.ts index 93bb791874e67..2aa6a3a1f6fab 100644 --- a/src/plugins/data/common/search/aggs/buckets/date_range_fn.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/date_range_fn.test.ts @@ -88,14 +88,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - - expect(() => { - fn({ - field: 'date_field', - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/buckets/date_range_fn.ts b/src/plugins/data/common/search/aggs/buckets/date_range_fn.ts index 0dc66be5b84f2..c09c87b8b9afd 100644 --- a/src/plugins/data/common/search/aggs/buckets/date_range_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/date_range_fn.ts @@ -107,7 +107,6 @@ export const aggDateRange = (): FunctionDefinition => ({ type: BUCKET_TYPES.DATE_RANGE, params: { ...rest, - json: getParsedValue(args, 'json'), ranges: getParsedValue(args, 'ranges'), }, }, diff --git a/src/plugins/data/common/search/aggs/buckets/filter_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/filter_fn.test.ts index c820a73b0a894..1d85b7dba8bd1 100644 --- a/src/plugins/data/common/search/aggs/buckets/filter_fn.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/filter_fn.test.ts @@ -73,13 +73,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - - expect(() => { - fn({ - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/buckets/filter_fn.ts b/src/plugins/data/common/search/aggs/buckets/filter_fn.ts index 8c8c0f430184a..07ed11e2249b1 100644 --- a/src/plugins/data/common/search/aggs/buckets/filter_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/filter_fn.ts @@ -95,7 +95,6 @@ export const aggFilter = (): FunctionDefinition => ({ type: BUCKET_TYPES.FILTER, params: { ...rest, - json: getParsedValue(args, 'json'), geo_bounding_box: getParsedValue(args, 'geo_bounding_box'), }, }, diff --git a/src/plugins/data/common/search/aggs/buckets/filters_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/filters_fn.test.ts index 99c4f7d8c2b65..da29afec1e1e6 100644 --- a/src/plugins/data/common/search/aggs/buckets/filters_fn.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/filters_fn.test.ts @@ -79,13 +79,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - - expect(() => { - fn({ - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/buckets/filters_fn.ts b/src/plugins/data/common/search/aggs/buckets/filters_fn.ts index 194feb67d3366..ccf9232d14d3f 100644 --- a/src/plugins/data/common/search/aggs/buckets/filters_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/filters_fn.ts @@ -90,7 +90,6 @@ export const aggFilters = (): FunctionDefinition => ({ params: { ...rest, filters: getParsedValue(args, 'filters'), - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/buckets/geo_hash_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/geo_hash_fn.test.ts index 07ab8e66f1def..1fbb69d7dd182 100644 --- a/src/plugins/data/common/search/aggs/buckets/geo_hash_fn.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/geo_hash_fn.test.ts @@ -99,14 +99,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - - expect(() => { - fn({ - field: 'geo_field', - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/buckets/geo_hash_fn.ts b/src/plugins/data/common/search/aggs/buckets/geo_hash_fn.ts index aa5f473f73f9d..cbd901a438eb4 100644 --- a/src/plugins/data/common/search/aggs/buckets/geo_hash_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/geo_hash_fn.ts @@ -126,7 +126,6 @@ export const aggGeoHash = (): FunctionDefinition => ({ params: { ...rest, boundingBox: getParsedValue(args, 'boundingBox'), - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/buckets/geo_tile_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/geo_tile_fn.test.ts index bfaf47ede8734..78bf16e8f067c 100644 --- a/src/plugins/data/common/search/aggs/buckets/geo_tile_fn.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/geo_tile_fn.test.ts @@ -78,14 +78,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - - expect(() => { - fn({ - field: 'geo_field', - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/buckets/geo_tile_fn.ts b/src/plugins/data/common/search/aggs/buckets/geo_tile_fn.ts index 346c70bba31fd..1e7258ea7da53 100644 --- a/src/plugins/data/common/search/aggs/buckets/geo_tile_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/geo_tile_fn.ts @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; export const aggGeoTileFnName = 'aggGeoTile'; @@ -105,7 +104,6 @@ export const aggGeoTile = (): FunctionDefinition => ({ type: BUCKET_TYPES.GEOTILE_GRID, params: { ...rest, - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/buckets/histogram_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/histogram_fn.test.ts index 354946f99a2f5..4a8e785da2974 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram_fn.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram_fn.test.ts @@ -98,15 +98,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - - expect(() => { - fn({ - field: 'field', - interval: '10', - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/buckets/histogram_fn.ts b/src/plugins/data/common/search/aggs/buckets/histogram_fn.ts index 62dbc7ca8ca45..6b5e2d02b271b 100644 --- a/src/plugins/data/common/search/aggs/buckets/histogram_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/histogram_fn.ts @@ -135,7 +135,6 @@ export const aggHistogram = (): FunctionDefinition => ({ params: { ...rest, extended_bounds: getParsedValue(args, 'extended_bounds'), - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/buckets/ip_range_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/ip_range_fn.test.ts index 5940345b25890..462672cf5dffe 100644 --- a/src/plugins/data/common/search/aggs/buckets/ip_range_fn.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/ip_range_fn.test.ts @@ -88,15 +88,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - - expect(() => { - fn({ - field: 'ip_field', - ipRangeType: IP_RANGE_TYPES.FROM_TO, - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/buckets/ip_range_fn.ts b/src/plugins/data/common/search/aggs/buckets/ip_range_fn.ts index 7ad61a9c27d86..92eedc35ce61b 100644 --- a/src/plugins/data/common/search/aggs/buckets/ip_range_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/ip_range_fn.ts @@ -110,7 +110,6 @@ export const aggIpRange = (): FunctionDefinition => ({ type: BUCKET_TYPES.IP_RANGE, params: { ...rest, - json: getParsedValue(args, 'json'), ranges: getParsedValue(args, 'ranges'), }, }, diff --git a/src/plugins/data/common/search/aggs/buckets/range_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/range_fn.test.ts index 93ae4490196a8..1a08cc1adaa28 100644 --- a/src/plugins/data/common/search/aggs/buckets/range_fn.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/range_fn.test.ts @@ -87,14 +87,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - - expect(() => { - fn({ - field: 'number_field', - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/buckets/range_fn.ts b/src/plugins/data/common/search/aggs/buckets/range_fn.ts index a52b2427b9845..f3d662ced8fcf 100644 --- a/src/plugins/data/common/search/aggs/buckets/range_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/range_fn.ts @@ -102,7 +102,6 @@ export const aggRange = (): FunctionDefinition => ({ type: BUCKET_TYPES.RANGE, params: { ...rest, - json: getParsedValue(args, 'json'), ranges: getParsedValue(args, 'ranges'), }, }, diff --git a/src/plugins/data/common/search/aggs/buckets/shard_delay_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/shard_delay_fn.test.ts index 89281490167b3..d2c1fc77d0514 100644 --- a/src/plugins/data/common/search/aggs/buckets/shard_delay_fn.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/shard_delay_fn.test.ts @@ -52,14 +52,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - - expect(() => { - fn({ - delay: '1000ms', - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/buckets/shard_delay_fn.ts b/src/plugins/data/common/search/aggs/buckets/shard_delay_fn.ts index 87f80192ca3cd..d13138ad43dad 100644 --- a/src/plugins/data/common/search/aggs/buckets/shard_delay_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/shard_delay_fn.ts @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggConfigSerialized } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; import { AggParamsShardDelay, SHARD_DELAY_AGG_NAME } from './shard_delay'; export const aggShardDelayFnName = 'aggShardDelay'; @@ -93,7 +92,6 @@ export const aggShardDelay = (): FunctionDefinition => ({ type: SHARD_DELAY_AGG_NAME, params: { ...rest, - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/buckets/significant_terms_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/significant_terms_fn.test.ts index 71be4e9cfa9ac..9e94654821913 100644 --- a/src/plugins/data/common/search/aggs/buckets/significant_terms_fn.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/significant_terms_fn.test.ts @@ -84,13 +84,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - expect(() => { - fn({ - field: 'machine.os.keyword', - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/buckets/significant_terms_fn.ts b/src/plugins/data/common/search/aggs/buckets/significant_terms_fn.ts index a1a7500678fd6..2998200ccae76 100644 --- a/src/plugins/data/common/search/aggs/buckets/significant_terms_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/significant_terms_fn.ts @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; export const aggSignificantTermsFnName = 'aggSignificantTerms'; @@ -113,7 +112,6 @@ export const aggSignificantTerms = (): FunctionDefinition => ({ type: BUCKET_TYPES.SIGNIFICANT_TERMS, params: { ...rest, - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/buckets/terms_fn.test.ts b/src/plugins/data/common/search/aggs/buckets/terms_fn.test.ts index 1384a9f17e4b6..9374497ddb664 100644 --- a/src/plugins/data/common/search/aggs/buckets/terms_fn.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/terms_fn.test.ts @@ -154,15 +154,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - expect(() => { - fn({ - field: 'machine.os.keyword', - order: 'asc', - orderBy: '1', - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/buckets/terms_fn.ts b/src/plugins/data/common/search/aggs/buckets/terms_fn.ts index 7737cb1e1c952..a0b491b4cb398 100644 --- a/src/plugins/data/common/search/aggs/buckets/terms_fn.ts +++ b/src/plugins/data/common/search/aggs/buckets/terms_fn.ts @@ -21,7 +21,6 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, BUCKET_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; export const aggTermsFnName = 'aggTerms'; @@ -160,7 +159,6 @@ export const aggTerms = (): FunctionDefinition => ({ params: { ...rest, orderAgg: args.orderAgg?.value, - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/metrics/avg_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/avg_fn.test.ts index 0e2ee00df49dd..8162b6e7554f0 100644 --- a/src/plugins/data/common/search/aggs/metrics/avg_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/avg_fn.test.ts @@ -52,13 +52,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - expect(() => { - fn({ - field: 'machine.os.keyword', - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/avg_fn.ts b/src/plugins/data/common/search/aggs/metrics/avg_fn.ts index 57dd3dae70fba..d28bd88095191 100644 --- a/src/plugins/data/common/search/aggs/metrics/avg_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/avg_fn.ts @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; export const aggAvgFnName = 'aggAvg'; @@ -87,7 +86,6 @@ export const aggAvg = (): FunctionDefinition => ({ type: METRIC_TYPES.AVG, params: { ...rest, - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/metrics/bucket_avg_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/bucket_avg_fn.test.ts index 7e08bc9954510..7329ea57ade83 100644 --- a/src/plugins/data/common/search/aggs/metrics/bucket_avg_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/bucket_avg_fn.test.ts @@ -67,12 +67,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - expect(() => { - fn({ - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/bucket_avg_fn.ts b/src/plugins/data/common/search/aggs/metrics/bucket_avg_fn.ts index 595d49647d9c2..cfed0205f1b61 100644 --- a/src/plugins/data/common/search/aggs/metrics/bucket_avg_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/bucket_avg_fn.ts @@ -21,7 +21,6 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; export const aggBucketAvgFnName = 'aggBucketAvg'; @@ -104,7 +103,6 @@ export const aggBucketAvg = (): FunctionDefinition => ({ ...rest, customBucket: args.customBucket?.value, customMetric: args.customMetric?.value, - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/metrics/bucket_max_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/bucket_max_fn.test.ts index b789bdf51ebd5..9e890331a848f 100644 --- a/src/plugins/data/common/search/aggs/metrics/bucket_max_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/bucket_max_fn.test.ts @@ -67,12 +67,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - expect(() => { - fn({ - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/bucket_max_fn.ts b/src/plugins/data/common/search/aggs/metrics/bucket_max_fn.ts index 482c73e7d3005..90a494f8cbc77 100644 --- a/src/plugins/data/common/search/aggs/metrics/bucket_max_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/bucket_max_fn.ts @@ -21,7 +21,6 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; export const aggBucketMaxFnName = 'aggBucketMax'; @@ -104,7 +103,6 @@ export const aggBucketMax = (): FunctionDefinition => ({ ...rest, customBucket: args.customBucket?.value, customMetric: args.customMetric?.value, - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/metrics/bucket_min_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/bucket_min_fn.test.ts index 6ebc83417813b..8a03849769a9a 100644 --- a/src/plugins/data/common/search/aggs/metrics/bucket_min_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/bucket_min_fn.test.ts @@ -67,12 +67,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - expect(() => { - fn({ - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/bucket_min_fn.ts b/src/plugins/data/common/search/aggs/metrics/bucket_min_fn.ts index 68beffbf05660..8f48166887f25 100644 --- a/src/plugins/data/common/search/aggs/metrics/bucket_min_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/bucket_min_fn.ts @@ -21,7 +21,6 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; export const aggBucketMinFnName = 'aggBucketMin'; @@ -104,7 +103,6 @@ export const aggBucketMin = (): FunctionDefinition => ({ ...rest, customBucket: args.customBucket?.value, customMetric: args.customMetric?.value, - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/metrics/bucket_sum_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/bucket_sum_fn.test.ts index 71549f41b1d15..ea26f58dd2f15 100644 --- a/src/plugins/data/common/search/aggs/metrics/bucket_sum_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/bucket_sum_fn.test.ts @@ -67,12 +67,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - expect(() => { - fn({ - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/bucket_sum_fn.ts b/src/plugins/data/common/search/aggs/metrics/bucket_sum_fn.ts index 7994bb85be2a7..d26f22b106c2f 100644 --- a/src/plugins/data/common/search/aggs/metrics/bucket_sum_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/bucket_sum_fn.ts @@ -21,7 +21,6 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; export const aggBucketSumFnName = 'aggBucketSum'; @@ -104,7 +103,6 @@ export const aggBucketSum = (): FunctionDefinition => ({ ...rest, customBucket: args.customBucket?.value, customMetric: args.customMetric?.value, - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/metrics/cardinality_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/cardinality_fn.test.ts index 4008819018ee5..5045fccf921a3 100644 --- a/src/plugins/data/common/search/aggs/metrics/cardinality_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/cardinality_fn.test.ts @@ -52,13 +52,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - expect(() => { - fn({ - field: 'machine.os.keyword', - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/cardinality_fn.ts b/src/plugins/data/common/search/aggs/metrics/cardinality_fn.ts index 6e78a42fea90f..e5599ab8599f7 100644 --- a/src/plugins/data/common/search/aggs/metrics/cardinality_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/cardinality_fn.ts @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; export const aggCardinalityFnName = 'aggCardinality'; @@ -92,7 +91,6 @@ export const aggCardinality = (): FunctionDefinition => ({ type: METRIC_TYPES.CARDINALITY, params: { ...rest, - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/metrics/cumulative_sum_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/cumulative_sum_fn.test.ts index 3cf53e3da153e..48120f9bdc338 100644 --- a/src/plugins/data/common/search/aggs/metrics/cumulative_sum_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/cumulative_sum_fn.test.ts @@ -108,13 +108,7 @@ describe('agg_expression_functions', () => { buckets_path: 'the_sum', }); - expect(actual.value.params.json).toEqual({ foo: true }); - expect(() => { - fn({ - json: '/// intentionally malformed json ///', - buckets_path: 'the_sum', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/cumulative_sum_fn.ts b/src/plugins/data/common/search/aggs/metrics/cumulative_sum_fn.ts index 040e26125079f..b20d03f06c854 100644 --- a/src/plugins/data/common/search/aggs/metrics/cumulative_sum_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/cumulative_sum_fn.ts @@ -21,7 +21,6 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; export const aggCumulativeSumFnName = 'aggCumulativeSum'; @@ -107,7 +106,6 @@ export const aggCumulativeSum = (): FunctionDefinition => ({ params: { ...rest, customMetric: args.customMetric?.value, - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/metrics/derivative_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/derivative_fn.test.ts index 79ea7292104ee..602ef8e1de48b 100644 --- a/src/plugins/data/common/search/aggs/metrics/derivative_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/derivative_fn.test.ts @@ -108,13 +108,7 @@ describe('agg_expression_functions', () => { buckets_path: 'the_sum', }); - expect(actual.value.params.json).toEqual({ foo: true }); - expect(() => { - fn({ - json: '/// intentionally malformed json ///', - buckets_path: 'the_sum', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/derivative_fn.ts b/src/plugins/data/common/search/aggs/metrics/derivative_fn.ts index 93ef0286a0c7e..b3fb448446c44 100644 --- a/src/plugins/data/common/search/aggs/metrics/derivative_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/derivative_fn.ts @@ -21,7 +21,6 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; export const aggDerivativeFnName = 'aggDerivative'; @@ -107,7 +106,6 @@ export const aggDerivative = (): FunctionDefinition => ({ params: { ...rest, customMetric: args.customMetric?.value, - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/metrics/geo_bounds_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/geo_bounds_fn.test.ts index 96bd31916784a..3f85a874b0dd5 100644 --- a/src/plugins/data/common/search/aggs/metrics/geo_bounds_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/geo_bounds_fn.test.ts @@ -52,13 +52,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - expect(() => { - fn({ - field: 'machine.os.keyword', - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/geo_bounds_fn.ts b/src/plugins/data/common/search/aggs/metrics/geo_bounds_fn.ts index af5ea3c80506c..1403d95371c1d 100644 --- a/src/plugins/data/common/search/aggs/metrics/geo_bounds_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/geo_bounds_fn.ts @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; export const aggGeoBoundsFnName = 'aggGeoBounds'; @@ -92,7 +91,6 @@ export const aggGeoBounds = (): FunctionDefinition => ({ type: METRIC_TYPES.GEO_BOUNDS, params: { ...rest, - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/metrics/geo_centroid_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/geo_centroid_fn.test.ts index bf9a4548bafbf..aea6ee1f2c332 100644 --- a/src/plugins/data/common/search/aggs/metrics/geo_centroid_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/geo_centroid_fn.test.ts @@ -52,13 +52,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - expect(() => { - fn({ - field: 'machine.os.keyword', - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/geo_centroid_fn.ts b/src/plugins/data/common/search/aggs/metrics/geo_centroid_fn.ts index 2c2d60711def3..9e36e07cbaf58 100644 --- a/src/plugins/data/common/search/aggs/metrics/geo_centroid_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/geo_centroid_fn.ts @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; export const aggGeoCentroidFnName = 'aggGeoCentroid'; @@ -92,7 +91,6 @@ export const aggGeoCentroid = (): FunctionDefinition => ({ type: METRIC_TYPES.GEO_CENTROID, params: { ...rest, - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/metrics/max_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/max_fn.test.ts index 156b51ca54af5..b042b82a048c2 100644 --- a/src/plugins/data/common/search/aggs/metrics/max_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/max_fn.test.ts @@ -52,13 +52,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - expect(() => { - fn({ - field: 'machine.os.keyword', - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/max_fn.ts b/src/plugins/data/common/search/aggs/metrics/max_fn.ts index 9624cd3012398..7f1e3b9ad9d69 100644 --- a/src/plugins/data/common/search/aggs/metrics/max_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/max_fn.ts @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; export const aggMaxFnName = 'aggMax'; @@ -87,7 +86,6 @@ export const aggMax = (): FunctionDefinition => ({ type: METRIC_TYPES.MAX, params: { ...rest, - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/metrics/median_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/median_fn.test.ts index 69200c35426c8..def9197bc3444 100644 --- a/src/plugins/data/common/search/aggs/metrics/median_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/median_fn.test.ts @@ -52,13 +52,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - expect(() => { - fn({ - field: 'machine.os.keyword', - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/median_fn.ts b/src/plugins/data/common/search/aggs/metrics/median_fn.ts index e2ea8ae0fe2e7..2323fa969c245 100644 --- a/src/plugins/data/common/search/aggs/metrics/median_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/median_fn.ts @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; export const aggMedianFnName = 'aggMedian'; @@ -92,7 +91,6 @@ export const aggMedian = (): FunctionDefinition => ({ type: METRIC_TYPES.MEDIAN, params: { ...rest, - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/metrics/min_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/min_fn.test.ts index ef32d086e41f7..44d0f0f4f4607 100644 --- a/src/plugins/data/common/search/aggs/metrics/min_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/min_fn.test.ts @@ -52,13 +52,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - expect(() => { - fn({ - field: 'machine.os.keyword', - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/min_fn.ts b/src/plugins/data/common/search/aggs/metrics/min_fn.ts index b880937eea2d7..40b2085a97e97 100644 --- a/src/plugins/data/common/search/aggs/metrics/min_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/min_fn.ts @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; export const aggMinFnName = 'aggMin'; @@ -87,7 +86,6 @@ export const aggMin = (): FunctionDefinition => ({ type: METRIC_TYPES.MIN, params: { ...rest, - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/metrics/moving_avg_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/moving_avg_fn.test.ts index d6c0e6b2cbd6e..a9334480ec602 100644 --- a/src/plugins/data/common/search/aggs/metrics/moving_avg_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/moving_avg_fn.test.ts @@ -118,13 +118,7 @@ describe('agg_expression_functions', () => { buckets_path: 'the_sum', }); - expect(actual.value.params.json).toEqual({ foo: true }); - expect(() => { - fn({ - json: '/// intentionally malformed json ///', - buckets_path: 'the_sum', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/moving_avg_fn.ts b/src/plugins/data/common/search/aggs/metrics/moving_avg_fn.ts index 85b7e536e66fc..5f30e088954c9 100644 --- a/src/plugins/data/common/search/aggs/metrics/moving_avg_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/moving_avg_fn.ts @@ -21,7 +21,6 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; export const aggMovingAvgFnName = 'aggMovingAvg'; @@ -120,7 +119,6 @@ export const aggMovingAvg = (): FunctionDefinition => ({ params: { ...rest, customMetric: args.customMetric?.value, - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/metrics/percentile_ranks_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/percentile_ranks_fn.test.ts index e3ce91bafd40a..c9393f68ce404 100644 --- a/src/plugins/data/common/search/aggs/metrics/percentile_ranks_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/percentile_ranks_fn.test.ts @@ -81,13 +81,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - expect(() => { - fn({ - field: 'machine.os.keyword', - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/percentile_ranks_fn.ts b/src/plugins/data/common/search/aggs/metrics/percentile_ranks_fn.ts index 9bf35c4dba9ff..bd4c83c1bdbbb 100644 --- a/src/plugins/data/common/search/aggs/metrics/percentile_ranks_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/percentile_ranks_fn.ts @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; export const aggPercentileRanksFnName = 'aggPercentileRanks'; @@ -99,7 +98,6 @@ export const aggPercentileRanks = (): FunctionDefinition => ({ type: METRIC_TYPES.PERCENTILE_RANKS, params: { ...rest, - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/metrics/percentiles_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/percentiles_fn.test.ts index 2074cc1d89527..4b8424b79b33a 100644 --- a/src/plugins/data/common/search/aggs/metrics/percentiles_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/percentiles_fn.test.ts @@ -81,13 +81,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - expect(() => { - fn({ - field: 'machine.os.keyword', - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/percentiles_fn.ts b/src/plugins/data/common/search/aggs/metrics/percentiles_fn.ts index d7bcefc23f711..0b5f5d4c1a890 100644 --- a/src/plugins/data/common/search/aggs/metrics/percentiles_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/percentiles_fn.ts @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; export const aggPercentilesFnName = 'aggPercentiles'; @@ -99,7 +98,6 @@ export const aggPercentiles = (): FunctionDefinition => ({ type: METRIC_TYPES.PERCENTILES, params: { ...rest, - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/metrics/serial_diff_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/serial_diff_fn.test.ts index 1bb859ad4bad8..a3410401a38cf 100644 --- a/src/plugins/data/common/search/aggs/metrics/serial_diff_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/serial_diff_fn.test.ts @@ -108,13 +108,7 @@ describe('agg_expression_functions', () => { buckets_path: 'the_sum', }); - expect(actual.value.params.json).toEqual({ foo: true }); - expect(() => { - fn({ - json: '/// intentionally malformed json ///', - buckets_path: 'the_sum', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/serial_diff_fn.ts b/src/plugins/data/common/search/aggs/metrics/serial_diff_fn.ts index f3602f5519d5e..f2c5b152e2f2f 100644 --- a/src/plugins/data/common/search/aggs/metrics/serial_diff_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/serial_diff_fn.ts @@ -21,7 +21,6 @@ import { i18n } from '@kbn/i18n'; import { Assign } from '@kbn/utility-types'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; export const aggSerialDiffFnName = 'aggSerialDiff'; @@ -107,7 +106,6 @@ export const aggSerialDiff = (): FunctionDefinition => ({ params: { ...rest, customMetric: args.customMetric?.value, - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/metrics/std_deviation_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/std_deviation_fn.test.ts index bfa6aa7cc4122..bbaa1d3eef3fc 100644 --- a/src/plugins/data/common/search/aggs/metrics/std_deviation_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/std_deviation_fn.test.ts @@ -52,13 +52,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - expect(() => { - fn({ - field: 'machine.os.keyword', - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/std_deviation_fn.ts b/src/plugins/data/common/search/aggs/metrics/std_deviation_fn.ts index 2a3c1bd33e17d..d0dadf7e0078e 100644 --- a/src/plugins/data/common/search/aggs/metrics/std_deviation_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/std_deviation_fn.ts @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; export const aggStdDeviationFnName = 'aggStdDeviation'; @@ -92,7 +91,6 @@ export const aggStdDeviation = (): FunctionDefinition => ({ type: METRIC_TYPES.STD_DEV, params: { ...rest, - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/metrics/sum_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/sum_fn.test.ts index 6e57632ba84cc..fed56246e087a 100644 --- a/src/plugins/data/common/search/aggs/metrics/sum_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/sum_fn.test.ts @@ -52,13 +52,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - expect(() => { - fn({ - field: 'machine.os.keyword', - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/sum_fn.ts b/src/plugins/data/common/search/aggs/metrics/sum_fn.ts index a42510dc594ad..d58231f732c9d 100644 --- a/src/plugins/data/common/search/aggs/metrics/sum_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/sum_fn.ts @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; export const aggSumFnName = 'aggSum'; @@ -87,7 +86,6 @@ export const aggSum = (): FunctionDefinition => ({ type: METRIC_TYPES.SUM, params: { ...rest, - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/common/search/aggs/metrics/top_hit_fn.test.ts b/src/plugins/data/common/search/aggs/metrics/top_hit_fn.test.ts index d0e9788f85025..7251909e44c81 100644 --- a/src/plugins/data/common/search/aggs/metrics/top_hit_fn.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/top_hit_fn.test.ts @@ -89,14 +89,7 @@ describe('agg_expression_functions', () => { json: '{ "foo": true }', }); - expect(actual.value.params.json).toEqual({ foo: true }); - expect(() => { - fn({ - field: 'machine.os.keyword', - aggregate: 'min', - json: '/// intentionally malformed json ///', - }); - }).toThrowErrorMatchingInlineSnapshot(`"Unable to parse json argument string"`); + expect(actual.value.params.json).toEqual('{ "foo": true }'); }); }); }); diff --git a/src/plugins/data/common/search/aggs/metrics/top_hit_fn.ts b/src/plugins/data/common/search/aggs/metrics/top_hit_fn.ts index 38a3bc6a59bfc..2636e9865df18 100644 --- a/src/plugins/data/common/search/aggs/metrics/top_hit_fn.ts +++ b/src/plugins/data/common/search/aggs/metrics/top_hit_fn.ts @@ -20,7 +20,6 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { AggExpressionType, AggExpressionFunctionArgs, METRIC_TYPES } from '../'; -import { getParsedValue } from '../utils/get_parsed_value'; export const aggTopHitFnName = 'aggTopHit'; @@ -119,7 +118,6 @@ export const aggTopHit = (): FunctionDefinition => ({ type: METRIC_TYPES.TOP_HITS, params: { ...rest, - json: getParsedValue(args, 'json'), }, }, }; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 935cb945678de..50b6b2223bd0a 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -2624,7 +2624,7 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:433:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:436:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:45:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/search/session/session_service.ts:51:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/search/session/session_service.ts:52:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/public/search/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor.test.ts index 947dac1b32640..0656d3ac3aa03 100644 --- a/src/plugins/data/public/search/search_interceptor.test.ts +++ b/src/plugins/data/public/search/search_interceptor.test.ts @@ -115,12 +115,14 @@ describe('SearchInterceptor', () => { }: { isRestore?: boolean; isStored?: boolean; - sessionId?: string; + sessionId: string; }) => { const sessionServiceMock = searchMock.session as jest.Mocked; - sessionServiceMock.getSessionId.mockImplementation(() => sessionId); - sessionServiceMock.isRestore.mockImplementation(() => isRestore); - sessionServiceMock.isStored.mockImplementation(() => isStored); + sessionServiceMock.getSearchOptions.mockImplementation(() => ({ + sessionId, + isRestore, + isStored, + })); fetchMock.mockResolvedValue({ result: 200 }); }; @@ -130,30 +132,14 @@ describe('SearchInterceptor', () => { afterEach(() => { const sessionServiceMock = searchMock.session as jest.Mocked; - sessionServiceMock.getSessionId.mockReset(); - sessionServiceMock.isRestore.mockReset(); - sessionServiceMock.isStored.mockReset(); + sessionServiceMock.getSearchOptions.mockReset(); fetchMock.mockReset(); }); - test('infers isRestore from session service state', async () => { + test('gets session search options from session service', async () => { const sessionId = 'sid'; setup({ isRestore: true, - sessionId, - }); - - await searchInterceptor.search(mockRequest, { sessionId }).toPromise(); - expect(fetchMock.mock.calls[0][0]).toEqual( - expect.objectContaining({ - options: { sessionId: 'sid', isStored: false, isRestore: true }, - }) - ); - }); - - test('infers isStored from session service state', async () => { - const sessionId = 'sid'; - setup({ isStored: true, sessionId, }); @@ -161,41 +147,13 @@ describe('SearchInterceptor', () => { await searchInterceptor.search(mockRequest, { sessionId }).toPromise(); expect(fetchMock.mock.calls[0][0]).toEqual( expect.objectContaining({ - options: { sessionId: 'sid', isStored: true, isRestore: false }, - }) - ); - }); - - test('skips isRestore & isStore in case not a current session Id', async () => { - setup({ - isStored: true, - isRestore: true, - sessionId: 'session id', - }); - - await searchInterceptor - .search(mockRequest, { sessionId: 'different session id' }) - .toPromise(); - expect(fetchMock.mock.calls[0][0]).toEqual( - expect.objectContaining({ - options: { sessionId: 'different session id', isStored: false, isRestore: false }, + options: { sessionId, isStored: true, isRestore: true }, }) ); - }); - test('skips isRestore & isStore in case no session Id', async () => { - setup({ - isStored: true, - isRestore: true, - sessionId: undefined, - }); - - await searchInterceptor.search(mockRequest, { sessionId: 'sessionId' }).toPromise(); - expect(fetchMock.mock.calls[0][0]).toEqual( - expect.objectContaining({ - options: { sessionId: 'sessionId', isStored: false, isRestore: false }, - }) - ); + expect( + (searchMock.session as jest.Mocked).getSearchOptions + ).toHaveBeenCalledWith(sessionId); }); }); diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index 8548a2a9f2b2a..b81a6c4450cc4 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -130,16 +130,12 @@ export class SearchInterceptor { ): Promise { const { abortSignal, ...requestOptions } = options || {}; - const isCurrentSession = - options?.sessionId && this.deps.session.getSessionId() === options.sessionId; - return this.batchedFetch( { request, options: { ...requestOptions, - isStored: isCurrentSession ? this.deps.session.isStored() : false, - isRestore: isCurrentSession ? this.deps.session.isRestore() : false, + ...(options?.sessionId && this.deps.session.getSearchOptions(options.sessionId)), }, }, abortSignal diff --git a/src/plugins/data/public/search/session/mocks.ts b/src/plugins/data/public/search/session/mocks.ts index ea0cd8be03f27..13dd054c122d5 100644 --- a/src/plugins/data/public/search/session/mocks.ts +++ b/src/plugins/data/public/search/session/mocks.ts @@ -49,5 +49,7 @@ export function getSessionServiceMock(): jest.Mocked { isStored: jest.fn(), isRestore: jest.fn(), save: jest.fn(), + isCurrentSession: jest.fn(), + getSearchOptions: jest.fn(), }; } diff --git a/src/plugins/data/public/search/session/session_service.test.ts b/src/plugins/data/public/search/session/session_service.test.ts index aeca7b4d63da7..8fbc034b3a4c8 100644 --- a/src/plugins/data/public/search/session/session_service.test.ts +++ b/src/plugins/data/public/search/session/session_service.test.ts @@ -33,10 +33,18 @@ describe('Session service', () => { beforeEach(() => { const initializerContext = coreMock.createPluginInitializerContext(); + const startService = coreMock.createSetup().getStartServices; nowProvider = createNowProviderMock(); sessionService = new SessionService( initializerContext, - coreMock.createSetup().getStartServices, + () => + startService().then(([coreStart, ...rest]) => [ + { + ...coreStart, + application: { ...coreStart.application, currentAppId$: new BehaviorSubject('app') }, + }, + ...rest, + ]), getSessionsClientMock(), nowProvider, { freezeState: false } // needed to use mocks inside state container @@ -100,4 +108,63 @@ describe('Session service', () => { expect(abort).toBeCalledTimes(3); }); }); + + test('getSearchOptions infers isRestore & isStored from state', async () => { + const sessionId = sessionService.start(); + const someOtherId = 'some-other-id'; + + expect(sessionService.getSearchOptions(someOtherId)).toEqual({ + isStored: false, + isRestore: false, + sessionId: someOtherId, + }); + expect(sessionService.getSearchOptions(sessionId)).toEqual({ + isStored: false, + isRestore: false, + sessionId, + }); + + sessionService.setSearchSessionInfoProvider({ + getName: async () => 'Name', + getUrlGeneratorData: async () => ({ + urlGeneratorId: 'id', + initialState: {}, + restoreState: {}, + }), + }); + await sessionService.save(); + + expect(sessionService.getSearchOptions(someOtherId)).toEqual({ + isStored: false, + isRestore: false, + sessionId: someOtherId, + }); + expect(sessionService.getSearchOptions(sessionId)).toEqual({ + isStored: true, + isRestore: false, + sessionId, + }); + + await sessionService.restore(sessionId); + + expect(sessionService.getSearchOptions(someOtherId)).toEqual({ + isStored: false, + isRestore: false, + sessionId: someOtherId, + }); + expect(sessionService.getSearchOptions(sessionId)).toEqual({ + isStored: true, + isRestore: true, + sessionId, + }); + }); + test('isCurrentSession', () => { + expect(sessionService.isCurrentSession()).toBeFalsy(); + + const sessionId = sessionService.start(); + + expect(sessionService.isCurrentSession()).toBeFalsy(); + expect(sessionService.isCurrentSession('some-other')).toBeFalsy(); + expect(sessionService.isCurrentSession(sessionId)).toBeTruthy(); + }); }); diff --git a/src/plugins/data/public/search/session/session_service.ts b/src/plugins/data/public/search/session/session_service.ts index e2185d8148708..26fa5bd10c485 100644 --- a/src/plugins/data/public/search/session/session_service.ts +++ b/src/plugins/data/public/search/session/session_service.ts @@ -29,6 +29,7 @@ import { SessionStateContainer, } from './search_session_state'; import { ISessionsClient } from './sessions_client'; +import { ISearchOptions } from '../../../common'; import { NowProviderInternalContract } from '../../now_provider'; export type ISessionService = PublicContract; @@ -256,4 +257,27 @@ export class SessionService { this.state.transitions.store(); } } + + /** + * Checks if passed sessionId is a current sessionId + * @param sessionId + */ + public isCurrentSession(sessionId?: string): boolean { + return !!sessionId && this.getSessionId() === sessionId; + } + + /** + * Infers search session options for sessionId using current session state + * @param sessionId + */ + public getSearchOptions( + sessionId: string + ): Required> { + const isCurrentSession = this.isCurrentSession(sessionId); + return { + sessionId, + isRestore: isCurrentSession ? this.isRestore() : false, + isStored: isCurrentSession ? this.isStored() : false, + }; + } } diff --git a/src/plugins/discover/common/index.ts b/src/plugins/discover/common/index.ts index 321a102e8d782..b721c9157fe16 100644 --- a/src/plugins/discover/common/index.ts +++ b/src/plugins/discover/common/index.ts @@ -29,3 +29,4 @@ export const CONTEXT_STEP_SETTING = 'context:step'; export const CONTEXT_TIE_BREAKER_FIELDS_SETTING = 'context:tieBreakerFields'; export const DOC_TABLE_LEGACY = 'doc_table:legacy'; export const MODIFY_COLUMNS_ON_SWITCH = 'discover:modifyColumnsOnSwitch'; +export const SEARCH_FIELDS_FROM_SOURCE = 'discover:searchFieldsFromSource'; diff --git a/src/fixtures/stubbed_saved_object_index_pattern.ts b/src/plugins/discover/public/__mocks__/stubbed_saved_object_index_pattern.ts similarity index 94% rename from src/fixtures/stubbed_saved_object_index_pattern.ts rename to src/plugins/discover/public/__mocks__/stubbed_saved_object_index_pattern.ts index 261e451db5452..a85734edba274 100644 --- a/src/fixtures/stubbed_saved_object_index_pattern.ts +++ b/src/plugins/discover/public/__mocks__/stubbed_saved_object_index_pattern.ts @@ -18,7 +18,7 @@ */ // @ts-expect-error -import stubbedLogstashFields from './logstash_fields'; +import stubbedLogstashFields from '../../../../fixtures/logstash_fields'; const mockLogstashFields = stubbedLogstashFields(); diff --git a/src/plugins/discover/public/application/angular/context/api/_stubs.js b/src/plugins/discover/public/application/angular/context/api/_stubs.js index d82189db60935..17d45756af148 100644 --- a/src/plugins/discover/public/application/angular/context/api/_stubs.js +++ b/src/plugins/discover/public/application/angular/context/api/_stubs.js @@ -47,6 +47,7 @@ export function createSearchSourceStub(hits, timeField) { searchSourceStub.setParent = sinon.spy(() => searchSourceStub); searchSourceStub.setField = sinon.spy(() => searchSourceStub); + searchSourceStub.removeField = sinon.spy(() => searchSourceStub); searchSourceStub.getField = sinon.spy((key) => { const previousSetCall = searchSourceStub.setField.withArgs(key).lastCall; 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 4df5ba989f798..31c106b95cbe5 100644 --- a/src/plugins/discover/public/application/angular/context/api/anchor.js +++ b/src/plugins/discover/public/application/angular/context/api/anchor.js @@ -20,7 +20,7 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -export function fetchAnchorProvider(indexPatterns, searchSource) { +export function fetchAnchorProvider(indexPatterns, searchSource, useNewFieldsApi = false) { return async function fetchAnchor(indexPatternId, anchorId, sort) { const indexPattern = await indexPatterns.get(indexPatternId); searchSource @@ -41,7 +41,10 @@ export function fetchAnchorProvider(indexPatterns, searchSource) { language: 'lucene', }) .setField('sort', sort); - + if (useNewFieldsApi) { + searchSource.removeField('fieldsFromSource'); + searchSource.setField('fields', ['*']); + } const response = await searchSource.fetch(); if (_.get(response, ['hits', 'total'], 0) < 1) { 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 993aefc4f59e3..d54b38c466a5c 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 @@ -144,4 +144,29 @@ describe('context app', function () { }); }); }); + + describe('useNewFields API', () => { + let fetchAnchor; + let searchSourceStub; + + beforeEach(() => { + searchSourceStub = createSearchSourceStub([{ _id: 'hit1' }]); + fetchAnchor = fetchAnchorProvider(createIndexPatternsStub(), searchSourceStub, true); + }); + + it('should request fields if useNewFieldsApi set', function () { + searchSourceStub._stubHits = [{ property1: 'value1' }, { property2: 'value2' }]; + + return fetchAnchor('INDEX_PATTERN_ID', 'id', [ + { '@timestamp': 'desc' }, + { _doc: 'desc' }, + ]).then(() => { + const setFieldsSpy = searchSourceStub.setField.withArgs('fields'); + const removeFieldsSpy = searchSourceStub.removeField.withArgs('fieldsFromSource'); + expect(setFieldsSpy.calledOnce).toBe(true); + expect(removeFieldsSpy.calledOnce).toBe(true); + expect(setFieldsSpy.firstCall.args[1]).toEqual(['*']); + }); + }); + }); }); diff --git a/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.js b/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.js index 4c0515906a494..ea181782470fa 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.js +++ b/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.js @@ -227,4 +227,81 @@ describe('context app', function () { }); }); }); + + describe('function fetchPredecessors with useNewFieldsApi set', function () { + let fetchPredecessors; + let mockSearchSource; + + beforeEach(() => { + mockSearchSource = createContextSearchSourceStub([], '@timestamp', MS_PER_DAY * 8); + + setServices({ + data: { + search: { + searchSource: { + create: jest.fn().mockImplementation(() => mockSearchSource), + }, + }, + }, + }); + + fetchPredecessors = ( + indexPatternId, + timeField, + sortDir, + timeValIso, + timeValNr, + tieBreakerField, + tieBreakerValue, + size + ) => { + const anchor = { + _source: { + [timeField]: timeValIso, + }, + sort: [timeValNr, tieBreakerValue], + }; + + return fetchContextProvider(createIndexPatternsStub(), true).fetchSurroundingDocs( + 'predecessors', + indexPatternId, + anchor, + timeField, + tieBreakerField, + sortDir, + size, + [] + ); + }; + }); + + it('should perform exactly one query when enough hits are returned', function () { + mockSearchSource._stubHits = [ + mockSearchSource._createStubHit(MS_PER_DAY * 3000 + 2), + mockSearchSource._createStubHit(MS_PER_DAY * 3000 + 1), + mockSearchSource._createStubHit(MS_PER_DAY * 3000), + mockSearchSource._createStubHit(MS_PER_DAY * 2000), + mockSearchSource._createStubHit(MS_PER_DAY * 1000), + ]; + + return fetchPredecessors( + 'INDEX_PATTERN_ID', + '@timestamp', + 'desc', + ANCHOR_TIMESTAMP_3000, + MS_PER_DAY * 3000, + '_doc', + 0, + 3, + [] + ).then((hits) => { + const setFieldsSpy = mockSearchSource.setField.withArgs('fields'); + const removeFieldsSpy = mockSearchSource.removeField.withArgs('fieldsFromSource'); + expect(mockSearchSource.fetch.calledOnce).toBe(true); + expect(removeFieldsSpy.calledOnce).toBe(true); + expect(setFieldsSpy.calledOnce).toBe(true); + expect(hits).toEqual(mockSearchSource._stubHits.slice(0, 3)); + }); + }); + }); }); diff --git a/src/plugins/discover/public/application/angular/context/api/context.successors.test.js b/src/plugins/discover/public/application/angular/context/api/context.successors.test.js index 285d39cd4d8a4..2c54de946c8d4 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.successors.test.js +++ b/src/plugins/discover/public/application/angular/context/api/context.successors.test.js @@ -231,4 +231,81 @@ describe('context app', function () { }); }); }); + + describe('function fetchSuccessors with useNewFieldsApi set', function () { + let fetchSuccessors; + let mockSearchSource; + + beforeEach(() => { + mockSearchSource = createContextSearchSourceStub([], '@timestamp'); + + setServices({ + data: { + search: { + searchSource: { + create: jest.fn().mockImplementation(() => mockSearchSource), + }, + }, + }, + }); + + fetchSuccessors = ( + indexPatternId, + timeField, + sortDir, + timeValIso, + timeValNr, + tieBreakerField, + tieBreakerValue, + size + ) => { + const anchor = { + _source: { + [timeField]: timeValIso, + }, + sort: [timeValNr, tieBreakerValue], + }; + + return fetchContextProvider(createIndexPatternsStub(), true).fetchSurroundingDocs( + 'successors', + indexPatternId, + anchor, + timeField, + tieBreakerField, + sortDir, + size, + [] + ); + }; + }); + + it('should perform exactly one query when enough hits are returned', function () { + mockSearchSource._stubHits = [ + mockSearchSource._createStubHit(MS_PER_DAY * 5000), + mockSearchSource._createStubHit(MS_PER_DAY * 4000), + mockSearchSource._createStubHit(MS_PER_DAY * 3000), + mockSearchSource._createStubHit(MS_PER_DAY * 3000 - 1), + mockSearchSource._createStubHit(MS_PER_DAY * 3000 - 2), + ]; + + return fetchSuccessors( + 'INDEX_PATTERN_ID', + '@timestamp', + 'desc', + ANCHOR_TIMESTAMP_3000, + MS_PER_DAY * 3000, + '_doc', + 0, + 3, + [] + ).then((hits) => { + expect(mockSearchSource.fetch.calledOnce).toBe(true); + expect(hits).toEqual(mockSearchSource._stubHits.slice(-3)); + const setFieldsSpy = mockSearchSource.setField.withArgs('fields'); + const removeFieldsSpy = mockSearchSource.removeField.withArgs('fieldsFromSource'); + expect(removeFieldsSpy.calledOnce).toBe(true); + expect(setFieldsSpy.calledOnce).toBe(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 ba8cffd1d7558..903e4e0f1b485 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.ts @@ -40,7 +40,7 @@ const DAY_MILLIS = 24 * 60 * 60 * 1000; // look from 1 day up to 10000 days into the past and future const LOOKUP_OFFSETS = [0, 1, 7, 30, 365, 10000].map((days) => days * DAY_MILLIS); -function fetchContextProvider(indexPatterns: IndexPatternsContract) { +function fetchContextProvider(indexPatterns: IndexPatternsContract, useNewFieldsApi?: boolean) { return { fetchSurroundingDocs, }; @@ -89,7 +89,14 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract) { break; } - const searchAfter = getEsQuerySearchAfter(type, documents, timeField, anchor, nanos); + const searchAfter = getEsQuerySearchAfter( + type, + documents, + timeField, + anchor, + nanos, + useNewFieldsApi + ); const sort = getEsQuerySort(timeField, tieBreakerField, sortDirToApply); @@ -116,6 +123,10 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract) { const { data } = getServices(); const searchSource = await data.search.searchSource.create(); + if (useNewFieldsApi) { + searchSource.removeField('fieldsFromSource'); + searchSource.setField('fields', ['*']); + } return searchSource .setParent(undefined) .setField('index', indexPattern) diff --git a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts b/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts index 24ac19a7e3bc3..348a0c04a84ad 100644 --- a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts +++ b/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts @@ -31,16 +31,30 @@ export function getEsQuerySearchAfter( documents: EsHitRecordList, timeFieldName: string, anchor: EsHitRecord, - nanoSeconds: string + nanoSeconds: string, + useNewFieldsApi?: boolean ): EsQuerySearchAfter { if (documents.length) { // already surrounding docs -> first or last record is used const afterTimeRecIdx = type === 'successors' && documents.length ? documents.length - 1 : 0; const afterTimeDoc = documents[afterTimeRecIdx]; - const afterTimeValue = nanoSeconds ? afterTimeDoc._source[timeFieldName] : afterTimeDoc.sort[0]; + let afterTimeValue = afterTimeDoc.sort[0]; + if (nanoSeconds) { + afterTimeValue = useNewFieldsApi + ? afterTimeDoc.fields[timeFieldName][0] + : afterTimeDoc._source[timeFieldName]; + } return [afterTimeValue, afterTimeDoc.sort[1]]; } // if data_nanos adapt timestamp value for sorting, since numeric value was rounded by browser // ES search_after also works when number is provided as string - return [nanoSeconds ? anchor._source[timeFieldName] : anchor.sort[0], anchor.sort[1]]; + const searchAfter = new Array(2) as EsQuerySearchAfter; + searchAfter[0] = anchor.sort[0]; + if (nanoSeconds) { + searchAfter[0] = useNewFieldsApi + ? anchor.fields[timeFieldName][0] + : anchor._source[timeFieldName]; + } + searchAfter[1] = anchor.sort[1]; + return searchAfter; } diff --git a/src/plugins/discover/public/application/angular/context/query/actions.js b/src/plugins/discover/public/application/angular/context/query/actions.js index d5c72d34006e2..42638cd90a1bb 100644 --- a/src/plugins/discover/public/application/angular/context/query/actions.js +++ b/src/plugins/discover/public/application/angular/context/query/actions.js @@ -27,11 +27,17 @@ import { fetchContextProvider } from '../api/context'; import { getQueryParameterActions } from '../query_parameters'; import { FAILURE_REASONS, LOADING_STATUS } from './index'; import { MarkdownSimple } from '../../../../../../kibana_react/public'; +import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../../common'; export function QueryActionsProvider(Promise) { - const { filterManager, indexPatterns, data } = getServices(); - const fetchAnchor = fetchAnchorProvider(indexPatterns, data.search.searchSource.createEmpty()); - const { fetchSurroundingDocs } = fetchContextProvider(indexPatterns); + const { filterManager, indexPatterns, data, uiSettings } = getServices(); + const useNewFieldsApi = !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); + const fetchAnchor = fetchAnchorProvider( + indexPatterns, + data.search.searchSource.createEmpty(), + useNewFieldsApi + ); + const { fetchSurroundingDocs } = fetchContextProvider(indexPatterns, useNewFieldsApi); const { setPredecessorCount, setQueryParameters, setSuccessorCount } = getQueryParameterActions( filterManager, indexPatterns diff --git a/src/plugins/discover/public/application/angular/context_app.html b/src/plugins/discover/public/application/angular/context_app.html index 8dc3e5c87e504..3d731459ad8d7 100644 --- a/src/plugins/discover/public/application/angular/context_app.html +++ b/src/plugins/discover/public/application/angular/context_app.html @@ -17,5 +17,6 @@ successor-available="contextApp.state.rows.successors.length" successor-status="contextApp.state.loadingStatus.successors.status" on-change-successor-count="contextApp.actions.fetchGivenSuccessorRows" + use-new-fields-api="contextApp.state.useNewFieldsApi" top-nav-menu="contextApp.topNavMenu" > diff --git a/src/plugins/discover/public/application/angular/context_app.js b/src/plugins/discover/public/application/angular/context_app.js index d9e2452eb8bd6..f18389df6d12d 100644 --- a/src/plugins/discover/public/application/angular/context_app.js +++ b/src/plugins/discover/public/application/angular/context_app.js @@ -18,7 +18,11 @@ */ import _ from 'lodash'; -import { CONTEXT_STEP_SETTING, CONTEXT_TIE_BREAKER_FIELDS_SETTING } from '../../../common'; +import { + CONTEXT_STEP_SETTING, + CONTEXT_TIE_BREAKER_FIELDS_SETTING, + SEARCH_FIELDS_FROM_SOURCE, +} from '../../../common'; import { getAngularModule, getServices } from '../../kibana_services'; import contextAppTemplate from './context_app.html'; import './context/components/action_bar'; @@ -59,9 +63,11 @@ function ContextAppController($scope, Private) { const { filterManager, indexPatterns, uiSettings, navigation } = getServices(); const queryParameterActions = getQueryParameterActions(filterManager, indexPatterns); const queryActions = Private(QueryActionsProvider); + const useNewFieldsApi = !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); this.state = createInitialState( parseInt(uiSettings.get(CONTEXT_STEP_SETTING), 10), - getFirstSortableField(this.indexPattern, uiSettings.get(CONTEXT_TIE_BREAKER_FIELDS_SETTING)) + getFirstSortableField(this.indexPattern, uiSettings.get(CONTEXT_TIE_BREAKER_FIELDS_SETTING)), + useNewFieldsApi ); this.topNavMenu = navigation.ui.TopNavMenu; @@ -127,7 +133,7 @@ function ContextAppController($scope, Private) { ); } -function createInitialState(defaultStepSize, tieBreakerField) { +function createInitialState(defaultStepSize, tieBreakerField, useNewFieldsApi) { return { queryParameters: createInitialQueryParametersState(defaultStepSize, tieBreakerField), rows: { @@ -137,5 +143,6 @@ function createInitialState(defaultStepSize, tieBreakerField) { successors: [], }, loadingStatus: createInitialLoadingStatusState(), + useNewFieldsApi, }; } diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 839add1aeb22d..6b552d92df0f0 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -64,6 +64,7 @@ import { DEFAULT_COLUMNS_SETTING, MODIFY_COLUMNS_ON_SWITCH, SAMPLE_SIZE_SETTING, + SEARCH_FIELDS_FROM_SOURCE, SEARCH_ON_PAGE_LOAD_SETTING, SORT_DEFAULT_ORDER_SETTING, } from '../../../common'; @@ -77,6 +78,7 @@ const services = getServices(); const { core, + capabilities, chrome, data, history: getHistory, @@ -106,8 +108,8 @@ app.config(($routeProvider) => { requireUICapability: 'discover.show', k7Breadcrumbs: ($route, $injector) => $injector.invoke($route.current.params.id ? getSavedSearchBreadcrumbs : getRootBreadcrumbs), - badge: (uiCapabilities) => { - if (uiCapabilities.discover.save) { + badge: () => { + if (capabilities.discover.save) { return undefined; } @@ -184,7 +186,7 @@ app.directive('discoverApp', function () { }; }); -function discoverController($element, $route, $scope, $timeout, Promise, uiCapabilities) { +function discoverController($element, $route, $scope, $timeout, Promise) { const { isDefault: isDefaultType } = indexPatternsUtils; const subscriptions = new Subscription(); const refetch$ = new Subject(); @@ -196,6 +198,8 @@ function discoverController($element, $route, $scope, $timeout, Promise, uiCapab $scope.searchSource, toastNotifications ); + $scope.useNewFieldsApi = !config.get(SEARCH_FIELDS_FROM_SOURCE); + //used for functional testing $scope.fetchCounter = 0; @@ -307,7 +311,8 @@ function discoverController($element, $route, $scope, $timeout, Promise, uiCapab nextIndexPattern, $scope.state.columns, $scope.state.sort, - config.get(MODIFY_COLUMNS_ON_SWITCH) + config.get(MODIFY_COLUMNS_ON_SWITCH), + $scope.useNewFieldsApi ); await setAppState(nextAppState); } @@ -341,7 +346,7 @@ function discoverController($element, $route, $scope, $timeout, Promise, uiCapab }; $scope.minimumVisibleRows = 50; $scope.fetchStatus = fetchStatuses.UNINITIALIZED; - $scope.showSaveQuery = uiCapabilities.discover.saveQuery; + $scope.showSaveQuery = capabilities.discover.saveQuery; $scope.showTimeCol = !config.get('doc_table:hideTimeColumn', false) && $scope.indexPattern.timeFieldName; @@ -414,19 +419,33 @@ function discoverController($element, $route, $scope, $timeout, Promise, uiCapab setBreadcrumbsTitle(savedSearch, chrome); + function removeSourceFromColumns(columns) { + return columns.filter((col) => col !== '_source'); + } + + function getDefaultColumns() { + const columns = [...savedSearch.columns]; + + if ($scope.useNewFieldsApi) { + return removeSourceFromColumns(columns); + } + if (columns.length > 0) { + return columns; + } + return [...config.get(DEFAULT_COLUMNS_SETTING)]; + } + function getStateDefaults() { const query = $scope.searchSource.getField('query') || data.query.queryString.getDefaultQuery(); const sort = getSortArray(savedSearch.sort, $scope.indexPattern); + const columns = getDefaultColumns(); const defaultState = { query, sort: !sort.length ? getDefaultSort($scope.indexPattern, config.get(SORT_DEFAULT_ORDER_SETTING, 'desc')) : sort, - columns: - savedSearch.columns.length > 0 - ? savedSearch.columns - : config.get(DEFAULT_COLUMNS_SETTING).slice(), + columns, index: $scope.indexPattern.id, interval: 'auto', filters: _.cloneDeep($scope.searchSource.getOwnField('filter')), @@ -738,10 +757,14 @@ function discoverController($element, $route, $scope, $timeout, Promise, uiCapab }; $scope.updateDataSource = () => { - updateSearchSource($scope.searchSource, { - indexPattern: $scope.indexPattern, + const { indexPattern, searchSource, useNewFieldsApi } = $scope; + const { columns, sort } = $scope.state; + updateSearchSource(searchSource, { + indexPattern, services, - sort: $scope.state.sort, + sort, + columns, + useNewFieldsApi, }); return Promise.resolve(); }; @@ -769,20 +792,20 @@ function discoverController($element, $route, $scope, $timeout, Promise, uiCapab }; $scope.addColumn = function addColumn(columnName) { - if (uiCapabilities.discover.save) { - const { indexPattern } = $scope; + const { indexPattern, useNewFieldsApi } = $scope; + if (capabilities.discover.save) { popularizeField(indexPattern, columnName, indexPatterns); } - const columns = columnActions.addColumn($scope.state.columns, columnName); + const columns = columnActions.addColumn($scope.state.columns, columnName, useNewFieldsApi); setAppState({ columns }); }; $scope.removeColumn = function removeColumn(columnName) { - if (uiCapabilities.discover.save) { - const { indexPattern } = $scope; + const { indexPattern, useNewFieldsApi } = $scope; + if (capabilities.discover.save) { popularizeField(indexPattern, columnName, indexPatterns); } - const columns = columnActions.removeColumn($scope.state.columns, columnName); + const columns = columnActions.removeColumn($scope.state.columns, columnName, useNewFieldsApi); // The state's sort property is an array of [sortByColumn,sortDirection] const sort = $scope.state.sort.length ? $scope.state.sort.filter((subArr) => subArr[0] !== columnName) diff --git a/src/plugins/discover/public/application/angular/discover_legacy.html b/src/plugins/discover/public/application/angular/discover_legacy.html index 3596c0a2519ed..9383980fd9fd6 100644 --- a/src/plugins/discover/public/application/angular/discover_legacy.html +++ b/src/plugins/discover/public/application/angular/discover_legacy.html @@ -29,6 +29,7 @@ top-nav-menu="topNavMenu" update-query="handleRefresh" update-saved-query-id="updateSavedQueryId" + use-new-fields-api="useNewFieldsApi" > diff --git a/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts b/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts index 8257c79af7e8a..1b6d8fcbc2544 100644 --- a/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts +++ b/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts @@ -21,28 +21,32 @@ * Helper function to provide a fallback to a single _source column if the given array of columns * is empty, and removes _source if there are more than 1 columns given * @param columns + * @param useNewFieldsApi should a new fields API be used */ -function buildColumns(columns: string[]) { +function buildColumns(columns: string[], useNewFieldsApi = false) { if (columns.length > 1 && columns.indexOf('_source') !== -1) { return columns.filter((col) => col !== '_source'); } else if (columns.length !== 0) { return columns; } - return ['_source']; + return useNewFieldsApi ? [] : ['_source']; } -export function addColumn(columns: string[], columnName: string) { +export function addColumn(columns: string[], columnName: string, useNewFieldsApi?: boolean) { if (columns.includes(columnName)) { return columns; } - return buildColumns([...columns, columnName]); + return buildColumns([...columns, columnName], useNewFieldsApi); } -export function removeColumn(columns: string[], columnName: string) { +export function removeColumn(columns: string[], columnName: string, useNewFieldsApi?: boolean) { if (!columns.includes(columnName)) { return columns; } - return buildColumns(columns.filter((col) => col !== columnName)); + return buildColumns( + columns.filter((col) => col !== columnName), + useNewFieldsApi + ); } export function moveColumn(columns: string[], columnName: string, newIndex: number) { diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_header/helpers.tsx b/src/plugins/discover/public/application/angular/doc_table/components/table_header/helpers.tsx index b456fa0773b85..bb855373c910f 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_header/helpers.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_header/helpers.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { i18n } from '@kbn/i18n'; import { IndexPattern } from '../../../../../kibana_services'; export type SortOrder = [string, string]; @@ -62,17 +63,33 @@ export function getDisplayedColumns( if (!Array.isArray(columns) || typeof indexPattern !== 'object' || !indexPattern.getFieldByName) { return []; } - const columnProps = columns.map((column, idx) => { - const field = indexPattern.getFieldByName(column); - return { - name: column, - displayName: field ? field.displayName : column, - isSortable: field && field.sortable ? true : false, - isRemoveable: column !== '_source' || columns.length > 1, - colLeftIdx: idx - 1 < 0 ? -1 : idx - 1, - colRightIdx: idx + 1 >= columns.length ? -1 : idx + 1, - }; - }); + + const columnProps = + columns.length === 0 + ? [ + { + name: '__document__', + displayName: i18n.translate('discover.docTable.tableHeader.documentHeader', { + defaultMessage: 'Document', + }), + isSortable: false, + isRemoveable: false, + colLeftIdx: -1, + colRightIdx: -1, + }, + ] + : columns.map((column, idx) => { + const field = indexPattern.getFieldByName(column); + return { + name: column, + displayName: field?.displayName ?? column, + isSortable: !!(field && field.sortable), + isRemoveable: column !== '_source' || columns.length > 1, + colLeftIdx: idx - 1 < 0 ? -1 : idx - 1, + colRightIdx: idx + 1 >= columns.length ? -1 : idx + 1, + }; + }); + return !hideTimeField && indexPattern.timeFieldName ? [getTimeColumn(indexPattern.timeFieldName), ...columnProps] : columnProps; diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts index e45f18606e3fc..75206d6bf2e84 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_row.ts @@ -27,6 +27,7 @@ import cellTemplateHtml from '../components/table_row/cell.html'; import truncateByHeightTemplateHtml from '../components/table_row/truncate_by_height.html'; import { getServices } from '../../../../kibana_services'; import { getContextUrl } from '../../../helpers/get_context_url'; +import { formatRow } from '../../helpers'; const TAGS_WITH_WS = />\s+ { $el.after(''); @@ -139,19 +141,33 @@ export function createTableRowDirective($compile: ng.ICompileService) { ); } - $scope.columns.forEach(function (column: any) { - const isFilterable = mapping(column) && mapping(column).filterable && $scope.filter; + if ($scope.columns.length === 0 && $scope.useNewFieldsApi) { + const formatted = formatRow(row, indexPattern); newHtmls.push( cellTemplate({ timefield: false, - sourcefield: column === '_source', - formatted: _displayField(row, column, true), - filterable: isFilterable, - column, + sourcefield: true, + formatted, + filterable: false, + column: '__document__', }) ); - }); + } else { + $scope.columns.forEach(function (column: string) { + const isFilterable = mapping(column) && mapping(column).filterable && $scope.filter; + + newHtmls.push( + cellTemplate({ + timefield: false, + sourcefield: column === '_source', + formatted: _displayField(row, column, true), + filterable: isFilterable, + column, + }) + ); + }); + } let $cells = $el.children(); newHtmls.forEach(function (html, i) { diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/_cell.scss b/src/plugins/discover/public/application/angular/doc_table/components/table_row/_cell.scss index b73a2598070b5..22b6e0f29268b 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/_cell.scss +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_row/_cell.scss @@ -1,9 +1,5 @@ -.kbnDocTableCell__dataField { - white-space: pre-wrap; -} - .kbnDocTableCell__toggleDetails { - padding: 4px 0 0 0!important; + padding: $euiSizeXS 0 0 0!important; } .kbnDocTableCell__filter { diff --git a/src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html b/src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html index fd20bea8fb3df..bb443b880e217 100644 --- a/src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html +++ b/src/plugins/discover/public/application/angular/doc_table/components/table_row/details.html @@ -1,4 +1,4 @@ - +
    diff --git a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx index f191fa2dc89e8..0a162673eec82 100644 --- a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx +++ b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx @@ -97,6 +97,7 @@ export interface DocTableLegacyProps { onMoveColumn?: (columns: string, newIdx: number) => void; onRemoveColumn?: (column: string) => void; sort?: string[][]; + useNewFieldsApi?: boolean; } export function DocTableLegacy(renderProps: DocTableLegacyProps) { @@ -118,6 +119,7 @@ export function DocTableLegacy(renderProps: DocTableLegacyProps) { on-move-column="onMoveColumn" on-remove-column="onRemoveColumn" render-complete + use-new-fields-api="useNewFieldsApi" sorting="sort">`, }, () => getServices().getEmbeddableInjector() diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table.html b/src/plugins/discover/public/application/angular/doc_table/doc_table.html index bb8cc4b9ee4c2..427893bd3e6fe 100644 --- a/src/plugins/discover/public/application/angular/doc_table/doc_table.html +++ b/src/plugins/discover/public/application/angular/doc_table/doc_table.html @@ -46,6 +46,7 @@ class="kbnDocTable__row" on-add-column="onAddColumn" on-remove-column="onRemoveColumn" + use-new-fields-api="useNewFieldsApi" > @@ -97,6 +98,7 @@ data-test-subj="docTableRow{{ row['$$_isAnchor'] ? ' docTableAnchorRow' : ''}}" on-add-column="onAddColumn" on-remove-column="onRemoveColumn" + use-new-fields-api="useNewFieldsApi" > diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table.ts b/src/plugins/discover/public/application/angular/doc_table/doc_table.ts index 735ee9f555740..2baf010b47c78 100644 --- a/src/plugins/discover/public/application/angular/doc_table/doc_table.ts +++ b/src/plugins/discover/public/application/angular/doc_table/doc_table.ts @@ -48,6 +48,7 @@ export function createDocTableDirective(pagerFactory: any, $filter: any) { onMoveColumn: '=?', onRemoveColumn: '=?', inspectorAdapters: '=?', + useNewFieldsApi: '<', }, link: ($scope: LazyScope, $el: JQuery) => { $scope.persist = { diff --git a/src/plugins/discover/public/application/angular/helpers/index.ts b/src/plugins/discover/public/application/angular/helpers/index.ts index 9bfba4de966be..cba50dfa58751 100644 --- a/src/plugins/discover/public/application/angular/helpers/index.ts +++ b/src/plugins/discover/public/application/angular/helpers/index.ts @@ -18,3 +18,4 @@ */ export { buildPointSeriesData } from './point_series'; +export { formatRow } from './row_formatter'; diff --git a/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts b/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts new file mode 100644 index 0000000000000..60ee1e4c2b68b --- /dev/null +++ b/src/plugins/discover/public/application/angular/helpers/row_formatter.test.ts @@ -0,0 +1,69 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { formatRow } from './row_formatter'; +import { stubbedSavedObjectIndexPattern } from '../../../__mocks__/stubbed_saved_object_index_pattern'; +import { IndexPattern } from '../../../../../data/common/index_patterns/index_patterns'; +import { fieldFormatsMock } from '../../../../../data/common/field_formats/mocks'; + +describe('Row formatter', () => { + const hit = { + foo: 'bar', + number: 42, + hello: '

    World

    ', + also: 'with "quotes" or \'single quotes\'', + }; + + const createIndexPattern = () => { + const id = 'my-index'; + const { + type, + version, + attributes: { timeFieldName, fields, title }, + } = stubbedSavedObjectIndexPattern(id); + + return new IndexPattern({ + spec: { id, type, version, timeFieldName, fields, title }, + fieldFormats: fieldFormatsMock, + shortDotsEnable: false, + metaFields: [], + }); + }; + + const indexPattern = createIndexPattern(); + + const formatHitReturnValue = { + also: 'with \\"quotes\\" or 'single qoutes'', + number: '42', + foo: 'bar', + hello: '<h1>World</h1>', + }; + const formatHitMock = jest.fn().mockReturnValueOnce(formatHitReturnValue); + + beforeEach(() => { + // @ts-ignore + indexPattern.formatHit = formatHitMock; + }); + + it('formats document properly', () => { + expect(formatRow(hit, indexPattern).trim()).toBe( + '
    also:
    with \\"quotes\\" or 'single qoutes'
    number:
    42
    foo:
    bar
    hello:
    <h1>World</h1>
    ' + ); + }); +}); diff --git a/src/plugins/discover/public/application/angular/helpers/row_formatter.ts b/src/plugins/discover/public/application/angular/helpers/row_formatter.ts new file mode 100644 index 0000000000000..4ad50ef7621c5 --- /dev/null +++ b/src/plugins/discover/public/application/angular/helpers/row_formatter.ts @@ -0,0 +1,47 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { template } from 'lodash'; +import { IndexPattern } from '../../../kibana_services'; + +function noWhiteSpace(html: string) { + const TAGS_WITH_WS = />\s+<'); +} + +const templateHtml = ` +
    + <% defPairs.forEach(function (def) { %> +
    <%- def[0] %>:
    +
    <%= def[1] %>
    + <%= ' ' %> + <% }); %> +
    `; +export const doTemplate = template(noWhiteSpace(templateHtml)); + +export const formatRow = (hit: Record, indexPattern: IndexPattern) => { + const highlights = hit?.highlight ?? {}; + const formatted = indexPattern.formatHit(hit); + const highlightPairs: Array<[string, unknown]> = []; + const sourcePairs: Array<[string, unknown]> = []; + Object.entries(formatted).forEach(([key, val]) => { + const pairs = highlights[key] ? highlightPairs : sourcePairs; + pairs.push([key, val]); + }); + return doTemplate({ defPairs: [...highlightPairs, ...sourcePairs] }); +}; diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx b/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx index f519df8a0b80d..4ace823471c45 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx @@ -48,6 +48,7 @@ export interface ContextAppProps { onChangeSuccessorCount: (count: number) => void; predecessorStatus: string; successorStatus: string; + useNewFieldsApi?: boolean; } const PREDECESSOR_TYPE = 'predecessors'; @@ -87,7 +88,15 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { }; const docTableProps = () => { - const { hits, filter, sorting, columns, indexPattern, minimumVisibleRows } = renderProps; + const { + hits, + filter, + sorting, + columns, + indexPattern, + minimumVisibleRows, + useNewFieldsApi, + } = renderProps; return { columns, indexPattern, @@ -95,6 +104,7 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { rows: hits, onFilter: filter, sort: sorting.map((el) => [el]), + useNewFieldsApi, } as DocTableLegacyProps; }; diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts b/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts index dfb5d90c2befe..e52226bee3785 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts @@ -37,6 +37,7 @@ export function createContextAppLegacy(reactDirective: any) { ['successorAvailable', { watchDepth: 'reference' }], ['successorStatus', { watchDepth: 'reference' }], ['onChangeSuccessorCount', { watchDepth: 'reference' }], + ['useNewFieldsApi', { watchDepth: 'reference' }], ['topNavMenu', { watchDepth: 'reference' }], ]); } diff --git a/src/plugins/discover/public/application/components/create_discover_legacy_directive.ts b/src/plugins/discover/public/application/components/create_discover_legacy_directive.ts index 6e5d47be987d8..fc877cab00e0b 100644 --- a/src/plugins/discover/public/application/components/create_discover_legacy_directive.ts +++ b/src/plugins/discover/public/application/components/create_discover_legacy_directive.ts @@ -51,5 +51,6 @@ export function createDiscoverLegacyDirective(reactDirective: any) { ['topNavMenu', { watchDepth: 'reference' }], ['updateQuery', { watchDepth: 'reference' }], ['updateSavedQueryId', { watchDepth: 'reference' }], + ['useNewFieldsApi', { watchDepth: 'reference' }], ]); } diff --git a/src/plugins/discover/public/application/components/discover_legacy.tsx b/src/plugins/discover/public/application/components/discover_legacy.tsx index 436a145024437..402e686979d67 100644 --- a/src/plugins/discover/public/application/components/discover_legacy.tsx +++ b/src/plugins/discover/public/application/components/discover_legacy.tsx @@ -219,6 +219,7 @@ export interface DiscoverProps { * Function to update the actual savedQuery id */ updateSavedQueryId: (savedQueryId?: string) => void; + useNewFieldsApi?: boolean; } export const DocTableLegacyMemoized = React.memo((props: DocTableLegacyProps) => ( @@ -257,6 +258,7 @@ export function DiscoverLegacy({ topNavMenu, updateQuery, updateSavedQueryId, + useNewFieldsApi, }: DiscoverProps) { const scrollableDesktop = useRef(null); const collapseIcon = useRef(null); @@ -278,6 +280,17 @@ export function DiscoverLegacy({ : undefined; const contentCentered = resultState === 'uninitialized'; + const getDisplayColumns = () => { + if (!state.columns) { + return []; + } + const columns = [...state.columns]; + if (useNewFieldsApi) { + return columns.filter((column) => column !== '_source'); + } + return columns.length === 0 ? ['_source'] : columns; + }; + return ( @@ -315,6 +328,7 @@ export function DiscoverLegacy({ setIndexPattern={setIndexPattern} isClosed={isSidebarClosed} trackUiMetric={trackUiMetric} + useNewFieldsApi={useNewFieldsApi} /> @@ -445,7 +459,7 @@ export function DiscoverLegacy({ {rows && rows.length && (
    {rows.length === opts.sampleSize ? (
    + +
    + +
    + +
    + + + +
    +
    +
    +
    +
    +
    + +`; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.scss b/src/plugins/discover/public/application/components/sidebar/discover_field.scss index 8e1dd41f66ab1..40bc58cef7023 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.scss +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.scss @@ -1,4 +1,10 @@ .dscSidebarItem__fieldPopoverPanel { - min-width: 260px; - max-width: 300px; + min-width: $euiSizeXXL * 6.5; + max-width: $euiSizeXXL * 7.5; +} + +.dscSidebarItem--multi { + .kbnFieldButton__button { + padding-left: 0; + } } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx index 0957ee101bd27..d22ef7cdcc28c 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.test.tsx @@ -82,7 +82,7 @@ function getComponent({ const props = { indexPattern, field: finalField, - getDetails: jest.fn(() => ({ buckets: [], error: '', exists: 1, total: true, columns: [] })), + getDetails: jest.fn(() => ({ buckets: [], error: '', exists: 1, total: 2, columns: [] })), onAddFilter: jest.fn(), onAddField: jest.fn(), onRemoveField: jest.fn(), diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx index f95e512dfb66e..b885bdab316b5 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx @@ -19,7 +19,7 @@ import './discover_field.scss'; import React, { useState } from 'react'; -import { EuiPopover, EuiPopoverTitle, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { EuiPopover, EuiPopoverTitle, EuiButtonIcon, EuiToolTip, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { UiCounterMetricType } from '@kbn/analytics'; import classNames from 'classnames'; @@ -28,6 +28,7 @@ import { FieldIcon, FieldButton } from '../../../../../kibana_react/public'; import { FieldDetails } from './types'; import { IndexPatternField, IndexPattern } from '../../../../../data/public'; import { getFieldTypeName } from './lib/get_field_type_name'; +import { DiscoverFieldDetailsFooter } from './discover_field_details_footer'; export interface DiscoverFieldProps { /** @@ -69,6 +70,8 @@ export interface DiscoverFieldProps { * @param eventName */ trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; + + multiFields?: Array<{ field: IndexPatternField; isSelected: boolean }>; } export function DiscoverField({ @@ -81,6 +84,7 @@ export function DiscoverField({ getDetails, selected, trackUiMetric, + multiFields, }: DiscoverFieldProps) { const addLabelAria = i18n.translate('discover.fieldChooser.discoverField.addButtonAriaLabel', { defaultMessage: 'Add {field} to table', @@ -96,8 +100,8 @@ export function DiscoverField({ const [infoIsOpen, setOpen] = useState(false); - const toggleDisplay = (f: IndexPatternField) => { - if (selected) { + const toggleDisplay = (f: IndexPatternField, isSelected: boolean) => { + if (isSelected) { onRemoveField(f.name); } else { onAddField(f.name); @@ -115,72 +119,100 @@ export function DiscoverField({ return str ? str.replace(/\./g, '.\u200B') : ''; } - const dscFieldIcon = ( - - ); + const getDscFieldIcon = (indexPatternField: IndexPatternField) => { + return ( + + ); + }; - const title = - field.displayName !== field.name ? `${field.name} (${field.displayName} )` : field.displayName; + const dscFieldIcon = getDscFieldIcon(field); + + const getTitle = (indexPatternField: IndexPatternField) => { + return indexPatternField.displayName !== indexPatternField.name + ? i18n.translate('discover.field.title', { + defaultMessage: '{fieldName} ({fieldDisplayName})', + values: { + fieldName: indexPatternField.name, + fieldDisplayName: indexPatternField.displayName, + }, + }) + : indexPatternField.displayName; + }; + + const getFieldName = (indexPatternField: IndexPatternField) => { + return ( + + {wrapOnDot(indexPatternField.displayName)} + + ); + }; + const fieldName = getFieldName(field); - const fieldName = ( - - {wrapOnDot(field.displayName)} - - ); const actionBtnClassName = classNames('dscSidebarItem__action', { ['dscSidebarItem__mobile']: alwaysShowActionButton, }); - let actionButton; - if (field.name !== '_source' && !selected) { - actionButton = ( - - ) => { - if (ev.type === 'click') { - ev.currentTarget.focus(); - } - ev.preventDefault(); - ev.stopPropagation(); - toggleDisplay(field); - }} - data-test-subj={`fieldToggle-${field.name}`} - aria-label={addLabelAria} - /> - - ); - } else if (field.name !== '_source' && selected) { - actionButton = ( - - ) => { - if (ev.type === 'click') { - ev.currentTarget.focus(); - } - ev.preventDefault(); - ev.stopPropagation(); - toggleDisplay(field); - }} - data-test-subj={`fieldToggle-${field.name}`} - aria-label={removeLabelAria} - /> - - ); - } + const getActionButton = (f: IndexPatternField, isSelected?: boolean) => { + if (f.name !== '_source' && !isSelected) { + return ( + + ) => { + if (ev.type === 'click') { + ev.currentTarget.focus(); + } + ev.preventDefault(); + ev.stopPropagation(); + toggleDisplay(f, false); + }} + data-test-subj={`fieldToggle-${f.name}`} + aria-label={addLabelAria} + /> + + ); + } else if (f.name !== '_source' && isSelected) { + return ( + + ) => { + if (ev.type === 'click') { + ev.currentTarget.focus(); + } + ev.preventDefault(); + ev.stopPropagation(); + toggleDisplay(f, isSelected); + }} + data-test-subj={`fieldToggle-${f.name}`} + aria-label={removeLabelAria} + /> + + ); + } + }; + + const actionButton = getActionButton(field, selected); if (field.type === '_source') { return ( @@ -195,6 +227,37 @@ export function DiscoverField({ ); } + const shouldRenderMultiFields = !!multiFields; + const renderMultiFields = () => { + if (!multiFields) { + return null; + } + return ( + + +
    + {i18n.translate('discover.fieldChooser.discoverField.multiFields', { + defaultMessage: 'Multi fields', + })} +
    +
    + {multiFields.map((entry) => ( + {}} + dataTestSubj={`field-${entry.field.name}-showDetails`} + fieldIcon={getDscFieldIcon(entry.field)} + fieldAction={getActionButton(entry.field, entry.isSelected)} + fieldName={getFieldName(entry.field)} + key={entry.field.name} + /> + ))} +
    + ); + }; + return ( - - {' '} - {i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', { - defaultMessage: 'Top 5 values', - })} - + {field.displayName} + +
    + {i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', { + defaultMessage: 'Top 5 values', + })} +
    +
    {infoIsOpen && ( )} + {shouldRenderMultiFields ? ( + <> + {renderMultiFields()} + + + ) : null}
    ); } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx index 0618e53d15dbb..8444f11ac912c 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_details.test.tsx @@ -38,7 +38,7 @@ const indexPattern = getStubIndexPattern( describe('discover sidebar field details', function () { const defaultProps = { indexPattern, - details: { buckets: [], error: '', exists: 1, total: true, columns: [] }, + details: { buckets: [], error: '', exists: 1, total: 2, columns: [] }, onAddFilter: jest.fn(), }; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx index 740de54ae0cf3..bf24337543037 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx @@ -17,7 +17,7 @@ * under the License. */ import React, { useState, useEffect } from 'react'; -import { EuiLink, EuiIconTip, EuiText, EuiPopoverFooter, EuiButton, EuiSpacer } from '@elastic/eui'; +import { EuiIconTip, EuiText, EuiButton, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics'; import { DiscoverFieldBucket } from './discover_field_bucket'; @@ -30,6 +30,7 @@ import { import { Bucket, FieldDetails } from './types'; import { IndexPatternField, IndexPattern } from '../../../../../data/public'; import './discover_field_details.scss'; +import { DiscoverFieldDetailsFooter } from './discover_field_details_footer'; interface DiscoverFieldDetailsProps { field: IndexPatternField; @@ -37,6 +38,7 @@ interface DiscoverFieldDetailsProps { details: FieldDetails; onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; + showFooter?: boolean; } export function DiscoverFieldDetails({ @@ -45,6 +47,7 @@ export function DiscoverFieldDetails({ details, onAddFilter, trackUiMetric, + showFooter = true, }: DiscoverFieldDetailsProps) { const warnings = getWarnings(field); const [showVisualizeLink, setShowVisualizeLink] = useState(false); @@ -118,27 +121,13 @@ export function DiscoverFieldDetails({ )}
    - {!details.error && ( - - - {!indexPattern.metaFields.includes(field.name) && !field.scripted ? ( - onAddFilter('_exists_', field.name, '+')}> - {' '} - {details.exists} - - ) : ( - {details.exists} - )}{' '} - / {details.total}{' '} - - - + {!details.error && showFooter && ( + )} ); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details_footer.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_details_footer.test.tsx new file mode 100644 index 0000000000000..028187569e977 --- /dev/null +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_details_footer.test.tsx @@ -0,0 +1,82 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { findTestSubject } from '@elastic/eui/lib/test'; +// @ts-ignore +import stubbedLogstashFields from 'fixtures/logstash_fields'; +import { mountWithIntl } from '@kbn/test/jest'; +import { coreMock } from '../../../../../../core/public/mocks'; +import { IndexPatternField } from '../../../../../data/public'; +import { getStubIndexPattern } from '../../../../../data/public/test_utils'; +import { DiscoverFieldDetailsFooter } from './discover_field_details_footer'; + +const indexPattern = getStubIndexPattern( + 'logstash-*', + (cfg: any) => cfg, + 'time', + stubbedLogstashFields(), + coreMock.createSetup() +); + +describe('discover sidebar field details footer', function () { + const onAddFilter = jest.fn(); + const defaultProps = { + indexPattern, + details: { buckets: [], error: '', exists: 1, total: 2, columns: [] }, + onAddFilter, + }; + + function mountComponent(field: IndexPatternField) { + const compProps = { ...defaultProps, field }; + return mountWithIntl(); + } + + it('renders properly', function () { + const visualizableField = new IndexPatternField({ + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }); + const component = mountComponent(visualizableField); + expect(component).toMatchSnapshot(); + }); + + it('click on addFilter calls the function', function () { + const visualizableField = new IndexPatternField({ + name: 'bytes', + type: 'number', + esTypes: ['long'], + count: 10, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }); + const component = mountComponent(visualizableField); + const onAddButton = findTestSubject(component, 'onAddFilterButton'); + onAddButton.simulate('click'); + expect(onAddFilter).toHaveBeenCalledWith('_exists_', visualizableField.name, '+'); + }); +}); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details_footer.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_details_footer.tsx new file mode 100644 index 0000000000000..58e91c85913a1 --- /dev/null +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_details_footer.tsx @@ -0,0 +1,70 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { EuiLink, EuiPopoverFooter, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { IndexPatternField } from '../../../../../data/common/index_patterns/fields'; +import { IndexPattern } from '../../../../../data/common/index_patterns/index_patterns'; +import { FieldDetails } from './types'; + +interface DiscoverFieldDetailsFooterProps { + field: IndexPatternField; + indexPattern: IndexPattern; + details: FieldDetails; + onAddFilter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; +} + +export function DiscoverFieldDetailsFooter({ + field, + indexPattern, + details, + onAddFilter, +}: DiscoverFieldDetailsFooterProps) { + return ( + + + {!indexPattern.metaFields.includes(field.name) && !field.scripted ? ( + onAddFilter('_exists_', field.name, '+')} + data-test-subj="onAddFilterButton" + > + + + ) : ( + + )} + + + ); +} diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index 57cc45b3c3e9f..6c312924fb7b7 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -100,6 +100,10 @@ export interface DiscoverSidebarProps { * Callback function to select another index pattern */ setIndexPattern: (id: string) => void; + /** + * If on, fields are read from the fields API, not from source + */ + useNewFieldsApi?: boolean; /** * Metric tracking function * @param metricType @@ -127,9 +131,11 @@ export function DiscoverSidebar({ setFieldFilter, setIndexPattern, trackUiMetric, + useNewFieldsApi = false, useFlyout = false, }: DiscoverSidebarProps) { const [fields, setFields] = useState(null); + useEffect(() => { const newFields = getIndexPatternFieldList(selectedIndexPattern, fieldCounts); setFields(newFields); @@ -154,13 +160,10 @@ export function DiscoverSidebar({ selected: selectedFields, popular: popularFields, unpopular: unpopularFields, - } = useMemo(() => groupFields(fields, columns, popularLimit, fieldCounts, fieldFilter), [ - fields, - columns, - popularLimit, - fieldCounts, - fieldFilter, - ]); + } = useMemo( + () => groupFields(fields, columns, popularLimit, fieldCounts, fieldFilter, useNewFieldsApi), + [fields, columns, popularLimit, fieldCounts, fieldFilter, useNewFieldsApi] + ); const fieldTypes = useMemo(() => { const result = ['any']; @@ -174,6 +177,27 @@ export function DiscoverSidebar({ return result; }, [fields]); + const multiFields = useMemo(() => { + if (!useNewFieldsApi || !fields) { + return undefined; + } + const map = new Map>(); + fields.forEach((field) => { + const parent = field.spec?.subType?.multi?.parent; + if (!parent) { + return; + } + const multiField = { + field, + isSelected: selectedFields.includes(field), + }; + const value = map.get(parent) ?? []; + value.push(multiField); + map.set(parent, value); + }); + return map; + }, [fields, useNewFieldsApi, selectedFields]); + if (!selectedIndexPattern || !fields) { return null; } @@ -278,6 +302,7 @@ export function DiscoverSidebar({ getDetails={getDetailsByField} selected={true} trackUiMetric={trackUiMetric} + multiFields={multiFields?.get(field.name)} /> ); @@ -338,6 +363,7 @@ export function DiscoverSidebar({ onAddFilter={onAddFilter} getDetails={getDetailsByField} trackUiMetric={trackUiMetric} + multiFields={multiFields?.get(field.name)} /> ); @@ -366,6 +392,7 @@ export function DiscoverSidebar({ onAddFilter={onAddFilter} getDetails={getDetailsByField} trackUiMetric={trackUiMetric} + multiFields={multiFields?.get(field.name)} /> ); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx index 0413ebd17d71b..3000291fc23bb 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar_responsive.tsx @@ -103,6 +103,10 @@ export interface DiscoverSidebarResponsiveProps { * Shows index pattern and a button that displays the sidebar in a flyout */ useFlyout?: boolean; + /** + * Read from the Fields API + */ + useNewFieldsApi?: boolean; } /** diff --git a/src/plugins/discover/public/application/components/sidebar/lib/get_index_pattern_field_list.ts b/src/plugins/discover/public/application/components/sidebar/lib/get_index_pattern_field_list.ts index eff7c2ec3c1c8..85f44ef9bff89 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/get_index_pattern_field_list.ts +++ b/src/plugins/discover/public/application/components/sidebar/lib/get_index_pattern_field_list.ts @@ -18,6 +18,7 @@ */ import { difference } from 'lodash'; import { IndexPattern, IndexPatternField } from 'src/plugins/data/public'; +import { isNestedFieldParent } from '../../../helpers/nested_fields'; export function getIndexPatternFieldList( indexPattern?: IndexPattern, @@ -27,15 +28,23 @@ export function getIndexPatternFieldList( const fieldNamesInDocs = Object.keys(fieldCounts); const fieldNamesInIndexPattern = indexPattern.fields.getAll().map((fld) => fld.name); - const unknownTypes: IndexPatternField[] = []; + const unknownFields: IndexPatternField[] = []; difference(fieldNamesInDocs, fieldNamesInIndexPattern).forEach((unknownFieldName) => { - unknownTypes.push({ - displayName: String(unknownFieldName), - name: String(unknownFieldName), - type: 'unknown', - } as IndexPatternField); + if (isNestedFieldParent(unknownFieldName, indexPattern)) { + unknownFields.push({ + displayName: String(unknownFieldName), + name: String(unknownFieldName), + type: 'nested', + } as IndexPatternField); + } else { + unknownFields.push({ + displayName: String(unknownFieldName), + name: String(unknownFieldName), + type: 'unknown', + } as IndexPatternField); + } }); - return [...indexPattern.fields.getAll(), ...unknownTypes]; + return [...indexPattern.fields.getAll(), ...unknownFields]; } 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 22cacae4c3b45..6cbfa03a070db 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 @@ -69,7 +69,8 @@ describe('group_fields', function () { ['currency'], 5, fieldCounts, - fieldFilterState + fieldFilterState, + false ); expect(actual).toMatchInlineSnapshot(` Object { @@ -118,6 +119,80 @@ describe('group_fields', function () { } `); }); + it('should group fields in selected, popular, unpopular group if they contain multifields', function () { + const category = { + name: 'category', + type: 'string', + esTypes: ['text'], + count: 1, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }; + const currency = { + name: 'currency', + displayName: 'currency', + kbnFieldType: { + esTypes: ['string', 'text', 'keyword', '_type', '_id'], + filterable: true, + name: 'string', + sortable: true, + }, + spec: { + esTypes: ['text'], + name: 'category', + }, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: true, + }; + const currencyKeyword = { + name: 'currency.keyword', + displayName: 'currency.keyword', + type: 'string', + esTypes: ['keyword'], + kbnFieldType: { + esTypes: ['string', 'text', 'keyword', '_type', '_id'], + filterable: true, + name: 'string', + sortable: true, + }, + spec: { + aggregatable: true, + esTypes: ['keyword'], + name: 'category.keyword', + readFromDocValues: true, + searchable: true, + shortDotsEnable: false, + subType: { + multi: { + parent: 'currency', + }, + }, + }, + scripted: false, + searchable: true, + aggregatable: true, + readFromDocValues: false, + }; + const fieldsToGroup = [category, currency, currencyKeyword]; + + const fieldFilterState = getDefaultFieldFilter(); + + const actual = groupFields( + fieldsToGroup as any, + ['currency'], + 5, + fieldCounts, + fieldFilterState, + true + ); + expect(actual.popular).toEqual([category]); + expect(actual.selected).toEqual([currency]); + expect(actual.unpopular).toEqual([]); + }); it('should sort selected fields by columns order ', function () { const fieldFilterState = getDefaultFieldFilter(); @@ -127,7 +202,8 @@ describe('group_fields', function () { ['customer_birth_date', 'currency', 'unknown'], 5, fieldCounts, - fieldFilterState + fieldFilterState, + false ); expect(actual1.selected.map((field) => field.name)).toEqual([ 'customer_birth_date', @@ -140,7 +216,8 @@ describe('group_fields', function () { ['currency', 'customer_birth_date', 'unknown'], 5, fieldCounts, - fieldFilterState + fieldFilterState, + false ); expect(actual2.selected.map((field) => field.name)).toEqual([ 'currency', 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 c34becc97cb93..e6c3d0fe3ea42 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 @@ -33,7 +33,8 @@ export function groupFields( columns: string[], popularLimit: number, fieldCounts: Record, - fieldFilterState: FieldFilterState + fieldFilterState: FieldFilterState, + useNewFieldsApi: boolean ): GroupedFields { const result: GroupedFields = { selected: [], @@ -62,12 +63,17 @@ export function groupFields( if (!isFieldFiltered(field, fieldFilterState, fieldCounts)) { continue; } + const isSubfield = useNewFieldsApi && field.spec?.subType?.multi?.parent; if (columns.includes(field.name)) { result.selected.push(field); } else if (popular.includes(field.name) && field.type !== '_source') { - result.popular.push(field); + if (!isSubfield) { + result.popular.push(field); + } } else if (field.type !== '_source') { - result.unpopular.push(field); + if (!isSubfield) { + result.unpopular.push(field); + } } } // add columns, that are not part of the index pattern, to be removeable diff --git a/src/plugins/discover/public/application/components/sidebar/types.ts b/src/plugins/discover/public/application/components/sidebar/types.ts index d80662b65cc7b..4ec731e852ce3 100644 --- a/src/plugins/discover/public/application/components/sidebar/types.ts +++ b/src/plugins/discover/public/application/components/sidebar/types.ts @@ -25,7 +25,7 @@ export interface IndexPatternRef { export interface FieldDetails { error: string; exists: number; - total: boolean; + total: number; buckets: Bucket[]; columns: string[]; } diff --git a/src/plugins/discover/public/application/components/table/table.tsx b/src/plugins/discover/public/application/components/table/table.tsx index 9c136e94a3d2a..2b1f51b8012f1 100644 --- a/src/plugins/discover/public/application/components/table/table.tsx +++ b/src/plugins/discover/public/application/components/table/table.tsx @@ -17,9 +17,9 @@ * under the License. */ import React, { useState } from 'react'; -import { escapeRegExp } from 'lodash'; import { DocViewTableRow } from './table_row'; import { trimAngularSpan } from './table_helper'; +import { isNestedFieldParent } from '../../helpers/nested_fields'; import { DocViewRenderProps } from '../../doc_views/doc_views_types'; const COLLAPSE_LINE_LENGTH = 350; @@ -74,48 +74,9 @@ export function DocViewTable({ : undefined; const displayUnderscoreWarning = !mapping(field) && field.indexOf('_') === 0; - // Discover doesn't flatten arrays of objects, so for documents with an `object` or `nested` field that - // contains an array, Discover will only detect the top level root field. We want to detect when those - // root fields are `nested` so that we can display the proper icon and label. However, those root - // `nested` fields are not a part of the index pattern. Their children are though, and contain nested path - // info. So to detect nested fields we look through the index pattern for nested children - // whose path begins with the current field. There are edge cases where - // this could incorrectly identify a plain `object` field as `nested`. Say we had the following document - // where `foo` is a plain object field and `bar` is a nested field. - // { - // "foo": [ - // { - // "bar": [ - // { - // "baz": "qux" - // } - // ] - // }, - // { - // "bar": [ - // { - // "baz": "qux" - // } - // ] - // } - // ] - // } - // - // The following code will search for `foo`, find it at the beginning of the path to the nested child field - // `foo.bar.baz` and incorrectly mark `foo` as nested. Any time we're searching for the name of a plain object - // field that happens to match a segment of a nested path, we'll get a false positive. - // We're aware of this issue and we'll have to live with - // it in the short term. The long term fix will be to add info about the `nested` and `object` root fields - // to the index pattern, but that has its own complications which you can read more about in the following - // issue: https://github.com/elastic/kibana/issues/54957 - const isNestedField = - !indexPattern.fields.getByName(field) && - !!indexPattern.fields.getAll().find((patternField) => { - // We only want to match a full path segment - const nestedRootRegex = new RegExp(escapeRegExp(field) + '(\\.|$)'); - return nestedRootRegex.test(patternField.subType?.nested?.path ?? ''); - }); - const fieldType = isNestedField ? 'nested' : indexPattern.fields.getByName(field)?.type; + const fieldType = isNestedFieldParent(field, indexPattern) + ? 'nested' + : indexPattern.fields.getByName(field)?.type; return ( { + // We only want to match a full path segment + const nestedRootRegex = new RegExp(escapeRegExp(fieldName) + '(\\.|$)'); + return nestedRootRegex.test(patternField.subType?.nested?.path ?? ''); + }) + ); +} 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 8ec2012b5843e..2f373c34eb17d 100644 --- a/src/plugins/discover/public/application/helpers/persist_saved_search.ts +++ b/src/plugins/discover/public/application/helpers/persist_saved_search.ts @@ -49,6 +49,8 @@ export async function persistSavedSearch( indexPattern, services, sort: state.sort as SortOrder[], + columns: state.columns || [], + useNewFieldsApi: false, }); savedSearch.columns = state.columns || []; 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 91832325432ef..615a414680469 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 @@ -44,8 +44,37 @@ describe('updateSearchSource', () => { } as unknown) as IUiSettingsClient, } as unknown) as DiscoverServices, sort: [] as SortOrder[], + columns: [], + useNewFieldsApi: false, }); expect(result.getField('index')).toEqual(indexPatternMock); expect(result.getField('size')).toEqual(sampleSize); + expect(result.getField('fields')).toBe(undefined); + }); + + test('updates a given search source with the usage of the new fields api', async () => { + const searchSourceMock = createSearchSourceMock({}); + const sampleSize = 250; + const result = updateSearchSource(searchSourceMock, { + indexPattern: indexPatternMock, + services: ({ + data: dataPluginMock.createStartContract(), + uiSettings: ({ + get: (key: string) => { + if (key === SAMPLE_SIZE_SETTING) { + return sampleSize; + } + return false; + }, + } as unknown) as IUiSettingsClient, + } as unknown) as DiscoverServices, + sort: [] as SortOrder[], + columns: [], + useNewFieldsApi: true, + }); + expect(result.getField('index')).toEqual(indexPatternMock); + expect(result.getField('size')).toEqual(sampleSize); + expect(result.getField('fields')).toEqual(['*']); + expect(result.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 324dc8a48457a..46f1c9f626054 100644 --- a/src/plugins/discover/public/application/helpers/update_search_source.ts +++ b/src/plugins/discover/public/application/helpers/update_search_source.ts @@ -31,10 +31,14 @@ export function updateSearchSource( indexPattern, services, sort, + columns, + useNewFieldsApi, }: { indexPattern: IndexPattern; services: DiscoverServices; sort: SortOrder[]; + columns: string[]; + useNewFieldsApi: boolean; } ) { const { uiSettings, data } = services; @@ -50,5 +54,13 @@ export function updateSearchSource( .setField('sort', usedSort) .setField('query', data.query.queryString.getQuery() || null) .setField('filter', data.query.filterManager.getFilters()); + if (useNewFieldsApi) { + searchSource.removeField('fieldsFromSource'); + searchSource.setField('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/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts index 425928385e64a..673f55c78a506 100644 --- a/src/plugins/discover/server/ui_settings.ts +++ b/src/plugins/discover/server/ui_settings.ts @@ -35,6 +35,7 @@ import { CONTEXT_TIE_BREAKER_FIELDS_SETTING, DOC_TABLE_LEGACY, MODIFY_COLUMNS_ON_SWITCH, + SEARCH_FIELDS_FROM_SOURCE, } from '../common'; export const uiSettings: Record = { @@ -198,4 +199,11 @@ export const uiSettings: Record = { name: 'discover:modifyColumnsOnSwitchTitle', }, }, + [SEARCH_FIELDS_FROM_SOURCE]: { + name: 'Read fields from _source', + description: `When enabled will load documents directly from \`_source\`. This is soon going to be deprecated. When disabled, will retrieve fields via the new Fields API in the high-level search service.`, + value: false, + category: ['discover'], + schema: schema.boolean(), + }, }; diff --git a/src/plugins/es_ui_shared/tsconfig.json b/src/plugins/es_ui_shared/tsconfig.json new file mode 100644 index 0000000000000..9bcda2e0614de --- /dev/null +++ b/src/plugins/es_ui_shared/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "__packages_do_not_import__/**/*", + "common/**/*", + "public/**/*", + "server/**/*", + "static/**/*" + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../data/tsconfig.json" }, + ] +} diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index 609022f8a55c0..a1841d8fa3743 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -183,6 +183,7 @@ export class Execution< logDatatable: (name: string, datatable: Datatable) => { inspectorAdapters.tables[name] = datatable; }, + isSyncColorsEnabled: () => execution.params.syncColors, ...(execution.params as any).extraContext, }; } diff --git a/src/plugins/expressions/common/execution/types.ts b/src/plugins/expressions/common/execution/types.ts index a1b25c3802f4b..bac9cda128697 100644 --- a/src/plugins/expressions/common/execution/types.ts +++ b/src/plugins/expressions/common/execution/types.ts @@ -83,6 +83,11 @@ export interface ExecutionContext< type: string, id: string ) => Promise>; + + /** + * Returns the state (true|false) of the sync colors across panels switch. + */ + isSyncColorsEnabled?: () => boolean; } /** diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts index ec1fffe64f102..3adf452ef8bd3 100644 --- a/src/plugins/expressions/common/service/expressions_services.ts +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -70,6 +70,8 @@ export interface ExpressionExecutionParams { searchSessionId?: string; + syncColors?: boolean; + inspectorAdapters?: Adapters; } diff --git a/src/plugins/expressions/public/loader.ts b/src/plugins/expressions/public/loader.ts index 1cf499ce2635a..4db070f88a17f 100644 --- a/src/plugins/expressions/public/loader.ts +++ b/src/plugins/expressions/public/loader.ts @@ -154,6 +154,7 @@ export class ExpressionLoader { inspectorAdapters: params.inspectorAdapters, searchSessionId: params.searchSessionId, debug: params.debug, + syncColors: params.syncColors, }); const prevDataHandler = this.execution; @@ -189,6 +190,7 @@ export class ExpressionLoader { if (params.searchSessionId && this.params) { this.params.searchSessionId = params.searchSessionId; } + this.params.syncColors = params.syncColors; this.params.debug = Boolean(params.debug); this.params.inspectorAdapters = (params.inspectorAdapters || diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 5c018adc0131b..df24882f6eb58 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -143,6 +143,7 @@ export interface ExecutionContext ExecutionContextSearch; getSearchSessionId: () => string | undefined; inspectorAdapters: InspectorAdapters; + isSyncColorsEnabled?: () => boolean; types: Record; variables: Record; } diff --git a/src/plugins/expressions/public/render.ts b/src/plugins/expressions/public/render.ts index e3091b908deca..c57f01dbd902e 100644 --- a/src/plugins/expressions/public/render.ts +++ b/src/plugins/expressions/public/render.ts @@ -110,7 +110,7 @@ export class ExpressionRenderHandler { }; } - render = async (value: any, uiState: any = {}) => { + render = async (value: any, uiState?: any) => { if (!value || typeof value !== 'object') { return this.handleRenderError(new Error('invalid data provided to the expression renderer')); } diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md index 71199560ee0c7..0f5c3c8ae8736 100644 --- a/src/plugins/expressions/server/server.api.md +++ b/src/plugins/expressions/server/server.api.md @@ -141,6 +141,7 @@ export interface ExecutionContext ExecutionContextSearch; getSearchSessionId: () => string | undefined; inspectorAdapters: InspectorAdapters; + isSyncColorsEnabled?: () => boolean; types: Record; variables: Record; } diff --git a/src/plugins/lens_oss/tsconfig.json b/src/plugins/lens_oss/tsconfig.json new file mode 100644 index 0000000000000..d7bbc593fa87b --- /dev/null +++ b/src/plugins/lens_oss/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + "*.ts" + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../visualizations/tsconfig.json" } + ] +} diff --git a/src/plugins/runtime_fields/common/constants.ts b/src/plugins/maps_legacy/public/common/constants/index.ts similarity index 88% rename from src/plugins/runtime_fields/common/constants.ts rename to src/plugins/maps_legacy/public/common/constants/index.ts index 568003508f4bd..8e677942b8bce 100644 --- a/src/plugins/runtime_fields/common/constants.ts +++ b/src/plugins/maps_legacy/public/common/constants/index.ts @@ -17,4 +17,6 @@ * under the License. */ -export const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; +export const TMS_IN_YML_ID = 'TMS in config/kibana.yml'; + +export { ORIGIN } from './origin'; diff --git a/src/plugins/maps_legacy/public/common/types.ts b/src/plugins/maps_legacy/public/common/types.ts new file mode 100644 index 0000000000000..802bed9f4b905 --- /dev/null +++ b/src/plugins/maps_legacy/public/common/types.ts @@ -0,0 +1,34 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { TmsLayer } from '..'; + +export interface WMSOptions { + selectedTmsLayer?: TmsLayer; + enabled: boolean; + url?: string; + options: { + version?: string; + layers?: string; + format: string; + transparent: boolean; + attribution?: string; + styles?: string; + }; +} diff --git a/src/plugins/maps_legacy/public/components/wms_internal_options.tsx b/src/plugins/maps_legacy/public/components/wms_internal_options.tsx index 86c15f10ae55d..b8a325ee539d2 100644 --- a/src/plugins/maps_legacy/public/components/wms_internal_options.tsx +++ b/src/plugins/maps_legacy/public/components/wms_internal_options.tsx @@ -22,7 +22,7 @@ import { EuiLink, EuiSpacer, EuiText, EuiScreenReaderOnly } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { TextInputOption } from '../../../vis_default_editor/public'; -import { WMSOptions } from '../common/types/external_basemap_types'; +import { WMSOptions } from '../common/types'; interface WmsInternalOptions { wms: WMSOptions; diff --git a/src/plugins/maps_legacy/public/components/wms_options.tsx b/src/plugins/maps_legacy/public/components/wms_options.tsx index 79e08478f2155..37ec3fcc90015 100644 --- a/src/plugins/maps_legacy/public/components/wms_options.tsx +++ b/src/plugins/maps_legacy/public/components/wms_options.tsx @@ -23,20 +23,19 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { TmsLayer } from '../index'; import { Vis } from '../../../visualizations/public'; -import { RegionMapVisParams } from '../common/types/region_map_types'; import { SelectOption, SwitchOption } from '../../../vis_default_editor/public'; import { WmsInternalOptions } from './wms_internal_options'; -import { WMSOptions, TileMapVisParams } from '../common/types/external_basemap_types'; +import { WMSOptions } from '../common/types'; -interface Props { - stateParams: TileMapVisParams | RegionMapVisParams; +interface Props { + stateParams: K; setValue: (title: 'wms', options: WMSOptions) => void; vis: Vis; } const mapLayerForOption = ({ id }: TmsLayer) => ({ text: id, value: id }); -function WmsOptions({ stateParams, setValue, vis }: Props) { +function WmsOptions({ stateParams, setValue, vis }: Props) { const { wms } = stateParams; const { tmsLayers } = vis.type.editorConfig.collections; const tmsLayerOptions = useMemo(() => tmsLayers.map(mapLayerForOption), [tmsLayers]); diff --git a/src/plugins/maps_legacy/public/index.ts b/src/plugins/maps_legacy/public/index.ts index 2654ded907cce..5b70d861900f9 100644 --- a/src/plugins/maps_legacy/public/index.ts +++ b/src/plugins/maps_legacy/public/index.ts @@ -17,17 +17,12 @@ * under the License. */ -// @ts-ignore import { PluginInitializerContext } from 'kibana/public'; import { MapsLegacyPlugin } from './plugin'; // @ts-ignore import * as colorUtil from './map/color_util'; // @ts-ignore import { KibanaMapLayer } from './map/kibana_map_layer'; -// @ts-ignore -import { convertToGeoJson } from './map/convert_to_geojson'; -// @ts-ignore -import { getPrecision, geoContains } from './map/decode_geo_hash'; import { VectorLayer, FileLayerField, @@ -46,10 +41,7 @@ export function plugin(initializerContext: PluginInitializerContext) { /** @public */ export { - getPrecision, - geoContains, colorUtil, - convertToGeoJson, IServiceSettings, KibanaMapLayer, VectorLayer, @@ -61,7 +53,7 @@ export { export * from '../common'; export * from './common/types'; -export { ORIGIN } from './common/constants/origin'; +export { ORIGIN, TMS_IN_YML_ID } from './common/constants'; export { WmsOptions } from './components/wms_options'; export { LegacyMapDeprecationMessage } from './components/legacy_map_deprecation_message'; diff --git a/src/plugins/maps_legacy/public/map/base_maps_visualization.js b/src/plugins/maps_legacy/public/map/base_maps_visualization.js index 406dae43c9b5e..39612a1b60240 100644 --- a/src/plugins/maps_legacy/public/map/base_maps_visualization.js +++ b/src/plugins/maps_legacy/public/map/base_maps_visualization.js @@ -34,8 +34,9 @@ export function BaseMapsVisualizationProvider() { * @constructor */ return class BaseMapsVisualization { - constructor(element, vis) { - this.vis = vis; + constructor(element, handlers, initialVisParams) { + this.handlers = handlers; + this._params = initialVisParams; this._container = element; this._kibanaMap = null; this._chartData = null; //reference to data currently on the map. @@ -61,25 +62,31 @@ export function BaseMapsVisualizationProvider() { * @param status * @return {Promise} */ - async render(esResponse, visParams) { + async render(esResponse = this._esResponse, visParams = this._params) { + await this._mapIsLoaded; + if (!this._kibanaMap) { //the visualization has been destroyed; return; } - await this._mapIsLoaded; - this._kibanaMap.resize(); + this.resize(); this._params = visParams; await this._updateParams(); if (this._hasESResponseChanged(esResponse)) { + this._esResponse = esResponse; await this._updateData(esResponse); } - this._kibanaMap.useUiStateFromVisualization(this.vis); + this._kibanaMap.useUiStateFromVisualization(this.handlers.uiState); await this._whenBaseLayerIsLoaded(); } + resize() { + this._kibanaMap?.resize(); + } + /** * Creates an instance of a kibana-map with a single baselayer and assigns it to the this._kibanaMap property. * Clients can override this method to customize the initialization. @@ -87,11 +94,11 @@ export function BaseMapsVisualizationProvider() { */ async _makeKibanaMap() { const options = {}; - const uiState = this.vis.getUiState(); - const zoomFromUiState = parseInt(uiState.get('mapZoom')); - const centerFromUIState = uiState.get('mapCenter'); - options.zoom = !isNaN(zoomFromUiState) ? zoomFromUiState : this.vis.params.mapZoom; - options.center = centerFromUIState ? centerFromUIState : this.vis.params.mapCenter; + const zoomFromUiState = parseInt(this.handlers.uiState?.get('mapZoom')); + const centerFromUIState = this.handlers.uiState?.get('mapCenter'); + const { mapZoom, mapCenter } = this._getMapsParams(); + options.zoom = !isNaN(zoomFromUiState) ? zoomFromUiState : mapZoom; + options.center = centerFromUIState ? centerFromUIState : mapCenter; const modules = await lazyLoadMapsLegacyModules(); this._kibanaMap = new modules.KibanaMap(this._container, options); @@ -100,7 +107,7 @@ export function BaseMapsVisualizationProvider() { this._kibanaMap.addLegendControl(); this._kibanaMap.addFitControl(); - this._kibanaMap.persistUiStateForVisualization(this.vis); + this._kibanaMap.persistUiStateForVisualization(this.handlers.uiState); this._kibanaMap.on('baseLayer:loaded', () => { this._baseLayerDirty = false; @@ -212,7 +219,7 @@ export function BaseMapsVisualizationProvider() { } _hasESResponseChanged(data) { - return this._chartData !== data; + return this._esResponse !== data; } /** @@ -223,15 +230,11 @@ export function BaseMapsVisualizationProvider() { await this._updateBaseLayer(); this._kibanaMap.setLegendPosition(mapParams.legendPosition); this._kibanaMap.setShowTooltip(mapParams.addTooltip); - this._kibanaMap.useUiStateFromVisualization(this.vis); + this._kibanaMap.useUiStateFromVisualization(this.handlers.uiState); } _getMapsParams() { - return { - ...this.vis.type.visConfig.defaults, - type: this.vis.type.name, - ...this._params, - }; + return this._params; } _whenBaseLayerIsLoaded() { diff --git a/src/plugins/maps_legacy/public/map/geohash_columns.test.ts b/src/plugins/maps_legacy/public/map/geohash_columns.test.ts new file mode 100644 index 0000000000000..e2a8eb575a9e2 --- /dev/null +++ b/src/plugins/maps_legacy/public/map/geohash_columns.test.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { geohashColumns } from './geohash_columns'; + +test('geohashColumns', () => { + expect(geohashColumns(1)).toBe(8); + expect(geohashColumns(2)).toBe(8 * 4); + expect(geohashColumns(3)).toBe(8 * 4 * 8); + expect(geohashColumns(4)).toBe(8 * 4 * 8 * 4); +}); diff --git a/src/plugins/maps_legacy/public/map/geohash_columns.ts b/src/plugins/maps_legacy/public/map/geohash_columns.ts new file mode 100644 index 0000000000000..db592306dc705 --- /dev/null +++ b/src/plugins/maps_legacy/public/map/geohash_columns.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export function geohashColumns(precision: number): number { + return geohashCells(precision, 0); +} + +/** + * Get the number of geohash cells for a given precision + * + * @param {number} precision the geohash precision (1<=precision<=12). + * @param {number} axis constant for the axis 0=lengthwise (ie. columns, along longitude), 1=heightwise (ie. rows, along latitude). + * @returns {number} Number of geohash cells (rows or columns) at that precision + */ +function geohashCells(precision: number, axis: number) { + let cells = 1; + for (let i = 1; i <= precision; i += 1) { + /* On odd precisions, rows divide by 4 and columns by 8. Vice-versa on even precisions */ + cells *= i % 2 === axis ? 4 : 8; + } + return cells; +} diff --git a/src/plugins/maps_legacy/public/map/kibana_map.js b/src/plugins/maps_legacy/public/map/kibana_map.js index 3948692e55676..074c11e77b8a7 100644 --- a/src/plugins/maps_legacy/public/map/kibana_map.js +++ b/src/plugins/maps_legacy/public/map/kibana_map.js @@ -672,14 +672,13 @@ export class KibanaMap extends EventEmitter { } } - persistUiStateForVisualization(visualization) { + persistUiStateForVisualization(uiState) { function persistMapStateInUiState() { - const uiState = visualization.getUiState(); const centerFromUIState = uiState.get('mapCenter'); const zoomFromUiState = parseInt(uiState.get('mapZoom')); if (isNaN(zoomFromUiState) || this.getZoomLevel() !== zoomFromUiState) { - visualization.uiStateVal('mapZoom', this.getZoomLevel()); + uiState.set('mapZoom', this.getZoomLevel()); } const centerFromMap = this.getCenter(); if ( @@ -687,24 +686,17 @@ export class KibanaMap extends EventEmitter { centerFromMap.lon !== centerFromUIState[1] || centerFromMap.lat !== centerFromUIState[0] ) { - visualization.uiStateVal('mapCenter', [centerFromMap.lat, centerFromMap.lon]); + uiState.set('mapCenter', [centerFromMap.lat, centerFromMap.lon]); } } - this._leafletMap.on('resize', () => { - visualization.sessionState.mapBounds = this.getBounds(); - }); - this._leafletMap.on('load', () => { - visualization.sessionState.mapBounds = this.getBounds(); - }); this.on('dragend', persistMapStateInUiState); this.on('zoomend', persistMapStateInUiState); } - useUiStateFromVisualization(visualization) { - const uiState = visualization.getUiState(); - const zoomFromUiState = parseInt(uiState.get('mapZoom')); - const centerFromUIState = uiState.get('mapCenter'); + useUiStateFromVisualization(uiState) { + const zoomFromUiState = parseInt(uiState?.get('mapZoom')); + const centerFromUIState = uiState?.get('mapCenter'); if (!isNaN(zoomFromUiState)) { this.setZoomLevel(zoomFromUiState); } diff --git a/src/plugins/maps_legacy/public/map/precision.ts b/src/plugins/maps_legacy/public/map/precision.ts index a1b3b72f201a4..3dafd0813a174 100644 --- a/src/plugins/maps_legacy/public/map/precision.ts +++ b/src/plugins/maps_legacy/public/map/precision.ts @@ -19,7 +19,7 @@ // @ts-ignore import { getUiSettings } from '../kibana_services'; -import { geohashColumns } from './decode_geo_hash'; +import { geohashColumns } from './geohash_columns'; /** * Get the number of geohash columns (world-wide) for a given precision diff --git a/src/plugins/maps_legacy/public/map/service_settings.js b/src/plugins/maps_legacy/public/map/service_settings.js index 7a00456b89a92..8f0cb81576b2e 100644 --- a/src/plugins/maps_legacy/public/map/service_settings.js +++ b/src/plugins/maps_legacy/public/map/service_settings.js @@ -22,9 +22,7 @@ import MarkdownIt from 'markdown-it'; import { EMSClient } from '@elastic/ems-client'; import { i18n } from '@kbn/i18n'; import { getKibanaVersion } from '../kibana_services'; -import { ORIGIN } from '../common/constants/origin'; - -const TMS_IN_YML_ID = 'TMS in config/kibana.yml'; +import { ORIGIN, TMS_IN_YML_ID } from '../common/constants'; export class ServiceSettings { constructor(mapConfig, tilemapsConfig) { diff --git a/src/plugins/maps_legacy/public/map/zoom_to_precision.ts b/src/plugins/maps_legacy/public/map/zoom_to_precision.ts index 552c509590286..abad0a730a12d 100644 --- a/src/plugins/maps_legacy/public/map/zoom_to_precision.ts +++ b/src/plugins/maps_legacy/public/map/zoom_to_precision.ts @@ -17,7 +17,7 @@ * under the License. */ -import { geohashColumns } from './decode_geo_hash'; +import { geohashColumns } from './geohash_columns'; const defaultMaxPrecision = 12; const minGeoHashPixels = 16; diff --git a/src/plugins/region_map/public/__snapshots__/region_map_fn.test.js.snap b/src/plugins/region_map/public/__snapshots__/region_map_fn.test.ts.snap similarity index 93% rename from src/plugins/region_map/public/__snapshots__/region_map_fn.test.js.snap rename to src/plugins/region_map/public/__snapshots__/region_map_fn.test.ts.snap index cb12712ae824f..df72e75f5ad6b 100644 --- a/src/plugins/region_map/public/__snapshots__/region_map_fn.test.js.snap +++ b/src/plugins/region_map/public/__snapshots__/region_map_fn.test.ts.snap @@ -2,12 +2,9 @@ exports[`interpreter/functions#regionmap returns an object with the correct structure 1`] = ` Object { - "as": "visualization", + "as": "region_map_vis", "type": "render", "value": Object { - "params": Object { - "listenOnChange": true, - }, "visConfig": Object { "addTooltip": true, "colorSchema": "Yellow to Red", diff --git a/src/plugins/region_map/public/components/index.tsx b/src/plugins/region_map/public/components/index.tsx new file mode 100644 index 0000000000000..871bcde41400b --- /dev/null +++ b/src/plugins/region_map/public/components/index.tsx @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { lazy } from 'react'; +import { IServiceSettings } from 'src/plugins/maps_legacy/public'; +import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; +import { RegionMapVisParams } from '../region_map_types'; + +const RegionMapOptions = lazy(() => import('./region_map_options')); + +export const createRegionMapOptions = (getServiceSettings: () => Promise) => ( + props: VisOptionsProps +) => ; diff --git a/src/plugins/region_map/public/components/region_map_options.tsx b/src/plugins/region_map/public/components/region_map_options.tsx index b2bb250d66ee2..497d75853ceb4 100644 --- a/src/plugins/region_map/public/components/region_map_options.tsx +++ b/src/plugins/region_map/public/components/region_map_options.tsx @@ -24,7 +24,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; import { FileLayerField, VectorLayer, IServiceSettings } from '../../../maps_legacy/public'; import { SelectOption, SwitchOption, NumberInputOption } from '../../../vis_default_editor/public'; -import { RegionMapVisParams, WmsOptions } from '../../../maps_legacy/public'; +import { WmsOptions } from '../../../maps_legacy/public'; +import { RegionMapVisParams } from '../region_map_types'; const mapLayerForOption = ({ layerId, name }: VectorLayer) => ({ text: name, @@ -212,4 +213,6 @@ function RegionMapOptions(props: RegionMapOptionsProps) { ); } -export { RegionMapOptions }; +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { RegionMapOptions as default }; diff --git a/src/plugins/region_map/public/plugin.ts b/src/plugins/region_map/public/plugin.ts index e9978803ad5e2..6bb98858ad124 100644 --- a/src/plugins/region_map/public/plugin.ts +++ b/src/plugins/region_map/public/plugin.ts @@ -44,9 +44,10 @@ import { RegionMapsConfigType } from './index'; import { MapsLegacyConfig } from '../../maps_legacy/config'; import { KibanaLegacyStart } from '../../kibana_legacy/public'; import { SharePluginStart } from '../../share/public'; +import { getRegionMapRenderer } from './region_map_renderer'; /** @private */ -interface RegionMapVisualizationDependencies { +export interface RegionMapVisualizationDependencies { uiSettings: IUiSettingsClient; regionmapsConfig: RegionMapsConfig; getServiceSettings: () => Promise; @@ -107,6 +108,7 @@ export class RegionMapPlugin implements Plugin { }; it('returns an object with the correct structure', () => { - const actual = fn( - context, - { visConfig: JSON.stringify(visConfig) }, - { logDatatable: jest.fn() } - ); + const actual = fn(context, { visConfig: JSON.stringify(visConfig) }); expect(actual).toMatchSnapshot(); }); }); diff --git a/src/plugins/region_map/public/region_map_fn.js b/src/plugins/region_map/public/region_map_fn.ts similarity index 67% rename from src/plugins/region_map/public/region_map_fn.js rename to src/plugins/region_map/public/region_map_fn.ts index cc99a5595d096..74f41cf647b5c 100644 --- a/src/plugins/region_map/public/region_map_fn.js +++ b/src/plugins/region_map/public/region_map_fn.ts @@ -19,7 +19,27 @@ import { i18n } from '@kbn/i18n'; -export const createRegionMapFn = () => ({ +import type { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public'; +import { RegionMapVisConfig } from './region_map_types'; + +interface Arguments { + visConfig: string | null; +} + +export interface RegionMapVisRenderValue { + visData: Datatable; + visType: 'region_map'; + visConfig: RegionMapVisConfig; +} + +export type RegionMapExpressionFunctionDefinition = ExpressionFunctionDefinition< + 'regionmap', + Datatable, + Arguments, + Render +>; + +export const createRegionMapFn = (): RegionMapExpressionFunctionDefinition => ({ name: 'regionmap', type: 'render', context: { @@ -32,24 +52,22 @@ export const createRegionMapFn = () => ({ visConfig: { types: ['string', 'null'], default: '"{}"', + help: '', }, }, fn(context, args, handlers) { - const visConfig = JSON.parse(args.visConfig); + const visConfig = args.visConfig && JSON.parse(args.visConfig); if (handlers?.inspectorAdapters?.tables) { handlers.inspectorAdapters.tables.logDatatable('default', context); } return { type: 'render', - as: 'visualization', + as: 'region_map_vis', value: { visData: context, visType: 'region_map', visConfig, - params: { - listenOnChange: true, - }, }, }; }, diff --git a/src/plugins/region_map/public/region_map_renderer.tsx b/src/plugins/region_map/public/region_map_renderer.tsx new file mode 100644 index 0000000000000..f0597389b3d0e --- /dev/null +++ b/src/plugins/region_map/public/region_map_renderer.tsx @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { lazy } from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; + +import { ExpressionRenderDefinition } from 'src/plugins/expressions'; +import { VisualizationContainer } from '../../visualizations/public'; +import { RegionMapVisualizationDependencies } from './plugin'; +import { RegionMapVisRenderValue } from './region_map_fn'; + +const RegionMapVisualization = lazy(() => import('./region_map_visualization_component')); + +export const getRegionMapRenderer: ( + deps: RegionMapVisualizationDependencies +) => ExpressionRenderDefinition = (deps) => ({ + name: 'region_map_vis', + reuseDomNode: true, + render: async (domNode, { visConfig, visData }, handlers) => { + handlers.onDestroy(() => { + unmountComponentAtNode(domNode); + }); + + render( + + + , + domNode + ); + }, +}); diff --git a/src/plugins/region_map/public/region_map_type.js b/src/plugins/region_map/public/region_map_type.ts similarity index 87% rename from src/plugins/region_map/public/region_map_type.js rename to src/plugins/region_map/public/region_map_type.ts index e7a339a6cc8b6..1603cfc1e2c0c 100644 --- a/src/plugins/region_map/public/region_map_type.js +++ b/src/plugins/region_map/public/region_map_type.ts @@ -16,19 +16,24 @@ * specific language governing permissions and limitations * under the License. */ -import React from 'react'; import { i18n } from '@kbn/i18n'; -import { mapToLayerWithId } from './util'; -import { createRegionMapVisualization } from './region_map_visualization'; -import { RegionMapOptions } from './components/region_map_options'; + +import { BaseVisTypeOptions } from '../../visualizations/public'; import { truncatedColorSchemas } from '../../charts/public'; import { ORIGIN } from '../../maps_legacy/public'; -import { getDeprecationMessage } from './get_deprecation_message'; -export function createRegionMapTypeDefinition(dependencies) { - const { uiSettings, regionmapsConfig, getServiceSettings } = dependencies; - const visualization = createRegionMapVisualization(dependencies); +import { getDeprecationMessage } from './get_deprecation_message'; +import { RegionMapVisualizationDependencies } from './plugin'; +import { createRegionMapOptions } from './components'; +import { toExpressionAst } from './to_ast'; +import { RegionMapVisParams } from './region_map_types'; +import { mapToLayerWithId } from './util'; +export function createRegionMapTypeDefinition({ + uiSettings, + regionmapsConfig, + getServiceSettings, +}: RegionMapVisualizationDependencies): BaseVisTypeOptions { return { name: 'region_map', getInfoMessage: getDeprecationMessage, @@ -50,14 +55,11 @@ provided base maps, or add your own. Darker colors represent higher values.', mapZoom: 2, mapCenter: [0, 0], outlineWeight: 1, - showAllShapes: true, //still under consideration + showAllShapes: true, // still under consideration }, }, - visualization, editorConfig: { - optionsTemplate: (props) => ( - - ), + optionsTemplate: createRegionMapOptions(getServiceSettings), collections: { colorSchemas: truncatedColorSchemas, vectorLayers: [], @@ -99,6 +101,7 @@ provided base maps, or add your own. Darker colors represent higher values.', }, ], }, + toExpressionAst, setup: async (vis) => { const serviceSettings = await getServiceSettings(); const tmsLayers = await serviceSettings.getTMSServices(); @@ -111,7 +114,7 @@ provided base maps, or add your own. Darker colors represent higher values.', mapToLayerWithId.bind(null, ORIGIN.KIBANA_YML) ); let selectedLayer = vectorLayers[0]; - let selectedJoinField = selectedLayer ? selectedLayer.fields[0] : null; + let selectedJoinField = selectedLayer ? selectedLayer.fields[0] : undefined; if (regionmapsConfig.includeElasticMapsService) { const layers = await serviceSettings.getFileLayers(); const newLayers = layers @@ -132,7 +135,7 @@ provided base maps, or add your own. Darker colors represent higher values.', vis.type.editorConfig.collections.vectorLayers = [...vectorLayers, ...newLayers]; [selectedLayer] = vis.type.editorConfig.collections.vectorLayers; - selectedJoinField = selectedLayer ? selectedLayer.fields[0] : null; + selectedJoinField = selectedLayer ? selectedLayer.fields[0] : undefined; if (selectedLayer && !vis.params.selectedLayer && selectedLayer.isEMS) { vis.params.emsHotLink = await serviceSettings.getEMSHotLink(selectedLayer); diff --git a/src/plugins/maps_legacy/public/common/types/region_map_types.ts b/src/plugins/region_map/public/region_map_types.ts similarity index 81% rename from src/plugins/maps_legacy/public/common/types/region_map_types.ts rename to src/plugins/region_map/public/region_map_types.ts index 0da597068f11e..044872be7a7ca 100644 --- a/src/plugins/maps_legacy/public/common/types/region_map_types.ts +++ b/src/plugins/region_map/public/region_map_types.ts @@ -17,8 +17,8 @@ * under the License. */ -import { VectorLayer, FileLayerField } from '../../index'; -import { WMSOptions } from './external_basemap_types'; +import { SchemaConfig } from 'src/plugins/visualizations/public'; +import { VectorLayer, FileLayerField, WMSOptions } from '../../maps_legacy/public/index'; export interface RegionMapVisParams { readonly addTooltip: true; @@ -34,3 +34,8 @@ export interface RegionMapVisParams { selectedJoinField?: FileLayerField; wms: WMSOptions; } + +export interface RegionMapVisConfig extends RegionMapVisParams { + metric: SchemaConfig; + bucket?: SchemaConfig; +} diff --git a/src/plugins/region_map/public/region_map_visualization.js b/src/plugins/region_map/public/region_map_visualization.js index ecb3541827cb3..4dfc6ead79449 100644 --- a/src/plugins/region_map/public/region_map_visualization.js +++ b/src/plugins/region_map/public/region_map_visualization.js @@ -30,9 +30,8 @@ export function createRegionMapVisualization({ getServiceSettings, }) { return class RegionMapsVisualization extends BaseMapsVisualization { - constructor(container, vis) { - super(container, vis); - this._vis = this.vis; + constructor(container, handlers, initialVisParams) { + super(container, handlers, initialVisParams); this._choroplethLayer = null; this._tooltipFormatter = mapTooltipProvider(container, tooltipFormatter); } @@ -88,7 +87,7 @@ export function createRegionMapVisualization({ ); } - this._kibanaMap.useUiStateFromVisualization(this._vis); + this._kibanaMap.useUiStateFromVisualization(this.handlers.uiState); } async _loadConfig(fileLayerConfig) { @@ -201,11 +200,18 @@ export function createRegionMapVisualization({ this._choroplethLayer.on('select', (event) => { const { rows, columns } = this._chartData; const rowIndex = rows.findIndex((row) => row[columns[0].id] === event); - this._vis.API.events.filter({ - table: this._chartData, - column: 0, - row: rowIndex, - value: event, + this.handlers.event({ + name: 'filterBucket', + data: { + data: [ + { + table: this._chartData, + column: 0, + row: rowIndex, + value: event, + }, + ], + }, }); }); diff --git a/src/plugins/region_map/public/region_map_visualization.scss b/src/plugins/region_map/public/region_map_visualization.scss new file mode 100644 index 0000000000000..ee593e2fc9c8c --- /dev/null +++ b/src/plugins/region_map/public/region_map_visualization.scss @@ -0,0 +1,4 @@ +.rgmChart__wrapper, .rgmChart { + flex: 1 1 0; + display: flex; +} diff --git a/src/plugins/region_map/public/region_map_visualization_component.tsx b/src/plugins/region_map/public/region_map_visualization_component.tsx new file mode 100644 index 0000000000000..7be210f953bf9 --- /dev/null +++ b/src/plugins/region_map/public/region_map_visualization_component.tsx @@ -0,0 +1,103 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect, useMemo, useRef } from 'react'; +import { EuiResizeObserver } from '@elastic/eui'; +import { throttle } from 'lodash'; + +import { IInterpreterRenderHandlers, Datatable } from 'src/plugins/expressions'; +import { PersistedState } from 'src/plugins/visualizations/public'; +import { RegionMapVisualizationDependencies } from './plugin'; +import { RegionMapVisConfig } from './region_map_types'; +// @ts-expect-error +import { createRegionMapVisualization } from './region_map_visualization'; + +import './region_map_visualization.scss'; + +interface RegionMapVisController { + render(visData?: Datatable, visConfig?: RegionMapVisConfig): Promise; + resize(): void; + destroy(): void; +} + +interface TileMapVisualizationProps { + deps: RegionMapVisualizationDependencies; + handlers: IInterpreterRenderHandlers; + visData: Datatable; + visConfig: RegionMapVisConfig; +} + +const RegionMapVisualization = ({ + deps, + handlers, + visData, + visConfig, +}: TileMapVisualizationProps) => { + const chartDiv = useRef(null); + const visController = useRef(null); + const isFirstRender = useRef(true); + const uiState = handlers.uiState as PersistedState | undefined; + + useEffect(() => { + if (chartDiv.current && isFirstRender.current) { + isFirstRender.current = false; + const Controller = createRegionMapVisualization(deps); + visController.current = new Controller(chartDiv.current, handlers, visConfig); + } + }, [deps, handlers, visConfig, visData]); + + useEffect(() => { + visController.current?.render(visData, visConfig).then(handlers.done); + }, [visData, visConfig, handlers.done]); + + useEffect(() => { + const onUiStateChange = () => { + visController.current?.render().then(handlers.done); + }; + + uiState?.on('change', onUiStateChange); + + return () => { + uiState?.off('change', onUiStateChange); + }; + }, [uiState, handlers.done]); + + useEffect(() => { + return () => { + visController.current?.destroy(); + visController.current = null; + }; + }, []); + + const updateChartSize = useMemo(() => throttle(() => visController.current?.resize(), 300), []); + + return ( + + {(resizeRef) => ( +
    +
    +
    + )} + + ); +}; + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { RegionMapVisualization as default }; diff --git a/src/plugins/region_map/public/to_ast.ts b/src/plugins/region_map/public/to_ast.ts new file mode 100644 index 0000000000000..90dc6583732b3 --- /dev/null +++ b/src/plugins/region_map/public/to_ast.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + EsaggsExpressionFunctionDefinition, + IndexPatternLoadExpressionFunctionDefinition, +} from '../../data/public'; +import { buildExpression, buildExpressionFunction } from '../../expressions/public'; +import { getVisSchemas, Vis, BuildPipelineParams } from '../../visualizations/public'; +import { RegionMapExpressionFunctionDefinition } from './region_map_fn'; +import { RegionMapVisConfig, RegionMapVisParams } from './region_map_types'; + +export const toExpressionAst = (vis: Vis, params: BuildPipelineParams) => { + const esaggs = buildExpressionFunction('esaggs', { + index: buildExpression([ + buildExpressionFunction('indexPatternLoad', { + id: vis.data.indexPattern!.id!, + }), + ]), + metricsAtAllLevels: false, + partialRows: false, + aggs: vis.data.aggs!.aggs.map((agg) => buildExpression(agg.toExpressionAst())), + }); + + const schemas = getVisSchemas(vis, params); + + const visConfig: RegionMapVisConfig = { + ...vis.params, + metric: schemas.metric[0], + }; + + if (schemas.segment) { + visConfig.bucket = schemas.segment[0]; + } + + const regionmap = buildExpressionFunction('regionmap', { + visConfig: JSON.stringify(visConfig), + }); + + const ast = buildExpression([esaggs, regionmap]); + + return ast.toAst(); +}; diff --git a/src/plugins/runtime_fields/README.mdx b/src/plugins/runtime_fields/README.mdx deleted file mode 100644 index 15985b07caf96..0000000000000 --- a/src/plugins/runtime_fields/README.mdx +++ /dev/null @@ -1,4 +0,0 @@ - -# Runtime Fields - -The runtime fields plugin provides types and constants for OSS and xpack runtime field related code. diff --git a/src/plugins/runtime_fields/kibana.json b/src/plugins/runtime_fields/kibana.json deleted file mode 100644 index e71116f81532e..0000000000000 --- a/src/plugins/runtime_fields/kibana.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "id": "runtimeFields", - "version": "kibana", - "server": false, - "ui": true -} diff --git a/src/plugins/saved_objects_management/tsconfig.json b/src/plugins/saved_objects_management/tsconfig.json new file mode 100644 index 0000000000000..eb047c346714c --- /dev/null +++ b/src/plugins/saved_objects_management/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../dashboard/tsconfig.json" }, + { "path": "../data/tsconfig.json" }, + { "path": "../discover/tsconfig.json" }, + { "path": "../home/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + { "path": "../management/tsconfig.json" }, + { "path": "../visualizations/tsconfig.json" }, + ] +} diff --git a/src/plugins/maps_legacy/public/common/types/index.ts b/src/plugins/security_oss/common/app_state.ts similarity index 77% rename from src/plugins/maps_legacy/public/common/types/index.ts rename to src/plugins/security_oss/common/app_state.ts index e6cabdde82cd9..e11ca5bd6294e 100644 --- a/src/plugins/maps_legacy/public/common/types/index.ts +++ b/src/plugins/security_oss/common/app_state.ts @@ -18,9 +18,12 @@ */ /** - * Use * syntax so that these exports do not break when internal - * types are stripped. + * Defines Security OSS application state. */ -export * from './external_basemap_types'; -export * from './map_types'; -export * from './region_map_types'; +export interface AppState { + insecureClusterAlert: { displayAlert: boolean }; + anonymousAccess: { + isEnabled: boolean; + accessURLParameters: Record | null; + }; +} diff --git a/src/plugins/vis_type_timeseries/server/validation_telemetry/index.ts b/src/plugins/security_oss/common/index.ts similarity index 94% rename from src/plugins/vis_type_timeseries/server/validation_telemetry/index.ts rename to src/plugins/security_oss/common/index.ts index 140f61fa2f3fd..f20d7dfd5e062 100644 --- a/src/plugins/vis_type_timeseries/server/validation_telemetry/index.ts +++ b/src/plugins/security_oss/common/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export * from './validation_telemetry_service'; +export type { AppState } from './app_state'; diff --git a/src/plugins/security_oss/public/app_state/app_state_service.mock.ts b/src/plugins/security_oss/public/app_state/app_state_service.mock.ts new file mode 100644 index 0000000000000..6eb628dd04b7e --- /dev/null +++ b/src/plugins/security_oss/public/app_state/app_state_service.mock.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import type { AppState } from '../../common'; +import type { AppStateServiceStart } from './app_state_service'; + +export const mockAppStateService = { + createStart: (): jest.Mocked => { + return { getState: jest.fn() }; + }, + createAppState: (appState: Partial = {}) => ({ + insecureClusterAlert: { displayAlert: false }, + anonymousAccess: { isEnabled: false, accessURLParameters: null }, + ...appState, + }), +}; diff --git a/src/plugins/security_oss/public/app_state/app_state_service.test.ts b/src/plugins/security_oss/public/app_state/app_state_service.test.ts new file mode 100644 index 0000000000000..c498405b90f8b --- /dev/null +++ b/src/plugins/security_oss/public/app_state/app_state_service.test.ts @@ -0,0 +1,72 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { coreMock } from '../../../../core/public/mocks'; +import { AppStateService } from './app_state_service'; + +describe('AppStateService', () => { + describe('#start', () => { + it('returns default state for the anonymous routes', async () => { + const coreStart = coreMock.createStart(); + coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(true); + + const appStateService = new AppStateService(); + await expect(appStateService.start({ core: coreStart }).getState()).resolves.toEqual({ + insecureClusterAlert: { displayAlert: false }, + anonymousAccess: { isEnabled: false, accessURLParameters: null }, + }); + + expect(coreStart.http.get).not.toHaveBeenCalled(); + }); + + it('returns default state if current state cannot be retrieved', async () => { + const coreStart = coreMock.createStart(); + coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(false); + + const failureReason = new Error('Uh oh.'); + coreStart.http.get.mockRejectedValue(failureReason); + + const appStateService = new AppStateService(); + await expect(appStateService.start({ core: coreStart }).getState()).resolves.toEqual({ + insecureClusterAlert: { displayAlert: false }, + anonymousAccess: { isEnabled: false, accessURLParameters: null }, + }); + + expect(coreStart.http.get).toHaveBeenCalledTimes(1); + expect(coreStart.http.get).toHaveBeenCalledWith('/internal/security_oss/app_state'); + }); + + it('returns retrieved state', async () => { + const coreStart = coreMock.createStart(); + coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(false); + + const state = { + insecureClusterAlert: { displayAlert: true }, + anonymousAccess: { isEnabled: true, accessURLParameters: { hint: 'some-hint' } }, + }; + coreStart.http.get.mockResolvedValue(state); + + const appStateService = new AppStateService(); + await expect(appStateService.start({ core: coreStart }).getState()).resolves.toEqual(state); + + expect(coreStart.http.get).toHaveBeenCalledTimes(1); + expect(coreStart.http.get).toHaveBeenCalledWith('/internal/security_oss/app_state'); + }); + }); +}); diff --git a/src/plugins/security_oss/public/app_state/app_state_service.ts b/src/plugins/security_oss/public/app_state/app_state_service.ts new file mode 100644 index 0000000000000..603073a66b0a0 --- /dev/null +++ b/src/plugins/security_oss/public/app_state/app_state_service.ts @@ -0,0 +1,47 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreStart } from 'kibana/public'; +import { AppState } from '../../common'; + +const DEFAULT_APP_STATE = Object.freeze({ + insecureClusterAlert: { displayAlert: false }, + anonymousAccess: { isEnabled: false, accessURLParameters: null }, +}); + +interface StartDeps { + core: Pick; +} + +export interface AppStateServiceStart { + getState: () => Promise; +} + +/** + * Service that allows to retrieve application state. + */ +export class AppStateService { + start({ core }: StartDeps): AppStateServiceStart { + const appStatePromise = core.http.anonymousPaths.isAnonymous(window.location.pathname) + ? Promise.resolve(DEFAULT_APP_STATE) + : core.http.get('/internal/security_oss/app_state').catch(() => DEFAULT_APP_STATE); + + return { getState: () => appStatePromise }; + } +} diff --git a/src/plugins/runtime_fields/common/index.ts b/src/plugins/security_oss/public/app_state/index.ts similarity index 91% rename from src/plugins/runtime_fields/common/index.ts rename to src/plugins/security_oss/public/app_state/index.ts index b08ac661a4bd6..d2f15560ec634 100644 --- a/src/plugins/runtime_fields/common/index.ts +++ b/src/plugins/security_oss/public/app_state/index.ts @@ -17,5 +17,4 @@ * under the License. */ -export * from './constants'; -export * from './types'; +export { AppStateService, AppStateServiceStart } from './app_state_service'; diff --git a/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.test.tsx b/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.test.tsx index 7bd2d9c4e5a0a..b044e2c36ec5e 100644 --- a/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.test.tsx +++ b/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.test.tsx @@ -17,10 +17,11 @@ * under the License. */ -import { InsecureClusterService } from './insecure_cluster_service'; -import { ConfigType } from '../config'; -import { coreMock } from '../../../../core/public/mocks'; import { nextTick } from '@kbn/test/jest'; +import { coreMock } from '../../../../core/public/mocks'; +import { mockAppStateService } from '../app_state/app_state_service.mock'; +import type { ConfigType } from '../config'; +import { InsecureClusterService } from './insecure_cluster_service'; let mockOnDismissCallback: (persist: boolean) => void = jest.fn().mockImplementation(() => { throw new Error('expected callback to be replaced!'); @@ -37,28 +38,14 @@ jest.mock('./components', () => { }); interface InitOpts { - displayAlert?: boolean; - isAnonymousPath?: boolean; tenant?: string; } -function initCore({ - displayAlert = true, - isAnonymousPath = false, - tenant = '/server-base-path', -}: InitOpts = {}) { +function initCore({ tenant = '/server-base-path' }: InitOpts = {}) { const coreSetup = coreMock.createSetup(); (coreSetup.http.basePath.serverBasePath as string) = tenant; const coreStart = coreMock.createStart(); - coreStart.http.get.mockImplementation(async (url: unknown) => { - if (url === '/internal/security_oss/display_insecure_cluster_alert') { - return { displayAlert }; - } - throw new Error(`unexpected call to http.get: ${url}`); - }); - coreStart.http.anonymousPaths.isAnonymous.mockReturnValue(isAnonymousPath); - coreStart.notifications.toasts.addWarning.mockReturnValue({ id: 'mock_alert_id' }); return { coreSetup, coreStart }; } @@ -67,52 +54,44 @@ describe('InsecureClusterService', () => { describe('display scenarios', () => { it('does not display an alert when the warning is explicitly disabled via config', async () => { const config: ConfigType = { showInsecureClusterWarning: false }; - const { coreSetup, coreStart } = initCore({ displayAlert: true }); + const { coreSetup, coreStart } = initCore(); const storage = coreMock.createStorage(); - const service = new InsecureClusterService(config, storage); - service.setup({ core: coreSetup }); - service.start({ core: coreStart }); - - await nextTick(); - - expect(coreStart.http.get).not.toHaveBeenCalled(); - expect(coreStart.notifications.toasts.addWarning).not.toHaveBeenCalled(); - - expect(coreStart.notifications.toasts.remove).not.toHaveBeenCalled(); - expect(storage.setItem).not.toHaveBeenCalled(); - }); - - it('does not display an alert when the endpoint check returns false', async () => { - const config: ConfigType = { showInsecureClusterWarning: true }; - const { coreSetup, coreStart } = initCore({ displayAlert: false }); - const storage = coreMock.createStorage(); + const appState = mockAppStateService.createStart(); + appState.getState.mockResolvedValue( + mockAppStateService.createAppState({ insecureClusterAlert: { displayAlert: true } }) + ); const service = new InsecureClusterService(config, storage); service.setup({ core: coreSetup }); - service.start({ core: coreStart }); + service.start({ core: coreStart, appState }); await nextTick(); - expect(coreStart.http.get).toHaveBeenCalledTimes(1); + expect(appState.getState).not.toHaveBeenCalled(); expect(coreStart.notifications.toasts.addWarning).not.toHaveBeenCalled(); expect(coreStart.notifications.toasts.remove).not.toHaveBeenCalled(); expect(storage.setItem).not.toHaveBeenCalled(); }); - it('does not display an alert when on an anonymous path', async () => { + it('does not display an alert when state indicates that alert should not be shown', async () => { const config: ConfigType = { showInsecureClusterWarning: true }; - const { coreSetup, coreStart } = initCore({ displayAlert: true, isAnonymousPath: true }); + const { coreSetup, coreStart } = initCore(); const storage = coreMock.createStorage(); + const appState = mockAppStateService.createStart(); + appState.getState.mockResolvedValue( + mockAppStateService.createAppState({ insecureClusterAlert: { displayAlert: false } }) + ); + const service = new InsecureClusterService(config, storage); service.setup({ core: coreSetup }); - service.start({ core: coreStart }); + service.start({ core: coreStart, appState }); await nextTick(); - expect(coreStart.http.get).not.toHaveBeenCalled(); + expect(appState.getState).toHaveBeenCalledTimes(1); expect(coreStart.notifications.toasts.addWarning).not.toHaveBeenCalled(); expect(coreStart.notifications.toasts.remove).not.toHaveBeenCalled(); @@ -121,17 +100,19 @@ describe('InsecureClusterService', () => { it('only reads storage information from the current tenant', async () => { const config: ConfigType = { showInsecureClusterWarning: true }; - const { coreSetup, coreStart } = initCore({ - displayAlert: true, - tenant: '/my-specific-tenant', - }); + const { coreSetup, coreStart } = initCore({ tenant: '/my-specific-tenant' }); const storage = coreMock.createStorage(); storage.getItem.mockReturnValue(JSON.stringify({ show: false })); + const appState = mockAppStateService.createStart(); + appState.getState.mockResolvedValue( + mockAppStateService.createAppState({ insecureClusterAlert: { displayAlert: true } }) + ); + const service = new InsecureClusterService(config, storage); service.setup({ core: coreSetup }); - service.start({ core: coreStart }); + service.start({ core: coreStart, appState }); await nextTick(); @@ -143,18 +124,23 @@ describe('InsecureClusterService', () => { it('does not display an alert when hidden via storage', async () => { const config: ConfigType = { showInsecureClusterWarning: true }; - const { coreSetup, coreStart } = initCore({ displayAlert: true }); + const { coreSetup, coreStart } = initCore(); const storage = coreMock.createStorage(); storage.getItem.mockReturnValue(JSON.stringify({ show: false })); + const appState = mockAppStateService.createStart(); + appState.getState.mockResolvedValue( + mockAppStateService.createAppState({ insecureClusterAlert: { displayAlert: true } }) + ); + const service = new InsecureClusterService(config, storage); service.setup({ core: coreSetup }); - service.start({ core: coreStart }); + service.start({ core: coreStart, appState }); await nextTick(); - expect(coreStart.http.get).not.toHaveBeenCalled(); + expect(appState.getState).not.toHaveBeenCalled(); expect(coreStart.notifications.toasts.addWarning).not.toHaveBeenCalled(); expect(coreStart.notifications.toasts.remove).not.toHaveBeenCalled(); @@ -163,18 +149,23 @@ describe('InsecureClusterService', () => { it('displays an alert when persisted preference is corrupted', async () => { const config: ConfigType = { showInsecureClusterWarning: true }; - const { coreSetup, coreStart } = initCore({ displayAlert: true }); + const { coreSetup, coreStart } = initCore(); const storage = coreMock.createStorage(); storage.getItem.mockReturnValue('{ this is a string of invalid JSON'); + const appState = mockAppStateService.createStart(); + appState.getState.mockResolvedValue( + mockAppStateService.createAppState({ insecureClusterAlert: { displayAlert: true } }) + ); + const service = new InsecureClusterService(config, storage); service.setup({ core: coreSetup }); - service.start({ core: coreStart }); + service.start({ core: coreStart, appState }); await nextTick(); - expect(coreStart.http.get).toHaveBeenCalledTimes(1); + expect(appState.getState).toHaveBeenCalledTimes(1); expect(coreStart.notifications.toasts.addWarning).toHaveBeenCalledTimes(1); expect(coreStart.notifications.toasts.remove).not.toHaveBeenCalled(); @@ -183,16 +174,21 @@ describe('InsecureClusterService', () => { it('displays an alert when enabled via config and endpoint checks', async () => { const config: ConfigType = { showInsecureClusterWarning: true }; - const { coreSetup, coreStart } = initCore({ displayAlert: true }); + const { coreSetup, coreStart } = initCore(); const storage = coreMock.createStorage(); + const appState = mockAppStateService.createStart(); + appState.getState.mockResolvedValue( + mockAppStateService.createAppState({ insecureClusterAlert: { displayAlert: true } }) + ); + const service = new InsecureClusterService(config, storage); service.setup({ core: coreSetup }); - service.start({ core: coreStart }); + service.start({ core: coreStart, appState }); await nextTick(); - expect(coreStart.http.get).toHaveBeenCalledTimes(1); + expect(appState.getState).toHaveBeenCalledTimes(1); expect(coreStart.notifications.toasts.addWarning).toHaveBeenCalledTimes(1); expect(coreStart.notifications.toasts.addWarning.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -213,16 +209,21 @@ describe('InsecureClusterService', () => { it('dismisses the alert when requested, and remembers this preference', async () => { const config: ConfigType = { showInsecureClusterWarning: true }; - const { coreSetup, coreStart } = initCore({ displayAlert: true }); + const { coreSetup, coreStart } = initCore(); const storage = coreMock.createStorage(); + const appState = mockAppStateService.createStart(); + appState.getState.mockResolvedValue( + mockAppStateService.createAppState({ insecureClusterAlert: { displayAlert: true } }) + ); + const service = new InsecureClusterService(config, storage); service.setup({ core: coreSetup }); - service.start({ core: coreStart }); + service.start({ core: coreStart, appState }); await nextTick(); - expect(coreStart.http.get).toHaveBeenCalledTimes(1); + expect(appState.getState).toHaveBeenCalledTimes(1); expect(coreStart.notifications.toasts.addWarning).toHaveBeenCalledTimes(1); mockOnDismissCallback(true); @@ -257,19 +258,24 @@ describe('InsecureClusterService', () => { it('allows the alert title and text to be replaced', async () => { const config: ConfigType = { showInsecureClusterWarning: true }; - const { coreSetup, coreStart } = initCore({ displayAlert: true }); + const { coreSetup, coreStart } = initCore(); const storage = coreMock.createStorage(); + const appState = mockAppStateService.createStart(); + appState.getState.mockResolvedValue( + mockAppStateService.createAppState({ insecureClusterAlert: { displayAlert: true } }) + ); + const service = new InsecureClusterService(config, storage); const { setAlertTitle, setAlertText } = service.setup({ core: coreSetup }); setAlertTitle('some new title'); setAlertText('some new alert text'); - service.start({ core: coreStart }); + service.start({ core: coreStart, appState }); await nextTick(); - expect(coreStart.http.get).toHaveBeenCalledTimes(1); + expect(appState.getState).toHaveBeenCalledTimes(1); expect(coreStart.notifications.toasts.addWarning).toHaveBeenCalledTimes(1); expect(coreStart.notifications.toasts.addWarning.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -292,16 +298,21 @@ describe('InsecureClusterService', () => { describe('#start', () => { it('allows the alert to be hidden via start contract, and remembers this preference', async () => { const config: ConfigType = { showInsecureClusterWarning: true }; - const { coreSetup, coreStart } = initCore({ displayAlert: true }); + const { coreSetup, coreStart } = initCore(); const storage = coreMock.createStorage(); + const appState = mockAppStateService.createStart(); + appState.getState.mockResolvedValue( + mockAppStateService.createAppState({ insecureClusterAlert: { displayAlert: true } }) + ); + const service = new InsecureClusterService(config, storage); service.setup({ core: coreSetup }); - const { hideAlert } = service.start({ core: coreStart }); + const { hideAlert } = service.start({ core: coreStart, appState }); await nextTick(); - expect(coreStart.http.get).toHaveBeenCalledTimes(1); + expect(appState.getState).toHaveBeenCalledTimes(1); expect(coreStart.notifications.toasts.addWarning).toHaveBeenCalledTimes(1); hideAlert(true); @@ -315,16 +326,21 @@ describe('InsecureClusterService', () => { it('allows the alert to be hidden via start contract, and does not remember the preference', async () => { const config: ConfigType = { showInsecureClusterWarning: true }; - const { coreSetup, coreStart } = initCore({ displayAlert: true }); + const { coreSetup, coreStart } = initCore(); const storage = coreMock.createStorage(); + const appState = mockAppStateService.createStart(); + appState.getState.mockResolvedValue( + mockAppStateService.createAppState({ insecureClusterAlert: { displayAlert: true } }) + ); + const service = new InsecureClusterService(config, storage); service.setup({ core: coreSetup }); - const { hideAlert } = service.start({ core: coreStart }); + const { hideAlert } = service.start({ core: coreStart, appState }); await nextTick(); - expect(coreStart.http.get).toHaveBeenCalledTimes(1); + expect(appState.getState).toHaveBeenCalledTimes(1); expect(coreStart.notifications.toasts.addWarning).toHaveBeenCalledTimes(1); hideAlert(false); diff --git a/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.tsx b/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.tsx index e6255233354b7..642ecf1dcf6e3 100644 --- a/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.tsx +++ b/src/plugins/security_oss/public/insecure_cluster_service/insecure_cluster_service.tsx @@ -21,7 +21,8 @@ import { CoreSetup, CoreStart, MountPoint, Toast } from 'kibana/public'; import { BehaviorSubject, combineLatest, from } from 'rxjs'; import { distinctUntilChanged, map } from 'rxjs/operators'; -import { ConfigType } from '../config'; +import type { ConfigType } from '../config'; +import type { AppStateServiceStart } from '../app_state'; import { defaultAlertText, defaultAlertTitle } from './components'; interface SetupDeps { @@ -29,7 +30,8 @@ interface SetupDeps { } interface StartDeps { - core: Pick; + core: Pick; + appState: AppStateServiceStart; } export interface InsecureClusterServiceSetup { @@ -84,12 +86,9 @@ export class InsecureClusterService { }; } - public start({ core }: StartDeps): InsecureClusterServiceStart { - const shouldInitialize = - this.enabled && !core.http.anonymousPaths.isAnonymous(window.location.pathname); - - if (shouldInitialize) { - this.initializeAlert(core); + public start({ core, appState }: StartDeps): InsecureClusterServiceStart { + if (this.enabled) { + this.initializeAlert(core, appState); } return { @@ -97,24 +96,20 @@ export class InsecureClusterService { }; } - private initializeAlert(core: StartDeps['core']) { - const displayAlert$ = from( - core.http - .get<{ displayAlert: boolean }>('/internal/security_oss/display_insecure_cluster_alert') - .catch((e) => { - // in the event we can't make this call, assume we shouldn't display this alert. - return { displayAlert: false }; - }) - ); + private initializeAlert(core: StartDeps['core'], appState: AppStateServiceStart) { + const appState$ = from(appState.getState()); // 10 days is reasonably long enough to call "forever" for a page load. // Can't go too much longer than this. See https://github.com/elastic/kibana/issues/64264#issuecomment-618400354 const oneMinute = 60000; const tenDays = oneMinute * 60 * 24 * 10; - combineLatest([displayAlert$, this.alertVisibility$]) + combineLatest([appState$, this.alertVisibility$]) .pipe( - map(([{ displayAlert }, isAlertVisible]) => displayAlert && isAlertVisible), + map( + ([{ insecureClusterAlert }, isAlertVisible]) => + insecureClusterAlert.displayAlert && isAlertVisible + ), distinctUntilChanged() ) .subscribe((showAlert) => { diff --git a/src/plugins/security_oss/public/plugin.ts b/src/plugins/security_oss/public/plugin.ts index 2f3eed0bde5eb..756e20f34cfde 100644 --- a/src/plugins/security_oss/public/plugin.ts +++ b/src/plugins/security_oss/public/plugin.ts @@ -17,13 +17,20 @@ * under the License. */ -import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; -import { ConfigType } from './config'; +import type { + Capabilities, + CoreSetup, + CoreStart, + Plugin, + PluginInitializerContext, +} from 'src/core/public'; +import type { ConfigType } from './config'; import { InsecureClusterService, InsecureClusterServiceSetup, InsecureClusterServiceStart, } from './insecure_cluster_service'; +import { AppStateService } from './app_state'; export interface SecurityOssPluginSetup { insecureCluster: InsecureClusterServiceSetup; @@ -31,18 +38,19 @@ export interface SecurityOssPluginSetup { export interface SecurityOssPluginStart { insecureCluster: InsecureClusterServiceStart; + anonymousAccess: { + getAccessURLParameters: () => Promise | null>; + getCapabilities: () => Promise; + }; } export class SecurityOssPlugin implements Plugin { - private readonly config: ConfigType; + private readonly config = this.initializerContext.config.get(); + private readonly insecureClusterService = new InsecureClusterService(this.config, localStorage); + private readonly appStateService = new AppStateService(); - private insecureClusterService: InsecureClusterService; - - constructor(private readonly initializerContext: PluginInitializerContext) { - this.config = this.initializerContext.config.get(); - this.insecureClusterService = new InsecureClusterService(this.config, localStorage); - } + constructor(private readonly initializerContext: PluginInitializerContext) {} public setup(core: CoreSetup) { return { @@ -51,8 +59,20 @@ export class SecurityOssPlugin } public start(core: CoreStart) { + const appState = this.appStateService.start({ core }); return { - insecureCluster: this.insecureClusterService.start({ core }), + insecureCluster: this.insecureClusterService.start({ core, appState }), + anonymousAccess: { + async getAccessURLParameters() { + const { anonymousAccess } = await appState.getState(); + return anonymousAccess.accessURLParameters; + }, + getCapabilities() { + return core.http.get( + '/internal/security_oss/anonymous_access/capabilities' + ); + }, + }, }; } } diff --git a/src/plugins/security_oss/server/plugin.test.ts b/src/plugins/security_oss/server/plugin.test.ts index 417da0c7e73bb..25a8fdc66c96b 100644 --- a/src/plugins/security_oss/server/plugin.test.ts +++ b/src/plugins/security_oss/server/plugin.test.ts @@ -30,6 +30,7 @@ describe('SecurityOss Plugin', () => { expect(Object.keys(contract)).toMatchInlineSnapshot(` Array [ "showInsecureClusterWarning$", + "setAnonymousAccessServiceProvider", ] `); }); diff --git a/src/plugins/security_oss/server/plugin.ts b/src/plugins/security_oss/server/plugin.ts index e48827f21a13a..43dd3eb758903 100644 --- a/src/plugins/security_oss/server/plugin.ts +++ b/src/plugins/security_oss/server/plugin.ts @@ -17,22 +17,55 @@ * under the License. */ -import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; +import type { + Capabilities, + CoreSetup, + KibanaRequest, + Logger, + Plugin, + PluginInitializerContext, +} from 'kibana/server'; import { BehaviorSubject, Observable } from 'rxjs'; import { createClusterDataCheck } from './check_cluster_data'; import { ConfigType } from './config'; -import { setupDisplayInsecureClusterAlertRoute } from './routes'; +import { setupAppStateRoute, setupAnonymousAccessCapabilitiesRoute } from './routes'; export interface SecurityOssPluginSetup { /** * Allows consumers to show/hide the insecure cluster warning. */ showInsecureClusterWarning$: BehaviorSubject; + + /** + * Set the provider function that returns a service to deal with the anonymous access. + * @param provider + */ + setAnonymousAccessServiceProvider: (provider: () => AnonymousAccessService) => void; +} + +export interface AnonymousAccessService { + /** + * Indicates whether anonymous access is enabled. + */ + readonly isAnonymousAccessEnabled: boolean; + + /** + * A map of query string parameters that should be specified in the URL pointing to Kibana so + * that anonymous user can automatically log in. + */ + readonly accessURLParameters: Readonly> | null; + + /** + * Gets capabilities of the anonymous service account. + * @param request Kibana request instance. + */ + getCapabilities: (request: KibanaRequest) => Promise; } export class SecurityOssPlugin implements Plugin { private readonly config$: Observable; private readonly logger: Logger; + private anonymousAccessServiceProvider?: () => AnonymousAccessService; constructor(initializerContext: PluginInitializerContext) { this.config$ = initializerContext.config.create(); @@ -43,16 +76,29 @@ export class SecurityOssPlugin implements Plugin(true); - setupDisplayInsecureClusterAlertRoute({ + setupAppStateRoute({ router, log: this.logger, config$: this.config$, displayModifier$: showInsecureClusterWarning$, doesClusterHaveUserData: createClusterDataCheck(), + getAnonymousAccessService: () => this.anonymousAccessServiceProvider?.() ?? null, + }); + + setupAnonymousAccessCapabilitiesRoute({ + router, + getAnonymousAccessService: () => this.anonymousAccessServiceProvider?.() ?? null, }); return { showInsecureClusterWarning$, + setAnonymousAccessServiceProvider: (provider: () => AnonymousAccessService) => { + if (this.anonymousAccessServiceProvider) { + throw new Error('Anonymous Access service provider is already set.'); + } + + this.anonymousAccessServiceProvider = provider; + }, }; } diff --git a/src/plugins/security_oss/server/routes/anonymous_access_capabilities.ts b/src/plugins/security_oss/server/routes/anonymous_access_capabilities.ts new file mode 100644 index 0000000000000..afa0aa340d94d --- /dev/null +++ b/src/plugins/security_oss/server/routes/anonymous_access_capabilities.ts @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import type { IRouter } from 'kibana/server'; +import type { AnonymousAccessService } from '../plugin'; + +interface Deps { + router: IRouter; + getAnonymousAccessService: () => AnonymousAccessService | null; +} + +/** + * Defines route that returns capabilities of the anonymous service account. + */ +export function setupAnonymousAccessCapabilitiesRoute({ router, getAnonymousAccessService }: Deps) { + router.get( + { path: '/internal/security_oss/anonymous_access/capabilities', validate: false }, + async (_context, request, response) => { + const anonymousAccessService = getAnonymousAccessService(); + if (!anonymousAccessService) { + return response.custom({ statusCode: 501, body: 'Not Implemented' }); + } + + return response.ok({ body: await anonymousAccessService.getCapabilities(request) }); + } + ); +} diff --git a/src/plugins/security_oss/server/routes/display_insecure_cluster_alert.ts b/src/plugins/security_oss/server/routes/app_state.ts similarity index 57% rename from src/plugins/security_oss/server/routes/display_insecure_cluster_alert.ts rename to src/plugins/security_oss/server/routes/app_state.ts index 0f0f72f054b4c..a20f1938d7c92 100644 --- a/src/plugins/security_oss/server/routes/display_insecure_cluster_alert.ts +++ b/src/plugins/security_oss/server/routes/app_state.ts @@ -17,10 +17,12 @@ * under the License. */ -import { IRouter, Logger } from 'kibana/server'; +import type { IRouter, Logger } from 'kibana/server'; import { combineLatest, Observable } from 'rxjs'; +import type { AppState } from '../../common'; import { createClusterDataCheck } from '../check_cluster_data'; -import { ConfigType } from '../config'; +import type { ConfigType } from '../config'; +import type { AnonymousAccessService } from '../plugin'; interface Deps { router: IRouter; @@ -28,14 +30,16 @@ interface Deps { config$: Observable; displayModifier$: Observable; doesClusterHaveUserData: ReturnType; + getAnonymousAccessService: () => AnonymousAccessService | null; } -export const setupDisplayInsecureClusterAlertRoute = ({ +export const setupAppStateRoute = ({ router, log, config$, displayModifier$, doesClusterHaveUserData, + getAnonymousAccessService, }: Deps) => { let showInsecureClusterWarning = false; @@ -44,20 +48,27 @@ export const setupDisplayInsecureClusterAlertRoute = ({ }); router.get( - { - path: '/internal/security_oss/display_insecure_cluster_alert', - validate: false, - }, + { path: '/internal/security_oss/app_state', validate: false }, async (context, request, response) => { - if (!showInsecureClusterWarning) { - return response.ok({ body: { displayAlert: false } }); + let displayAlert = false; + if (showInsecureClusterWarning) { + displayAlert = await doesClusterHaveUserData( + context.core.elasticsearch.client.asInternalUser, + log + ); } - const hasData = await doesClusterHaveUserData( - context.core.elasticsearch.client.asInternalUser, - log - ); - return response.ok({ body: { displayAlert: hasData } }); + const anonymousAccessService = getAnonymousAccessService(); + const appState: AppState = { + insecureClusterAlert: { displayAlert }, + anonymousAccess: { + isEnabled: anonymousAccessService?.isAnonymousAccessEnabled ?? false, + accessURLParameters: anonymousAccessService?.accessURLParameters + ? Object.fromEntries(anonymousAccessService.accessURLParameters.entries()) + : null, + }, + }; + return response.ok({ body: appState }); } ); }; diff --git a/src/plugins/security_oss/server/routes/index.ts b/src/plugins/security_oss/server/routes/index.ts index ceff0b12c9cb1..8b6dc6b6f217f 100644 --- a/src/plugins/security_oss/server/routes/index.ts +++ b/src/plugins/security_oss/server/routes/index.ts @@ -17,4 +17,5 @@ * under the License. */ -export { setupDisplayInsecureClusterAlertRoute } from './display_insecure_cluster_alert'; +export { setupAppStateRoute } from './app_state'; +export { setupAnonymousAccessCapabilitiesRoute } from './anonymous_access_capabilities'; diff --git a/src/plugins/security_oss/server/routes/integration_tests/anonymous_access_capabilities.test.ts b/src/plugins/security_oss/server/routes/integration_tests/anonymous_access_capabilities.test.ts new file mode 100644 index 0000000000000..c8c847f628133 --- /dev/null +++ b/src/plugins/security_oss/server/routes/integration_tests/anonymous_access_capabilities.test.ts @@ -0,0 +1,86 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import type { UnwrapPromise } from '@kbn/utility-types'; +import supertest from 'supertest'; +import { setupServer } from '../../../../../core/server/test_utils'; +import { AnonymousAccessService } from '../../plugin'; +import { setupAnonymousAccessCapabilitiesRoute } from '../anonymous_access_capabilities'; + +type SetupServerReturn = UnwrapPromise>; +const pluginId = Symbol('securityOss'); + +interface SetupOpts { + getAnonymousAccessService?: () => AnonymousAccessService | null; +} + +describe('GET /internal/security_oss/anonymous_access/capabilities', () => { + let server: SetupServerReturn['server']; + let httpSetup: SetupServerReturn['httpSetup']; + + const setupTestServer = async ({ getAnonymousAccessService = () => null }: SetupOpts = {}) => { + ({ server, httpSetup } = await setupServer(pluginId)); + + const router = httpSetup.createRouter('/'); + + setupAnonymousAccessCapabilitiesRoute({ router, getAnonymousAccessService }); + + await server.start(); + }; + + afterEach(async () => { + await server.stop(); + }); + + it('responds with 501 if anonymous access service is provided', async () => { + await setupTestServer(); + + await supertest(httpSetup.server.listener) + .get('/internal/security_oss/anonymous_access/capabilities') + .expect(501, { + statusCode: 501, + error: 'Not Implemented', + message: 'Not Implemented', + }); + }); + + it('returns anonymous access state if anonymous access service is provided', async () => { + await setupTestServer({ + getAnonymousAccessService: () => ({ + isAnonymousAccessEnabled: true, + accessURLParameters: new Map([['auth_provider_hint', 'anonymous1']]), + getCapabilities: jest.fn().mockResolvedValue({ + navLinks: {}, + management: {}, + catalogue: {}, + custom: { something: true }, + }), + }), + }); + + await supertest(httpSetup.server.listener) + .get('/internal/security_oss/anonymous_access/capabilities') + .expect(200, { + navLinks: {}, + management: {}, + catalogue: {}, + custom: { something: true }, + }); + }); +}); diff --git a/src/plugins/security_oss/server/routes/integration_tests/display_insecure_cluster_alert.test.ts b/src/plugins/security_oss/server/routes/integration_tests/app_state.test.ts similarity index 51% rename from src/plugins/security_oss/server/routes/integration_tests/display_insecure_cluster_alert.test.ts rename to src/plugins/security_oss/server/routes/integration_tests/app_state.test.ts index d62a5040be6b3..e68dd40bb01bb 100644 --- a/src/plugins/security_oss/server/routes/integration_tests/display_insecure_cluster_alert.test.ts +++ b/src/plugins/security_oss/server/routes/integration_tests/app_state.test.ts @@ -19,7 +19,8 @@ import { loggingSystemMock } from '../../../../../core/server/mocks'; import { setupServer } from '../../../../../core/server/test_utils'; -import { setupDisplayInsecureClusterAlertRoute } from '../display_insecure_cluster_alert'; +import { AnonymousAccessService } from '../../plugin'; +import { setupAppStateRoute } from '../app_state'; import { ConfigType } from '../../config'; import { BehaviorSubject, of } from 'rxjs'; import { UnwrapPromise } from '@kbn/utility-types'; @@ -33,9 +34,10 @@ interface SetupOpts { config?: ConfigType; displayModifier$?: BehaviorSubject; doesClusterHaveUserData?: ReturnType; + getAnonymousAccessService?: () => AnonymousAccessService | null; } -describe('GET /internal/security_oss/display_insecure_cluster_alert', () => { +describe('GET /internal/security_oss/app_state', () => { let server: SetupServerReturn['server']; let httpSetup: SetupServerReturn['httpSetup']; @@ -43,18 +45,20 @@ describe('GET /internal/security_oss/display_insecure_cluster_alert', () => { config = { showInsecureClusterWarning: true }, displayModifier$ = new BehaviorSubject(true), doesClusterHaveUserData = jest.fn().mockResolvedValue(true), + getAnonymousAccessService = () => null, }: SetupOpts) => { ({ server, httpSetup } = await setupServer(pluginId)); const router = httpSetup.createRouter('/'); const log = loggingSystemMock.createLogger(); - setupDisplayInsecureClusterAlertRoute({ + setupAppStateRoute({ router, log, config$: of(config), displayModifier$, doesClusterHaveUserData, + getAnonymousAccessService, }); await server.start(); @@ -68,28 +72,34 @@ describe('GET /internal/security_oss/display_insecure_cluster_alert', () => { await server.stop(); }); - it('responds `false` if plugin is not configured to display alerts', async () => { + it('responds `insecureClusterAlert.displayAlert == false` if plugin is not configured to display alerts', async () => { await setupTestServer({ config: { showInsecureClusterWarning: false }, }); await supertest(httpSetup.server.listener) - .get('/internal/security_oss/display_insecure_cluster_alert') - .expect(200, { displayAlert: false }); + .get('/internal/security_oss/app_state') + .expect(200, { + insecureClusterAlert: { displayAlert: false }, + anonymousAccess: { isEnabled: false, accessURLParameters: null }, + }); }); - it('responds `false` if cluster does not contain user data', async () => { + it('responds `insecureClusterAlert.displayAlert == false` if cluster does not contain user data', async () => { await setupTestServer({ config: { showInsecureClusterWarning: true }, doesClusterHaveUserData: jest.fn().mockResolvedValue(false), }); await supertest(httpSetup.server.listener) - .get('/internal/security_oss/display_insecure_cluster_alert') - .expect(200, { displayAlert: false }); + .get('/internal/security_oss/app_state') + .expect(200, { + insecureClusterAlert: { displayAlert: false }, + anonymousAccess: { isEnabled: false, accessURLParameters: null }, + }); }); - it('responds `false` if displayModifier$ is set to false', async () => { + it('responds `insecureClusterAlert.displayAlert == false` if displayModifier$ is set to false', async () => { await setupTestServer({ config: { showInsecureClusterWarning: true }, doesClusterHaveUserData: jest.fn().mockResolvedValue(true), @@ -97,19 +107,25 @@ describe('GET /internal/security_oss/display_insecure_cluster_alert', () => { }); await supertest(httpSetup.server.listener) - .get('/internal/security_oss/display_insecure_cluster_alert') - .expect(200, { displayAlert: false }); + .get('/internal/security_oss/app_state') + .expect(200, { + insecureClusterAlert: { displayAlert: false }, + anonymousAccess: { isEnabled: false, accessURLParameters: null }, + }); }); - it('responds `true` if cluster contains user data', async () => { + it('responds `insecureClusterAlert.displayAlert == true` if cluster contains user data', async () => { await setupTestServer({ config: { showInsecureClusterWarning: true }, doesClusterHaveUserData: jest.fn().mockResolvedValue(true), }); await supertest(httpSetup.server.listener) - .get('/internal/security_oss/display_insecure_cluster_alert') - .expect(200, { displayAlert: true }); + .get('/internal/security_oss/app_state') + .expect(200, { + insecureClusterAlert: { displayAlert: true }, + anonymousAccess: { isEnabled: false, accessURLParameters: null }, + }); }); it('responds to changing displayModifier$ values', async () => { @@ -122,13 +138,56 @@ describe('GET /internal/security_oss/display_insecure_cluster_alert', () => { }); await supertest(httpSetup.server.listener) - .get('/internal/security_oss/display_insecure_cluster_alert') - .expect(200, { displayAlert: true }); + .get('/internal/security_oss/app_state') + .expect(200, { + insecureClusterAlert: { displayAlert: true }, + anonymousAccess: { isEnabled: false, accessURLParameters: null }, + }); displayModifier$.next(false); await supertest(httpSetup.server.listener) - .get('/internal/security_oss/display_insecure_cluster_alert') - .expect(200, { displayAlert: false }); + .get('/internal/security_oss/app_state') + .expect(200, { + insecureClusterAlert: { displayAlert: false }, + anonymousAccess: { isEnabled: false, accessURLParameters: null }, + }); + }); + + it('returns anonymous access state if anonymous access service is provided', async () => { + const displayModifier$ = new BehaviorSubject(true); + + await setupTestServer({ + config: { showInsecureClusterWarning: true }, + doesClusterHaveUserData: jest.fn().mockResolvedValue(true), + displayModifier$, + getAnonymousAccessService: () => ({ + isAnonymousAccessEnabled: true, + accessURLParameters: new Map([['auth_provider_hint', 'anonymous1']]), + getCapabilities: jest.fn(), + }), + }); + + await supertest(httpSetup.server.listener) + .get('/internal/security_oss/app_state') + .expect(200, { + insecureClusterAlert: { displayAlert: true }, + anonymousAccess: { + isEnabled: true, + accessURLParameters: { auth_provider_hint: 'anonymous1' }, + }, + }); + + displayModifier$.next(false); + + await supertest(httpSetup.server.listener) + .get('/internal/security_oss/app_state') + .expect(200, { + insecureClusterAlert: { displayAlert: false }, + anonymousAccess: { + isEnabled: true, + accessURLParameters: { auth_provider_hint: 'anonymous1' }, + }, + }); }); }); diff --git a/src/plugins/security_oss/tsconfig.json b/src/plugins/security_oss/tsconfig.json index d211a70f12df3..530e01a034b00 100644 --- a/src/plugins/security_oss/tsconfig.json +++ b/src/plugins/security_oss/tsconfig.json @@ -7,6 +7,6 @@ "declaration": true, "declarationMap": true }, - "include": ["public/**/*", "server/**/*"], + "include": ["common/**/*", "public/**/*", "server/**/*"], "references": [{ "path": "../../core/tsconfig.json" }] } diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index dd3c06a159b81..e1c369dc003bd 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -4942,13 +4942,6 @@ } } }, - "tsvb-validation": { - "properties": { - "failed_validations": { - "type": "long" - } - } - }, "vis_type_vega": { "properties": { "vega_lib_specs_total": { diff --git a/src/plugins/telemetry_management_section/tsconfig.json b/src/plugins/telemetry_management_section/tsconfig.json new file mode 100644 index 0000000000000..48e40814b8570 --- /dev/null +++ b/src/plugins/telemetry_management_section/tsconfig.json @@ -0,0 +1,28 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "public/**/*", + "../../../typings/**/*" + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../usage_collection/tsconfig.json" }, + { "path": "../telemetry/tsconfig.json" }, + { "path": "../kibana_legacy/tsconfig.json"}, + { "path": "../ui_actions/tsconfig.json" }, + { "path": "../expressions/tsconfig.json" }, + { "path": "../home/tsconfig.json" }, + { "path": "../bfetch/tsconfig.json"}, + { "path": "../data/tsconfig.json"}, + { "path": "../advanced_settings/tsconfig.json" }, + { "path": "../management/tsconfig.json"} + ] +} diff --git a/src/plugins/tile_map/public/__snapshots__/tilemap_fn.test.js.snap b/src/plugins/tile_map/public/__snapshots__/tile_map_fn.test.ts.snap similarity index 93% rename from src/plugins/tile_map/public/__snapshots__/tilemap_fn.test.js.snap rename to src/plugins/tile_map/public/__snapshots__/tile_map_fn.test.ts.snap index d5f5127bae919..7aab8b02890c0 100644 --- a/src/plugins/tile_map/public/__snapshots__/tilemap_fn.test.js.snap +++ b/src/plugins/tile_map/public/__snapshots__/tile_map_fn.test.ts.snap @@ -2,12 +2,9 @@ exports[`interpreter/functions#tilemap returns an object with the correct structure 1`] = ` Object { - "as": "visualization", + "as": "tile_map_vis", "type": "render", "value": Object { - "params": Object { - "listenOnChange": true, - }, "visConfig": Object { "addTooltip": true, "colorSchema": "Yellow to Red", diff --git a/src/plugins/tile_map/public/_tile_map.scss b/src/plugins/tile_map/public/_tile_map.scss deleted file mode 100644 index 5e4b20f79fedf..0000000000000 --- a/src/plugins/tile_map/public/_tile_map.scss +++ /dev/null @@ -1,15 +0,0 @@ -// SASSTODO: Does this selector exist today? -.tilemap { - margin-bottom: 6px; - border: $euiBorderThin; - position: relative; -} - -/** -* 1. Visualizations have some padding by default but tilemaps look nice flush against the edge to maximize viewing -* space. -*/ -// SASSTODO: Does this selector exist today? -.tile_map { - padding: 0; /* 1. */ -} diff --git a/src/plugins/tile_map/public/components/index.tsx b/src/plugins/tile_map/public/components/index.tsx new file mode 100644 index 0000000000000..4a4f887046fe2 --- /dev/null +++ b/src/plugins/tile_map/public/components/index.tsx @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { lazy } from 'react'; +import type { TileMapOptionsProps } from './tile_map_options'; + +const TileMapOptions = lazy(() => import('./tile_map_options')); + +export const TileMapOptionsLazy = (props: TileMapOptionsProps) => ; diff --git a/src/plugins/tile_map/public/components/tile_map_options.tsx b/src/plugins/tile_map/public/components/tile_map_options.tsx index a6c0bb8a50dda..eda6b7b52fb45 100644 --- a/src/plugins/tile_map/public/components/tile_map_options.tsx +++ b/src/plugins/tile_map/public/components/tile_map_options.tsx @@ -28,7 +28,9 @@ import { SwitchOption, RangeOption, } from '../../../vis_default_editor/public'; -import { WmsOptions, TileMapVisParams, MapTypes } from '../../../maps_legacy/public'; +import { WmsOptions } from '../../../maps_legacy/public'; +import { TileMapVisParams } from '../types'; +import { MapTypes } from '../utils/map_types'; export type TileMapOptionsProps = VisOptionsProps; @@ -102,4 +104,6 @@ function TileMapOptions(props: TileMapOptionsProps) { ); } -export { TileMapOptions }; +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { TileMapOptions as default }; diff --git a/src/plugins/tile_map/public/geohash_layer.js b/src/plugins/tile_map/public/geohash_layer.js index ca992a0d09ec9..83c86149de531 100644 --- a/src/plugins/tile_map/public/geohash_layer.js +++ b/src/plugins/tile_map/public/geohash_layer.js @@ -19,11 +19,12 @@ import { min, isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { KibanaMapLayer, MapTypes } from '../../maps_legacy/public'; +import { KibanaMapLayer } from '../../maps_legacy/public'; import { HeatmapMarkers } from './markers/heatmap'; import { ScaledCirclesMarkers } from './markers/scaled_circles'; import { ShadedCirclesMarkers } from './markers/shaded_circles'; import { GeohashGridMarkers } from './markers/geohash_grid'; +import { MapTypes } from './utils/map_types'; export class GeohashLayer extends KibanaMapLayer { constructor(featureCollection, featureCollectionMetaData, options, zoom, kibanaMap, leaflet) { diff --git a/src/plugins/tile_map/public/index.scss b/src/plugins/tile_map/public/index.scss deleted file mode 100644 index f4b86b0c31190..0000000000000 --- a/src/plugins/tile_map/public/index.scss +++ /dev/null @@ -1,8 +0,0 @@ -// Prefix all styles with "tlm" to avoid conflicts. -// Examples -// tlmChart -// tlmChart__legend -// tlmChart__legend--small -// tlmChart__legend-isLoading - -@import 'tile_map'; diff --git a/src/plugins/tile_map/public/plugin.ts b/src/plugins/tile_map/public/plugin.ts index dfcafafbe47f7..0a877d9595ec9 100644 --- a/src/plugins/tile_map/public/plugin.ts +++ b/src/plugins/tile_map/public/plugin.ts @@ -25,13 +25,6 @@ import { } from 'kibana/public'; import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; import { VisualizationsSetup } from '../../visualizations/public'; -// TODO: Determine why visualizations don't populate without this -import 'angular-sanitize'; - -// @ts-ignore -import { createTileMapFn } from './tile_map_fn'; -// @ts-ignore -import { createTileMapTypeDefinition } from './tile_map_type'; import { IServiceSettings, MapsLegacyPluginSetup } from '../../maps_legacy/public'; import { DataPublicPluginStart } from '../../data/public'; import { @@ -44,12 +37,16 @@ import { import { KibanaLegacyStart } from '../../kibana_legacy/public'; import { SharePluginStart } from '../../share/public'; +import { createTileMapFn } from './tile_map_fn'; +import { createTileMapTypeDefinition } from './tile_map_type'; +import { getTileMapRenderer } from './tile_map_renderer'; + export interface TileMapConfigType { tilemap: any; } /** @private */ -interface TileMapVisualizationDependencies { +export interface TileMapVisualizationDependencies { uiSettings: IUiSettingsClient; getZoomPrecision: any; getPrecision: any; @@ -98,7 +95,8 @@ export class TileMapPlugin implements Plugin createTileMapFn(visualizationDependencies)); + expressions.registerFunction(createTileMapFn); + expressions.registerRenderer(getTileMapRenderer(visualizationDependencies)); visualizations.createBaseVisualization(createTileMapTypeDefinition(visualizationDependencies)); diff --git a/src/plugins/tile_map/public/tilemap_fn.test.js b/src/plugins/tile_map/public/tile_map_fn.test.ts similarity index 83% rename from src/plugins/tile_map/public/tilemap_fn.test.js rename to src/plugins/tile_map/public/tile_map_fn.test.ts index 895842ea1e8f4..dde98b7fe84c0 100644 --- a/src/plugins/tile_map/public/tilemap_fn.test.js +++ b/src/plugins/tile_map/public/tile_map_fn.test.ts @@ -17,11 +17,10 @@ * under the License. */ -// eslint-disable-next-line import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils'; import { createTileMapFn } from './tile_map_fn'; -jest.mock('../../maps_legacy/public', () => ({ +jest.mock('./utils', () => ({ convertToGeoJson: jest.fn().mockReturnValue({ featureCollection: { type: 'FeatureCollection', @@ -36,7 +35,7 @@ jest.mock('../../maps_legacy/public', () => ({ }), })); -import { convertToGeoJson } from '../../maps_legacy/public'; +import { convertToGeoJson } from './utils'; describe('interpreter/functions#tilemap', () => { const fn = functionWrapper(createTileMapFn()); @@ -79,18 +78,14 @@ describe('interpreter/functions#tilemap', () => { jest.clearAllMocks(); }); - it('returns an object with the correct structure', () => { - const actual = fn( - context, - { visConfig: JSON.stringify(visConfig) }, - { logDatatable: jest.fn() } - ); + it('returns an object with the correct structure', async () => { + const actual = await fn(context, { visConfig: JSON.stringify(visConfig) }); expect(actual).toMatchSnapshot(); }); - it('calls response handler with correct values', () => { + it('calls response handler with correct values', async () => { const { geohash, metric, geocentroid } = visConfig.dimensions; - fn(context, { visConfig: JSON.stringify(visConfig) }, { logDatatable: jest.fn() }); + await fn(context, { visConfig: JSON.stringify(visConfig) }); expect(convertToGeoJson).toHaveBeenCalledTimes(1); expect(convertToGeoJson).toHaveBeenCalledWith(context, { geohash, diff --git a/src/plugins/tile_map/public/tile_map_fn.js b/src/plugins/tile_map/public/tile_map_fn.ts similarity index 66% rename from src/plugins/tile_map/public/tile_map_fn.js rename to src/plugins/tile_map/public/tile_map_fn.ts index 7a5f36be1eb9d..786913a1d2b1e 100644 --- a/src/plugins/tile_map/public/tile_map_fn.js +++ b/src/plugins/tile_map/public/tile_map_fn.ts @@ -16,10 +16,30 @@ * specific language governing permissions and limitations * under the License. */ -import { convertToGeoJson } from '../../maps_legacy/public'; + import { i18n } from '@kbn/i18n'; -export const createTileMapFn = () => ({ +import type { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public'; +import { TileMapVisConfig, TileMapVisData } from './types'; + +interface Arguments { + visConfig: string | null; +} + +export interface TileMapVisRenderValue { + visData: TileMapVisData; + visType: 'tile_map'; + visConfig: TileMapVisConfig; +} + +export type TileMapExpressionFunctionDefinition = ExpressionFunctionDefinition< + 'tilemap', + Datatable, + Arguments, + Promise> +>; + +export const createTileMapFn = (): TileMapExpressionFunctionDefinition => ({ name: 'tilemap', type: 'render', context: { @@ -32,34 +52,30 @@ export const createTileMapFn = () => ({ visConfig: { types: ['string', 'null'], default: '"{}"', + help: '', }, }, - fn(context, args, handlers) { - const visConfig = JSON.parse(args.visConfig); + async fn(context, args, handlers) { + const visConfig = args.visConfig && JSON.parse(args.visConfig); const { geohash, metric, geocentroid } = visConfig.dimensions; + + const { convertToGeoJson } = await import('./utils'); const convertedData = convertToGeoJson(context, { geohash, metric, geocentroid, }); - if (geohash && geohash.accessor) { - convertedData.meta.geohash = context.columns[geohash.accessor].meta; - } - if (handlers?.inspectorAdapters?.tables) { handlers.inspectorAdapters.tables.logDatatable('default', context); } return { type: 'render', - as: 'visualization', + as: 'tile_map_vis', value: { visData: convertedData, visType: 'tile_map', visConfig, - params: { - listenOnChange: true, - }, }, }; }, diff --git a/src/plugins/tile_map/public/tile_map_renderer.tsx b/src/plugins/tile_map/public/tile_map_renderer.tsx new file mode 100644 index 0000000000000..a61d4a0cb79dc --- /dev/null +++ b/src/plugins/tile_map/public/tile_map_renderer.tsx @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { lazy } from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; + +import { ExpressionRenderDefinition } from 'src/plugins/expressions'; +import { VisualizationContainer } from '../../visualizations/public'; +import { TileMapVisualizationDependencies } from './plugin'; +import { TileMapVisRenderValue } from './tile_map_fn'; + +const TileMapVisualization = lazy(() => import('./tile_map_visualization_component')); + +export const getTileMapRenderer: ( + deps: TileMapVisualizationDependencies +) => ExpressionRenderDefinition = (deps) => ({ + name: 'tile_map_vis', + reuseDomNode: true, + render: async (domNode, { visConfig, visData }, handlers) => { + handlers.onDestroy(() => { + unmountComponentAtNode(domNode); + }); + + render( + + + , + domNode + ); + }, +}); diff --git a/src/plugins/tile_map/public/tile_map_type.js b/src/plugins/tile_map/public/tile_map_type.ts similarity index 90% rename from src/plugins/tile_map/public/tile_map_type.js rename to src/plugins/tile_map/public/tile_map_type.ts index 3e9b5516322d9..e1d0dfb9a116f 100644 --- a/src/plugins/tile_map/public/tile_map_type.js +++ b/src/plugins/tile_map/public/tile_map_type.ts @@ -17,17 +17,22 @@ * under the License. */ -import React from 'react'; import { i18n } from '@kbn/i18n'; -import { convertToGeoJson, MapTypes } from '../../maps_legacy/public'; -import { createTileMapVisualization } from './tile_map_visualization'; -import { TileMapOptions } from './components/tile_map_options'; -import { supportsCssFilters } from './css_filters'; +import { BaseVisTypeOptions } from 'src/plugins/visualizations/public'; import { truncatedColorSchemas } from '../../charts/public'; + +// @ts-expect-error +import { supportsCssFilters } from './css_filters'; +import { TileMapOptionsLazy } from './components'; import { getDeprecationMessage } from './get_deprecation_message'; +import { TileMapVisualizationDependencies } from './plugin'; +import { toExpressionAst } from './to_ast'; +import { TileMapVisParams } from './types'; +import { MapTypes } from './utils/map_types'; -export function createTileMapTypeDefinition(dependencies) { - const CoordinateMapsVisualization = createTileMapVisualization(dependencies); +export function createTileMapTypeDefinition( + dependencies: TileMapVisualizationDependencies +): BaseVisTypeOptions { const { uiSettings, getServiceSettings } = dependencies; return { @@ -54,8 +59,7 @@ export function createTileMapTypeDefinition(dependencies) { wms: uiSettings.get('visualization:tileMap:WMSdefaults'), }, }, - visualization: CoordinateMapsVisualization, - responseHandler: convertToGeoJson, + toExpressionAst, editorConfig: { collections: { colorSchemas: truncatedColorSchemas, @@ -113,7 +117,7 @@ export function createTileMapTypeDefinition(dependencies) { ], tmsLayers: [], }, - optionsTemplate: (props) => , + optionsTemplate: TileMapOptionsLazy, schemas: [ { group: 'metrics', diff --git a/src/plugins/tile_map/public/tile_map_visualization.js b/src/plugins/tile_map/public/tile_map_visualization.js index 80084be283658..2128aec49992f 100644 --- a/src/plugins/tile_map/public/tile_map_visualization.js +++ b/src/plugins/tile_map/public/tile_map_visualization.js @@ -19,12 +19,9 @@ import { get, round } from 'lodash'; import { getFormatService, getQueryService, getKibanaLegacy } from './services'; -import { - geoContains, - mapTooltipProvider, - lazyLoadMapsLegacyModules, -} from '../../maps_legacy/public'; +import { mapTooltipProvider, lazyLoadMapsLegacyModules } from '../../maps_legacy/public'; import { tooltipFormatter } from './tooltip_formatter'; +import { geoContains } from './utils'; function scaleBounds(bounds) { const scale = 0.5; // scale bounds by 50% @@ -57,8 +54,8 @@ export const createTileMapVisualization = (dependencies) => { const { getZoomPrecision, getPrecision, BaseMapsVisualization } = dependencies; return class CoordinateMapsVisualization extends BaseMapsVisualization { - constructor(element, vis) { - super(element, vis); + constructor(element, handlers, initialVisParams) { + super(element, handlers, initialVisParams); this._geohashLayer = null; this._tooltipFormatter = mapTooltipProvider(element, tooltipFormatter); @@ -84,10 +81,10 @@ export const createTileMapVisualization = (dependencies) => { // todo: autoPrecision should be vis parameter, not aggConfig one const zoomPrecision = getZoomPrecision(); updateVarsObject.data.precision = geohashAgg.sourceParams.params.autoPrecision - ? zoomPrecision[this.vis.getUiState().get('mapZoom')] + ? zoomPrecision[this.handlers.uiState.get('mapZoom')] : getPrecision(geohashAgg.sourceParams.params.precision); - this.vis.eventsSubject.next(updateVarsObject); + this.handlers.event(updateVarsObject); }; async render(esResponse, visParams) { @@ -96,13 +93,12 @@ export const createTileMapVisualization = (dependencies) => { } async _makeKibanaMap() { - await super._makeKibanaMap(); + await super._makeKibanaMap(this._params); let previousPrecision = this._kibanaMap.getGeohashPrecision(); let precisionChange = false; - const uiState = this.vis.getUiState(); - uiState.on('change', (prop) => { + this.handlers.uiState.on('change', (prop) => { if (prop === 'mapZoom' || prop === 'mapCenter') { this.updateGeohashAgg(); } @@ -250,8 +246,6 @@ export const createTileMapVisualization = (dependencies) => { const { filterManager } = getQueryService(); filterManager.addFilters([filter]); - - this.vis.updateState(); } _getGeoHashAgg() { diff --git a/src/plugins/tile_map/public/tile_map_visualization.scss b/src/plugins/tile_map/public/tile_map_visualization.scss new file mode 100644 index 0000000000000..4298b06c763da --- /dev/null +++ b/src/plugins/tile_map/public/tile_map_visualization.scss @@ -0,0 +1,4 @@ +.tlmChart__wrapper, .tlmChart { + flex: 1 1 0; + display: flex; +} diff --git a/src/plugins/tile_map/public/tile_map_visualization_component.tsx b/src/plugins/tile_map/public/tile_map_visualization_component.tsx new file mode 100644 index 0000000000000..f087ab59fe73b --- /dev/null +++ b/src/plugins/tile_map/public/tile_map_visualization_component.tsx @@ -0,0 +1,103 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { useEffect, useMemo, useRef } from 'react'; +import { EuiResizeObserver } from '@elastic/eui'; +import { throttle } from 'lodash'; + +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; +import { PersistedState } from 'src/plugins/visualizations/public'; +import { TileMapVisualizationDependencies } from './plugin'; +import { TileMapVisConfig, TileMapVisData } from './types'; +// @ts-expect-error +import { createTileMapVisualization } from './tile_map_visualization'; + +import './tile_map_visualization.scss'; + +interface TileMapVisController { + render(visData?: TileMapVisData, visConfig?: TileMapVisConfig): Promise; + resize(): void; + destroy(): void; +} + +interface TileMapVisualizationProps { + deps: TileMapVisualizationDependencies; + handlers: IInterpreterRenderHandlers; + visData: TileMapVisData; + visConfig: TileMapVisConfig; +} + +const TileMapVisualization = ({ + deps, + handlers, + visData, + visConfig, +}: TileMapVisualizationProps) => { + const chartDiv = useRef(null); + const visController = useRef(null); + const isFirstRender = useRef(true); + const uiState = handlers.uiState as PersistedState; + + useEffect(() => { + if (chartDiv.current && isFirstRender.current) { + isFirstRender.current = false; + const Controller = createTileMapVisualization(deps); + visController.current = new Controller(chartDiv.current, handlers, visConfig); + } + }, [deps, handlers, visConfig, visData]); + + useEffect(() => { + visController.current?.render(visData, visConfig).then(handlers.done); + }, [visData, visConfig, handlers.done]); + + useEffect(() => { + const onUiStateChange = () => { + visController.current?.render().then(handlers.done); + }; + + uiState.on('change', onUiStateChange); + + return () => { + uiState.off('change', onUiStateChange); + }; + }, [uiState, handlers.done]); + + useEffect(() => { + return () => { + visController.current?.destroy(); + visController.current = null; + }; + }, []); + + const updateChartSize = useMemo(() => throttle(() => visController.current?.resize(), 300), []); + + return ( + + {(resizeRef) => ( +
    +
    +
    + )} + + ); +}; + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { TileMapVisualization as default }; diff --git a/src/plugins/tile_map/public/to_ast.ts b/src/plugins/tile_map/public/to_ast.ts new file mode 100644 index 0000000000000..313ca3218591e --- /dev/null +++ b/src/plugins/tile_map/public/to_ast.ts @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + EsaggsExpressionFunctionDefinition, + IndexPatternLoadExpressionFunctionDefinition, +} from '../../data/public'; +import { buildExpression, buildExpressionFunction } from '../../expressions/public'; +import { getVisSchemas, Vis, BuildPipelineParams } from '../../visualizations/public'; +import { TileMapExpressionFunctionDefinition } from './tile_map_fn'; +import { TileMapVisConfig, TileMapVisParams } from './types'; + +export const toExpressionAst = (vis: Vis, params: BuildPipelineParams) => { + const esaggs = buildExpressionFunction('esaggs', { + index: buildExpression([ + buildExpressionFunction('indexPatternLoad', { + id: vis.data.indexPattern!.id!, + }), + ]), + metricsAtAllLevels: false, + partialRows: false, + aggs: vis.data.aggs!.aggs.map((agg) => buildExpression(agg.toExpressionAst())), + }); + + const schemas = getVisSchemas(vis, params); + + const visConfig: TileMapVisConfig = { + ...vis.params, + dimensions: { + metric: schemas.metric[0], + geohash: schemas.segment ? schemas.segment[0] : null, + geocentroid: schemas.geo_centroid ? schemas.geo_centroid[0] : null, + }, + }; + + const tilemap = buildExpressionFunction('tilemap', { + visConfig: JSON.stringify(visConfig), + }); + + const ast = buildExpression([esaggs, tilemap]); + + return ast.toAst(); +}; diff --git a/src/plugins/maps_legacy/public/common/types/external_basemap_types.ts b/src/plugins/tile_map/public/types.ts similarity index 58% rename from src/plugins/maps_legacy/public/common/types/external_basemap_types.ts rename to src/plugins/tile_map/public/types.ts index be9c4d0d9c37b..360e0c474b21c 100644 --- a/src/plugins/maps_legacy/public/common/types/external_basemap_types.ts +++ b/src/plugins/tile_map/public/types.ts @@ -17,23 +17,29 @@ * under the License. */ -import { TmsLayer } from '../../index'; -import { MapTypes } from './map_types'; +import { FeatureCollection } from 'geojson'; +import type { SchemaConfig } from 'src/plugins/visualizations/public'; +import type { DatatableColumnMeta } from 'src/plugins/expressions'; +import type { WMSOptions } from 'src/plugins/maps_legacy/public'; +import type { MapTypes } from './utils/map_types'; -export interface WMSOptions { - selectedTmsLayer?: TmsLayer; - enabled: boolean; - url?: string; - options: { - version?: string; - layers?: string; - format: string; - transparent: boolean; - attribution?: string; - styles?: string; +export interface TileMapVisData { + featureCollection: FeatureCollection; + meta: { + min: number; + max: number; + geohash?: DatatableColumnMeta; + geohashPrecision: number | undefined; + geohashGridDimensionsAtEquator: [number, number] | undefined; }; } +export interface TileMapVisDimensions { + metric: SchemaConfig; + geohash: SchemaConfig | null; + geocentroid: SchemaConfig | null; +} + export interface TileMapVisParams { colorSchema: string; mapType: MapTypes; @@ -45,3 +51,7 @@ export interface TileMapVisParams { mapCenter: [number, number]; wms: WMSOptions; } + +export interface TileMapVisConfig extends TileMapVisParams { + dimensions: TileMapVisDimensions; +} diff --git a/src/plugins/maps_legacy/public/map/convert_to_geojson.js b/src/plugins/tile_map/public/utils/convert_to_geojson.ts similarity index 75% rename from src/plugins/maps_legacy/public/map/convert_to_geojson.js rename to src/plugins/tile_map/public/utils/convert_to_geojson.ts index bca21e4deea97..34fa0d8d4a6c0 100644 --- a/src/plugins/maps_legacy/public/map/convert_to_geojson.js +++ b/src/plugins/tile_map/public/utils/convert_to_geojson.ts @@ -17,11 +17,17 @@ * under the License. */ +import { Feature } from 'geojson'; +import type { Datatable } from '../../../expressions/public'; +import type { TileMapVisDimensions, TileMapVisData } from '../types'; import { decodeGeoHash } from './decode_geo_hash'; import { gridDimensions } from './grid_dimensions'; -export function convertToGeoJson(tabifiedResponse, { geohash, geocentroid, metric }) { - let features; +export function convertToGeoJson( + tabifiedResponse: Datatable, + { geohash, geocentroid, metric }: TileMapVisDimensions +): TileMapVisData { + let features: Feature[]; let min = Infinity; let max = -Infinity; @@ -41,7 +47,7 @@ export function convertToGeoJson(tabifiedResponse, { geohash, geocentroid, metri if (!geohashValue) return false; const geohashLocation = decodeGeoHash(geohashValue); - let pointCoordinates; + let pointCoordinates: number[]; if (geocentroidColumn) { const location = row[geocentroidColumn.id]; pointCoordinates = [location.lon, location.lat]; @@ -58,7 +64,7 @@ export function convertToGeoJson(tabifiedResponse, { geohash, geocentroid, metri const centerLatLng = [geohashLocation.latitude[2], geohashLocation.longitude[2]]; - if (geohash.params.useGeocentroid) { + if (geohash?.params.useGeocentroid) { // see https://github.com/elastic/elasticsearch/issues/24694 for why clampGrid is used pointCoordinates[0] = clampGrid( pointCoordinates[0], @@ -86,35 +92,41 @@ export function convertToGeoJson(tabifiedResponse, { geohash, geocentroid, metri geohash: geohashValue, geohash_meta: { center: centerLatLng, - rectangle: rectangle, + rectangle, }, - value: value, + value, }, - }; + } as Feature; }) - .filter((row) => row); + .filter((row): row is Feature => !!row); } } else { features = []; } - const featureCollection = { - type: 'FeatureCollection', - features: features, - }; - - return { - featureCollection: featureCollection, + const convertedData: TileMapVisData = { + featureCollection: { + type: 'FeatureCollection', + features, + }, meta: { - min: min, - max: max, - geohashPrecision: geohash && geohash.params.precision, - geohashGridDimensionsAtEquator: geohash && gridDimensions(geohash.params.precision), + min, + max, + geohashPrecision: geohash?.params.precision, + geohashGridDimensionsAtEquator: geohash?.params.precision + ? gridDimensions(geohash.params.precision) + : undefined, }, }; + + if (geohash && geohash.accessor) { + convertedData.meta.geohash = tabifiedResponse.columns[geohash.accessor].meta; + } + + return convertedData; } -function clampGrid(val, min, max) { +function clampGrid(val: number, min: number, max: number) { if (val > max) val = max; else if (val < min) val = min; return val; diff --git a/src/plugins/maps_legacy/public/map/decode_geo_hash.test.ts b/src/plugins/tile_map/public/utils/decode_geo_hash.test.ts similarity index 79% rename from src/plugins/maps_legacy/public/map/decode_geo_hash.test.ts rename to src/plugins/tile_map/public/utils/decode_geo_hash.test.ts index c1ca7e4c80383..5114314f2c0af 100644 --- a/src/plugins/maps_legacy/public/map/decode_geo_hash.test.ts +++ b/src/plugins/tile_map/public/utils/decode_geo_hash.test.ts @@ -17,14 +17,7 @@ * under the License. */ -import { geohashColumns, decodeGeoHash } from './decode_geo_hash'; - -test('geohashColumns', () => { - expect(geohashColumns(1)).toBe(8); - expect(geohashColumns(2)).toBe(8 * 4); - expect(geohashColumns(3)).toBe(8 * 4 * 8); - expect(geohashColumns(4)).toBe(8 * 4 * 8 * 4); -}); +import { decodeGeoHash } from './decode_geo_hash'; test('decodeGeoHash', () => { expect(decodeGeoHash('drm3btev3e86')).toEqual({ diff --git a/src/plugins/maps_legacy/public/map/decode_geo_hash.ts b/src/plugins/tile_map/public/utils/decode_geo_hash.ts similarity index 79% rename from src/plugins/maps_legacy/public/map/decode_geo_hash.ts rename to src/plugins/tile_map/public/utils/decode_geo_hash.ts index 65184a8244777..3ee8ab69e996f 100644 --- a/src/plugins/maps_legacy/public/map/decode_geo_hash.ts +++ b/src/plugins/tile_map/public/utils/decode_geo_hash.ts @@ -55,10 +55,11 @@ export function decodeGeoHash(geohash: string): DecodedGeoHash { }); lat[2] = (lat[0] + lat[1]) / 2; lon[2] = (lon[0] + lon[1]) / 2; + return { latitude: lat, longitude: lon, - } as DecodedGeoHash; + }; } function refineInterval(interval: number[], cd: number, mask: number) { @@ -69,26 +70,6 @@ function refineInterval(interval: number[], cd: number, mask: number) { } } -export function geohashColumns(precision: number): number { - return geohashCells(precision, 0); -} - -/** - * Get the number of geohash cells for a given precision - * - * @param {number} precision the geohash precision (1<=precision<=12). - * @param {number} axis constant for the axis 0=lengthwise (ie. columns, along longitude), 1=heightwise (ie. rows, along latitude). - * @returns {number} Number of geohash cells (rows or columns) at that precision - */ -function geohashCells(precision: number, axis: number) { - let cells = 1; - for (let i = 1; i <= precision; i += 1) { - /* On odd precisions, rows divide by 4 and columns by 8. Vice-versa on even precisions */ - cells *= i % 2 === axis ? 4 : 8; - } - return cells; -} - interface GeoBoundingBoxCoordinate { lat: number; lon: number; diff --git a/src/plugins/maps_legacy/public/map/grid_dimensions.js b/src/plugins/tile_map/public/utils/grid_dimensions.ts similarity index 92% rename from src/plugins/maps_legacy/public/map/grid_dimensions.js rename to src/plugins/tile_map/public/utils/grid_dimensions.ts index 0f84e972104ba..113909930598e 100644 --- a/src/plugins/maps_legacy/public/map/grid_dimensions.js +++ b/src/plugins/tile_map/public/utils/grid_dimensions.ts @@ -19,7 +19,7 @@ // geohash precision mapping of geohash grid cell dimensions (width x height, in meters) at equator. // https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-geohashgrid-aggregation.html#_cell_dimensions_at_the_equator -const gridAtEquator = { +const gridAtEquator: { [key: number]: [number, number] } = { 1: [5009400, 4992600], 2: [1252300, 624100], 3: [156500, 156000], @@ -34,6 +34,6 @@ const gridAtEquator = { 12: [0.037, 0.019], }; -export function gridDimensions(precision) { +export function gridDimensions(precision: number) { return gridAtEquator[precision]; } diff --git a/src/plugins/tile_map/public/utils/index.ts b/src/plugins/tile_map/public/utils/index.ts new file mode 100644 index 0000000000000..1c3dd02f48425 --- /dev/null +++ b/src/plugins/tile_map/public/utils/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { convertToGeoJson } from './convert_to_geojson'; +export { geoContains } from './decode_geo_hash'; diff --git a/src/plugins/maps_legacy/public/common/types/map_types.ts b/src/plugins/tile_map/public/utils/map_types.ts similarity index 100% rename from src/plugins/maps_legacy/public/common/types/map_types.ts rename to src/plugins/tile_map/public/utils/map_types.ts diff --git a/src/plugins/timelion/public/services/_saved_sheet.ts b/src/plugins/timelion/public/services/_saved_sheet.ts index 3fe66fabebe73..eafc8068fbfd1 100644 --- a/src/plugins/timelion/public/services/_saved_sheet.ts +++ b/src/plugins/timelion/public/services/_saved_sheet.ts @@ -69,5 +69,5 @@ export function createSavedSheetClass(savedObjects: SavedObjectsStart, config: I } } - return SavedSheet; + return SavedSheet as unknown; } diff --git a/src/plugins/timelion/tsconfig.json b/src/plugins/timelion/tsconfig.json new file mode 100644 index 0000000000000..5b96d69a878ea --- /dev/null +++ b/src/plugins/timelion/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "public/**/*", + "server/**/*" + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../data/tsconfig.json" }, + { "path": "../visualizations/tsconfig.json" }, + { "path": "../navigation/tsconfig.json" }, + { "path": "../vis_type_timelion/tsconfig.json" }, + { "path": "../saved_objects/tsconfig.json" }, + { "path": "../kibana_legacy/tsconfig.json" }, + ] +} diff --git a/src/plugins/vis_default_editor/public/components/controls/palette_picker.test.tsx b/src/plugins/vis_default_editor/public/components/controls/palette_picker.test.tsx new file mode 100644 index 0000000000000..1b4d7e060296a --- /dev/null +++ b/src/plugins/vis_default_editor/public/components/controls/palette_picker.test.tsx @@ -0,0 +1,59 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { ReactWrapper } from 'enzyme'; +import { PalettePicker, PalettePickerProps } from './palette_picker'; +import { chartPluginMock } from '../../../../charts/public/mocks'; +import { EuiColorPalettePicker } from '@elastic/eui'; + +describe('PalettePicker', function () { + let props: PalettePickerProps<'palette'>; + let component: ReactWrapper>; + + beforeAll(() => { + props = { + palettes: chartPluginMock.createPaletteRegistry(), + paramName: 'palette', + activePalette: { + type: 'palette', + name: 'kibana_palette', + }, + setPalette: jest.fn(), + }; + }); + + it('renders the EuiPalettePicker', () => { + component = mountWithIntl(); + expect(component.find(EuiColorPalettePicker).length).toBe(1); + }); + + it('renders the default palette if not activePalette is given', function () { + const { activePalette, ...newProps } = props; + component = mountWithIntl(); + const palettePicker = component.find(EuiColorPalettePicker); + expect(palettePicker.props().valueOfSelected).toBe('default'); + }); + + it('renders the activePalette palette if given', function () { + component = mountWithIntl(); + const palettePicker = component.find(EuiColorPalettePicker); + expect(palettePicker.props().valueOfSelected).toBe('kibana_palette'); + }); +}); diff --git a/src/plugins/vis_default_editor/public/components/controls/palette_picker.tsx b/src/plugins/vis_default_editor/public/components/controls/palette_picker.tsx new file mode 100644 index 0000000000000..8985ded413d46 --- /dev/null +++ b/src/plugins/vis_default_editor/public/components/controls/palette_picker.tsx @@ -0,0 +1,75 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; +import { EuiColorPalettePicker } from '@elastic/eui'; +import { EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export interface PalettePickerProps { + activePalette?: PaletteOutput; + palettes: PaletteRegistry; + paramName: ParamName; + setPalette: (paramName: ParamName, value: PaletteOutput) => void; +} + +export function PalettePicker({ + activePalette, + palettes, + paramName, + setPalette, +}: PalettePickerProps) { + return ( + + !internal) + .map(({ id, title, getColors }) => { + return { + value: id, + title, + type: 'fixed', + palette: getColors( + 10, + id === activePalette?.name ? activePalette?.params : undefined + ), + }; + })} + onChange={(newPalette) => { + setPalette(paramName, { + type: 'palette', + name: newPalette, + }); + }} + valueOfSelected={activePalette?.name || 'default'} + selectionDisplay={'palette'} + /> + + ); +} diff --git a/src/plugins/vis_default_editor/public/default_editor.tsx b/src/plugins/vis_default_editor/public/default_editor.tsx index 719766544d4f2..f5e83396136a8 100644 --- a/src/plugins/vis_default_editor/public/default_editor.tsx +++ b/src/plugins/vis_default_editor/public/default_editor.tsx @@ -23,11 +23,8 @@ import 'brace/mode/json'; import React, { useEffect, useRef, useState, useCallback } from 'react'; import { EventEmitter } from 'events'; -import { - Vis, - VisualizeEmbeddableContract, - EditorRenderProps, -} from 'src/plugins/visualizations/public'; +import { Vis, VisualizeEmbeddableContract } from 'src/plugins/visualizations/public'; +import { EditorRenderProps } from 'src/plugins/visualize/public'; import { KibanaContextProvider, PanelsContainer, Panel } from '../../kibana_react/public'; import { Storage } from '../../kibana_utils/public'; diff --git a/src/plugins/vis_default_editor/public/default_editor_controller.tsx b/src/plugins/vis_default_editor/public/default_editor_controller.tsx index ebeb056cc89c1..2bf1d3f9f6747 100644 --- a/src/plugins/vis_default_editor/public/default_editor_controller.tsx +++ b/src/plugins/vis_default_editor/public/default_editor_controller.tsx @@ -22,12 +22,8 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { EventEmitter } from 'events'; import { EuiErrorBoundary, EuiLoadingChart } from '@elastic/eui'; -import { - Vis, - IEditorController, - EditorRenderProps, - VisualizeEmbeddableContract, -} from 'src/plugins/visualizations/public'; +import { Vis, VisualizeEmbeddableContract } from 'src/plugins/visualizations/public'; +import { IEditorController, EditorRenderProps } from 'src/plugins/visualize/public'; // @ts-ignore const DefaultEditor = lazy(() => import('./default_editor')); diff --git a/src/plugins/vis_default_editor/public/index.ts b/src/plugins/vis_default_editor/public/index.ts index 06834ab19c876..bd907095baeeb 100644 --- a/src/plugins/vis_default_editor/public/index.ts +++ b/src/plugins/vis_default_editor/public/index.ts @@ -23,6 +23,7 @@ import { VisDefaultEditorPlugin } from './plugin'; export { DefaultEditorController }; export { useValidation } from './components/controls/utils'; +export { PalettePicker } from './components/controls/palette_picker'; export * from './components/options'; export { RangesParamEditor, RangeValues } from './components/controls/ranges'; export * from './editor_size'; diff --git a/src/plugins/vis_default_editor/public/plugin.ts b/src/plugins/vis_default_editor/public/plugin.ts index a7a5c6146a6e8..669ed2173c4ce 100644 --- a/src/plugins/vis_default_editor/public/plugin.ts +++ b/src/plugins/vis_default_editor/public/plugin.ts @@ -30,7 +30,7 @@ export class VisDefaultEditorPlugin implements Plugin { public setup(core: CoreSetup, { visualize }: VisDefaultEditorSetupDependencies) { if (visualize) { - visualize.setDefaultEditor(DefaultEditorController); + visualize.visEditorsRegistry.registerDefault(DefaultEditorController); } } diff --git a/src/plugins/vis_default_editor/tsconfig.json b/src/plugins/vis_default_editor/tsconfig.json new file mode 100644 index 0000000000000..27bb775c2d0e8 --- /dev/null +++ b/src/plugins/vis_default_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": "../visualize/tsconfig.json" }, + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + ] +} diff --git a/src/plugins/vis_type_markdown/tsconfig.json b/src/plugins/vis_type_markdown/tsconfig.json new file mode 100644 index 0000000000000..d5ab89b98081b --- /dev/null +++ b/src/plugins/vis_type_markdown/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "public/**/*", + "server/**/*", + "*.ts" + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../expressions/tsconfig.json" }, + { "path": "../visualizations/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + { "path": "../vis_default_editor/tsconfig.json" }, + ] +} diff --git a/src/plugins/vis_type_metric/tsconfig.json b/src/plugins/vis_type_metric/tsconfig.json new file mode 100644 index 0000000000000..7441848d5a430 --- /dev/null +++ b/src/plugins/vis_type_metric/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["public/**/*", "server/**/*", "*.ts"], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../data/tsconfig.json" }, + { "path": "../visualizations/tsconfig.json" }, + { "path": "../charts/tsconfig.json" }, + { "path": "../expressions/tsconfig.json" }, + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../vis_default_editor/tsconfig.json" } + ] +} diff --git a/src/plugins/vis_type_table/public/legacy/vis_controller.ts b/src/plugins/vis_type_table/public/legacy/vis_controller.ts index eff8e34f3e778..7584397e78dc6 100644 --- a/src/plugins/vis_type_table/public/legacy/vis_controller.ts +++ b/src/plugins/vis_type_table/public/legacy/vis_controller.ts @@ -42,8 +42,8 @@ export function getTableVisualizationControllerClass( context: PluginInitializerContext ) { return class TableVisualizationController { - private tableVisModule: IModule | undefined; - private injector: auto.IInjectorService | undefined; + tableVisModule: IModule | undefined; + injector: auto.IInjectorService | undefined; el: JQuery; $rootScope: IRootScopeService | null = null; $scope: (IScope & { [key: string]: any }) | undefined; diff --git a/src/plugins/vis_type_table/tsconfig.json b/src/plugins/vis_type_table/tsconfig.json new file mode 100644 index 0000000000000..bda86d06c0ff7 --- /dev/null +++ b/src/plugins/vis_type_table/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "public/**/*", + "server/**/*", + "*.ts" + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../data/tsconfig.json" }, + { "path": "../visualizations/tsconfig.json" }, + { "path": "../share/tsconfig.json" }, + { "path": "../usage_collection/tsconfig.json" }, + { "path": "../expressions/tsconfig.json" }, + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../kibana_legacy/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + { "path": "../vis_default_editor/tsconfig.json" }, + ] +} diff --git a/src/plugins/vis_type_tagcloud/tsconfig.json b/src/plugins/vis_type_tagcloud/tsconfig.json new file mode 100644 index 0000000000000..18bbad2257466 --- /dev/null +++ b/src/plugins/vis_type_tagcloud/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "public/**/*", + "server/**/*", + "*.ts" + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../data/tsconfig.json" }, + { "path": "../expressions/tsconfig.json" }, + { "path": "../visualizations/tsconfig.json" }, + { "path": "../charts/tsconfig.json" }, + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + { "path": "../vis_default_editor/tsconfig.json" }, + ] +} diff --git a/src/plugins/vis_type_timelion/common/types.ts b/src/plugins/vis_type_timelion/common/types.ts index f7084948a14f7..fed380130b26d 100644 --- a/src/plugins/vis_type_timelion/common/types.ts +++ b/src/plugins/vis_type_timelion/common/types.ts @@ -19,7 +19,7 @@ type TimelionFunctionArgsTypes = 'seriesList' | 'number' | 'string' | 'boolean' | 'null'; -interface TimelionFunctionArgsSuggestion { +export interface TimelionFunctionArgsSuggestion { name: string; help: string; } diff --git a/src/plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts b/src/plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts index 7a630f36b51f4..896a827f6248c 100644 --- a/src/plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts +++ b/src/plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts @@ -73,12 +73,15 @@ export function getTimelionRequestHandler({ filters, query, visParams, + searchSessionId, }: { timeRange: TimeRange; filters: Filter[]; query: Query; visParams: TimelionVisParams; + searchSessionId?: string; }): Promise { + const dataSearch = getDataSearch(); const expression = visParams.expression; if (!expression) { @@ -93,7 +96,13 @@ export function getTimelionRequestHandler({ // parse the time range client side to make sure it behaves like other charts const timeRangeBounds = timefilter.calculateBounds(timeRange); - const sessionId = getDataSearch().session.getSessionId(); + const untrackSearch = + dataSearch.session.isCurrentSession(searchSessionId) && + dataSearch.session.trackSearch({ + abort: () => { + // TODO: support search cancellations + }, + }); try { return await http.post('/api/timelion/run', { @@ -110,7 +119,9 @@ export function getTimelionRequestHandler({ interval: visParams.interval, timezone, }, - sessionId, + ...(searchSessionId && { + searchSession: dataSearch.session.getSearchOptions(searchSessionId), + }), }), }); } catch (e) { @@ -125,6 +136,11 @@ export function getTimelionRequestHandler({ } else { throw e; } + } finally { + if (untrackSearch && dataSearch.session.isCurrentSession(searchSessionId)) { + // call `untrack` if this search still belongs to current session + untrackSearch(); + } } }; } diff --git a/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts b/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts index 2e8878b11e915..d4bf27ea53819 100644 --- a/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts +++ b/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts @@ -30,23 +30,21 @@ import { KibanaContext, Filter, Query, TimeRange } from '../../data/public'; type Input = KibanaContext | null; type Output = Promise>; -interface Arguments { - expression: string; - interval: string; -} - export interface TimelionRenderValue { visData: TimelionSuccessResponse; visType: 'timelion'; visParams: TimelionVisParams; } -export type TimelionVisParams = Arguments; +export interface TimelionVisParams { + expression: string; + interval: string; +} export type TimelionExpressionFunctionDefinition = ExpressionFunctionDefinition< 'timelion_vis', Input, - Arguments, + TimelionVisParams, Output >; @@ -72,7 +70,7 @@ export const getTimelionVisualizationConfig = ( help: '', }, }, - async fn(input, args) { + async fn(input, args, { getSearchSessionId }) { const timelionRequestHandler = getTimelionRequestHandler(dependencies); const visParams = { expression: args.expression, interval: args.interval }; @@ -82,6 +80,7 @@ export const getTimelionVisualizationConfig = ( query: get(input, 'query') as Query, filters: get(input, 'filters') as Filter[], visParams, + searchSessionId: getSearchSessionId(), }); response.visType = TIMELION_VIS_NAME; diff --git a/src/plugins/vis_type_timelion/server/routes/run.ts b/src/plugins/vis_type_timelion/server/routes/run.ts index 5766705d9873d..af5af35e53b3f 100644 --- a/src/plugins/vis_type_timelion/server/routes/run.ts +++ b/src/plugins/vis_type_timelion/server/routes/run.ts @@ -75,7 +75,13 @@ export function runRoute( to: schema.maybe(schema.string()), }) ), - sessionId: schema.maybe(schema.string()), + searchSession: schema.maybe( + schema.object({ + sessionId: schema.string(), + isRestore: schema.boolean({ defaultValue: false }), + isStored: schema.boolean({ defaultValue: false }), + }) + ), }), }, }, diff --git a/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js b/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js index f4ba36e4fdd67..b02baefea9919 100644 --- a/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js +++ b/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js @@ -60,19 +60,26 @@ describe('es', () => { }); }); - test('should call data search with sessionId', async () => { + test('should call data search with sessionId, isRestore and isStored', async () => { tlConfig = { ...stubRequestAndServer({ rawResponse: esResponse }), request: { body: { - sessionId: 1, + searchSession: { + sessionId: '1', + isRestore: true, + isStored: false, + }, }, }, }; await invoke(es, [5], tlConfig); - expect(tlConfig.context.search.search.mock.calls[0][1]).toHaveProperty('sessionId', 1); + const res = tlConfig.context.search.search.mock.calls[0][1]; + expect(res).toHaveProperty('sessionId', '1'); + expect(res).toHaveProperty('isRestore', true); + expect(res).toHaveProperty('isStored', false); }); test('returns a seriesList', () => { diff --git a/src/plugins/vis_type_timelion/server/series_functions/es/index.js b/src/plugins/vis_type_timelion/server/series_functions/es/index.js index 24b3668b5cd3c..579563088a0a8 100644 --- a/src/plugins/vis_type_timelion/server/series_functions/es/index.js +++ b/src/plugins/vis_type_timelion/server/series_functions/es/index.js @@ -133,7 +133,7 @@ export default new Datasource('es', { .search( body, { - sessionId: tlConfig.request?.body.sessionId, + ...tlConfig.request?.body.searchSession, }, tlConfig.context ) diff --git a/src/plugins/vis_type_timelion/tsconfig.json b/src/plugins/vis_type_timelion/tsconfig.json new file mode 100644 index 0000000000000..77f97de28366d --- /dev/null +++ b/src/plugins/vis_type_timelion/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + "*.ts" + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../visualizations/tsconfig.json" }, + { "path": "../data/tsconfig.json" }, + { "path": "../expressions/tsconfig.json" }, + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + { "path": "../vis_default_editor/tsconfig.json" }, + ] +} diff --git a/src/plugins/vis_type_timeseries/common/vis_schema.ts b/src/plugins/vis_type_timeseries/common/vis_schema.ts index 40198ab98026e..e3773aada4c19 100644 --- a/src/plugins/vis_type_timeseries/common/vis_schema.ts +++ b/src/plugins/vis_type_timeseries/common/vis_schema.ts @@ -284,5 +284,12 @@ export const visPayloadSchema = schema.object({ min: stringRequired, max: stringRequired, }), - sessionId: schema.maybe(schema.string()), + + searchSession: schema.maybe( + schema.object({ + sessionId: schema.string(), + isRestore: schema.boolean({ defaultValue: false }), + isStored: schema.boolean({ defaultValue: false }), + }) + ), }); diff --git a/src/plugins/vis_type_timeseries/kibana.json b/src/plugins/vis_type_timeseries/kibana.json index f2284726c463f..aa5eac84663ad 100644 --- a/src/plugins/vis_type_timeseries/kibana.json +++ b/src/plugins/vis_type_timeseries/kibana.json @@ -4,7 +4,6 @@ "kibanaVersion": "kibana", "server": true, "ui": true, - "requiredPlugins": ["charts", "data", "expressions", "visualizations"], - "optionalPlugins": ["usageCollection"], + "requiredPlugins": ["charts", "data", "expressions", "visualizations", "visualize"], "requiredBundles": ["kibanaUtils", "kibanaReact"] } diff --git a/src/plugins/vis_type_timeseries/public/application/editor_controller.js b/src/plugins/vis_type_timeseries/public/application/editor_controller.js index e147f4be6c46f..953ebc4e5b75e 100644 --- a/src/plugins/vis_type_timeseries/public/application/editor_controller.js +++ b/src/plugins/vis_type_timeseries/public/application/editor_controller.js @@ -22,6 +22,8 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { getUISettings, getI18n } from '../services'; import { VisEditor } from './components/vis_editor_lazy'; +export const TSVB_EDITOR_NAME = 'tsvbEditor'; + export class EditorController { constructor(el, vis, eventEmitter, embeddableHandler) { this.el = el; diff --git a/src/plugins/vis_type_timeseries/public/application/index.ts b/src/plugins/vis_type_timeseries/public/application/index.ts index 5e70169c4e483..feecedb78e095 100644 --- a/src/plugins/vis_type_timeseries/public/application/index.ts +++ b/src/plugins/vis_type_timeseries/public/application/index.ts @@ -18,5 +18,5 @@ */ // @ts-ignore -export { EditorController } from './editor_controller'; +export { EditorController, TSVB_EDITOR_NAME } from './editor_controller'; export * from './lib'; diff --git a/src/plugins/vis_type_timeseries/public/metrics_fn.ts b/src/plugins/vis_type_timeseries/public/metrics_fn.ts index 60acd35b22402..825b964270794 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_fn.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_fn.ts @@ -65,7 +65,7 @@ export const createMetricsFn = (): TimeseriesExpressionFunctionDefinition => ({ help: '', }, }, - async fn(input, args) { + async fn(input, args, { getSearchSessionId }) { const visParams: TimeseriesVisParams = JSON.parse(args.params); const uiState = JSON.parse(args.uiState); @@ -73,6 +73,7 @@ export const createMetricsFn = (): TimeseriesExpressionFunctionDefinition => ({ input, visParams, uiState, + searchSessionId: getSearchSessionId(), }); return { diff --git a/src/plugins/vis_type_timeseries/public/metrics_type.ts b/src/plugins/vis_type_timeseries/public/metrics_type.ts index 41dc26c8c130d..9a3df612e7391 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_type.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_type.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; -import { EditorController } from './application'; +import { TSVB_EDITOR_NAME } from './application'; import { PANEL_TYPES } from '../common/panel_types'; import { toExpressionAst } from './to_ast'; import { VIS_EVENT_TO_TRIGGER, VisGroups, VisParams } from '../../visualizations/public'; @@ -70,7 +70,9 @@ export const metricsVisDefinition = { tooltip_mode: 'show_all', }, }, - editor: EditorController, + editorConfig: { + editor: TSVB_EDITOR_NAME, + }, options: { showQueryBar: false, showFilterBar: false, diff --git a/src/plugins/vis_type_timeseries/public/plugin.ts b/src/plugins/vis_type_timeseries/public/plugin.ts index d36b3611680af..a7f8d0ec61a03 100644 --- a/src/plugins/vis_type_timeseries/public/plugin.ts +++ b/src/plugins/vis_type_timeseries/public/plugin.ts @@ -22,6 +22,8 @@ import './application/index.scss'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'kibana/public'; import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; import { VisualizationsSetup } from '../../visualizations/public'; +import { VisualizePluginSetup } from '../../visualize/public'; +import { EditorController, TSVB_EDITOR_NAME } from './application'; import { createMetricsFn } from './metrics_fn'; import { metricsVisDefinition } from './metrics_type'; @@ -43,6 +45,7 @@ export interface MetricsPluginSetupDependencies { expressions: ReturnType; visualizations: VisualizationsSetup; charts: ChartsPluginSetup; + visualize: VisualizePluginSetup; } /** @internal */ @@ -60,8 +63,9 @@ export class MetricsPlugin implements Plugin, void> { public async setup( core: CoreSetup, - { expressions, visualizations, charts }: MetricsPluginSetupDependencies + { expressions, visualizations, charts, visualize }: MetricsPluginSetupDependencies ) { + visualize.visEditorsRegistry.register(TSVB_EDITOR_NAME, EditorController); expressions.registerFunction(createMetricsFn); expressions.registerRenderer( getTimeseriesVisRenderer({ diff --git a/src/plugins/vis_type_timeseries/public/request_handler.ts b/src/plugins/vis_type_timeseries/public/request_handler.ts index aa45453515277..16b5e4c2dc4fa 100644 --- a/src/plugins/vis_type_timeseries/public/request_handler.ts +++ b/src/plugins/vis_type_timeseries/public/request_handler.ts @@ -29,39 +29,57 @@ interface MetricsRequestHandlerParams { input: KibanaContext | null; uiState: Record; visParams: TimeseriesVisParams; + searchSessionId?: string; } export const metricsRequestHandler = async ({ input, uiState, visParams, + searchSessionId, }: MetricsRequestHandlerParams): Promise => { const config = getUISettings(); const timezone = getTimezone(config); const uiStateObj = uiState[visParams.type] ?? {}; - const dataSearch = getDataStart(); - const parsedTimeRange = dataSearch.query.timefilter.timefilter.calculateBounds(input?.timeRange!); + const data = getDataStart(); + const dataSearch = getDataStart().search; + const parsedTimeRange = data.query.timefilter.timefilter.calculateBounds(input?.timeRange!); if (visParams && visParams.id && !visParams.isModelInvalid) { const maxBuckets = config.get(MAX_BUCKETS_SETTING); validateInterval(parsedTimeRange, visParams, maxBuckets); - const resp = await getCoreStart().http.post(ROUTES.VIS_DATA, { - body: JSON.stringify({ - timerange: { - timezone, - ...parsedTimeRange, + const untrackSearch = + dataSearch.session.isCurrentSession(searchSessionId) && + dataSearch.session.trackSearch({ + abort: () => { + // TODO: support search cancellations }, - query: input?.query, - filters: input?.filters, - panels: [visParams], - state: uiStateObj, - sessionId: dataSearch.search.session.getSessionId(), - }), - }); + }); - return resp; + try { + return await getCoreStart().http.post(ROUTES.VIS_DATA, { + body: JSON.stringify({ + timerange: { + timezone, + ...parsedTimeRange, + }, + query: input?.query, + filters: input?.filters, + panels: [visParams], + state: uiStateObj, + ...(searchSessionId && { + searchSession: dataSearch.session.getSearchOptions(searchSessionId), + }), + }), + }); + } finally { + if (untrackSearch && dataSearch.session.isCurrentSession(searchSessionId)) { + // untrack if this search still belongs to current session + untrackSearch(); + } + } } return {}; diff --git a/src/plugins/vis_type_timeseries/server/index.ts b/src/plugins/vis_type_timeseries/server/index.ts index 1037dc81b2b17..993a424e570e4 100644 --- a/src/plugins/vis_type_timeseries/server/index.ts +++ b/src/plugins/vis_type_timeseries/server/index.ts @@ -37,8 +37,6 @@ export const config: PluginConfigDescriptor = { schema: configSchema, }; -export { ValidationTelemetryServiceSetup } from './validation_telemetry'; - export { AbstractSearchStrategy, ReqFacade, diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js index 3e3e654617e76..704f865bf7f6e 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js @@ -66,7 +66,11 @@ describe('AbstractSearchStrategy', () => { const responses = await abstractSearchStrategy.search( { payload: { - sessionId: 1, + searchSession: { + sessionId: '1', + isRestore: false, + isStored: true, + }, }, requestContext: { search: { search: searchFn }, @@ -85,7 +89,9 @@ describe('AbstractSearchStrategy', () => { indexType: undefined, }, { - sessionId: 1, + sessionId: '1', + isRestore: false, + isStored: true, } ); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts index e27241c5eef62..77da99fb29330 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts @@ -66,7 +66,6 @@ const toSanitizedFieldType = (fields: IFieldType[]) => { export abstract class AbstractSearchStrategy { async search(req: ReqFacade, bodies: any[], indexType?: string) { const requests: any[] = []; - const { sessionId } = req.payload; bodies.forEach((body) => { requests.push( @@ -78,9 +77,7 @@ export abstract class AbstractSearchStrategy { ...body, }, }, - { - sessionId, - } + req.payload.searchSession ) .toPromise() ); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.ts b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.ts index 8950e05c85d4f..22dfc590245ac 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.ts +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/unit_to_seconds.ts @@ -37,7 +37,7 @@ const units: Record = { const sortedUnits = sortBy(Object.keys(units), (key: Unit) => units[key]); -interface ParsedInterval { +export interface ParsedInterval { value: number; unit: Unit; } diff --git a/src/plugins/vis_type_timeseries/server/plugin.ts b/src/plugins/vis_type_timeseries/server/plugin.ts index aabfa8a40e064..5dd67eaa21a84 100644 --- a/src/plugins/vis_type_timeseries/server/plugin.ts +++ b/src/plugins/vis_type_timeseries/server/plugin.ts @@ -31,7 +31,6 @@ import { Observable } from 'rxjs'; import { Server } from '@hapi/hapi'; import { VisTypeTimeseriesConfig } from './config'; import { getVisData, GetVisData, GetVisDataOptions } from './lib/get_vis_data'; -import { ValidationTelemetryService } from './validation_telemetry'; import { UsageCollectionSetup } from '../../usage_collection/server'; import { PluginStart } from '../../data/server'; import { visDataRoutes } from './routes/vis'; @@ -72,11 +71,8 @@ export interface Framework { } export class VisTypeTimeseriesPlugin implements Plugin { - private validationTelementryService: ValidationTelemetryService; - constructor(private readonly initializerContext: PluginInitializerContext) { this.initializerContext = initializerContext; - this.validationTelementryService = new ValidationTelemetryService(); } public setup( @@ -102,15 +98,8 @@ export class VisTypeTimeseriesPlugin implements Plugin { searchStrategyRegistry, }; - (async () => { - const validationTelemetry = await this.validationTelementryService.setup(core, { - ...plugins, - globalConfig$, - }); - visDataRoutes(router, framework, validationTelemetry); - - fieldsRoutes(framework); - })(); + visDataRoutes(router, framework); + fieldsRoutes(framework); return { getVisData: async ( diff --git a/src/plugins/vis_type_timeseries/server/routes/vis.ts b/src/plugins/vis_type_timeseries/server/routes/vis.ts index 3ed9aaaaea226..67e0676ec1f5c 100644 --- a/src/plugins/vis_type_timeseries/server/routes/vis.ts +++ b/src/plugins/vis_type_timeseries/server/routes/vis.ts @@ -23,16 +23,11 @@ import { ensureNoUnsafeProperties } from '@kbn/std'; import { getVisData, GetVisDataOptions } from '../lib/get_vis_data'; import { visPayloadSchema } from '../../common/vis_schema'; import { ROUTES } from '../../common/constants'; -import { ValidationTelemetryServiceSetup } from '../index'; import { Framework } from '../plugin'; const escapeHatch = schema.object({}, { unknowns: 'allow' }); -export const visDataRoutes = ( - router: IRouter, - framework: Framework, - { logFailedValidation }: ValidationTelemetryServiceSetup -) => { +export const visDataRoutes = (router: IRouter, framework: Framework) => { router.post( { path: ROUTES.VIS_DATA, @@ -52,9 +47,7 @@ export const visDataRoutes = ( try { visPayloadSchema.validate(request.body); } catch (error) { - logFailedValidation(); - - framework.logger.warn( + framework.logger.debug( `Request validation error: ${error.message}. This most likely means your TSVB visualization contains outdated configuration. You can report this problem under https://github.com/elastic/kibana/issues/new?template=Bug_report.md` ); } diff --git a/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts b/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts deleted file mode 100644 index dd748ea2d3815..0000000000000 --- a/src/plugins/vis_type_timeseries/server/saved_objects/tsvb_telemetry.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { flow } from 'lodash'; -import { SavedObjectMigrationFn, SavedObjectsType } from 'kibana/server'; - -const resetCount: SavedObjectMigrationFn = (doc) => ({ - ...doc, - attributes: { - ...doc.attributes, - failedRequests: 0, - }, -}); - -export const tsvbTelemetrySavedObjectType: SavedObjectsType = { - name: 'tsvb-validation-telemetry', - hidden: false, - namespaceType: 'agnostic', - mappings: { - properties: { - failedRequests: { - type: 'long', - }, - }, - }, - migrations: { - '7.7.0': flow(resetCount), - '7.8.0': flow(resetCount), - '7.9.0': flow(resetCount), - '7.10.0': flow(resetCount), - }, -}; diff --git a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts deleted file mode 100644 index 0e9196eb165e9..0000000000000 --- a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; -import { UsageCollectionSetup } from '../../../usage_collection/server'; -import { tsvbTelemetrySavedObjectType } from '../saved_objects'; - -export interface ValidationTelemetryServiceSetup { - logFailedValidation: () => void; -} -export interface Usage { - failed_validations: number; -} - -export class ValidationTelemetryService implements Plugin { - private kibanaIndex: string = ''; - async setup( - core: CoreSetup, - { - usageCollection, - globalConfig$, - }: { - usageCollection?: UsageCollectionSetup; - globalConfig$: PluginInitializerContext['config']['legacy']['globalConfig$']; - } - ) { - core.savedObjects.registerType(tsvbTelemetrySavedObjectType); - globalConfig$.subscribe((config) => { - this.kibanaIndex = config.kibana.index; - }); - if (usageCollection) { - usageCollection.registerCollector( - usageCollection.makeUsageCollector({ - type: 'tsvb-validation', - isReady: () => this.kibanaIndex !== '', - fetch: async ({ esClient }) => { - try { - const { body: response } = await esClient.get( - { - index: this.kibanaIndex, - id: 'tsvb-validation-telemetry:tsvb-validation-telemetry', - }, - { ignore: [404] } - ); - return { - failed_validations: - response?._source?.['tsvb-validation-telemetry']?.failedRequests || 0, - }; - } catch (err) { - return { - failed_validations: 0, - }; - } - }, - schema: { - failed_validations: { type: 'long' }, - }, - }) - ); - } - const internalRepositoryPromise = core - .getStartServices() - .then(([start]) => start.savedObjects.createInternalRepository()); - - return { - logFailedValidation: async () => { - try { - const internalRepository = await internalRepositoryPromise; - await internalRepository.incrementCounter( - 'tsvb-validation-telemetry', - 'tsvb-validation-telemetry', - ['failedRequests'] - ); - } catch (e) { - // swallow error, validation telemetry shouldn't fail anything else - } - }, - }; - } - start() {} -} diff --git a/src/plugins/vis_type_timeseries/tsconfig.json b/src/plugins/vis_type_timeseries/tsconfig.json new file mode 100644 index 0000000000000..7b2dd4b608c1c --- /dev/null +++ b/src/plugins/vis_type_timeseries/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + "*.ts" + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../charts/tsconfig.json" }, + { "path": "../data/tsconfig.json" }, + { "path": "../expressions/tsconfig.json" }, + { "path": "../visualizations/tsconfig.json" }, + { "path": "../visualize/tsconfig.json" }, + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + { "path": "../usage_collection/tsconfig.json" }, + ] +} diff --git a/src/plugins/vis_type_vega/public/data_model/search_api.ts b/src/plugins/vis_type_vega/public/data_model/search_api.ts index be58e3e3951eb..1a91079848eba 100644 --- a/src/plugins/vis_type_vega/public/data_model/search_api.ts +++ b/src/plugins/vis_type_vega/public/data_model/search_api.ts @@ -40,7 +40,8 @@ export class SearchAPI { constructor( private readonly dependencies: SearchAPIDependencies, private readonly abortSignal?: AbortSignal, - public readonly inspectorAdapters?: VegaInspectorAdapters + public readonly inspectorAdapters?: VegaInspectorAdapters, + private readonly searchSessionId?: string ) {} search(searchRequests: SearchRequest[]) { @@ -60,10 +61,7 @@ export class SearchAPI { } return search - .search( - { params }, - { abortSignal: this.abortSignal, sessionId: search.session.getSessionId() } - ) + .search({ params }, { abortSignal: this.abortSignal, sessionId: this.searchSessionId }) .pipe( tap((data) => this.inspectSearchResult(data, requestResponders[requestId])), map((data) => ({ diff --git a/src/plugins/vis_type_vega/public/services.ts b/src/plugins/vis_type_vega/public/services.ts index 43856c8324847..8d8a0e27ec225 100644 --- a/src/plugins/vis_type_vega/public/services.ts +++ b/src/plugins/vis_type_vega/public/services.ts @@ -45,4 +45,3 @@ export const [getMapsLegacyConfig, setMapsLegacyConfig] = createGetterSetter getInjectedVars().enableExternalUrls; -export const getEmsTileLayerId = () => getMapsLegacyConfig().emsTileLayerId; diff --git a/src/plugins/vis_type_vega/public/vega_fn.ts b/src/plugins/vis_type_vega/public/vega_fn.ts index 5a8113aeeea11..70f01aa65f822 100644 --- a/src/plugins/vis_type_vega/public/vega_fn.ts +++ b/src/plugins/vis_type_vega/public/vega_fn.ts @@ -74,6 +74,7 @@ export const createVegaFn = ( query: get(input, 'query') as Query, filters: get(input, 'filters') as any, visParams: { spec: args.spec }, + searchSessionId: context.getSearchSessionId(), }); return { diff --git a/src/plugins/vis_type_vega/public/vega_request_handler.ts b/src/plugins/vis_type_vega/public/vega_request_handler.ts index f48b61ed70822..1f60feebe7efa 100644 --- a/src/plugins/vis_type_vega/public/vega_request_handler.ts +++ b/src/plugins/vis_type_vega/public/vega_request_handler.ts @@ -32,6 +32,7 @@ interface VegaRequestHandlerParams { filters: Filter; timeRange: TimeRange; visParams: VisParams; + searchSessionId?: string; } interface VegaRequestHandlerContext { @@ -52,6 +53,7 @@ export function createVegaRequestHandler( filters, query, visParams, + searchSessionId, }: VegaRequestHandlerParams) { if (!searchAPI) { searchAPI = new SearchAPI( @@ -61,7 +63,8 @@ export function createVegaRequestHandler( injectedMetadata: getInjectedMetadata(), }, context.abortSignal, - context.inspectorAdapters + context.inspectorAdapters, + searchSessionId ); } diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js index d925aaeeced66..ce3cd1549b0c9 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js @@ -21,41 +21,65 @@ import { i18n } from '@kbn/i18n'; import { vega } from '../lib/vega'; import { VegaBaseView } from './vega_base_view'; import { VegaMapLayer } from './vega_map_layer'; -import { getEmsTileLayerId, getUISettings } from '../services'; -import { lazyLoadMapsLegacyModules } from '../../../maps_legacy/public'; +import { getMapsLegacyConfig, getUISettings } from '../services'; +import { lazyLoadMapsLegacyModules, TMS_IN_YML_ID } from '../../../maps_legacy/public'; + +const isUserConfiguredTmsLayer = ({ tilemap }) => Boolean(tilemap.url); export class VegaMapView extends VegaBaseView { constructor(opts) { super(opts); } + async getMapStyleOptions() { + const isDarkMode = getUISettings().get('theme:darkMode'); + const mapsLegacyConfig = getMapsLegacyConfig(); + const tmsServices = await this._serviceSettings.getTMSServices(); + const mapConfig = this._parser.mapConfig; + + let mapStyle; + + if (mapConfig.mapStyle !== 'default') { + mapStyle = mapConfig.mapStyle; + } else { + if (isUserConfiguredTmsLayer(mapsLegacyConfig)) { + mapStyle = TMS_IN_YML_ID; + } else { + mapStyle = mapsLegacyConfig.emsTileLayerId.bright; + } + } + + const mapOptions = tmsServices.find((s) => s.id === mapStyle); + + if (!mapOptions) { + this.onWarn( + i18n.translate('visTypeVega.mapView.mapStyleNotFoundWarningMessage', { + defaultMessage: '{mapStyleParam} was not found', + values: { mapStyleParam: `"mapStyle":${mapStyle}` }, + }) + ); + return null; + } + + return { + ...mapOptions, + ...(await this._serviceSettings.getAttributesForTMSLayer(mapOptions, true, isDarkMode)), + }; + } + async _initViewCustomizations() { const mapConfig = this._parser.mapConfig; let baseMapOpts; let limitMinZ = 0; let limitMaxZ = 25; + // In some cases, Vega may be initialized twice, e.g. after awaiting... + if (!this._$container) return; + if (mapConfig.mapStyle !== false) { - const tmsServices = await this._serviceSettings.getTMSServices(); - // In some cases, Vega may be initialized twice, e.g. after awaiting... - if (!this._$container) return; - const emsTileLayerId = getEmsTileLayerId(); - const mapStyle = - mapConfig.mapStyle === 'default' ? emsTileLayerId.bright : mapConfig.mapStyle; - const isDarkMode = getUISettings().get('theme:darkMode'); - baseMapOpts = tmsServices.find((s) => s.id === mapStyle); - baseMapOpts = { - ...baseMapOpts, - ...(await this._serviceSettings.getAttributesForTMSLayer(baseMapOpts, true, isDarkMode)), - }; - if (!baseMapOpts) { - this.onWarn( - i18n.translate('visTypeVega.mapView.mapStyleNotFoundWarningMessage', { - defaultMessage: '{mapStyleParam} was not found', - values: { mapStyleParam: `"mapStyle": ${JSON.stringify(mapStyle)}` }, - }) - ); - } else { + baseMapOpts = await this.getMapStyleOptions(); + + if (baseMapOpts) { limitMinZ = baseMapOpts.minZoom; limitMaxZ = baseMapOpts.maxZoom; } diff --git a/src/plugins/vis_type_vislib/public/to_ast.test.ts b/src/plugins/vis_type_vislib/public/to_ast.test.ts index 48d3dfe254d0b..28049be291723 100644 --- a/src/plugins/vis_type_vislib/public/to_ast.test.ts +++ b/src/plugins/vis_type_vislib/public/to_ast.test.ts @@ -22,7 +22,7 @@ import { buildExpression } from '../../expressions/public'; import { BasicVislibParams } from './types'; import { toExpressionAst } from './to_ast'; -import { sampleAreaVis } from './sample_vis.test.mocks'; +import { sampleAreaVis } from '../../vis_type_xy/public/sample_vis.test.mocks'; jest.mock('../../expressions/public', () => ({ ...(jest.requireActual('../../expressions/public') as any), diff --git a/src/plugins/vis_type_vislib/public/to_ast_pie.test.ts b/src/plugins/vis_type_vislib/public/to_ast_pie.test.ts index 36a9a17341bc5..08a74a37f380c 100644 --- a/src/plugins/vis_type_vislib/public/to_ast_pie.test.ts +++ b/src/plugins/vis_type_vislib/public/to_ast_pie.test.ts @@ -21,7 +21,7 @@ import { Vis } from '../../visualizations/public'; import { buildExpression } from '../../expressions/public'; import { PieVisParams } from './pie'; -import { samplePieVis } from './sample_vis.test.mocks'; +import { samplePieVis } from '../../vis_type_xy/public/sample_vis.test.mocks'; import { toExpressionAst } from './to_ast_pie'; jest.mock('../../expressions/public', () => ({ diff --git a/src/plugins/vis_type_vislib/public/vis_controller.tsx b/src/plugins/vis_type_vislib/public/vis_controller.tsx index 2a32d19874c22..a536ac4b207ea 100644 --- a/src/plugins/vis_type_vislib/public/vis_controller.tsx +++ b/src/plugins/vis_type_vislib/public/vis_controller.tsx @@ -44,8 +44,8 @@ export const createVislibVisController = ( charts: ChartsPluginSetup ) => { return class VislibVisController { - private removeListeners?: () => void; - private unmountLegend?: () => void; + removeListeners?: () => void; + unmountLegend?: () => void; legendRef: RefObject; container: HTMLDivElement; diff --git a/src/plugins/vis_type_vislib/tsconfig.json b/src/plugins/vis_type_vislib/tsconfig.json new file mode 100644 index 0000000000000..74bc1440d9dbc --- /dev/null +++ b/src/plugins/vis_type_vislib/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": "../../core/tsconfig.json" }, + { "path": "../charts/tsconfig.json" }, + { "path": "../data/tsconfig.json" }, + { "path": "../expressions/tsconfig.json" }, + { "path": "../visualizations/tsconfig.json" }, + { "path": "../kibana_legacy/tsconfig.json" }, + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../vis_default_editor/tsconfig.json" }, + { "path": "../vis_type_xy/tsconfig.json" }, + ] +} diff --git a/src/plugins/vis_type_xy/public/components/xy_settings.tsx b/src/plugins/vis_type_xy/public/components/xy_settings.tsx index 3682fdf3350b0..18099173c56a9 100644 --- a/src/plugins/vis_type_xy/public/components/xy_settings.tsx +++ b/src/plugins/vis_type_xy/public/components/xy_settings.tsx @@ -32,6 +32,8 @@ import { LegendColorPicker, TooltipProps, TickFormatter, + VerticalAlignment, + HorizontalAlignment, } from '@elastic/charts'; import { renderEndzoneTooltip } from '../../../charts/public'; @@ -70,6 +72,27 @@ type XYSettingsProps = Pick< legendPosition: Position; }; +function getValueLabelsStyling(isHorizontal: boolean) { + const VALUE_LABELS_MAX_FONTSIZE = 15; + const VALUE_LABELS_MIN_FONTSIZE = 10; + const VALUE_LABELS_VERTICAL_OFFSET = -10; + const VALUE_LABELS_HORIZONTAL_OFFSET = 10; + + return { + displayValue: { + fontSize: { min: VALUE_LABELS_MIN_FONTSIZE, max: VALUE_LABELS_MAX_FONTSIZE }, + fill: { textInverted: true, textBorder: 2 }, + alignment: isHorizontal + ? { + vertical: VerticalAlignment.Middle, + } + : { horizontal: HorizontalAlignment.Center }, + offsetX: isHorizontal ? VALUE_LABELS_HORIZONTAL_OFFSET : 0, + offsetY: isHorizontal ? 0 : VALUE_LABELS_VERTICAL_OFFSET, + }, + }; +} + export const XYSettings: FC = ({ markSizeRatio, rotation, @@ -92,10 +115,7 @@ export const XYSettings: FC = ({ const theme = themeService.useChartsTheme(); const baseTheme = themeService.useChartsBaseTheme(); const dimmingOpacity = getUISettings().get('visualization:dimmingOpacity'); - const fontSize = - typeof theme.barSeriesStyle?.displayValue?.fontSize === 'number' - ? { min: theme.barSeriesStyle?.displayValue?.fontSize } - : theme.barSeriesStyle?.displayValue?.fontSize ?? { min: 8 }; + const valueLabelsStyling = getValueLabelsStyling(rotation === 90 || rotation === -90); const themeOverrides: PartialTheme = { markSizeRatio, @@ -105,13 +125,7 @@ export const XYSettings: FC = ({ }, }, barSeriesStyle: { - displayValue: { - fontSize, - alignment: { - horizontal: 'center', - vertical: 'middle', - }, - }, + ...valueLabelsStyling, }, axes: { axisTitle: { diff --git a/src/plugins/vis_type_xy/public/editor/components/options/point_series/elastic_charts_options.tsx b/src/plugins/vis_type_xy/public/editor/components/options/point_series/elastic_charts_options.tsx index a3e573741644c..08fd0155b0d4b 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/point_series/elastic_charts_options.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/point_series/elastic_charts_options.tsx @@ -17,20 +17,26 @@ * under the License. */ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { METRIC_TYPE } from '@kbn/analytics'; -import { SelectOption, SwitchOption } from '../../../../../../vis_default_editor/public'; +import { + SelectOption, + SwitchOption, + PalettePicker, +} from '../../../../../../vis_default_editor/public'; +import { PaletteRegistry } from '../../../../../../charts/public'; import { ChartType } from '../../../../../common'; import { VisParams } from '../../../../types'; import { ValidationVisOptionsProps } from '../../common'; -import { getTrackUiMetric } from '../../../../services'; +import { getPalettesService, getTrackUiMetric } from '../../../../services'; export function ElasticChartsOptions(props: ValidationVisOptionsProps) { const trackUiMetric = getTrackUiMetric(); + const [palettesRegistry, setPalettesRegistry] = useState(null); const { stateParams, setValue, vis, aggs } = props; const hasLineChart = stateParams.seriesParams.some( @@ -39,6 +45,14 @@ export function ElasticChartsOptions(props: ValidationVisOptionsProps aggs.aggs.find(({ id }) => id === paramId)?.enabled ); + useEffect(() => { + const fetchPalettes = async () => { + const palettes = await getPalettesService().getPalettes(); + setPalettesRegistry(palettes); + }; + fetchPalettes(); + }, []); + return ( <> }} /> )} + + {palettesRegistry && ( + { + if (trackUiMetric) { + trackUiMetric(METRIC_TYPE.CLICK, 'palette_selected'); + } + setValue(paramName, value); + }} + /> + )} ); } diff --git a/src/plugins/vis_type_xy/public/plugin.ts b/src/plugins/vis_type_xy/public/plugin.ts index ab22ae57ebbdf..cdacc1bfaca6a 100644 --- a/src/plugins/vis_type_xy/public/plugin.ts +++ b/src/plugins/vis_type_xy/public/plugin.ts @@ -29,10 +29,10 @@ import { setDataActions, setFormatService, setThemeService, - setColorsService, setTimefilter, setUISettings, setDocLinks, + setPalettesService, setTrackUiMetric, } from './services'; import { visTypesDefinitions } from './vis_types'; @@ -77,8 +77,7 @@ export class VisTypeXyPlugin if (!core.uiSettings.get(LEGACY_CHARTS_LIBRARY, false)) { setUISettings(core.uiSettings); setThemeService(charts.theme); - setColorsService(charts.legacyColors); - + setPalettesService(charts.palettes); [createVisTypeXyVisFn].forEach(expressions.registerFunction); expressions.registerRenderer(xyVisRenderer); visTypesDefinitions.forEach(visualizations.createBaseVisualization); diff --git a/src/plugins/vis_type_vislib/public/sample_vis.test.mocks.ts b/src/plugins/vis_type_xy/public/sample_vis.test.mocks.ts similarity index 100% rename from src/plugins/vis_type_vislib/public/sample_vis.test.mocks.ts rename to src/plugins/vis_type_xy/public/sample_vis.test.mocks.ts diff --git a/src/plugins/vis_type_xy/public/services.ts b/src/plugins/vis_type_xy/public/services.ts index 086cab8fb217a..67d38d6647a98 100644 --- a/src/plugins/vis_type_xy/public/services.ts +++ b/src/plugins/vis_type_xy/public/services.ts @@ -43,9 +43,9 @@ export const [getThemeService, setThemeService] = createGetterSetter('xy charts.color'); +export const [getPalettesService, setPalettesService] = createGetterSetter< + ChartsPluginSetup['palettes'] +>('xy charts.palette'); export const [getDocLinks, setDocLinks] = createGetterSetter('DocLinks'); diff --git a/src/plugins/vis_type_xy/public/to_ast.test.ts b/src/plugins/vis_type_xy/public/to_ast.test.ts index 678a9faaec585..b6dedd694eb95 100644 --- a/src/plugins/vis_type_xy/public/to_ast.test.ts +++ b/src/plugins/vis_type_xy/public/to_ast.test.ts @@ -19,7 +19,7 @@ import { Vis } from '../../visualizations/public'; import { buildExpression } from '../../expressions/public'; -import { sampleAreaVis } from '../../vis_type_vislib/public/sample_vis.test.mocks'; +import { sampleAreaVis } from './sample_vis.test.mocks'; import { toExpressionAst } from './to_ast'; import { VisParams } from './types'; diff --git a/src/plugins/vis_type_xy/public/types/param.ts b/src/plugins/vis_type_xy/public/types/param.ts index c8cd020dec03c..69b5fe6158bd7 100644 --- a/src/plugins/vis_type_xy/public/types/param.ts +++ b/src/plugins/vis_type_xy/public/types/param.ts @@ -18,7 +18,7 @@ */ import { Fit, Position } from '@elastic/charts'; -import { Style, Labels } from '../../../charts/public'; +import { Style, Labels, PaletteOutput } from '../../../charts/public'; import { SchemaConfig } from '../../../visualizations/public'; import { ChartType } from '../../common'; @@ -156,5 +156,6 @@ export interface VisParams { * Add for detailed tooltip option */ detailedTooltip?: boolean; + palette: PaletteOutput; fittingFunction?: Exclude; } diff --git a/src/plugins/vis_type_xy/public/utils/domain.ts b/src/plugins/vis_type_xy/public/utils/domain.ts index 132c8df71428b..b5e3c15a42c43 100644 --- a/src/plugins/vis_type_xy/public/utils/domain.ts +++ b/src/plugins/vis_type_xy/public/utils/domain.ts @@ -52,7 +52,7 @@ export const getAdjustedDomain = ( data: Datatable['rows'], { accessor, params }: Aspect, timeZone: string, - domain?: DomainRange, + domain: DomainRange | undefined, hasBars?: boolean ): DomainRange => { if ( diff --git a/src/plugins/vis_type_xy/public/utils/get_all_series.test.ts b/src/plugins/vis_type_xy/public/utils/get_all_series.test.ts new file mode 100644 index 0000000000000..0f4f8fd297343 --- /dev/null +++ b/src/plugins/vis_type_xy/public/utils/get_all_series.test.ts @@ -0,0 +1,193 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { getAllSeries } from './get_all_series'; + +const rowsNoSplitSeries = [ + { + 'col-0-4': 'Kibana Airlines', + 'col-1-1': 85, + }, + { + 'col-0-4': 'ES-Air', + 'col-1-1': 84, + }, + { + 'col-0-4': 'Logstash Airways', + 'col-1-1': 82, + }, + { + 'col-0-4': 'JetBeats', + 'col-1-1': 81, + }, +]; + +const rowsWithSplitSeries = [ + { + 'col-0-4': 'ES-Air', + 'col-1-5': 0, + 'col-2-1': 71, + }, + { + 'col-0-4': 'ES-Air', + 'col-1-5': 1, + 'col-2-1': 14, + }, + { + 'col-0-4': 'Kibana Airlines', + 'col-1-5': 0, + 'col-2-1': 71, + }, + { + 'col-0-4': 'Kibana Airlines', + 'col-1-5': 1, + 'col-2-1': 13, + }, + { + 'col-0-4': 'JetBeats', + 'col-1-5': 0, + 'col-2-1': 72, + }, + { + 'col-0-4': 'JetBeats', + 'col-1-5': 1, + 'col-2-1': 9, + }, + { + 'col-0-4': 'Logstash Airways', + 'col-1-5': 0, + 'col-2-1': 71, + }, + { + 'col-0-4': 'Logstash Airways', + 'col-1-5': 1, + 'col-2-1': 10, + }, +]; + +const yAspects = [ + { + accessor: 'col-2-1', + column: 2, + title: 'Count', + format: { + id: 'number', + }, + aggType: 'count', + aggId: '1', + params: {}, + }, +]; + +const myltipleYAspects = [ + { + accessor: 'col-2-1', + column: 2, + title: 'Count', + format: { + id: 'number', + }, + aggType: 'count', + aggId: '1', + params: {}, + }, + { + accessor: 'col-3-4', + column: 3, + title: 'Average AvgTicketPrice', + format: { + id: 'number', + params: { + pattern: '$0,0.[00]', + }, + }, + aggType: 'avg', + aggId: '4', + params: {}, + }, +]; + +describe('getFilterClickData', () => { + it('returns empty array if splitAccessors is undefined', () => { + const splitAccessors = undefined; + const series = getAllSeries(rowsNoSplitSeries, splitAccessors, yAspects); + expect(series).toStrictEqual([]); + }); + + it('returns an array of series names if splitAccessors is an array', () => { + const splitAccessors = [ + { + accessor: 'col-1-5', + }, + ]; + const series = getAllSeries(rowsWithSplitSeries, splitAccessors, yAspects); + expect(series).toStrictEqual([0, 1]); + }); + + it('returns the correct array of series names for two splitAccessors without duplicates', () => { + const splitAccessors = [ + { + accessor: 'col-0-4', + }, + { + accessor: 'col-1-5', + }, + ]; + const series = getAllSeries(rowsWithSplitSeries, splitAccessors, yAspects); + expect(series).toStrictEqual([ + 'ES-Air - 0', + 'ES-Air - 1', + 'Kibana Airlines - 0', + 'Kibana Airlines - 1', + 'JetBeats - 0', + 'JetBeats - 1', + 'Logstash Airways - 0', + 'Logstash Airways - 1', + ]); + }); + + it('returns the correct array of series names for two splitAccessors and two y axis', () => { + const splitAccessors = [ + { + accessor: 'col-0-4', + }, + { + accessor: 'col-1-5', + }, + ]; + const series = getAllSeries(rowsWithSplitSeries, splitAccessors, myltipleYAspects); + expect(series).toStrictEqual([ + 'ES-Air - 0: Count', + 'ES-Air - 0: Average AvgTicketPrice', + 'ES-Air - 1: Count', + 'ES-Air - 1: Average AvgTicketPrice', + 'Kibana Airlines - 0: Count', + 'Kibana Airlines - 0: Average AvgTicketPrice', + 'Kibana Airlines - 1: Count', + 'Kibana Airlines - 1: Average AvgTicketPrice', + 'JetBeats - 0: Count', + 'JetBeats - 0: Average AvgTicketPrice', + 'JetBeats - 1: Count', + 'JetBeats - 1: Average AvgTicketPrice', + 'Logstash Airways - 0: Count', + 'Logstash Airways - 0: Average AvgTicketPrice', + 'Logstash Airways - 1: Count', + 'Logstash Airways - 1: Average AvgTicketPrice', + ]); + }); +}); diff --git a/src/plugins/vis_type_xy/public/utils/get_all_series.ts b/src/plugins/vis_type_xy/public/utils/get_all_series.ts new file mode 100644 index 0000000000000..8ffc59846c1fc --- /dev/null +++ b/src/plugins/vis_type_xy/public/utils/get_all_series.ts @@ -0,0 +1,62 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { TickFormatter } from '@elastic/charts'; +import { DatatableRow } from '../../../expressions/public'; +import { Column, Aspect } from '../types'; + +interface SplitAccessors { + accessor: Column['id']; + formatter?: TickFormatter; +} + +export const getAllSeries = ( + rows: DatatableRow[], + splitAccessors: SplitAccessors[] | undefined, + yAspects: Aspect[] +) => { + const allSeries: string[] = []; + if (!splitAccessors) return []; + + rows.forEach((row) => { + let seriesName = ''; + splitAccessors?.forEach(({ accessor, formatter }) => { + if (!accessor) return; + const name = formatter ? formatter(row[accessor]) : row[accessor]; + if (seriesName) { + seriesName += ` - ${name}`; + } else { + seriesName = name; + } + }); + + // multiple y axis + if (yAspects.length > 1) { + yAspects.forEach((aspect) => { + if (!allSeries.includes(`${seriesName}: ${aspect.title}`)) { + allSeries.push(`${seriesName}: ${aspect.title}`); + } + }); + } else { + if (!allSeries.includes(seriesName)) { + allSeries.push(seriesName); + } + } + }); + return allSeries; +}; diff --git a/src/plugins/vis_type_xy/public/utils/index.tsx b/src/plugins/vis_type_xy/public/utils/index.tsx index 20b5d7a3c8881..9143a57454d13 100644 --- a/src/plugins/vis_type_xy/public/utils/index.tsx +++ b/src/plugins/vis_type_xy/public/utils/index.tsx @@ -24,3 +24,4 @@ export { getSeriesNameFn } from './get_series_name_fn'; export { getXDomain, getAdjustedDomain } from './domain'; export { useColorPicker } from './use_color_picker'; export { getXAccessor } from './accessors'; +export { getAllSeries } from './get_all_series'; 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 05de38b7700a9..666dcee29fc9d 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 @@ -17,10 +17,10 @@ * under the License. */ -import React, { BaseSyntheticEvent, useMemo } from 'react'; +import React, { BaseSyntheticEvent, useCallback, useMemo } from 'react'; import { LegendColorPicker, Position, XYChartSeriesIdentifier, SeriesName } from '@elastic/charts'; -import { PopoverAnchorPosition, EuiWrappingPopover } from '@elastic/eui'; +import { PopoverAnchorPosition, EuiWrappingPopover, EuiOutsideClickDetector } from '@elastic/eui'; import { ColorPicker } from '../../../charts/public'; @@ -61,18 +61,26 @@ export const useColorPicker = ( onClose(); }; + // rule doesn't know this is inside a functional component + // eslint-disable-next-line react-hooks/rules-of-hooks + const handleOutsideClick = useCallback(() => { + onClose?.(); + }, [onClose]); + return ( - - - + + + + + ); }, [getSeriesName, legendPosition, setColor] diff --git a/src/plugins/vis_type_xy/public/vis_component.tsx b/src/plugins/vis_type_xy/public/vis_component.tsx index dcf9f69cc291d..8d27d0161785a 100644 --- a/src/plugins/vis_type_xy/public/vis_component.tsx +++ b/src/plugins/vis_type_xy/public/vis_component.tsx @@ -48,6 +48,7 @@ import { LegendToggle, getBrushFromChartBrushEventFn, ClickTriggerEvent, + PaletteRegistry, } from '../../charts/public'; import { Datatable, IInterpreterRenderHandlers } from '../../expressions/public'; import type { PersistedState } from '../../visualizations/public'; @@ -62,10 +63,11 @@ import { getLegendActions, useColorPicker, getXAccessor, + getAllSeries, } from './utils'; import { XYAxis, XYEndzones, XYCurrentTime, XYSettings, XYThresholdLine } from './components'; import { getConfig } from './config'; -import { getThemeService, getColorsService, getDataActions } from './services'; +import { getThemeService, getDataActions, getPalettesService } from './services'; import { ChartType } from '../common'; import './_chart.scss'; @@ -81,22 +83,19 @@ export interface VisComponentProps { uiState: PersistedState; fireEvent: IInterpreterRenderHandlers['event']; renderComplete: IInterpreterRenderHandlers['done']; + syncColors: boolean; } export type VisComponentType = typeof VisComponent; const VisComponent = (props: VisComponentProps) => { - /** - * Stores all series labels to replicate vislib color map lookup - */ - const allSeries: Array = useMemo(() => [], []); const [showLegend, setShowLegend] = useState(() => { // TODO: Check when this bwc can safely be removed const bwcLegendStateDefault = props.visParams.addLegend == null ? true : props.visParams.addLegend; return props.uiState?.get('vis.legendOpen', bwcLegendStateDefault) as boolean; }); - + const [palettesRegistry, setPalettesRegistry] = useState(null); useEffect(() => { const fn = () => { props?.uiState?.emit?.('reload'); @@ -117,6 +116,14 @@ const VisComponent = (props: VisComponentProps) => { [props] ); + useEffect(() => { + const fetchPalettes = async () => { + const palettes = await getPalettesService().getPalettes(); + setPalettesRegistry(palettes); + }; + fetchPalettes(); + }, []); + const handleFilterClick = useCallback( ( visData: Datatable, @@ -225,7 +232,8 @@ const VisComponent = (props: VisComponentProps) => { [props.uiState] ); - const { visData, visParams } = props; + const { visData, visParams, syncColors } = props; + const config = getConfig(visData, visParams); const timeZone = getTimeZone(); const xDomain = @@ -245,21 +253,58 @@ const VisComponent = (props: VisComponentProps) => { const isDarkMode = getThemeService().useDarkMode(); const getSeriesName = getSeriesNameFn(config.aspects, config.aspects.y.length > 1); + const splitAccessors = config.aspects.series?.map(({ accessor, formatter }) => { + return { accessor, formatter }; + }); + + const allSeries = useMemo(() => getAllSeries(visData.rows, splitAccessors, config.aspects.y), [ + config.aspects.y, + splitAccessors, + visData.rows, + ]); + const getSeriesColor = useCallback( (series: XYChartSeriesIdentifier) => { - const seriesName = getSeriesName(series); + const seriesName = getSeriesName(series) as string; if (!seriesName) { - return; + return null; } - const overwriteColors: Record = props.uiState?.get ? props.uiState.get('vis.colors', {}) : {}; - allSeries.push(seriesName); - return getColorsService().createColorLookupFunction(allSeries, overwriteColors)(seriesName); + if (Object.keys(overwriteColors).includes(seriesName)) { + return overwriteColors[seriesName]; + } + const outputColor = palettesRegistry?.get(visParams.palette.name).getColor( + [ + { + name: seriesName, + rankAtDepth: splitAccessors + ? allSeries.findIndex((name) => name === seriesName) + : config.aspects.y.findIndex((aspect) => aspect.accessor === series.yAccessor), + totalSeriesAtDepth: splitAccessors ? allSeries.length : config.aspects.y.length, + }, + ], + { + maxDepth: 1, + totalSeries: splitAccessors ? allSeries.length : config.aspects.y.length, + behindText: false, + syncColors, + } + ); + return outputColor || null; }, - [allSeries, getSeriesName, props.uiState] + [ + allSeries, + config.aspects.y, + getSeriesName, + props.uiState, + splitAccessors, + syncColors, + visParams.palette.name, + palettesRegistry, + ] ); const xAccessor = getXAccessor(config.aspects.x); const splitSeriesAccessors = config.aspects.series diff --git a/src/plugins/vis_type_xy/public/vis_renderer.tsx b/src/plugins/vis_type_xy/public/vis_renderer.tsx index 16463abb07631..ee0c9983435e9 100644 --- a/src/plugins/vis_type_xy/public/vis_renderer.tsx +++ b/src/plugins/vis_type_xy/public/vis_renderer.tsx @@ -45,9 +45,9 @@ export const xyVisRenderer: ExpressionRenderDefinition = { name: visName, displayName: 'XY visualization', reuseDomNode: true, - render: (domNode, { visData, visConfig, visType }, handlers) => { + render: async (domNode, { visData, visConfig, visType, syncColors }, handlers) => { const showNoResult = shouldShowNoResultsMessage(visData, visType); - const isSplitChart = Boolean(visConfig.dimensions.splitRow || visConfig.dimensions.splitRow); + const isSplitChart = Boolean(visConfig.dimensions.splitRow); handlers.onDestroy(() => unmountComponentAtNode(domNode)); render( @@ -61,6 +61,7 @@ export const xyVisRenderer: ExpressionRenderDefinition = { renderComplete={handlers.done} fireEvent={handlers.event} uiState={handlers.uiState as PersistedState} + syncColors={syncColors} /> diff --git a/src/plugins/vis_type_xy/public/vis_types/area.tsx b/src/plugins/vis_type_xy/public/vis_types/area.tsx index 58423d2f619fa..f04105470e01d 100644 --- a/src/plugins/vis_type_xy/public/vis_types/area.tsx +++ b/src/plugins/vis_type_xy/public/vis_types/area.tsx @@ -119,6 +119,10 @@ export const getAreaVisTypeDefinition = ( ], addTooltip: true, detailedTooltip: true, + palette: { + type: 'palette', + name: 'default', + }, addLegend: true, legendPosition: Position.Right, fittingFunction: Fit.Linear, diff --git a/src/plugins/vis_type_xy/public/vis_types/histogram.tsx b/src/plugins/vis_type_xy/public/vis_types/histogram.tsx index 5bc5f1b49e5da..9e032b34c98de 100644 --- a/src/plugins/vis_type_xy/public/vis_types/histogram.tsx +++ b/src/plugins/vis_type_xy/public/vis_types/histogram.tsx @@ -122,6 +122,10 @@ export const getHistogramVisTypeDefinition = ( radiusRatio: 0, addTooltip: true, detailedTooltip: true, + palette: { + type: 'palette', + name: 'default', + }, addLegend: true, legendPosition: Position.Right, times: [], diff --git a/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.tsx b/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.tsx index 3029b3dcd6765..4b1e95368d9fc 100644 --- a/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.tsx +++ b/src/plugins/vis_type_xy/public/vis_types/horizontal_bar.tsx @@ -122,6 +122,10 @@ export const getHorizontalBarVisTypeDefinition = ( ], addTooltip: true, detailedTooltip: true, + palette: { + type: 'palette', + name: 'default', + }, addLegend: true, legendPosition: Position.Right, times: [], diff --git a/src/plugins/vis_type_xy/public/vis_types/line.tsx b/src/plugins/vis_type_xy/public/vis_types/line.tsx index e0f83ce649d23..e146b91a6141b 100644 --- a/src/plugins/vis_type_xy/public/vis_types/line.tsx +++ b/src/plugins/vis_type_xy/public/vis_types/line.tsx @@ -119,6 +119,10 @@ export const getLineVisTypeDefinition = ( ], addTooltip: true, detailedTooltip: true, + palette: { + type: 'palette', + name: 'default', + }, addLegend: true, legendPosition: Position.Right, fittingFunction: Fit.Linear, diff --git a/src/plugins/vis_type_xy/public/xy_vis_fn.ts b/src/plugins/vis_type_xy/public/xy_vis_fn.ts index 47c4caf243b36..f24b7391c1391 100644 --- a/src/plugins/vis_type_xy/public/xy_vis_fn.ts +++ b/src/plugins/vis_type_xy/public/xy_vis_fn.ts @@ -34,6 +34,7 @@ export interface RenderValue { visData: Datatable; visType: ChartType; visConfig: VisParams; + syncColors: boolean; } export type VisTypeXyExpressionFunctionDefinition = ExpressionFunctionDefinition< @@ -80,6 +81,7 @@ export const createVisTypeXyVisFn = (): VisTypeXyExpressionFunctionDefinition => visType, visConfig, visData: context, + syncColors: handlers?.isSyncColorsEnabled?.() ?? false, }, }; }, diff --git a/src/plugins/vis_type_xy/tsconfig.json b/src/plugins/vis_type_xy/tsconfig.json new file mode 100644 index 0000000000000..5cb0bc8d0bc8e --- /dev/null +++ b/src/plugins/vis_type_xy/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*" + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../charts/tsconfig.json" }, + { "path": "../data/tsconfig.json" }, + { "path": "../expressions/tsconfig.json" }, + { "path": "../visualizations/tsconfig.json" }, + { "path": "../usage_collection/tsconfig.json" }, + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../vis_default_editor/tsconfig.json" }, + ] +} diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index 590b65e1af13d..4eaf70c98a26e 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -102,6 +102,7 @@ export class VisualizeEmbeddable private query?: Query; private filters?: Filter[]; private searchSessionId?: string; + private syncColors?: boolean; private visCustomizations?: Pick; private subscriptions: Subscription[] = []; private expression: string = ''; @@ -145,6 +146,7 @@ export class VisualizeEmbeddable ); this.deps = deps; this.timefilter = timefilter; + this.syncColors = this.input.syncColors; this.vis = vis; this.vis.uiState.on('change', this.uiStateChangeHandler); this.vis.uiState.on('reload', this.reload); @@ -248,6 +250,11 @@ export class VisualizeEmbeddable dirty = true; } + if (this.syncColors !== this.input.syncColors) { + this.syncColors = this.input.syncColors; + dirty = true; + } + if (this.vis.description && this.domNode) { this.domNode.setAttribute('data-description', this.vis.description); } @@ -377,6 +384,7 @@ export class VisualizeEmbeddable filters: this.input.filters, }, searchSessionId: this.input.searchSessionId, + syncColors: this.input.syncColors, uiState: this.vis.uiState, inspectorAdapters: this.inspectorAdapters, }; diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index ad56f6a34c368..854e04325b078 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -44,9 +44,6 @@ export type { ReactVisTypeOptions, Schema, ISchemas, - VisEditorConstructor, - IEditorController, - EditorRenderProps, } from './vis_types'; export { VisParams, SerializedVis, SerializedVisData, VisData } from './vis'; export type VisualizeEmbeddableFactoryContract = PublicContract; diff --git a/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap b/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap index 3ff0c83961e2a..3d685064111dc 100644 --- a/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap +++ b/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap @@ -1,9 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`visualize loader pipeline helpers: build pipeline buildPipeline calls toExpression on vis_type if it exists 1`] = `"kibana | kibana_context | test"`; - -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function with buckets 1`] = `"regionmap visConfig='{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},\\"bucket\\":1}' "`; - -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function without buckets 1`] = `"regionmap visConfig='{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}}' "`; - -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles tile_map function 1`] = `"tilemap visConfig='{\\"metric\\":{},\\"dimensions\\":{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},\\"geohash\\":1,\\"geocentroid\\":3}}' "`; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts index 57c58a99f09ea..1b86488c06354 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts @@ -17,14 +17,7 @@ * under the License. */ -import { - prepareJson, - prepareString, - buildPipelineVisFunction, - buildPipeline, - SchemaConfig, - Schemas, -} from './build_pipeline'; +import { prepareJson, prepareString, buildPipeline } from './build_pipeline'; import { Vis } from '..'; import { dataPluginMock } from '../../../../plugins/data/public/mocks'; import { parseExpression } from '../../../expressions/common'; @@ -74,53 +67,6 @@ describe('visualize loader pipeline helpers: build pipeline', () => { }); }); - describe('buildPipelineVisFunction', () => { - let schemaConfig: SchemaConfig; - let schemasDef: Schemas; - let uiState: any; - - beforeEach(() => { - schemaConfig = { - accessor: 0, - label: '', - format: {}, - params: {}, - aggType: '', - }; - - schemasDef = { metric: [schemaConfig] }; - uiState = {}; - }); - - describe('handles region_map function', () => { - it('without buckets', () => { - const params = { metric: {} }; - const actual = buildPipelineVisFunction.region_map(params, schemasDef, uiState); - expect(actual).toMatchSnapshot(); - }); - - it('with buckets', () => { - const schemas = { - ...schemasDef, - segment: [1, 2], - }; - const actual = buildPipelineVisFunction.region_map({}, schemas, uiState); - expect(actual).toMatchSnapshot(); - }); - }); - - it('handles tile_map function', () => { - const params = { metric: {} }; - const schemas = { - ...schemasDef, - segment: [1, 2], - geo_centroid: [3, 4], - }; - const actual = buildPipelineVisFunction.tile_map(params, schemas, uiState); - expect(actual).toMatchSnapshot(); - }); - }); - describe('buildPipeline', () => { const dataStart = dataPluginMock.createStartContract(); diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index 0b422802a7e1b..7c5991d498faf 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -23,9 +23,7 @@ import { SerializedFieldFormat, } from '../../../../plugins/expressions/public'; import { IAggConfig, search, TimefilterContract } from '../../../../plugins/data/public'; - -import { Vis, VisParams } from '../types'; - +import { Vis } from '../types'; const { isDateHistogramBucketAggConfig } = search.aggs; interface SchemaConfigParams { @@ -55,25 +53,6 @@ export interface Schemas { // catch all for schema name [key: string]: any[] | undefined; } - -type BuildVisFunction = ( - params: VisParams, - schemas: Schemas, - uiState: any, - meta?: { savedObjectId?: string } -) => string; - -// eslint-disable-next-line @typescript-eslint/naming-convention -type buildVisConfigFunction = (schemas: Schemas, visParams?: VisParams) => VisParams; - -interface BuildPipelineVisFunction { - [key: string]: BuildVisFunction; -} - -interface BuildVisConfigFunction { - [key: string]: buildVisConfigFunction; -} - export interface BuildPipelineParams { timefilter: TimefilterContract; timeRange?: any; @@ -224,43 +203,6 @@ export const prepareDimension = (variable: string, data: any) => { return expr; }; -export const buildPipelineVisFunction: BuildPipelineVisFunction = { - region_map: (params, schemas) => { - const visConfig = { - ...params, - ...buildVisConfig.region_map(schemas), - }; - return `regionmap ${prepareJson('visConfig', visConfig)}`; - }, - tile_map: (params, schemas) => { - const visConfig = { - ...params, - ...buildVisConfig.tile_map(schemas), - }; - return `tilemap ${prepareJson('visConfig', visConfig)}`; - }, -}; - -const buildVisConfig: BuildVisConfigFunction = { - region_map: (schemas) => { - const visConfig = {} as any; - visConfig.metric = schemas.metric[0]; - if (schemas.segment) { - visConfig.bucket = schemas.segment[0]; - } - return visConfig; - }, - tile_map: (schemas) => { - const visConfig = {} as any; - visConfig.dimensions = { - metric: schemas.metric[0], - geohash: schemas.segment ? schemas.segment[0] : null, - geocentroid: schemas.geo_centroid ? schemas.geo_centroid[0] : null, - }; - return visConfig; - }, -}; - export const buildPipeline = async (vis: Vis, params: BuildPipelineParams) => { const { indexPattern, searchSource } = vis.data; const query = searchSource!.getField('query'); @@ -299,17 +241,8 @@ export const buildPipeline = async (vis: Vis, params: BuildPipelineParams) => { }); } pipeline += `| `; - } - - const schemas = getSchemas(vis, params); - - if (buildPipelineVisFunction[vis.type.name]) { - pipeline += buildPipelineVisFunction[vis.type.name]( - { title, ...vis.params }, - schemas, - uiState - ); } else { + const schemas = getSchemas(vis, params); const visConfig = { ...vis.params }; visConfig.dimensions = schemas; visConfig.title = title; diff --git a/src/plugins/runtime_fields/public/index.ts b/src/plugins/visualizations/public/legacy/vis_update_state.d.ts similarity index 85% rename from src/plugins/runtime_fields/public/index.ts rename to src/plugins/visualizations/public/legacy/vis_update_state.d.ts index a7a94b07ac6e8..a400ac3d1500f 100644 --- a/src/plugins/runtime_fields/public/index.ts +++ b/src/plugins/visualizations/public/legacy/vis_update_state.d.ts @@ -17,12 +17,8 @@ * under the License. */ -export * from '../common'; +import { SavedVisState } from '../types'; -export function plugin() { - return { - setup() {}, - start() {}, - stop() {}, - }; -} +declare function updateOldState(oldState: unknown): SavedVisState; + +export { updateOldState }; diff --git a/src/plugins/visualizations/public/vis_types/base_vis_type.ts b/src/plugins/visualizations/public/vis_types/base_vis_type.ts index 2088f52428aa7..223fe1a852f57 100644 --- a/src/plugins/visualizations/public/vis_types/base_vis_type.ts +++ b/src/plugins/visualizations/public/vis_types/base_vis_type.ts @@ -27,7 +27,6 @@ interface CommonBaseVisTypeOptions extends Pick< VisType, | 'description' - | 'editor' | 'getInfoMessage' | 'getSupportedTriggers' | 'hierarchicalData' @@ -91,7 +90,6 @@ export class BaseVisType implements VisType public readonly options; public readonly visualization; public readonly visConfig; - public readonly editor; public readonly editorConfig; public hidden; public readonly requestHandler; @@ -119,7 +117,6 @@ export class BaseVisType implements VisType this.image = opts.image; this.visualization = opts.visualization; this.visConfig = defaultsDeep({}, opts.visConfig, { defaults: {} }); - this.editor = opts.editor; this.editorConfig = defaultsDeep({}, opts.editorConfig, { collections: {} }); this.options = defaultsDeep({}, opts.options, defaultOptions); this.stage = opts.stage ?? 'production'; diff --git a/src/plugins/visualizations/public/vis_types/index.ts b/src/plugins/visualizations/public/vis_types/index.ts index 68c613f11f8df..43de5d1ecce53 100644 --- a/src/plugins/visualizations/public/vis_types/index.ts +++ b/src/plugins/visualizations/public/vis_types/index.ts @@ -20,13 +20,6 @@ export * from './types_service'; export { Schemas } from './schemas'; export { VisGroups } from './types'; -export type { - VisType, - ISchemas, - Schema, - IEditorController, - VisEditorConstructor, - EditorRenderProps, -} from './types'; +export type { VisType, ISchemas, Schema } from './types'; export type { BaseVisTypeOptions } from './base_vis_type'; export type { ReactVisTypeOptions } from './react_vis_type'; diff --git a/src/plugins/visualizations/public/vis_types/types.ts b/src/plugins/visualizations/public/vis_types/types.ts index c51c2415af041..6ac798554fceb 100644 --- a/src/plugins/visualizations/public/vis_types/types.ts +++ b/src/plugins/visualizations/public/vis_types/types.ts @@ -16,24 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { EventEmitter } from 'events'; import { IconType } from '@elastic/eui'; import React, { ReactNode } from 'react'; import { Adapters } from 'src/plugins/inspector'; -import { CoreStart } from 'src/core/public'; -import { SavedObject } from 'src/plugins/saved_objects/public'; -import { - IndexPattern, - AggGroupNames, - AggParam, - AggGroupName, - DataPublicPluginStart, - Filter, - TimeRange, - Query, -} from '../../../data/public'; +import { IndexPattern, AggGroupNames, AggParam, AggGroupName } from '../../../data/public'; import { Vis, VisParams, VisToExpressionAst, VisualizationControllerConstructor } from '../types'; -import { PersistedState, VisualizeEmbeddableContract } from '../index'; export interface VisTypeOptions { showTimePicker: boolean; @@ -152,40 +139,7 @@ export interface VisType { readonly options: VisTypeOptions; - /** - * The editor that should be used to edit visualizations of this type. - * If this is not specified the default visualize editor will be used (and should be configured via schemas) - * and editorConfig. - */ - readonly editor?: VisEditorConstructor; - // TODO: The following types still need to be refined properly. readonly editorConfig: Record; readonly visConfig: Record; } - -export type VisEditorConstructor = new ( - element: HTMLElement, - vis: Vis, - eventEmitter: EventEmitter, - embeddableHandler: VisualizeEmbeddableContract -) => IEditorController; - -export interface IEditorController { - render(props: EditorRenderProps): Promise | void; - destroy(): void; -} - -export interface EditorRenderProps { - core: CoreStart; - data: DataPublicPluginStart; - filters: Filter[]; - timeRange: TimeRange; - query?: Query; - savedSearch?: SavedObject; - uiState: PersistedState; - /** - * Flag to determine if visualiztion is linked to the saved search - */ - linked: boolean; -} diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts index d3fc5de93c366..4f1b7b3a163be 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts @@ -1720,6 +1720,13 @@ describe('migration visualization', () => { expect(isVislibVis).toEqual(true); }); + it('should decorate existing docs with the kibana legacy palette', () => { + const migratedTestDoc = migrate(getTestDoc()); + const { palette } = JSON.parse(migratedTestDoc.attributes.visState).params; + + expect(palette.name).toEqual('kibana_palette'); + }); + describe('labels.filter', () => { it('should keep existing categoryAxes labels.filter value', () => { const migratedTestDoc = migrate(getTestDoc('area', [{ labels: { filter: false } }])); diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts index c625662ab7fe3..8317536f4b28a 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts @@ -21,7 +21,6 @@ import { cloneDeep, get, omit, has, flow } from 'lodash'; import { SavedObjectMigrationFn } from 'kibana/server'; -import { ChartType } from '../../../vis_type_xy/common'; import { DEFAULT_QUERY_LANGUAGE } from '../../../data/common'; const migrateIndexPattern: SavedObjectMigrationFn = (doc) => { @@ -804,6 +803,11 @@ const decorateAxes = ( }, })); +// Inlined from vis_type_xy +const CHART_TYPE_AREA = 'area'; +const CHART_TYPE_LINE = 'line'; +const CHART_TYPE_HISTOGRAM = 'histogram'; + /** * Migrate vislib bar, line and area charts to use new vis_type_xy plugin */ @@ -819,11 +823,11 @@ const migrateVislibAreaLineBarTypes: SavedObjectMigrationFn = (doc) => } if ( visState && - [ChartType.Area, ChartType.Line, ChartType.Histogram].includes(visState?.params?.type) + [CHART_TYPE_AREA, CHART_TYPE_LINE, CHART_TYPE_HISTOGRAM].includes(visState?.params?.type) ) { const isHorizontalBar = visState.type === 'horizontal_bar'; const isLineOrArea = - visState?.params?.type === ChartType.Area || visState?.params?.type === ChartType.Line; + visState?.params?.type === CHART_TYPE_AREA || visState?.params?.type === CHART_TYPE_LINE; return { ...doc, attributes: { @@ -832,6 +836,10 @@ const migrateVislibAreaLineBarTypes: SavedObjectMigrationFn = (doc) => ...visState, params: { ...visState.params, + palette: { + type: 'palette', + name: 'kibana_palette', + }, categoryAxes: visState.params.categoryAxes && decorateAxes(visState.params.categoryAxes, !isHorizontalBar), diff --git a/src/plugins/visualizations/tsconfig.json b/src/plugins/visualizations/tsconfig.json new file mode 100644 index 0000000000000..d7c5e6a4b4366 --- /dev/null +++ b/src/plugins/visualizations/tsconfig.json @@ -0,0 +1,29 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*" + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../data/tsconfig.json" }, + { "path": "../dashboard/tsconfig.json" }, + { "path": "../expressions/tsconfig.json" }, + { "path": "../ui_actions/tsconfig.json" }, + { "path": "../embeddable/tsconfig.json" }, + { "path": "../inspector/tsconfig.json" }, + { "path": "../saved_objects/tsconfig.json" }, + { "path": "../saved_objects_tagging_oss/tsconfig.json" }, + { "path": "../usage_collection/tsconfig.json" }, + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../discover/tsconfig.json" }, + ] +} diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts index 78727492ac012..a36ff852f5731 100644 --- a/src/plugins/visualize/public/application/types.ts +++ b/src/plugins/visualize/public/application/types.ts @@ -18,13 +18,14 @@ */ import { History } from 'history'; -import { Query, Filter, DataPublicPluginStart } from 'src/plugins/data/public'; +import { Query, Filter, DataPublicPluginStart, TimeRange } from 'src/plugins/data/public'; import { SavedVisState, VisualizationsStart, Vis, VisualizeEmbeddableContract, VisSavedObject, + PersistedState, } from 'src/plugins/visualizations/public'; import { CoreStart, @@ -44,6 +45,7 @@ import { SharePluginStart } from 'src/plugins/share/public'; import { SavedObjectsStart, SavedObject } from 'src/plugins/saved_objects/public'; import { EmbeddableStart, EmbeddableStateTransfer } from 'src/plugins/embeddable/public'; import { UrlForwardingStart } from 'src/plugins/url_forwarding/public'; +import { EventEmitter } from 'events'; import { DashboardStart } from '../../../dashboard/public'; import type { SavedObjectsTaggingApi } from '../../../saved_objects_tagging_oss/public'; @@ -119,3 +121,29 @@ export interface ByValueVisInstance { } export type VisualizeEditorVisInstance = SavedVisInstance | ByValueVisInstance; + +export type VisEditorConstructor = new ( + element: HTMLElement, + vis: Vis, + eventEmitter: EventEmitter, + embeddableHandler: VisualizeEmbeddableContract +) => IEditorController; + +export interface IEditorController { + render(props: EditorRenderProps): Promise | void; + destroy(): void; +} + +export interface EditorRenderProps { + core: CoreStart; + data: DataPublicPluginStart; + filters: Filter[]; + timeRange: TimeRange; + query?: Query; + savedSearch?: SavedObject; + uiState: PersistedState; + /** + * Flag to determine if visualiztion is linked to the saved search + */ + linked: boolean; +} diff --git a/src/plugins/visualize/public/application/utils/use/use_editor_updates.test.ts b/src/plugins/visualize/public/application/utils/use/use_editor_updates.test.ts index c9ffc8c27a978..414489eaced9c 100644 --- a/src/plugins/visualize/public/application/utils/use/use_editor_updates.test.ts +++ b/src/plugins/visualize/public/application/utils/use/use_editor_updates.test.ts @@ -22,7 +22,7 @@ import { EventEmitter } from 'events'; import { useEditorUpdates } from './use_editor_updates'; import { VisualizeServices, VisualizeAppStateContainer, SavedVisInstance } from '../../types'; -import type { IEditorController } from '../../../../../visualizations/public'; +import type { IEditorController } from '../../types'; import { visualizeAppStateStub } from '../stubs'; import { createVisualizeServicesMock } from '../mocks'; diff --git a/src/plugins/visualize/public/application/utils/use/use_editor_updates.ts b/src/plugins/visualize/public/application/utils/use/use_editor_updates.ts index 1edb5564b48a7..9fdcbe1571893 100644 --- a/src/plugins/visualize/public/application/utils/use/use_editor_updates.ts +++ b/src/plugins/visualize/public/application/utils/use/use_editor_updates.ts @@ -26,8 +26,8 @@ import { VisualizeAppState, VisualizeAppStateContainer, VisualizeEditorVisInstance, + IEditorController, } from '../../types'; -import type { IEditorController } from '../../../../../visualizations/public'; export const useEditorUpdates = ( services: VisualizeServices, diff --git a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts index 48afc9d468204..fe052f4ca133e 100644 --- a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts +++ b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts @@ -26,7 +26,8 @@ import { redirectWhenMissing } from '../../../../../kibana_utils/public'; import { getEditBreadcrumbs, getCreateBreadcrumbs } from '../breadcrumbs'; import { VisualizeServices } from '../../types'; import { VisualizeConstants } from '../../visualize_constants'; -import { setDefaultEditor } from '../../../services'; +import { setVisEditorsRegistry } from '../../../services'; +import { createVisEditorsRegistry } from '../../../vis_editors_registry'; import { createEmbeddableStateTransferMock } from '../../../../../embeddable/public/mocks'; const mockDefaultEditorControllerDestroy = jest.fn(); @@ -75,10 +76,14 @@ describe('useSavedVisInstance', () => { const eventEmitter = new EventEmitter(); beforeEach(() => { - setDefaultEditor( + const registry = createVisEditorsRegistry(); + + registry.registerDefault( jest.fn().mockImplementation(() => ({ destroy: mockDefaultEditorControllerDestroy })) ); + setVisEditorsRegistry(registry); + mockServices = ({ ...coreStartMock, toastNotifications, diff --git a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts index a8d17a3cdfadb..6e5b3a012aabd 100644 --- a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts +++ b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts @@ -26,10 +26,9 @@ import { redirectWhenMissing } from '../../../../../kibana_utils/public'; import { getVisualizationInstance } from '../get_visualization_instance'; import { getEditBreadcrumbs, getCreateBreadcrumbs } from '../breadcrumbs'; -import { SavedVisInstance, VisualizeServices } from '../../types'; +import { SavedVisInstance, VisualizeServices, IEditorController } from '../../types'; import { VisualizeConstants } from '../../visualize_constants'; -import { getDefaultEditor } from '../../../services'; -import type { IEditorController } from '../../../../../visualizations/public'; +import { getVisEditorsRegistry } from '../../../services'; /** * This effect is responsible for instantiating a saved vis or creating a new one @@ -123,13 +122,16 @@ export const useSavedVisInstance = ( // do not create editor in embeded mode if (visEditorRef.current) { if (isChromeVisible) { - const Editor = vis.type.editor || getDefaultEditor(); - visEditorController = new Editor( - visEditorRef.current, - vis, - eventEmitter, - embeddableHandler - ); + const Editor = getVisEditorsRegistry().get(vis.type.editorConfig?.editor); + + if (Editor) { + visEditorController = new Editor( + visEditorRef.current, + vis, + eventEmitter, + embeddableHandler + ); + } } else { embeddableHandler.render(visEditorRef.current); } diff --git a/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts b/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts index 6c9a464d64a72..45f31d57736a9 100644 --- a/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts +++ b/src/plugins/visualize/public/application/utils/use/use_vis_byvalue.ts @@ -20,11 +20,10 @@ import { EventEmitter } from 'events'; import { useEffect, useRef, useState } from 'react'; import { VisualizeInput } from 'src/plugins/visualizations/public'; -import { ByValueVisInstance, VisualizeServices } from '../../types'; -import type { IEditorController } from '../../../../../visualizations/public'; +import { ByValueVisInstance, VisualizeServices, IEditorController } from '../../types'; import { getVisualizationInstanceFromInput } from '../get_visualization_instance'; import { getEditBreadcrumbs } from '../breadcrumbs'; -import { getDefaultEditor } from '../../../services'; +import { getVisEditorsRegistry } from '../../../services'; export const useVisByValue = ( services: VisualizeServices, @@ -51,14 +50,18 @@ export const useVisByValue = ( } const byValueVisInstance = await getVisualizationInstanceFromInput(services, valueInput); const { embeddableHandler, vis } = byValueVisInstance; + let visEditorController; - const Editor = vis.type.editor || getDefaultEditor(); - const visEditorController = new Editor( - visEditorRef.current, - vis, - eventEmitter, - embeddableHandler - ); + const Editor = getVisEditorsRegistry().get(vis.type.editorConfig?.editor); + + if (Editor) { + visEditorController = new Editor( + visEditorRef.current, + vis, + eventEmitter, + embeddableHandler + ); + } const originatingAppName = originatingApp ? stateTransferService.getAppNameFromId(originatingApp) diff --git a/src/plugins/visualize/public/index.ts b/src/plugins/visualize/public/index.ts index 385313f81ff68..b749e7288f59f 100644 --- a/src/plugins/visualize/public/index.ts +++ b/src/plugins/visualize/public/index.ts @@ -22,6 +22,8 @@ import { VisualizePlugin, VisualizePluginSetup } from './plugin'; export { VisualizeConstants } from './application/visualize_constants'; +export { IEditorController, EditorRenderProps } from './application/types'; + export { VisualizePluginSetup }; export const plugin = (context: PluginInitializerContext) => { diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index b1507155316e2..75716ab43a6fc 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -41,7 +41,7 @@ import { DataPublicPluginStart, DataPublicPluginSetup, esFilters } from '../../d import { NavigationPublicPluginStart as NavigationStart } from '../../navigation/public'; import { SharePluginStart, SharePluginSetup } from '../../share/public'; import { UrlForwardingSetup, UrlForwardingStart } from '../../url_forwarding/public'; -import { VisualizationsStart, VisEditorConstructor } from '../../visualizations/public'; +import { VisualizationsStart } from '../../visualizations/public'; import { VisualizeConstants } from './application/visualize_constants'; import { FeatureCatalogueCategory, HomePublicPluginSetup } from '../../home/public'; import { VisualizeServices } from './application/types'; @@ -57,10 +57,11 @@ import { setIndexPatterns, setQueryService, setShareService, - setDefaultEditor, + setVisEditorsRegistry, } from './services'; import { visualizeFieldAction } from './actions/visualize_field_action'; import { createVisualizeUrlGenerator } from './url_generator'; +import { createVisEditorsRegistry, VisEditorsRegistry } from './vis_editors_registry'; export interface VisualizePluginStartDependencies { data: DataPublicPluginStart; @@ -83,7 +84,7 @@ export interface VisualizePluginSetupDependencies { } export interface VisualizePluginSetup { - setDefaultEditor: (editor: VisEditorConstructor) => void; + visEditorsRegistry: VisEditorsRegistry; } export class VisualizePlugin @@ -98,6 +99,8 @@ export class VisualizePlugin private stopUrlTracking: (() => void) | undefined = undefined; private currentHistory: ScopedHistory | undefined = undefined; + private readonly visEditorsRegistry = createVisEditorsRegistry(); + constructor(private initializerContext: PluginInitializerContext) {} public async setup( @@ -244,13 +247,12 @@ export class VisualizePlugin } return { - setDefaultEditor: (editor) => { - setDefaultEditor(editor); - }, + visEditorsRegistry: this.visEditorsRegistry, } as VisualizePluginSetup; } public start(core: CoreStart, plugins: VisualizePluginStartDependencies) { + setVisEditorsRegistry(this.visEditorsRegistry); setApplication(core.application); setIndexPatterns(plugins.data.indexPatterns); setQueryService(plugins.data.query); diff --git a/src/plugins/visualize/public/services.ts b/src/plugins/visualize/public/services.ts index 5a8ac00430fbf..5c5ab6072e494 100644 --- a/src/plugins/visualize/public/services.ts +++ b/src/plugins/visualize/public/services.ts @@ -20,8 +20,8 @@ import { ApplicationStart, IUiSettingsClient } from '../../../core/public'; import { createGetterSetter } from '../../../plugins/kibana_utils/public'; import { IndexPatternsContract, DataPublicPluginStart } from '../../../plugins/data/public'; -import { VisEditorConstructor } from '../../../plugins/visualizations/public'; import { SharePluginStart } from '../../../plugins/share/public'; +import { VisEditorsRegistry } from './vis_editors_registry'; export const [getUISettings, setUISettings] = createGetterSetter('UISettings'); @@ -33,9 +33,10 @@ export const [getIndexPatterns, setIndexPatterns] = createGetterSetter( - 'DefaultEditor' -); +export const [ + getVisEditorsRegistry, + setVisEditorsRegistry, +] = createGetterSetter('VisEditorsRegistry'); export const [getQueryService, setQueryService] = createGetterSetter< DataPublicPluginStart['query'] diff --git a/src/plugins/visualize/public/vis_editors_registry.ts b/src/plugins/visualize/public/vis_editors_registry.ts new file mode 100644 index 0000000000000..acab183bf8a0c --- /dev/null +++ b/src/plugins/visualize/public/vis_editors_registry.ts @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { VisEditorConstructor } from './application/types'; + +const DEFAULT_NAME = 'default'; + +export const createVisEditorsRegistry = () => { + const map = new Map(); + + return { + registerDefault: (editor: VisEditorConstructor) => { + map.set(DEFAULT_NAME, editor); + }, + register: (name: string, editor: VisEditorConstructor) => { + if (name) { + map.set(name, editor); + } + }, + get: (name: string) => map.get(name || DEFAULT_NAME), + }; +}; + +export type VisEditorsRegistry = ReturnType; diff --git a/src/plugins/visualize/tsconfig.json b/src/plugins/visualize/tsconfig.json new file mode 100644 index 0000000000000..bc0891f391746 --- /dev/null +++ b/src/plugins/visualize/tsconfig.json @@ -0,0 +1,34 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*" + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../data/tsconfig.json" }, + { "path": "../url_forwarding/tsconfig.json" }, + { "path": "../navigation/tsconfig.json" }, + { "path": "../saved_objects/tsconfig.json" }, + { "path": "../visualizations/tsconfig.json" }, + { "path": "../embeddable/tsconfig.json" }, + { "path": "../dashboard/tsconfig.json" }, + { "path": "../ui_actions/tsconfig.json" }, + { "path": "../home/tsconfig.json" }, + { "path": "../share/tsconfig.json" }, + { "path": "../saved_objects_tagging_oss/tsconfig.json" }, + { "path": "../kibana_utils/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + { "path": "../home/tsconfig.json" }, + { "path": "../presentation_util/tsconfig.json" }, + { "path": "../discover/tsconfig.json" }, + ] +} diff --git a/tasks/licenses_csv_report.js b/tasks/licenses_csv_report.js deleted file mode 100644 index f504cbcaa5fc8..0000000000000 --- a/tasks/licenses_csv_report.js +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { writeFileSync } from 'fs'; -import { resolve } from 'path'; -import { getInstalledPackages } from '../src/dev/npm'; -import { engines } from '../package'; -import { LICENSE_OVERRIDES } from '../src/dev/license_checker'; - -import { isNull, isUndefined } from 'lodash'; - -const allDoubleQuoteRE = /"/g; - -function escapeValue(value) { - if (isNull(value)) { - return; - } - - return `"${value.replace(allDoubleQuoteRE, '""')}"`; -} - -function formatCsvValues(fields, values) { - return fields - .map((field) => { - const value = values[field]; - - if (isNull(value) || isUndefined(value)) { - return null; - } - - return value.toString(); - }) - .map(escapeValue) - .join(','); -} - -export default function licensesCSVReport(grunt) { - grunt.registerTask('licenses:csv_report', 'Report of 3rd party dependencies', async function () { - const fields = ['name', 'version', 'url', 'license', 'sourceURL']; - const done = this.async(); - - try { - const file = grunt.option('csv'); - const directory = grunt.option('directory'); - const dev = Boolean(grunt.option('dev')); - - const packages = await getInstalledPackages({ - directory: directory ? resolve(directory) : grunt.config.get('root'), - licenseOverrides: LICENSE_OVERRIDES, - dev, - }); - - packages.unshift( - { - name: 'Node.js', - version: engines.node, - repository: 'https://nodejs.org', - licenses: ['MIT'], - }, - { - name: 'Red Hat Universal Base Image minimal', - version: '8', - repository: - 'https://catalog.redhat.com/software/containers/ubi8/ubi-minimal/5c359a62bed8bd75a2c3fba8', - licenses: [ - 'Custom;https://www.redhat.com/licenses/EULA_Red_Hat_Universal_Base_Image_English_20190422.pdf', - ], - sourceURL: 'https://oss-dependencies.elastic.co/redhat/ubi/ubi-minimal-8-source.tar.gz', - } - ); - - const csv = packages - .map((pkg) => { - const data = { - name: pkg.name, - version: pkg.version, - url: pkg.repository || `https://www.npmjs.com/package/${pkg.name}`, - license: pkg.licenses.join(','), - sourceURL: pkg.sourceURL, - }; - - return formatCsvValues(fields, data); - }) - .join('\n'); - - if (file) { - writeFileSync(file, `${fields.join(',')}\n${csv}`); - grunt.log.writeln(`wrote to ${file}`); - } else { - grunt.log.writeln(csv); - grunt.log.writeln('\nspecify "--csv [filepath]" to write the data to a specific file'); - } - - done(); - } catch (err) { - grunt.fail.fatal(err); - done(err); - } - }); -} diff --git a/test/api_integration/apis/telemetry/telemetry_local.js b/test/api_integration/apis/telemetry/telemetry_local.js index dcc8e989d3ad5..22fdcb7f7ff3e 100644 --- a/test/api_integration/apis/telemetry/telemetry_local.js +++ b/test/api_integration/apis/telemetry/telemetry_local.js @@ -77,7 +77,6 @@ export default function ({ getService }) { expect(stats.stack_stats.kibana.plugins.ui_counters).to.be.an('object'); expect(stats.stack_stats.kibana.plugins.application_usage).to.be.an('object'); expect(stats.stack_stats.kibana.plugins.kql.defaultQueryLanguage).to.be.a('string'); - expect(stats.stack_stats.kibana.plugins['tsvb-validation']).to.be.an('object'); expect(stats.stack_stats.kibana.plugins.localization).to.be.an('object'); expect(stats.stack_stats.kibana.plugins.csp.strict).to.be(true); expect(stats.stack_stats.kibana.plugins.csp.warnLegacyBrowsers).to.be(true); diff --git a/test/functional/apps/console/_console.ts b/test/functional/apps/console/_console.ts index 6e524b2cd33df..fcd136a2720e4 100644 --- a/test/functional/apps/console/_console.ts +++ b/test/functional/apps/console/_console.ts @@ -34,7 +34,6 @@ GET _search export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const log = getService('log'); - const find = getService('find'); const browser = getService('browser'); const PageObjects = getPageObjects(['common', 'console']); @@ -84,12 +83,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should resize the editor', async () => { - const editor = await find.byCssSelector('.conApp'); + const editor = await PageObjects.console.getEditor(); await browser.setWindowSize(1300, 1100); const initialSize = await editor.getSize(); await browser.setWindowSize(1000, 1100); const afterSize = await editor.getSize(); expect(initialSize.width).to.be.greaterThan(afterSize.width); }); + + it('should provide basic auto-complete functionality', async () => { + // Ensure that the text area can be interacted with + await PageObjects.console.dismissTutorial(); + expect(await PageObjects.console.hasAutocompleter()).to.be(false); + await PageObjects.console.promptAutocomplete(); + retry.waitFor('autocomplete to be visible', () => PageObjects.console.hasAutocompleter()); + }); }); } diff --git a/test/functional/apps/context/_date_nanos_custom_timestamp.js b/test/functional/apps/context/_date_nanos_custom_timestamp.js index 8fe08d13af0aa..8772b10a4b8c8 100644 --- a/test/functional/apps/context/_date_nanos_custom_timestamp.js +++ b/test/functional/apps/context/_date_nanos_custom_timestamp.js @@ -38,6 +38,7 @@ export default function ({ getService, getPageObjects }) { await kibanaServer.uiSettings.update({ 'context:defaultSize': `${TEST_DEFAULT_CONTEXT_SIZE}`, 'context:step': `${TEST_STEP_SIZE}`, + 'discover:searchFieldsFromSource': true, }); }); diff --git a/test/functional/apps/discover/_data_grid_doc_navigation.ts b/test/functional/apps/discover/_data_grid_doc_navigation.ts index 92d9893cab0b6..97b8eb564a256 100644 --- a/test/functional/apps/discover/_data_grid_doc_navigation.ts +++ b/test/functional/apps/discover/_data_grid_doc_navigation.ts @@ -56,7 +56,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(hasDocHit).to.be(true); }); - it('add filter should create an exists filter if value is null (#7189)', async function () { + // no longer relevant as null field won't be returned in the Fields API response + xit('add filter should create an exists filter if value is null (#7189)', async function () { await PageObjects.discover.waitUntilSearchingHasFinished(); // Filter special document await filterBar.addFilter('agent', 'is', 'Missing/Fields'); diff --git a/test/functional/apps/discover/_data_grid_field_data.ts b/test/functional/apps/discover/_data_grid_field_data.ts index 8224f59f7fabf..137c19149d274 100644 --- a/test/functional/apps/discover/_data_grid_field_data.ts +++ b/test/functional/apps/discover/_data_grid_field_data.ts @@ -53,7 +53,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('the search term should be highlighted in the field data', async function () { // marks is the style that highlights the text in yellow const marks = await PageObjects.discover.getMarks(); - expect(marks.length).to.be(25); + expect(marks.length).to.be(50); expect(marks.indexOf('php')).to.be(0); }); diff --git a/test/functional/apps/discover/_discover_fields_api.ts b/test/functional/apps/discover/_discover_fields_api.ts new file mode 100644 index 0000000000000..94cb4ed5fa52e --- /dev/null +++ b/test/functional/apps/discover/_discover_fields_api.ts @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from './ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const log = getService('log'); + const retry = getService('retry'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']); + const defaultSettings = { + defaultIndex: 'logstash-*', + 'discover:searchFieldsFromSource': false, + }; + describe('discover uses fields API test', function describeIndexTests() { + before(async function () { + log.debug('load kibana index with default index pattern'); + await esArchiver.load('discover'); + await esArchiver.loadIfNeeded('logstash_functional'); + await kibanaServer.uiSettings.replace(defaultSettings); + log.debug('discover'); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.timePicker.setDefaultAbsoluteRange(); + }); + + after(async () => { + await kibanaServer.uiSettings.replace({ 'discover:searchFieldsFromSource': true }); + }); + + it('should correctly display documents', async function () { + log.debug('check if Document title exists in the grid'); + expect(await PageObjects.discover.getDocHeader()).to.have.string('Document'); + const rowData = await PageObjects.discover.getDocTableIndex(1); + log.debug('check the newest doc timestamp in UTC (check diff timezone in last test)'); + expect(rowData.startsWith('Sep 22, 2015 @ 23:50:13.253')).to.be.ok(); + const expectedHitCount = '14,004'; + await retry.try(async function () { + expect(await PageObjects.discover.getHitCount()).to.be(expectedHitCount); + }); + }); + + it('adding a column removes a default column', async function () { + await PageObjects.discover.clickFieldListItemAdd('_score'); + expect(await PageObjects.discover.getDocHeader()).to.have.string('_score'); + expect(await PageObjects.discover.getDocHeader()).not.to.have.string('Document'); + }); + + it('removing a column adds a default column', async function () { + await PageObjects.discover.clickFieldListItemRemove('_score'); + expect(await PageObjects.discover.getDocHeader()).not.to.have.string('_score'); + expect(await PageObjects.discover.getDocHeader()).to.have.string('Document'); + }); + }); +} diff --git a/test/functional/apps/discover/_doc_navigation.ts b/test/functional/apps/discover/_doc_navigation.ts index 76612b255ac23..79632942cf04a 100644 --- a/test/functional/apps/discover/_doc_navigation.ts +++ b/test/functional/apps/discover/_doc_navigation.ts @@ -55,7 +55,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(hasDocHit).to.be(true); }); - it('add filter should create an exists filter if value is null (#7189)', async function () { + // no longer relevant as null field won't be returned in the Fields API response + xit('add filter should create an exists filter if value is null (#7189)', async function () { await PageObjects.discover.waitUntilSearchingHasFinished(); // Filter special document await filterBar.addFilter('agent', 'is', 'Missing/Fields'); diff --git a/test/functional/apps/discover/_field_data.ts b/test/functional/apps/discover/_field_data.ts index e08325a81a3e8..3811cde8a6367 100644 --- a/test/functional/apps/discover/_field_data.ts +++ b/test/functional/apps/discover/_field_data.ts @@ -36,6 +36,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await esArchiver.load('discover'); await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*', + 'discover:searchFieldsFromSource': true, }); await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await PageObjects.common.navigateToApp('discover'); diff --git a/test/functional/apps/discover/_field_data_with_fields_api.ts b/test/functional/apps/discover/_field_data_with_fields_api.ts new file mode 100644 index 0000000000000..923a021f5fad6 --- /dev/null +++ b/test/functional/apps/discover/_field_data_with_fields_api.ts @@ -0,0 +1,105 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const retry = getService('retry'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const toasts = getService('toasts'); + const queryBar = getService('queryBar'); + const PageObjects = getPageObjects(['common', 'header', 'discover', 'visualize', 'timePicker']); + + describe('discover tab with new fields API', function describeIndexTests() { + this.tags('includeFirefox'); + before(async function () { + await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.load('discover'); + await kibanaServer.uiSettings.replace({ + defaultIndex: 'logstash-*', + 'discover:searchFieldsFromSource': false, + }); + await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); + await PageObjects.common.navigateToApp('discover'); + }); + describe('field data', function () { + it('search php should show the correct hit count', async function () { + const expectedHitCount = '445'; + await retry.try(async function () { + await queryBar.setQuery('php'); + await queryBar.submitQuery(); + const hitCount = await PageObjects.discover.getHitCount(); + expect(hitCount).to.be(expectedHitCount); + }); + }); + + it('the search term should be highlighted in the field data', async function () { + // marks is the style that highlights the text in yellow + const marks = await PageObjects.discover.getMarks(); + expect(marks.length).to.be(100); + expect(marks.indexOf('php')).to.be(0); + }); + + it('search type:apache should show the correct hit count', async function () { + const expectedHitCount = '11,156'; + await queryBar.setQuery('type:apache'); + await queryBar.submitQuery(); + await retry.try(async function tryingForTime() { + const hitCount = await PageObjects.discover.getHitCount(); + expect(hitCount).to.be(expectedHitCount); + }); + }); + + it('doc view should show Time and Document columns', async function () { + const expectedHeader = 'Time Document'; + const Docheader = await PageObjects.discover.getDocHeader(); + expect(Docheader).to.be(expectedHeader); + }); + + it('doc view should sort ascending', async function () { + const expectedTimeStamp = 'Sep 20, 2015 @ 00:00:00.000'; + await PageObjects.discover.clickDocSortDown(); + + // we don't technically need this sleep here because the tryForTime will retry and the + // results will match on the 2nd or 3rd attempt, but that debug output is huge in this + // case and it can be avoided with just a few seconds sleep. + await PageObjects.common.sleep(2000); + await retry.try(async function tryingForTime() { + const rowData = await PageObjects.discover.getDocTableIndex(1); + + expect(rowData.startsWith(expectedTimeStamp)).to.be.ok(); + }); + }); + + it('a bad syntax query should show an error message', async function () { + const expectedError = + 'Expected ":", "<", "<=", ">", ">=", AND, OR, end of input, ' + + 'whitespace but "(" found.'; + await queryBar.setQuery('xxx(yyy))'); + await queryBar.submitQuery(); + const { message } = await toasts.getErrorToast(); + expect(message).to.contain(expectedError); + await toasts.dismissToast(); + }); + }); + }); +} diff --git a/test/functional/apps/discover/_large_string.ts b/test/functional/apps/discover/_large_string.ts index fe5613a4e3f19..e8ad6131bcc21 100644 --- a/test/functional/apps/discover/_large_string.ts +++ b/test/functional/apps/discover/_large_string.ts @@ -40,7 +40,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('verify the large string book present', async function () { const ExpectedDoc = - 'mybook:Project Gutenberg EBook of Hamlet, by William Shakespeare' + + '_id:1 _type: - _index:testlargestring _score:0' + + ' mybook:Project Gutenberg EBook of Hamlet, by William Shakespeare' + ' This eBook is for the use of anyone anywhere in the United States' + ' and most other parts of the world at no cost and with almost no restrictions whatsoever.' + ' You may copy it, give it away or re-use it under the terms of the' + diff --git a/test/functional/apps/discover/_shared_links.ts b/test/functional/apps/discover/_shared_links.ts index 51ea5f997e859..b15f5b0aae39f 100644 --- a/test/functional/apps/discover/_shared_links.ts +++ b/test/functional/apps/discover/_shared_links.ts @@ -88,7 +88,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/discover/_source_filters.ts b/test/functional/apps/discover/_source_filters.ts index 0af7c0ade79ba..d2ae02ef25de4 100644 --- a/test/functional/apps/discover/_source_filters.ts +++ b/test/functional/apps/discover/_source_filters.ts @@ -40,6 +40,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // and load a set of makelogs data await esArchiver.loadIfNeeded('logstash_functional'); + await kibanaServer.uiSettings.update({ + 'discover:searchFieldsFromSource': true, + }); + log.debug('discover'); await PageObjects.common.navigateToApp('discover'); diff --git a/src/plugins/runtime_fields/common/types.ts b/test/functional/apps/discover/ftr_provider_context.d.ts similarity index 75% rename from src/plugins/runtime_fields/common/types.ts rename to test/functional/apps/discover/ftr_provider_context.d.ts index f16d3d75d6ecf..a4894e024b612 100644 --- a/src/plugins/runtime_fields/common/types.ts +++ b/test/functional/apps/discover/ftr_provider_context.d.ts @@ -16,14 +16,8 @@ * specific language governing permissions and limitations * under the License. */ +import { GenericFtrProviderContext } from '@kbn/test/types/ftr'; +import { services } from '../../services'; +import { pageObjects } from '../../page_objects'; -import { RUNTIME_FIELD_TYPES } from './constants'; - -export type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; -export interface RuntimeField { - name: string; - type: RuntimeType; - script: { - source: string; - }; -} +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/test/functional/apps/discover/index.ts b/test/functional/apps/discover/index.ts index 450049af66abf..5fd49a1d35216 100644 --- a/test/functional/apps/discover/index.ts +++ b/test/functional/apps/discover/index.ts @@ -42,6 +42,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_filter_editor')); loadTestFile(require.resolve('./_errors')); loadTestFile(require.resolve('./_field_data')); + loadTestFile(require.resolve('./_field_data_with_fields_api')); loadTestFile(require.resolve('./_shared_links')); loadTestFile(require.resolve('./_sidebar')); loadTestFile(require.resolve('./_source_filters')); @@ -51,6 +52,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_date_nanos')); loadTestFile(require.resolve('./_date_nanos_mixed')); loadTestFile(require.resolve('./_indexpattern_without_timefield')); + loadTestFile(require.resolve('./_discover_fields_api')); loadTestFile(require.resolve('./_data_grid')); loadTestFile(require.resolve('./_data_grid_context')); loadTestFile(require.resolve('./_data_grid_field_data')); diff --git a/test/functional/fixtures/es_archiver/date_nanos/mappings.json b/test/functional/fixtures/es_archiver/date_nanos/mappings.json index bea82767f6cbb..f9ef429a0f97c 100644 --- a/test/functional/fixtures/es_archiver/date_nanos/mappings.json +++ b/test/functional/fixtures/es_archiver/date_nanos/mappings.json @@ -5,7 +5,8 @@ "mappings": { "properties": { "@timestamp": { - "type": "date_nanos" + "type": "date_nanos", + "format": "strict_date_optional_time_nanos" } } }, diff --git a/test/functional/fixtures/es_archiver/date_nanos_mixed/mappings.json b/test/functional/fixtures/es_archiver/date_nanos_mixed/mappings.json index c62918abced58..b29f6b111b06d 100644 --- a/test/functional/fixtures/es_archiver/date_nanos_mixed/mappings.json +++ b/test/functional/fixtures/es_archiver/date_nanos_mixed/mappings.json @@ -29,7 +29,8 @@ "mappings": { "properties": { "timestamp": { - "type": "date_nanos" + "type": "date_nanos", + "format": "strict_date_optional_time_nanos" } } }, diff --git a/test/functional/page_objects/console_page.ts b/test/functional/page_objects/console_page.ts index f67a2722da367..5b0fcd0d331b5 100644 --- a/test/functional/page_objects/console_page.ts +++ b/test/functional/page_objects/console_page.ts @@ -17,12 +17,14 @@ * under the License. */ +import { Key } from 'selenium-webdriver'; import { FtrProviderContext } from '../ftr_provider_context'; import { WebElementWrapper } from '../services/lib/web_element_wrapper'; export function ConsolePageProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); + const find = getService('find'); class ConsolePage { public async getVisibleTextFromAceEditor(editor: WebElementWrapper) { @@ -79,6 +81,38 @@ export function ConsolePageProvider({ getService }: FtrProviderContext) { public async getRequestFontSize() { return await this.getFontSize(await this.getRequestEditor()); } + + public async getEditor() { + return testSubjects.find('console-application'); + } + + public async dismissTutorial() { + try { + const closeButton = await testSubjects.find('help-close-button'); + await closeButton.click(); + } catch (e) { + // Ignore because it is probably not there. + } + } + + public async promptAutocomplete() { + // This focusses the cursor on the bottom of the text area + const editor = await this.getEditor(); + const content = await editor.findByCssSelector('.ace_content'); + await content.click(); + const textArea = await testSubjects.find('console-textarea'); + // There should be autocomplete for this on all license levels + await textArea.pressKeys('\nGET s'); + await textArea.pressKeys([Key.CONTROL, Key.SPACE]); + } + + public async hasAutocompleter(): Promise { + try { + return Boolean(await find.byCssSelector('.ace_autocomplete')); + } catch (e) { + return false; + } + } } return new ConsolePage(); diff --git a/test/interpreter_functional/screenshots/baseline/partial_test_3.png b/test/interpreter_functional/screenshots/baseline/partial_test_3.png index c43169bfb7101..93a8e53540744 100644 Binary files a/test/interpreter_functional/screenshots/baseline/partial_test_3.png and b/test/interpreter_functional/screenshots/baseline/partial_test_3.png differ diff --git a/test/interpreter_functional/snapshots/baseline/partial_test_3.json b/test/interpreter_functional/snapshots/baseline/partial_test_3.json index 595127526156e..e011b69de2022 100644 --- a/test/interpreter_functional/snapshots/baseline/partial_test_3.json +++ b/test/interpreter_functional/snapshots/baseline/partial_test_3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"region_map"}} \ No newline at end of file +{"as":"region_map_vis","type":"render","value":{"visConfig":{"addTooltip":true,"bucket":{"accessor":0},"colorSchema":"Yellow to Red","isDisplayWarning":true,"legendPosition":"bottomright","mapCenter":[0,0],"mapZoom":2,"metric":{"accessor":1,"format":{"id":"number"}},"outlineWeight":1,"selectedJoinField":{},"selectedLayer":{},"showAllShapes":true,"wms":{}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"region_map"}} \ No newline at end of file diff --git a/test/interpreter_functional/snapshots/session/partial_test_3.json b/test/interpreter_functional/snapshots/session/partial_test_3.json index 595127526156e..e011b69de2022 100644 --- a/test/interpreter_functional/snapshots/session/partial_test_3.json +++ b/test/interpreter_functional/snapshots/session/partial_test_3.json @@ -1 +1 @@ -{"as":"visualization","type":"render","value":{"params":{"listenOnChange":true},"visConfig":{"bucket":{"accessor":0},"metric":{"accessor":1,"format":{"id":"number"}}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"region_map"}} \ No newline at end of file +{"as":"region_map_vis","type":"render","value":{"visConfig":{"addTooltip":true,"bucket":{"accessor":0},"colorSchema":"Yellow to Red","isDisplayWarning":true,"legendPosition":"bottomright","mapCenter":[0,0],"mapZoom":2,"metric":{"accessor":1,"format":{"id":"number"}},"outlineWeight":1,"selectedJoinField":{},"selectedLayer":{},"showAllShapes":true,"wms":{}},"visData":{"columns":[{"id":"col-0-2","meta":{"field":"response.raw","index":"logstash-*","params":{"id":"terms","params":{"id":"string","missingBucketLabel":"Missing","otherBucketLabel":"Other"}},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"2","indexPatternId":"logstash-*","params":{"field":"response.raw","missingBucket":false,"missingBucketLabel":"Missing","order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","size":4},"schema":"segment","type":"terms"},"type":"string"},"name":"response.raw: Descending"},{"id":"col-1-1","meta":{"field":null,"index":"logstash-*","params":{"id":"number"},"source":"esaggs","sourceParams":{"appliedTimeRange":null,"enabled":true,"id":"1","indexPatternId":"logstash-*","params":{},"schema":"metric","type":"count"},"type":"number"},"name":"Count"}],"rows":[{"col-0-2":"200","col-1-1":12891},{"col-0-2":"404","col-1-1":696},{"col-0-2":"503","col-1-1":417}],"type":"datatable"},"visType":"region_map"}} \ No newline at end of file diff --git a/test/interpreter_functional/test_suites/run_pipeline/basic.ts b/test/interpreter_functional/test_suites/run_pipeline/basic.ts index fc7c0428631c6..6bd00b42502d0 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/basic.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/basic.ts @@ -110,7 +110,7 @@ export default function ({ await expectExpression('partial_test_2', metricExpr, context).toMatchSnapshot() ).toMatchScreenshot(); - const regionMapExpr = `regionmap visConfig='{"metric":{"accessor":1,"format":{"id":"number"}},"bucket":{"accessor":0}}'`; + const regionMapExpr = `regionmap visConfig='{"metric":{"accessor":1,"format":{"id":"number"}},"bucket":{"accessor":0},"legendPosition":"bottomright","addTooltip":true,"colorSchema":"Yellow to Red","isDisplayWarning":true,"wms":{},"mapZoom":2,"mapCenter":[0,0],"outlineWeight":1,"showAllShapes":true,"selectedLayer":{},"selectedJoinField":{}}'`; await ( await expectExpression('partial_test_3', regionMapExpr, context).toMatchSnapshot() ).toMatchScreenshot(); diff --git a/test/tsconfig.json b/test/tsconfig.json index f1d5115bf35fb..684efc145e4d0 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -8,13 +8,16 @@ "exclude": ["plugin_functional/plugins/**/*", "interpreter_functional/plugins/**/*"], "references": [ { "path": "../src/core/tsconfig.json" }, + { "path": "../src/plugins/telemetry_management_section/tsconfig.json" }, { "path": "../src/plugins/advanced_settings/tsconfig.json" }, { "path": "../src/plugins/management/tsconfig.json" }, { "path": "../src/plugins/bfetch/tsconfig.json" }, { "path": "../src/plugins/charts/tsconfig.json" }, + { "path": "../src/plugins/console/tsconfig.json" }, { "path": "../src/plugins/dashboard/tsconfig.json" }, { "path": "../src/plugins/discover/tsconfig.json" }, { "path": "../src/plugins/embeddable/tsconfig.json" }, + { "path": "../src/plugins/es_ui_shared/tsconfig.json" }, { "path": "../src/plugins/expressions/tsconfig.json" }, { "path": "../src/plugins/home/tsconfig.json" }, { "path": "../src/plugins/inspector/tsconfig.json" }, @@ -24,6 +27,7 @@ { "path": "../src/plugins/navigation/tsconfig.json" }, { "path": "../src/plugins/newsfeed/tsconfig.json" }, { "path": "../src/plugins/saved_objects/tsconfig.json" }, + { "path": "../src/plugins/saved_objects_management/tsconfig.json" }, { "path": "../src/plugins/saved_objects_tagging_oss/tsconfig.json" }, { "path": "../src/plugins/telemetry_collection_manager/tsconfig.json" }, { "path": "../src/plugins/telemetry/tsconfig.json" }, diff --git a/tsconfig.json b/tsconfig.json index 1d8c61d515fdf..b6742bffeab55 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,15 +7,18 @@ "exclude": [ "src/**/__fixtures__/**/*", "src/core/**/*", + "src/plugins/telemetry_management_section/**/*", "src/plugins/advanced_settings/**/*", "src/plugins/apm_oss/**/*", "src/plugins/bfetch/**/*", "src/plugins/charts/**/*", + "src/plugins/console/**/*", "src/plugins/dashboard/**/*", "src/plugins/discover/**/*", "src/plugins/data/**/*", "src/plugins/dev_tools/**/*", "src/plugins/embeddable/**/*", + "src/plugins/es_ui_shared/**/*", "src/plugins/expressions/**/*", "src/plugins/home/**/*", "src/plugins/inspector/**/*", @@ -23,20 +26,34 @@ "src/plugins/kibana_react/**/*", "src/plugins/kibana_usage_collection/**/*", "src/plugins/kibana_utils/**/*", + "src/plugins/lens_oss/**/*", "src/plugins/management/**/*", "src/plugins/navigation/**/*", "src/plugins/newsfeed/**/*", "src/plugins/saved_objects/**/*", + "src/plugins/saved_objects_management/**/*", "src/plugins/saved_objects_tagging_oss/**/*", "src/plugins/security_oss/**/*", "src/plugins/share/**/*", "src/plugins/spaces_oss/**/*", "src/plugins/telemetry/**/*", "src/plugins/telemetry_collection_manager/**/*", + "src/plugins/timelion/**/*", "src/plugins/ui_actions/**/*", "src/plugins/url_forwarding/**/*", "src/plugins/usage_collection/**/*", - "src/plugins/presentation_util/**/*" + "src/plugins/presentation_util/**/*", + "src/plugins/vis_default_editor/**/*", + "src/plugins/vis_type_markdown/**/*", + "src/plugins/vis_type_metric/**/*", + "src/plugins/vis_type_table/**/*", + "src/plugins/vis_type_tagcloud/**/*", + "src/plugins/vis_type_timelion/**/*", + "src/plugins/vis_type_timeseries/**/*", + "src/plugins/vis_type_vislib/**/*", + "src/plugins/vis_type_xy/**/*", + "src/plugins/visualizations/**/*", + "src/plugins/visualize/**/*", // In the build we actually exclude **/public/**/* from this config so that // we can run the TSC on both this and the .browser version of this config // file, but if we did it during development IDEs would not be able to find @@ -45,15 +62,18 @@ ], "references": [ { "path": "./src/core/tsconfig.json" }, + { "path": "./src/plugins/telemetry_management_section/tsconfig.json" }, { "path": "./src/plugins/advanced_settings/tsconfig.json" }, { "path": "./src/plugins/apm_oss/tsconfig.json" }, { "path": "./src/plugins/bfetch/tsconfig.json" }, { "path": "./src/plugins/charts/tsconfig.json" }, + { "path": "./src/plugins/console/tsconfig.json" }, { "path": "./src/plugins/dashboard/tsconfig.json" }, { "path": "./src/plugins/discover/tsconfig.json" }, { "path": "./src/plugins/data/tsconfig.json" }, { "path": "./src/plugins/dev_tools/tsconfig.json" }, { "path": "./src/plugins/embeddable/tsconfig.json" }, + { "path": "./src/plugins/es_ui_shared/tsconfig.json" }, { "path": "./src/plugins/expressions/tsconfig.json" }, { "path": "./src/plugins/home/tsconfig.json" }, { "path": "./src/plugins/inspector/tsconfig.json" }, @@ -61,18 +81,32 @@ { "path": "./src/plugins/kibana_react/tsconfig.json" }, { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "./src/plugins/kibana_utils/tsconfig.json" }, + { "path": "./src/plugins/lens_oss/tsconfig.json" }, { "path": "./src/plugins/management/tsconfig.json" }, { "path": "./src/plugins/navigation/tsconfig.json" }, { "path": "./src/plugins/newsfeed/tsconfig.json" }, { "path": "./src/plugins/saved_objects/tsconfig.json" }, + { "path": "./src/plugins/saved_objects_management/tsconfig.json" }, { "path": "./src/plugins/saved_objects_tagging_oss/tsconfig.json" }, { "path": "./src/plugins/security_oss/tsconfig.json" }, { "path": "./src/plugins/share/tsconfig.json" }, { "path": "./src/plugins/spaces_oss/tsconfig.json" }, { "path": "./src/plugins/telemetry/tsconfig.json" }, { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, + { "path": "./src/plugins/timelion/tsconfig.json" }, { "path": "./src/plugins/ui_actions/tsconfig.json" }, { "path": "./src/plugins/url_forwarding/tsconfig.json" }, - { "path": "./src/plugins/usage_collection/tsconfig.json" } + { "path": "./src/plugins/usage_collection/tsconfig.json" }, + { "path": "./src/plugins/vis_default_editor/tsconfig.json" }, + { "path": "./src/plugins/vis_type_markdown/tsconfig.json" }, + { "path": "./src/plugins/vis_type_metric/tsconfig.json" }, + { "path": "./src/plugins/vis_type_table/tsconfig.json" }, + { "path": "./src/plugins/vis_type_tagcloud/tsconfig.json" }, + { "path": "./src/plugins/vis_type_timelion/tsconfig.json" }, + { "path": "./src/plugins/vis_type_timeseries/tsconfig.json" }, + { "path": "./src/plugins/vis_type_vislib/tsconfig.json" }, + { "path": "./src/plugins/vis_type_xy/tsconfig.json" }, + { "path": "./src/plugins/visualizations/tsconfig.json" }, + { "path": "./src/plugins/visualize/tsconfig.json" }, ] } diff --git a/tsconfig.refs.json b/tsconfig.refs.json index d6e87da6ac19d..1bce19e2ee44a 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -2,14 +2,17 @@ "include": [], "references": [ { "path": "./src/core/tsconfig.json" }, + { "path": "./src/plugins/telemetry_management_section/tsconfig.json" }, { "path": "./src/plugins/advanced_settings/tsconfig.json" }, { "path": "./src/plugins/apm_oss/tsconfig.json" }, { "path": "./src/plugins/bfetch/tsconfig.json" }, { "path": "./src/plugins/charts/tsconfig.json" }, + { "path": "./src/plugins/console/tsconfig.json" }, { "path": "./src/plugins/data/tsconfig.json" }, { "path": "./src/plugins/dev_tools/tsconfig.json" }, { "path": "./src/plugins/discover/tsconfig.json" }, { "path": "./src/plugins/embeddable/tsconfig.json" }, + { "path": "./src/plugins/es_ui_shared/tsconfig.json" }, { "path": "./src/plugins/expressions/tsconfig.json" }, { "path": "./src/plugins/home/tsconfig.json" }, { "path": "./src/plugins/dashboard/tsconfig.json" }, @@ -19,10 +22,12 @@ { "path": "./src/plugins/kibana_react/tsconfig.json" }, { "path": "./src/plugins/kibana_usage_collection/tsconfig.json" }, { "path": "./src/plugins/kibana_utils/tsconfig.json" }, + { "path": "./src/plugins/lens_oss/tsconfig.json" }, { "path": "./src/plugins/management/tsconfig.json" }, { "path": "./src/plugins/navigation/tsconfig.json" }, { "path": "./src/plugins/newsfeed/tsconfig.json" }, { "path": "./src/plugins/saved_objects/tsconfig.json" }, + { "path": "./src/plugins/saved_objects_management/tsconfig.json" }, { "path": "./src/plugins/saved_objects_tagging_oss/tsconfig.json" }, { "path": "./src/plugins/presentation_util/tsconfig.json" }, { "path": "./src/plugins/security_oss/tsconfig.json" }, @@ -30,9 +35,20 @@ { "path": "./src/plugins/spaces_oss/tsconfig.json" }, { "path": "./src/plugins/telemetry/tsconfig.json" }, { "path": "./src/plugins/telemetry_collection_manager/tsconfig.json" }, + { "path": "./src/plugins/timelion/tsconfig.json" }, { "path": "./src/plugins/ui_actions/tsconfig.json" }, { "path": "./src/plugins/url_forwarding/tsconfig.json" }, { "path": "./src/plugins/usage_collection/tsconfig.json" }, - { "path": "./src/plugins/management/tsconfig.json" }, + { "path": "./src/plugins/vis_default_editor/tsconfig.json" }, + { "path": "./src/plugins/vis_type_markdown/tsconfig.json" }, + { "path": "./src/plugins/vis_type_metric/tsconfig.json" }, + { "path": "./src/plugins/vis_type_table/tsconfig.json" }, + { "path": "./src/plugins/vis_type_tagcloud/tsconfig.json" }, + { "path": "./src/plugins/vis_type_timelion/tsconfig.json" }, + { "path": "./src/plugins/vis_type_timeseries/tsconfig.json" }, + { "path": "./src/plugins/vis_type_vislib/tsconfig.json" }, + { "path": "./src/plugins/vis_type_xy/tsconfig.json" }, + { "path": "./src/plugins/visualizations/tsconfig.json" }, + { "path": "./src/plugins/visualize/tsconfig.json" }, ] } diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 7380d25930bc0..6937862d20536 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -42,7 +42,7 @@ "xpack.remoteClusters": "plugins/remote_clusters", "xpack.reporting": ["plugins/reporting"], "xpack.rollupJobs": ["plugins/rollup"], - "xpack.runtimeFields": "plugins/runtime_field_editor", + "xpack.runtimeFields": "plugins/runtime_fields", "xpack.searchProfiler": "plugins/searchprofiler", "xpack.security": "plugins/security", "xpack.server": "legacy/server", diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts index 32e1b233274c9..e106b17ad223f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts @@ -46,12 +46,9 @@ describe('request', () => { expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'get', data: {}, - headers: undefined, httpAgent: undefined, httpsAgent: undefined, - params: undefined, proxy: false, - validateStatus: undefined, }); expect(res).toEqual({ status: 200, @@ -80,12 +77,9 @@ describe('request', () => { expect(axiosMock).toHaveBeenCalledWith('http://testProxy', { method: 'get', data: {}, - headers: undefined, httpAgent, httpsAgent, - params: undefined, proxy: false, - validateStatus: undefined, }); expect(res).toEqual({ status: 200, @@ -108,12 +102,9 @@ describe('request', () => { expect(axiosMock).toHaveBeenCalledWith('https://testProxy', { method: 'get', data: {}, - headers: undefined, httpAgent: undefined, httpsAgent: undefined, - params: undefined, proxy: false, - validateStatus: undefined, }); expect(res).toEqual({ status: 200, @@ -128,12 +119,9 @@ describe('request', () => { expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'post', data: { id: '123' }, - headers: undefined, httpAgent: undefined, httpsAgent: undefined, - params: undefined, proxy: false, - validateStatus: undefined, }); expect(res).toEqual({ status: 200, @@ -156,12 +144,9 @@ describe('patch', () => { expect(axiosMock).toHaveBeenCalledWith('/test', { method: 'patch', data: { id: '123' }, - headers: undefined, httpAgent: undefined, httpsAgent: undefined, - params: undefined, proxy: false, - validateStatus: undefined, }); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts index 322da1077af18..78c6b91b57dc0 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.ts @@ -15,11 +15,8 @@ export const request = async ({ logger, method = 'get', data, - params, proxySettings, - headers, - validateStatus, - auth, + ...rest }: { axios: AxiosInstance; url: string; @@ -35,16 +32,13 @@ export const request = async ({ const { httpAgent, httpsAgent } = getProxyAgents(proxySettings, logger); return await axios(url, { + ...rest, method, data: data ?? {}, - params, - auth, // use httpAgent and httpsAgent and set axios proxy: false, to be able to handle fail on invalid certs httpAgent, httpsAgent, proxy: false, - headers, - validateStatus, }); }; diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index 7c41bf99af472..133e5f9c6aa2c 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -19,6 +19,7 @@ import { ElasticsearchServiceStart, ILegacyClusterClient, SavedObjectsClientContract, + SavedObjectsBulkGetObject, } from '../../../../src/core/server'; import { @@ -333,7 +334,12 @@ export class ActionsPlugin implements Plugin, Plugi this.eventLogService!.registerSavedObjectProvider('action', (request) => { const client = secureGetActionsClientWithRequest(request); - return async (type: string, id: string) => (await client).get({ id }); + return (objects?: SavedObjectsBulkGetObject[]) => + objects + ? Promise.all( + objects.map(async (objectItem) => await (await client).get({ id: objectItem.id })) + ) + : Promise.resolve([]); }); const getScopedSavedObjectsClientWithoutAccessToActions = (request: KibanaRequest) => diff --git a/x-pack/plugins/alerts/common/alert_navigation.ts b/x-pack/plugins/alerts/common/alert_navigation.ts index 188764069e84f..b04278b6517c8 100644 --- a/x-pack/plugins/alerts/common/alert_navigation.ts +++ b/x-pack/plugins/alerts/common/alert_navigation.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { JsonObject } from '../../infra/common/typed_json'; +import { JsonObject } from '../../../../src/plugins/kibana_utils/common'; + export interface AlertUrlNavigation { path: string; } diff --git a/x-pack/plugins/alerts/kibana.json b/x-pack/plugins/alerts/kibana.json index c0ab242831428..a2cb237467f11 100644 --- a/x-pack/plugins/alerts/kibana.json +++ b/x-pack/plugins/alerts/kibana.json @@ -5,7 +5,15 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "alerts"], - "requiredPlugins": ["licensing", "taskManager", "encryptedSavedObjects", "actions", "eventLog", "features"], + "requiredPlugins": [ + "actions", + "encryptedSavedObjects", + "eventLog", + "features", + "kibanaUtils", + "licensing", + "taskManager" + ], "optionalPlugins": ["usageCollection", "spaces", "security"], "extraPublicDirs": ["common", "common/parse_duration"] } diff --git a/x-pack/plugins/alerts/public/alert_navigation_registry/types.ts b/x-pack/plugins/alerts/public/alert_navigation_registry/types.ts index 0038652f47f12..e0ab2dcf405f3 100644 --- a/x-pack/plugins/alerts/public/alert_navigation_registry/types.ts +++ b/x-pack/plugins/alerts/public/alert_navigation_registry/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { JsonObject } from '../../../infra/common/typed_json'; +import { JsonObject } from '../../../../../src/plugins/kibana_utils/common'; import { AlertType, SanitizedAlert } from '../../common'; export type AlertNavigationHandler = ( diff --git a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts index a47af44d330c3..457079229de94 100644 --- a/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts +++ b/x-pack/plugins/alerts/server/alerts_client/alerts_client.ts @@ -412,7 +412,7 @@ export class AlertsClient { this.logger.debug(`getAlertInstanceSummary(): search the event log for alert ${id}`); let events: IEvent[]; try { - const queryResults = await eventLogClient.findEventsBySavedObject('alert', id, { + const queryResults = await eventLogClient.findEventsBySavedObjectIds('alert', [id], { page: 1, per_page: 10000, start: parsedDateStart.toISOString(), @@ -817,10 +817,7 @@ export class AlertsClient { attributes.consumer, WriteOperations.UpdateApiKey ); - if ( - attributes.actions.length && - !this.authorization.shouldUseLegacyAuthorization(attributes) - ) { + if (attributes.actions.length) { await this.actionsAuthorization.ensureAuthorized('execute'); } } catch (error) { diff --git a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts index 555c316038daa..6c0612df030dd 100644 --- a/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts +++ b/x-pack/plugins/alerts/server/alerts_client/tests/get_alert_instance_summary.test.ts @@ -131,7 +131,7 @@ describe('getAlertInstanceSummary()', () => { total: events.length, data: events, }; - eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(eventsResult); + eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(eventsResult); const dateStart = new Date(Date.now() - 60 * 1000).toISOString(); @@ -188,18 +188,20 @@ describe('getAlertInstanceSummary()', () => { test('calls saved objects and event log client with default params', async () => { unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); - eventLogClient.findEventsBySavedObject.mockResolvedValueOnce( + eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce( AlertInstanceSummaryFindEventsResult ); await alertsClient.getAlertInstanceSummary({ id: '1' }); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1); - expect(eventLogClient.findEventsBySavedObject.mock.calls[0]).toMatchInlineSnapshot(` + expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObjectIds.mock.calls[0]).toMatchInlineSnapshot(` Array [ "alert", - "1", + Array [ + "1", + ], Object { "end": "2019-02-12T21:01:22.479Z", "page": 1, @@ -210,7 +212,7 @@ describe('getAlertInstanceSummary()', () => { ] `); // calculate the expected start/end date for one test - const { start, end } = eventLogClient.findEventsBySavedObject.mock.calls[0][2]!; + const { start, end } = eventLogClient.findEventsBySavedObjectIds.mock.calls[0][2]!; expect(end).toBe(mockedDateString); const startMillis = Date.parse(start!); @@ -222,7 +224,7 @@ describe('getAlertInstanceSummary()', () => { test('calls event log client with start date', async () => { unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); - eventLogClient.findEventsBySavedObject.mockResolvedValueOnce( + eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce( AlertInstanceSummaryFindEventsResult ); @@ -232,8 +234,8 @@ describe('getAlertInstanceSummary()', () => { await alertsClient.getAlertInstanceSummary({ id: '1', dateStart }); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1); - const { start, end } = eventLogClient.findEventsBySavedObject.mock.calls[0][2]!; + expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); + const { start, end } = eventLogClient.findEventsBySavedObjectIds.mock.calls[0][2]!; expect({ start, end }).toMatchInlineSnapshot(` Object { @@ -245,7 +247,7 @@ describe('getAlertInstanceSummary()', () => { test('calls event log client with relative start date', async () => { unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); - eventLogClient.findEventsBySavedObject.mockResolvedValueOnce( + eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce( AlertInstanceSummaryFindEventsResult ); @@ -253,8 +255,8 @@ describe('getAlertInstanceSummary()', () => { await alertsClient.getAlertInstanceSummary({ id: '1', dateStart }); expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledTimes(1); - expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1); - const { start, end } = eventLogClient.findEventsBySavedObject.mock.calls[0][2]!; + expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); + const { start, end } = eventLogClient.findEventsBySavedObjectIds.mock.calls[0][2]!; expect({ start, end }).toMatchInlineSnapshot(` Object { @@ -266,7 +268,7 @@ describe('getAlertInstanceSummary()', () => { test('invalid start date throws an error', async () => { unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); - eventLogClient.findEventsBySavedObject.mockResolvedValueOnce( + eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce( AlertInstanceSummaryFindEventsResult ); @@ -280,7 +282,7 @@ describe('getAlertInstanceSummary()', () => { test('saved object get throws an error', async () => { unsecuredSavedObjectsClient.get.mockRejectedValueOnce(new Error('OMG!')); - eventLogClient.findEventsBySavedObject.mockResolvedValueOnce( + eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce( AlertInstanceSummaryFindEventsResult ); @@ -291,7 +293,7 @@ describe('getAlertInstanceSummary()', () => { test('findEvents throws an error', async () => { unsecuredSavedObjectsClient.get.mockResolvedValueOnce(getAlertInstanceSummarySavedObject()); - eventLogClient.findEventsBySavedObject.mockRejectedValueOnce(new Error('OMG 2!')); + eventLogClient.findEventsBySavedObjectIds.mockRejectedValueOnce(new Error('OMG 2!')); // error eaten but logged await alertsClient.getAlertInstanceSummary({ id: '1' }); diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.mock.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.mock.ts index 171e3978d0d0d..30de2c79732ce 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.mock.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.mock.ts @@ -14,7 +14,6 @@ const createAlertsAuthorizationMock = () => { ensureAuthorized: jest.fn(), filterByAlertTypeAuthorization: jest.fn(), getFindAuthorizationFilter: jest.fn(), - shouldUseLegacyAuthorization: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index 6814e4ac1cc1b..29f2078bc61e4 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -8,13 +8,12 @@ import Boom from '@hapi/boom'; import { map, mapValues, fromPairs, has } from 'lodash'; import { KibanaRequest } from 'src/core/server'; import { ALERTS_FEATURE_ID } from '../../common'; -import { AlertTypeRegistry, RawAlert } from '../types'; +import { AlertTypeRegistry } from '../types'; import { SecurityPluginSetup } from '../../../security/server'; import { RegistryAlertType } from '../alert_type_registry'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { AlertsAuthorizationAuditLogger, ScopeType } from './audit_logger'; import { Space } from '../../../spaces/server'; -import { LEGACY_LAST_MODIFIED_VERSION } from '../saved_objects/migrations'; import { asFiltersByAlertTypeAndConsumer } from './alerts_authorization_kuery'; import { KueryNode } from '../../../../../src/plugins/data/server'; @@ -112,10 +111,6 @@ export class AlertsAuthorization { ); } - public shouldUseLegacyAuthorization(alert: RawAlert): boolean { - return alert.meta?.versionApiKeyLastmodified === LEGACY_LAST_MODIFIED_VERSION; - } - private shouldCheckAuthorization(): boolean { return this.authorization?.mode?.useRbacForRequest(this.request) ?? false; } diff --git a/x-pack/plugins/alerts/server/lib/retry_if_conflicts.ts b/x-pack/plugins/alerts/server/lib/retry_if_conflicts.ts index 9cb1d7975855c..59ecc59ab57f8 100644 --- a/x-pack/plugins/alerts/server/lib/retry_if_conflicts.ts +++ b/x-pack/plugins/alerts/server/lib/retry_if_conflicts.ts @@ -15,9 +15,7 @@ import { Logger, SavedObjectsErrorHelpers } from '../../../../../src/core/server type RetryableForConflicts = () => Promise; // number of times to retry when conflicts occur -// note: it seems unlikely that we'd need more than one retry, but leaving -// this statically configurable in case we DO need > 1 -export const RetryForConflictsAttempts = 1; +export const RetryForConflictsAttempts = 2; // milliseconds to wait before retrying when conflicts occur // note: we considered making this random, to help avoid a stampede, but diff --git a/x-pack/plugins/alerts/server/plugin.ts b/x-pack/plugins/alerts/server/plugin.ts index d15ae0ca55ef9..cb165fa56d046 100644 --- a/x-pack/plugins/alerts/server/plugin.ts +++ b/x-pack/plugins/alerts/server/plugin.ts @@ -33,6 +33,7 @@ import { ILegacyClusterClient, StatusServiceSetup, ServiceStatus, + SavedObjectsBulkGetObject, } from '../../../../src/core/server'; import { @@ -370,7 +371,10 @@ export class AlertingPlugin { this.eventLogService!.registerSavedObjectProvider('alert', (request) => { const client = getAlertsClientWithRequest(request); - return (type: string, id: string) => client.get({ id }); + return (objects?: SavedObjectsBulkGetObject[]) => + objects + ? Promise.all(objects.map(async (objectItem) => await client.get({ id: objectItem.id }))) + : Promise.resolve([]); }); scheduleAlertingTelemetry(this.telemetryLogger, plugins.taskManager); diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts index bd2e18b66c8e3..a41faba2e9382 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts @@ -125,7 +125,7 @@ export const generalSettings: RawSettingDefinition[] = [ defaultMessage: 'When recording, the agent instruments incoming HTTP requests, tracks errors, and collects and sends metrics. When set to non-recording, the agent works as a noop, not collecting data and not communicating with the APM Server except for polling for updated configuration. As this is a reversible switch, agent threads are not being killed when set to non-recording, but they will be mostly idle in this state, so the overhead should be negligible. You can use this setting to dynamically control whether Elastic APM is enabled or disabled.', }), - excludeAgents: ['nodejs'], + excludeAgents: ['nodejs', 'rum-js', 'js-base'], }, // SERVER_TIMEOUT diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts index f7209d8b46edb..4f319e4dd7016 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts @@ -87,14 +87,12 @@ describe('filterByAgent', () => { it('js-base', () => { expect(getSettingKeysForAgent('js-base')).toEqual([ - 'recording', 'transaction_sample_rate', ]); }); it('rum-js', () => { expect(getSettingKeysForAgent('rum-js')).toEqual([ - 'recording', 'transaction_sample_rate', ]); }); diff --git a/x-pack/plugins/apm/common/license_check.test.ts b/x-pack/plugins/apm/common/license_check.test.ts new file mode 100644 index 0000000000000..2cad197719be5 --- /dev/null +++ b/x-pack/plugins/apm/common/license_check.test.ts @@ -0,0 +1,217 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { License } from '../../licensing/common/license'; +import { isActiveGoldLicense, isActivePlatinumLicense } from './license_check'; + +describe('License check', () => { + describe('isActivePlatinumLicense', () => { + describe('with an expired license', () => { + it('returns false', () => { + const license = new License({ + license: { + uid: 'test uid', + expiryDateInMillis: 0, + mode: 'platinum', + type: 'platinum', + status: 'expired', + }, + signature: 'test signature', + }); + + expect(isActivePlatinumLicense(license)).toEqual(false); + }); + }); + + describe('with a basic license', () => { + it('returns false', () => { + const license = new License({ + license: { + uid: 'test uid', + expiryDateInMillis: 0, + mode: 'basic', + type: 'basic', + status: 'active', + }, + signature: 'test signature', + }); + + expect(isActivePlatinumLicense(license)).toEqual(false); + }); + }); + + describe('with a gold license', () => { + it('returns true', () => { + const license = new License({ + license: { + uid: 'test uid', + expiryDateInMillis: 0, + mode: 'gold', + type: 'gold', + status: 'active', + }, + signature: 'test signature', + }); + + expect(isActivePlatinumLicense(license)).toEqual(false); + }); + }); + + describe('with a platinum license', () => { + it('returns true', () => { + const license = new License({ + license: { + uid: 'test uid', + expiryDateInMillis: 0, + mode: 'platinum', + type: 'platinum', + status: 'active', + }, + signature: 'test signature', + }); + + expect(isActivePlatinumLicense(license)).toEqual(true); + }); + }); + + describe('with an enterprise license', () => { + it('returns true', () => { + const license = new License({ + license: { + uid: 'test uid', + expiryDateInMillis: 0, + mode: 'enterprise', + type: 'enterprise', + status: 'active', + }, + signature: 'test signature', + }); + + expect(isActivePlatinumLicense(license)).toEqual(true); + }); + }); + + describe('with a trial license', () => { + it('returns true', () => { + const license = new License({ + license: { + uid: 'test uid', + expiryDateInMillis: 0, + mode: 'trial', + type: 'trial', + status: 'active', + }, + signature: 'test signature', + }); + + expect(isActivePlatinumLicense(license)).toEqual(true); + }); + }); + }); + describe('isActiveGoldLicense', () => { + describe('with an expired license', () => { + it('returns false', () => { + const license = new License({ + license: { + uid: 'test uid', + expiryDateInMillis: 0, + mode: 'gold', + type: 'gold', + status: 'expired', + }, + signature: 'test signature', + }); + + expect(isActiveGoldLicense(license)).toEqual(false); + }); + }); + + describe('with a basic license', () => { + it('returns false', () => { + const license = new License({ + license: { + uid: 'test uid', + expiryDateInMillis: 0, + mode: 'basic', + type: 'basic', + status: 'active', + }, + signature: 'test signature', + }); + + expect(isActiveGoldLicense(license)).toEqual(false); + }); + }); + + describe('with a gold license', () => { + it('returns true', () => { + const license = new License({ + license: { + uid: 'test uid', + expiryDateInMillis: 0, + mode: 'gold', + type: 'gold', + status: 'active', + }, + signature: 'test signature', + }); + + expect(isActiveGoldLicense(license)).toEqual(true); + }); + }); + + describe('with a platinum license', () => { + it('returns true', () => { + const license = new License({ + license: { + uid: 'test uid', + expiryDateInMillis: 0, + mode: 'platinum', + type: 'platinum', + status: 'active', + }, + signature: 'test signature', + }); + + expect(isActiveGoldLicense(license)).toEqual(true); + }); + }); + + describe('with an enterprise license', () => { + it('returns true', () => { + const license = new License({ + license: { + uid: 'test uid', + expiryDateInMillis: 0, + mode: 'enterprise', + type: 'enterprise', + status: 'active', + }, + signature: 'test signature', + }); + + expect(isActiveGoldLicense(license)).toEqual(true); + }); + }); + + describe('with a trial license', () => { + it('returns true', () => { + const license = new License({ + license: { + uid: 'test uid', + expiryDateInMillis: 0, + mode: 'trial', + type: 'trial', + status: 'active', + }, + signature: 'test signature', + }); + + expect(isActiveGoldLicense(license)).toEqual(true); + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/common/license_check.ts b/x-pack/plugins/apm/common/license_check.ts new file mode 100644 index 0000000000000..81b9196173cff --- /dev/null +++ b/x-pack/plugins/apm/common/license_check.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; + * you may not use this file except in compliance with the Elastic License. + */ +import { ILicense, LicenseType } from '../../licensing/common/types'; + +function isActiveLicense(licenseType: LicenseType, license?: ILicense) { + return license && license.isActive && license.hasAtLeast(licenseType); +} + +export function isActivePlatinumLicense(license?: ILicense) { + return isActiveLicense('platinum', license); +} + +export function isActiveGoldLicense(license?: ILicense) { + return isActiveLicense('gold', license); +} diff --git a/x-pack/plugins/apm/common/service_map.test.ts b/x-pack/plugins/apm/common/service_map.test.ts deleted file mode 100644 index 31f439a7aaec9..0000000000000 --- a/x-pack/plugins/apm/common/service_map.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { License } from '../../licensing/common/license'; -import * as serviceMap from './service_map'; - -describe('service map helpers', () => { - describe('isActivePlatinumLicense', () => { - describe('with an expired license', () => { - it('returns false', () => { - const license = new License({ - license: { - uid: 'test uid', - expiryDateInMillis: 0, - mode: 'platinum', - type: 'platinum', - status: 'expired', - }, - signature: 'test signature', - }); - - expect(serviceMap.isActivePlatinumLicense(license)).toEqual(false); - }); - }); - - describe('with a basic license', () => { - it('returns false', () => { - const license = new License({ - license: { - uid: 'test uid', - expiryDateInMillis: 0, - mode: 'basic', - type: 'basic', - status: 'active', - }, - signature: 'test signature', - }); - - expect(serviceMap.isActivePlatinumLicense(license)).toEqual(false); - }); - }); - - describe('with a platinum license', () => { - it('returns true', () => { - const license = new License({ - license: { - uid: 'test uid', - expiryDateInMillis: 0, - mode: 'platinum', - type: 'platinum', - status: 'active', - }, - signature: 'test signature', - }); - - expect(serviceMap.isActivePlatinumLicense(license)).toEqual(true); - }); - }); - - describe('with an enterprise license', () => { - it('returns true', () => { - const license = new License({ - license: { - uid: 'test uid', - expiryDateInMillis: 0, - mode: 'enterprise', - type: 'enterprise', - status: 'active', - }, - signature: 'test signature', - }); - - expect(serviceMap.isActivePlatinumLicense(license)).toEqual(true); - }); - }); - - describe('with a trial license', () => { - it('returns true', () => { - const license = new License({ - license: { - uid: 'test uid', - expiryDateInMillis: 0, - mode: 'trial', - type: 'trial', - status: 'active', - }, - signature: 'test signature', - }); - - expect(serviceMap.isActivePlatinumLicense(license)).toEqual(true); - }); - }); - }); -}); diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index 6edf56fb9a1ae..17f0d110cf432 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -6,7 +6,6 @@ import { i18n } from '@kbn/i18n'; import cytoscape from 'cytoscape'; -import { ILicense } from '../../licensing/common/types'; import { AGENT_NAME, SERVICE_ENVIRONMENT, @@ -61,10 +60,6 @@ export interface ServiceNodeStats { avgErrorRate: number | null; } -export function isActivePlatinumLicense(license: ILicense) { - return license.isActive && license.hasAtLeast('platinum'); -} - export const invalidLicenseMessage = i18n.translate( 'xpack.apm.serviceMap.invalidLicenseMessage', { diff --git a/x-pack/plugins/apm/common/ui_settings_keys.ts b/x-pack/plugins/apm/common/ui_settings_keys.ts index 38922fa445a47..ffc2a2ef21fe9 100644 --- a/x-pack/plugins/apm/common/ui_settings_keys.ts +++ b/x-pack/plugins/apm/common/ui_settings_keys.ts @@ -4,5 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export const enableCorrelations = 'apm:enableCorrelations'; +export const enableSignificantTerms = 'apm:enableSignificantTerms'; export const enableServiceOverview = 'apm:enableServiceOverview'; diff --git a/x-pack/plugins/apm/common/utils/formatters/duration.test.ts b/x-pack/plugins/apm/common/utils/formatters/duration.test.ts index a39d9b47f41c2..086224f092cae 100644 --- a/x-pack/plugins/apm/common/utils/formatters/duration.test.ts +++ b/x-pack/plugins/apm/common/utils/formatters/duration.test.ts @@ -46,5 +46,8 @@ describe('duration formatters', () => { it('converts to formatted decimal milliseconds', () => { expect(asMillisecondDuration(0)).toEqual('0 ms'); }); + it('formats correctly with undefined values', () => { + expect(asMillisecondDuration(undefined)).toEqual('N/A'); + }); }); }); diff --git a/x-pack/plugins/apm/common/utils/formatters/duration.ts b/x-pack/plugins/apm/common/utils/formatters/duration.ts index cf2d99fe5119d..d2c87f5d30474 100644 --- a/x-pack/plugins/apm/common/utils/formatters/duration.ts +++ b/x-pack/plugins/apm/common/utils/formatters/duration.ts @@ -186,9 +186,9 @@ export function asDuration( * `asDuration`, but this is used in places like tables where we always want * the same units. */ -export function asMillisecondDuration(time: number) { +export function asMillisecondDuration(value: Maybe) { return convertTo({ unit: 'milliseconds', - microseconds: time, + microseconds: value, }).formatted; } diff --git a/x-pack/plugins/apm/common/utils/is_finite_number.ts b/x-pack/plugins/apm/common/utils/is_finite_number.ts index 47c4f5fdbd0ee..f5ec78de475cc 100644 --- a/x-pack/plugins/apm/common/utils/is_finite_number.ts +++ b/x-pack/plugins/apm/common/utils/is_finite_number.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { isFinite } from 'lodash'; -import { Maybe } from '../../typings/common'; // _.isNumber() returns true for NaN, _.isFinite() does not refine -export function isFiniteNumber(value: Maybe): value is number { +export function isFiniteNumber(value: any): value is number { return isFinite(value); } diff --git a/x-pack/plugins/apm/common/utils/join_by_key/index.ts b/x-pack/plugins/apm/common/utils/join_by_key/index.ts index 6678bf68afbae..d296fc44c1f24 100644 --- a/x-pack/plugins/apm/common/utils/join_by_key/index.ts +++ b/x-pack/plugins/apm/common/utils/join_by_key/index.ts @@ -9,11 +9,11 @@ import { isEqual, pull, merge, castArray } from 'lodash'; /** * Joins a list of records by a given key. Key can be any type of value, from * strings to plain objects, as long as it is present in all records. `isEqual` - * is used for comparing keys. - * + * is used for comparing keys. + * * UnionToIntersection is needed to get all keys of union types, see below for * example. - * + * const agentNames = [{ serviceName: '', agentName: '' }]; const transactionRates = [{ serviceName: '', transactionsPerMinute: 1 }]; const flattened = joinByKey( diff --git a/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/popover_expression/index.tsx b/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/popover_expression/index.tsx index a95ea3cf11e7a..48e9b540a2d1d 100644 --- a/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/popover_expression/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/service_alert_trigger/popover_expression/index.tsx @@ -30,6 +30,7 @@ export function PopoverExpression(props: Props) { onClick={() => setPopoverOpen(true)} /> } + repositionOnScroll > {children} diff --git a/x-pack/plugins/apm/public/components/app/Correlations/index.tsx b/x-pack/plugins/apm/public/components/app/Correlations/index.tsx index 217400bbcc0f1..fada90039aecf 100644 --- a/x-pack/plugins/apm/public/components/app/Correlations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Correlations/index.tsx @@ -19,19 +19,25 @@ import { } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { EuiSpacer } from '@elastic/eui'; -import { enableCorrelations } from '../../../../common/ui_settings_keys'; +import { isActivePlatinumLicense } from '../../../../common/license_check'; +import { enableSignificantTerms } 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)) { + if ( + !uiSettings.get(enableSignificantTerms) || + !isActivePlatinumLicense(license) + ) { return null; } diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx index 3b737c6fa4170..f7a4a322e99eb 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx @@ -56,8 +56,7 @@ export function ServiceStatsFetcher({ } ); - const isLoading = - status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING; + const isLoading = status === FETCH_STATUS.LOADING; if (isLoading) { return ; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/Cytoscape.stories.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/Cytoscape.stories.tsx index ee334e2ae9567..a4985d2f5ab0c 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/Cytoscape.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/Cytoscape.stories.tsx @@ -184,6 +184,13 @@ export function NodeIcons() { 'agent.name': 'dotNet', }, }, + { + data: { + id: 'erlang', + 'service.name': 'erlang service', + 'agent.name': 'erlang', + }, + }, { data: { id: 'go', @@ -219,6 +226,13 @@ export function NodeIcons() { 'agent.name': 'nodejs', }, }, + { + data: { + id: 'ocaml', + 'service.name': 'ocaml service', + 'agent.name': 'ocaml', + }, + }, { data: { id: 'opentelemetry', diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx index e7ce4bb24b38f..c6f82e3492750 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.test.tsx @@ -11,7 +11,6 @@ import React, { ReactNode } from 'react'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; import { License } from '../../../../../licensing/common/license'; import { EuiThemeProvider } from '../../../../../observability/public'; -import { FETCH_STATUS } from '../../../../../observability/public/hooks/use_fetcher'; import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; import { LicenseContext } from '../../../context/license/license_context'; import * as useFetcherModule from '../../../hooks/use_fetcher'; @@ -92,7 +91,7 @@ describe('ServiceMap', () => { jest.spyOn(useFetcherModule, 'useFetcher').mockReturnValueOnce({ data: { elements: [] }, refetch: () => {}, - status: FETCH_STATUS.SUCCESS, + status: useFetcherModule.FETCH_STATUS.SUCCESS, }); expect( diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx index da4a8596970ec..6f8d058903183 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -7,10 +7,10 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; import React, { PropsWithChildren, ReactNode } from 'react'; import styled from 'styled-components'; +import { isActivePlatinumLicense } from '../../../../common/license_check'; import { useTrackPageview } from '../../../../../observability/public'; import { invalidLicenseMessage, - isActivePlatinumLicense, SERVICE_MAP_TIMEOUT_ERROR, } from '../../../../common/service_map'; import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx index 9f3a65583ddb7..23f688e66966d 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/List/index.tsx @@ -61,29 +61,38 @@ export function AgentConfigurationList({ status, data, refetch }: Props) { } body={ - <> -

    - {i18n.translate( - 'xpack.apm.agentConfig.configTable.emptyPromptText', +

    + {i18n.translate('xpack.apm.agentConfig.configTable.emptyPromptText', { + defaultMessage: + "Let's change that! You can fine-tune agent configuration directly from Kibana without having to redeploy. Get started by creating your first configuration.", + })} +

    + } + actions={ + - - } - actions={ - - {i18n.translate( - 'xpack.apm.agentConfig.configTable.createConfigButtonLabel', - { defaultMessage: 'Create configuration' } - )} - + + {i18n.translate( + 'xpack.apm.agentConfig.configTable.createConfigButtonLabel', + { defaultMessage: 'Create configuration' } + )} + + } /> ); diff --git a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx index ba08af32d65b6..8c10b96c51ce2 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/ApmIndices/index.tsx @@ -248,7 +248,7 @@ export function ApmIndices() { fill onClick={handleApplyChangesEvent} isLoading={isSaving} - disabled={!canSave} + isDisabled={!canSave} > {i18n.translate( 'xpack.apm.settings.apmIndices.applyButton', diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx index 4477ee5a99be3..63dd486544124 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx @@ -219,7 +219,8 @@ describe('CustomLink', () => { expect(saveCustomLinkSpy).toHaveBeenCalledTimes(1); }); - it('deletes a custom link', async () => { + // FLAKY: https://github.com/elastic/kibana/issues/75106 + it.skip('deletes a custom link', async () => { const mockContext = getMockAPMContext({ canSave: true }); const component = render( diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx index e709c7e104472..16aad0a9a8420 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx @@ -71,8 +71,7 @@ export function AddEnvironments({ ); } - const isLoading = - status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING; + const isLoading = status === FETCH_STATUS.LOADING; return ( diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx index b185685f0720a..c180ffb523f8e 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx @@ -124,8 +124,7 @@ export function JobsList({ data, status, onAddEnvironments }: Props) { function getNoItemsMessage({ status }: { status: FETCH_STATUS }) { // loading state - const isLoading = - status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING; + const isLoading = status === FETCH_STATUS.LOADING; if (isLoading) { return ; } diff --git a/x-pack/plugins/apm/public/components/app/service_details/service_icons/icon_popover.tsx b/x-pack/plugins/apm/public/components/app/service_details/service_icons/icon_popover.tsx index 5fe371c33475a..4ee4e793ef4ef 100644 --- a/x-pack/plugins/apm/public/components/app/service_details/service_icons/icon_popover.tsx +++ b/x-pack/plugins/apm/public/components/app/service_details/service_icons/icon_popover.tsx @@ -35,10 +35,7 @@ export function IconPopover({ if (!icon) { return null; } - const isLoading = - detailsFetchStatus === FETCH_STATUS.LOADING || - detailsFetchStatus === FETCH_STATUS.PENDING; - + const isLoading = detailsFetchStatus === FETCH_STATUS.LOADING; return ( ; diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx index 1f8ff6fdcaf19..2991264b5c0c1 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/index.tsx @@ -137,9 +137,7 @@ export function getServiceColumns({ field: 'transactionType', name: i18n.translate( 'xpack.apm.servicesTable.transactionColumnLabel', - { - defaultMessage: 'Transaction type', - } + { defaultMessage: 'Transaction type' } ), width: px(unit * 10), sortable: true, @@ -148,12 +146,9 @@ export function getServiceColumns({ : []), { field: 'avgResponseTime', - name: i18n.translate( - 'xpack.apm.servicesTable.avgResponseTimeColumnLabel', - { - defaultMessage: 'Avg. response time', - } - ), + name: i18n.translate('xpack.apm.servicesTable.latencyAvgColumnLabel', { + defaultMessage: 'Latency (avg.)', + }), sortable: true, dataType: 'number', render: (_, { avgResponseTime }) => ( @@ -168,12 +163,9 @@ export function getServiceColumns({ }, { field: 'transactionsPerMinute', - name: i18n.translate( - 'xpack.apm.servicesTable.transactionsPerMinuteColumnLabel', - { - defaultMessage: 'Trans. per minute', - } - ), + name: i18n.translate('xpack.apm.servicesTable.throughputColumnLabel', { + defaultMessage: 'Throughput', + }), sortable: true, dataType: 'number', render: (_, { transactionsPerMinute }) => ( diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/service_list.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/service_list.test.tsx index 45a4afeb53235..275ea3aa13a20 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/service_list.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/ServiceList/service_list.test.tsx @@ -76,13 +76,13 @@ describe('ServiceList', () => { expect(healthHeading).toBeNull(); }); - it('sorts by transactions per minute', async () => { + it('sorts by throughput', async () => { const { findByTitle } = renderWithTheme(, { wrapper: Wrapper, }); expect( - await findByTitle('Trans. per minute; Sorted in descending order') + await findByTitle('Throughput; Sorted in descending order') ).toBeInTheDocument(); }); }); 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 fba9cecac0144..8776b7e9355f1 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 @@ -13,7 +13,6 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useMemo } from 'react'; -import url from 'url'; import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; import { useTrackPageview } from '../../../../../observability/public'; import { Projection } from '../../../../common/projections'; @@ -27,6 +26,7 @@ import { NoServicesMessage } from './no_services_message'; import { ServiceList } from './ServiceList'; import { MLCallout } from './ServiceList/MLCallout'; import { useAnomalyDetectionJobsFetcher } from './use_anomaly_detection_jobs_fetcher'; +import { useUpgradeAssistantHref } from '../../shared/Links/kibana'; const initialData = { items: [], @@ -39,6 +39,7 @@ let hasDisplayedToast = false; function useServicesFetcher() { const { urlParams, uiFilters } = useUrlParams(); const { core } = useApmPluginContext(); + const upgradeAssistantHref = useUpgradeAssistantHref(); const { start, end } = urlParams; const { data = initialData, status } = useFetcher( (callApmApi) => { @@ -70,12 +71,7 @@ function useServicesFetcher() { "You're running Elastic Stack 7.0+ and we've detected incompatible data from a previous 6.x version. If you want to view this data in APM, you should migrate it. See more in ", })} - + {i18n.translate( 'xpack.apm.serviceInventory.upgradeAssistantLinkText', { @@ -87,7 +83,7 @@ function useServicesFetcher() { ), }); } - }, [data.hasLegacyData, core.http.basePath, core.notifications.toasts]); + }, [data.hasLegacyData, upgradeAssistantHref, core.notifications.toasts]); return { servicesData: data, servicesStatus: status }; } diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/no_services_message.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/no_services_message.tsx index b20efc440312c..b6806f3de5e27 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/no_services_message.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/no_services_message.tsx @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiEmptyPrompt } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { KibanaLink } from '../../shared/Links/KibanaLink'; import { SetupInstructionsLink } from '../../shared/Links/SetupInstructionsLink'; import { LoadingStatePrompt } from '../../shared/LoadingStatePrompt'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { ErrorStatePrompt } from '../../shared/ErrorStatePrompt'; +import { useUpgradeAssistantHref } from '../../shared/Links/kibana'; interface Props { // any data submitted from APM agents found (not just in the given time range) @@ -20,6 +20,8 @@ interface Props { } export function NoServicesMessage({ historicalDataFound, status }: Props) { + const upgradeAssistantHref = useUpgradeAssistantHref(); + if (status === 'loading') { return ; } @@ -66,12 +68,12 @@ export function NoServicesMessage({ historicalDataFound, status }: Props) { defaultMessage: 'You may also have old data that needs to be migrated.', })}{' '} - + {i18n.translate('xpack.apm.servicesTable.UpgradeAssistantLink', { defaultMessage: 'Learn more by visiting the Kibana Upgrade Assistant', })} - + .

    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 ae297b840ebc8..079c599f8f7ba 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 @@ -15,7 +15,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; import { - asDuration, + asMillisecondDuration, asPercent, asTransactionRate, } from '../../../../../common/utils/formatters'; @@ -92,7 +92,7 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { ); }, @@ -102,9 +102,7 @@ export function ServiceOverviewDependenciesTable({ serviceName }: Props) { field: 'throughputValue', name: i18n.translate( 'xpack.apm.serviceOverview.dependenciesTableColumnThroughput', - { - defaultMessage: 'Traffic', - } + { defaultMessage: 'Throughput' } ), width: px(unit * 10), render: (_, { throughput }) => { 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 51a4ef649a3ba..1d0e1e50c1489 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 @@ -17,7 +17,7 @@ import { UNIDENTIFIED_SERVICE_NODES_LABEL } from '../../../../../common/i18n'; import { SERVICE_NODE_NAME_MISSING } from '../../../../../common/service_nodes'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { - asDuration, + asMillisecondDuration, asPercent, asTransactionRate, } from '../../../../../common/utils/formatters'; @@ -107,7 +107,7 @@ export function ServiceOverviewInstancesTable({ serviceName }: Props) { ); }, @@ -117,9 +117,7 @@ export function ServiceOverviewInstancesTable({ serviceName }: Props) { field: 'throughputValue', name: i18n.translate( 'xpack.apm.serviceOverview.instancesTableColumnThroughput', - { - defaultMessage: 'Traffic', - } + { defaultMessage: 'Throughput' } ), width: px(unit * 10), render: (_, { throughput }) => { @@ -232,8 +230,7 @@ export function ServiceOverviewInstancesTable({ serviceName }: Props) { memoryUsageValue: item.memoryUsage?.value ?? 0, })); - const isLoading = - status === FETCH_STATUS.LOADING || status === FETCH_STATUS.PENDING; + const isLoading = status === FETCH_STATUS.LOADING; return ( diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx index e63d30022360e..b79e011bde488 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx @@ -51,7 +51,7 @@ export function ServiceOverviewThroughputChart({

    {i18n.translate('xpack.apm.serviceOverview.throughtputChartTitle', { - defaultMessage: 'Traffic', + defaultMessage: 'Throughput', })}

    @@ -66,10 +66,8 @@ export function ServiceOverviewThroughputChart({ type: 'linemark', color: theme.eui.euiColorVis0, title: i18n.translate( - 'xpack.apm.serviceOverview.throughputChart.traffic', - { - defaultMessage: 'Traffic', - } + 'xpack.apm.serviceOverview.throughtputChartTitle', + { defaultMessage: 'Throughput' } ), }, ]} 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 307997731e5ef..c77e80d0176de 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 @@ -16,7 +16,7 @@ import React, { useState } from 'react'; import { ValuesType } from 'utility-types'; import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types'; import { - asDuration, + asMillisecondDuration, asPercent, asTransactionRate, } from '../../../../../common/utils/formatters'; @@ -55,30 +55,29 @@ const DEFAULT_SORT = { function getLatencyAggregationTypeLabel(latencyAggregationType?: string) { switch (latencyAggregationType) { - case 'avg': { - i18n.translate( + case 'avg': + return i18n.translate( 'xpack.apm.serviceOverview.transactionsTableColumnLatency.avg', { defaultMessage: 'Latency (avg.)', } ); - } - case 'p95': { + + case 'p95': return i18n.translate( 'xpack.apm.serviceOverview.transactionsTableColumnLatency.p95', { defaultMessage: 'Latency (95th)', } ); - } - case 'p99': { + + case 'p99': return i18n.translate( 'xpack.apm.serviceOverview.transactionsTableColumnLatency.p99', { defaultMessage: 'Latency (99th)', } ); - } } } @@ -202,7 +201,7 @@ export function ServiceOverviewTransactionsTable(props: Props) { color="euiColorVis1" compact series={latency.timeseries ?? undefined} - valueLabel={asDuration(latency.value)} + valueLabel={asMillisecondDuration(latency.value)} /> ); }, @@ -210,10 +209,8 @@ export function ServiceOverviewTransactionsTable(props: Props) { { field: 'throughput', name: i18n.translate( - 'xpack.apm.serviceOverview.transactionsTableColumnTroughput', - { - defaultMessage: 'Traffic', - } + 'xpack.apm.serviceOverview.transactionsTableColumnThroughput', + { defaultMessage: 'Throughput' } ), width: px(unit * 10), render: (_, { throughput }) => { diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx index 1699b7e7474fe..d2a3dc54c2a48 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/TransactionList/index.tsx @@ -101,10 +101,8 @@ export function TransactionList({ items, isLoading }: Props) { { field: 'transactionsPerMinute', name: i18n.translate( - 'xpack.apm.transactionsTable.transactionsPerMinuteColumnLabel', - { - defaultMessage: 'Trans. per minute', - } + 'xpack.apm.transactionsTable.throughputColumnLabel', + { defaultMessage: 'Throughput' } ), sortable: true, dataType: 'number', 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 ad2b68ae8a4ef..948facae222e7 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 @@ -83,6 +83,7 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) { typeof LocalUIFilters > = useMemo( () => ({ + shouldFetch: !!transactionType, filterNames: [ 'transactionResult', 'host', @@ -101,7 +102,7 @@ export function TransactionOverview({ serviceName }: TransactionOverviewProps) { // TODO: improve urlParams typings. // `serviceName` or `transactionType` will never be undefined here, and this check should not be needed - if (!serviceName || !transactionType) { + if (!serviceName) { return null; } diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.test.ts b/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.test.ts index 6a739a668c404..e6328fc768a48 100644 --- a/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.test.ts +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.test.ts @@ -9,13 +9,15 @@ import { getAgentIconKey } from './get_agent_icon'; const examples = { DotNet: 'dotnet', // Test for case sensitivity dotnet: 'dotnet', + erlang: 'erlang', go: 'go', java: 'java', 'js-base': 'rum', nodejs: 'nodejs', + ocaml: 'ocaml', 'opentelemetry/cpp': 'opentelemetry', 'opentelemetry/dotnet': 'dotnet', - 'opentelemetry/erlang': 'opentelemetry', + 'opentelemetry/erlang': 'erlang', 'opentelemetry/go': 'go', 'opentelemetry/java': 'java', 'opentelemetry/nodejs': 'nodejs', diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.ts b/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.ts index 12f84a1f35091..f2a1894b8d421 100644 --- a/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.ts +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.ts @@ -11,9 +11,11 @@ import { import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; import defaultIcon from '../span_icon/icons/default.svg'; import dotNetIcon from './icons/dot-net.svg'; +import erlangIcon from './icons/erlang.svg'; import goIcon from './icons/go.svg'; import javaIcon from './icons/java.svg'; import nodeJsIcon from './icons/nodejs.svg'; +import ocamlIcon from './icons/ocaml.svg'; import openTelemetryIcon from './icons/opentelemetry.svg'; import phpIcon from './icons/php.svg'; import pythonIcon from './icons/python.svg'; @@ -22,9 +24,11 @@ import rumJsIcon from './icons/rumjs.svg'; const agentIcons: { [key: string]: string } = { dotnet: dotNetIcon, + erlang: erlangIcon, go: goIcon, java: javaIcon, nodejs: nodeJsIcon, + ocaml: ocamlIcon, opentelemetry: openTelemetryIcon, php: phpIcon, python: pythonIcon, diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/erlang.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/erlang.svg new file mode 100644 index 0000000000000..8d4b89eb14016 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/erlang.svg @@ -0,0 +1 @@ + diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/ocaml.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/ocaml.svg new file mode 100644 index 0000000000000..7e9fffe4a2424 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/ocaml.svg @@ -0,0 +1 @@ + diff --git a/x-pack/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx deleted file mode 100644 index 8768ce9d62e6f..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/Links/KibanaLink.test.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Location } from 'history'; -import React from 'react'; -import { getRenderedHref } from '../../../utils/testHelpers'; -import { KibanaLink } from './KibanaLink'; - -describe('KibanaLink', () => { - it('produces the correct URL', async () => { - const href = await getRenderedHref(() => , { - search: '?rangeFrom=now-5h&rangeTo=now-2h', - } as Location); - expect(href).toMatchInlineSnapshot(`"/basepath/app/kibana#/some/path"`); - }); -}); diff --git a/x-pack/plugins/apm/public/components/shared/Links/KibanaLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/KibanaLink.tsx deleted file mode 100644 index ab44374f48167..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/Links/KibanaLink.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui'; -import React from 'react'; -import url from 'url'; -import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; - -interface Props extends EuiLinkAnchorProps { - path?: string; - children?: React.ReactNode; -} - -export function KibanaLink({ path, ...rest }: Props) { - const { core } = useApmPluginContext(); - const href = url.format({ - pathname: core.http.basePath.prepend('/app/kibana'), - hash: path, - }); - return ; -} diff --git a/x-pack/plugins/apm/public/components/shared/Links/kibana.ts b/x-pack/plugins/apm/public/components/shared/Links/kibana.ts new file mode 100644 index 0000000000000..6a609a8cf47c8 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/Links/kibana.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IBasePath } from '../../../../../../../src/core/public'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; + +export function getUpgradeAssistantHref(basePath: IBasePath) { + return basePath.prepend('/app/management/stack/upgrade_assistant'); +} + +export function useUpgradeAssistantHref() { + const { core } = useApmPluginContext(); + + return getUpgradeAssistantHref(core.http.basePath); +} diff --git a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx index 65164a43bf10e..eaecd86820524 100644 --- a/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/LocalUIFilters/index.tsx @@ -24,6 +24,7 @@ interface Props { params?: Record; showCount?: boolean; children?: React.ReactNode; + shouldFetch?: boolean; } const ButtonWrapper = styled.div` @@ -36,11 +37,13 @@ function LocalUIFilters({ filterNames, children, showCount = true, + shouldFetch = true, }: Props) { const { filters, setFilterValue, clearValues } = useLocalUIFilters({ filterNames, projection, params, + shouldFetch, }); const hasValues = filters.some((filter) => filter.value.length > 0); diff --git a/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx index 359eadfc55cff..6d430d4c8f49c 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/chart_container.test.tsx @@ -11,7 +11,7 @@ import { ChartContainer } from './chart_container'; describe('ChartContainer', () => { describe('loading indicator', () => { it('shows loading when status equals to Loading or Pending and has no data', () => { - [FETCH_STATUS.PENDING, FETCH_STATUS.LOADING].map((status) => { + [FETCH_STATUS.NOT_INITIATED, FETCH_STATUS.LOADING].map((status) => { const { queryAllByTestId } = render( { }); }); it('does not show loading when status equals to Loading or Pending and has data', () => { - [FETCH_STATUS.PENDING, FETCH_STATUS.LOADING].map((status) => { + [FETCH_STATUS.NOT_INITIATED, FETCH_STATUS.LOADING].map((status) => { const { queryAllByText } = render(
    My amazing component
    diff --git a/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx b/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx index ef58430e1e31e..06a2b879f0622 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/chart_container.tsx @@ -17,10 +17,7 @@ interface Props { } export function ChartContainer({ children, height, status, hasData }: Props) { - if ( - !hasData && - (status === FETCH_STATUS.LOADING || status === FETCH_STATUS.PENDING) - ) { + if (!hasData && status === FETCH_STATUS.LOADING) { return ; } diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/helper.test.ts b/x-pack/plugins/apm/public/components/shared/charts/helper/helper.test.ts index 585eef546e754..057b04c2ecc2b 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/helper/helper.test.ts +++ b/x-pack/plugins/apm/public/components/shared/charts/helper/helper.test.ts @@ -3,8 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { onBrushEnd } from './helper'; +import { onBrushEnd, isTimeseriesEmpty } from './helper'; import { History } from 'history'; +import { TimeSeries } from '../../../../../typings/timeseries'; describe('Chart helper', () => { describe('onBrushEnd', () => { @@ -36,4 +37,60 @@ describe('Chart helper', () => { }); }); }); + + describe('isTimeseriesEmpty', () => { + it('returns true when timeseries is undefined', () => { + expect(isTimeseriesEmpty()).toBeTruthy(); + }); + it('returns true when timeseries data is empty', () => { + const timeseries = [ + { + title: 'foo', + data: [], + type: 'line', + color: 'red', + }, + ] as TimeSeries[]; + expect(isTimeseriesEmpty(timeseries)).toBeTruthy(); + }); + it('returns true when y coordinate is null', () => { + const timeseries = [ + { + title: 'foo', + data: [{ x: 1, y: null }], + type: 'line', + color: 'red', + }, + ] as TimeSeries[]; + expect(isTimeseriesEmpty(timeseries)).toBeTruthy(); + }); + it('returns true when y coordinate is undefined', () => { + const timeseries = [ + { + title: 'foo', + data: [{ x: 1, y: undefined }], + type: 'line', + color: 'red', + }, + ] as TimeSeries[]; + expect(isTimeseriesEmpty(timeseries)).toBeTruthy(); + }); + it('returns false when at least one coordinate is filled', () => { + const timeseries = [ + { + title: 'foo', + data: [{ x: 1, y: undefined }], + type: 'line', + color: 'red', + }, + { + title: 'bar', + data: [{ x: 1, y: 1 }], + type: 'line', + color: 'green', + }, + ] as TimeSeries[]; + expect(isTimeseriesEmpty(timeseries)).toBeFalsy(); + }); + }); }); diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/helper.ts b/x-pack/plugins/apm/public/components/shared/charts/helper/helper.ts index a9c1337feac99..5239b3bd49bd4 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/helper/helper.ts +++ b/x-pack/plugins/apm/public/components/shared/charts/helper/helper.ts @@ -6,6 +6,7 @@ import { XYBrushArea } from '@elastic/charts'; import { History } from 'history'; +import { TimeSeries } from '../../../../../typings/timeseries'; import { fromQuery, toQuery } from '../../Links/url_helpers'; export const onBrushEnd = ({ @@ -33,3 +34,16 @@ export const onBrushEnd = ({ }); } }; + +export function isTimeseriesEmpty(timeseries?: TimeSeries[]) { + return ( + !timeseries || + timeseries + .map((serie) => serie.data) + .flat() + .every( + ({ y }: { x?: number | null; y?: number | null }) => + y === null || y === undefined + ) + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx index 475877c0edad3..65bc58c04b57b 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/index.tsx @@ -100,7 +100,7 @@ export function LatencyChart({ height }: Props) { id="latencyChart" timeseries={latencyTimeseries} yLabelFormat={getResponseTimeTickFormatter(latencyFormatter)} - anomalySeries={anomalyTimeseries} + anomalyTimeseries={anomalyTimeseries} />
    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 2c71e75994a4a..fffd97d090bf6 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 @@ -36,7 +36,7 @@ import { useAnnotationsContext } from '../../../context/annotations/use_annotati import { useChartPointerEventContext } from '../../../context/chart_pointer_event/use_chart_pointer_event_context'; import { unit } from '../../../style/variables'; import { ChartContainer } from './chart_container'; -import { onBrushEnd } from './helper/helper'; +import { onBrushEnd, isTimeseriesEmpty } from './helper/helper'; import { getLatencyChartSelector } from '../../../selectors/latency_chart_selectors'; interface Props { @@ -55,7 +55,7 @@ interface Props { yTickFormat?: (y: number) => string; showAnnotations?: boolean; yDomain?: YDomainRange; - anomalySeries?: ReturnType< + anomalyTimeseries?: ReturnType< typeof getLatencyChartSelector >['anomalyTimeseries']; } @@ -70,7 +70,7 @@ export function TimeseriesChart({ yTickFormat, showAnnotations = true, yDomain, - anomalySeries, + anomalyTimeseries, }: Props) { const history = useHistory(); const { annotations } = useAnnotationsContext(); @@ -86,16 +86,12 @@ export function TimeseriesChart({ const xFormatter = niceTimeFormatter([min, max]); - const isEmpty = timeseries - .map((serie) => serie.data) - .flat() - .every( - ({ y }: { x?: number | null; y?: number | null }) => - y === null || y === undefined - ); + const isEmpty = isTimeseriesEmpty(timeseries); const annotationColor = theme.eui.euiColorSecondary; + const allSeries = [...timeseries, ...(anomalyTimeseries?.boundaries ?? [])]; + return ( @@ -156,7 +152,7 @@ export function TimeseriesChart({ /> )} - {timeseries.map((serie) => { + {allSeries.map((serie) => { const Series = serie.type === 'area' ? AreaSeries : LineSeries; return ( @@ -170,37 +166,28 @@ export function TimeseriesChart({ data={isEmpty ? [] : serie.data} color={serie.color} curve={CurveType.CURVE_MONOTONE_X} + hideInLegend={serie.hideLegend} + fit={serie.fit ?? undefined} + filterSeriesInTooltip={ + serie.hideTooltipValue ? () => false : undefined + } + stackAccessors={serie.stackAccessors ?? undefined} + areaSeriesStyle={serie.areaSeriesStyle} + lineSeriesStyle={serie.lineSeriesStyle} /> ); })} - {anomalySeries?.bounderies && ( - false} - /> - )} - - {anomalySeries?.scores && ( + {anomalyTimeseries?.scores && ( ({ coordinates: { x0, x1 }, }) )} - style={{ fill: anomalySeries.scores.color }} + style={{ fill: anomalyTimeseries.scores.color }} /> )} diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/index.tsx index 38a980fbcd90a..bbae0cd61264f 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/index.tsx @@ -26,7 +26,7 @@ export function TransactionBreakdownChart({

    {i18n.translate('xpack.apm.transactionBreakdown.chartTitle', { - defaultMessage: 'Average duration by span type', + defaultMessage: 'Time spent by span type', })}

    diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx index 19c29815ab655..f1b29fe4b87bd 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_breakdown_chart/transaction_breakdown_chart_contents.tsx @@ -35,7 +35,7 @@ import { useAnnotationsContext } from '../../../../context/annotations/use_annot import { useChartPointerEventContext } from '../../../../context/chart_pointer_event/use_chart_pointer_event_context'; import { unit } from '../../../../style/variables'; import { ChartContainer } from '../../charts/chart_container'; -import { onBrushEnd } from '../../charts/helper/helper'; +import { isTimeseriesEmpty, onBrushEnd } from '../../charts/helper/helper'; interface Props { fetchStatus: FETCH_STATUS; @@ -66,8 +66,10 @@ export function TransactionBreakdownChartContents({ const annotationColor = theme.eui.euiColorSecondary; + const isEmpty = isTimeseriesEmpty(timeseries); + return ( - + onBrushEnd({ x, history })} diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/helper.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/helper.tsx index a8583b4b0dada..c15c197786293 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/helper.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/helper.tsx @@ -4,18 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; +import { isFiniteNumber } from '../../../../../common/utils/is_finite_number'; +import { APMChartSpec, Coordinate } from '../../../../../typings/timeseries'; import { TimeFormatter } from '../../../../../common/utils/formatters'; export function getResponseTimeTickFormatter(formatter: TimeFormatter) { return (t: number) => formatter(t).formatted; } -export function getMaxY(timeSeries?: Array>) { - if (timeSeries) { - const coordinates = timeSeries.flatMap((serie) => serie.data); - const numbers = coordinates.map((c) => (c.y ? c.y : 0)); - return Math.max(...numbers, 0); +export function getMaxY(specs?: Array>) { + const values = specs + ?.flatMap((spec) => spec.data) + .map((coord) => coord.y) + .filter(isFiniteNumber); + + if (values?.length) { + return Math.max(...values, 0); } return 0; } diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx index 0ea0ee3e5a456..297cf050c4bbc 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx @@ -13,11 +13,9 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; -import { TRANSACTION_REQUEST } from '../../../../../common/transaction_types'; import { asTransactionRate } from '../../../../../common/utils/formatters'; import { AnnotationsContextProvider } from '../../../../context/annotations/annotations_context'; import { ChartPointerEventContextProvider } from '../../../../context/chart_pointer_event/chart_pointer_event_context'; -import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { useTransactionThroughputChartsFetcher } from '../../../../hooks/use_transaction_throughput_chart_fetcher'; import { LatencyChart } from '../latency_chart'; import { TimeseriesChart } from '../timeseries_chart'; @@ -25,9 +23,6 @@ import { TransactionBreakdownChart } from '../transaction_breakdown_chart'; import { TransactionErrorRateChart } from '../transaction_error_rate_chart/'; export function TransactionCharts() { - const { urlParams } = useUrlParams(); - const { transactionType } = urlParams; - const { throughputChartsData, throughputChartsStatus, @@ -49,11 +44,16 @@ export function TransactionCharts() { - {tpmLabel(transactionType)} + + {i18n.translate( + 'xpack.apm.metrics.transactionChart.throughputLabel', + { defaultMessage: 'Throughput' } + )} + @@ -76,19 +76,3 @@ export function TransactionCharts() { ); } - -function tpmLabel(type?: string) { - return type === TRANSACTION_REQUEST - ? i18n.translate( - 'xpack.apm.metrics.transactionChart.requestsPerMinuteLabel', - { - defaultMessage: 'Requests per minute', - } - ) - : i18n.translate( - 'xpack.apm.metrics.transactionChart.transactionsPerMinuteLabel', - { - defaultMessage: 'Transactions per minute', - } - ); -} 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 new file mode 100644 index 0000000000000..587cb172eeab7 --- /dev/null +++ b/x-pack/plugins/apm/public/context/url_params_context/helpers.test.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import datemath from '@elastic/datemath'; +import moment from 'moment-timezone'; +import * as helpers from './helpers'; + +describe('url_params_context helpers', () => { + describe('getParsedDate', () => { + describe('given undefined', () => { + it('returns undefined', () => { + expect(helpers.getParsedDate(undefined)).toBeUndefined(); + }); + }); + + describe('given a parsable date', () => { + it('returns the parsed date', () => { + expect(helpers.getParsedDate('1970-01-01T00:00:00.000Z')).toEqual( + '1970-01-01T00:00:00.000Z' + ); + }); + }); + + describe('given a non-parsable date', () => { + it('returns null', () => { + expect(helpers.getParsedDate('nope')).toEqual(null); + }); + }); + }); + + describe('getDateRange', () => { + describe('when rangeFrom and rangeTo are not changed', () => { + it('returns the previous state', () => { + expect( + helpers.getDateRange({ + state: { + rangeFrom: 'now-1m', + rangeTo: 'now', + start: '1970-01-01T00:00:00.000Z', + end: '1971-01-01T00:00:00.000Z', + }, + rangeFrom: 'now-1m', + rangeTo: 'now', + }) + ).toEqual({ + start: '1970-01-01T00:00:00.000Z', + end: '1971-01-01T00:00:00.000Z', + }); + }); + }); + + describe('when rangeFrom or rangeTo have changed', () => { + it('returns new state', () => { + jest.spyOn(datemath, 'parse').mockReturnValue(moment(0).utc()); + + expect( + helpers.getDateRange({ + state: { + rangeFrom: 'now-1m', + rangeTo: 'now', + start: '1972-01-01T00:00:00.000Z', + end: '1973-01-01T00:00:00.000Z', + }, + rangeFrom: 'now-2m', + rangeTo: 'now', + }) + ).toEqual({ + start: '1970-01-01T00:00:00.000Z', + end: '1970-01-01T00:00:00.000Z', + }); + }); + }); + }); +}); 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 45db4dcc94cce..bff2ef5deb86c 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 @@ -17,18 +17,23 @@ export function getParsedDate(rawDate?: string, opts = {}) { } } -export function getStart(prevState: IUrlParams, rangeFrom?: string) { - if (prevState.rangeFrom !== rangeFrom) { - return getParsedDate(rangeFrom); +export function getDateRange({ + state, + rangeFrom, + rangeTo, +}: { + state: IUrlParams; + rangeFrom?: string; + rangeTo?: string; +}) { + if (state.rangeFrom === rangeFrom && state.rangeTo === rangeTo) { + return { start: state.start, end: state.end }; } - return prevState.start; -} -export function getEnd(prevState: IUrlParams, rangeTo?: string) { - if (prevState.rangeTo !== rangeTo) { - return getParsedDate(rangeTo, { roundUp: true }); - } - return prevState.end; + return { + start: getParsedDate(rangeFrom), + end: getParsedDate(rangeTo, { roundUp: true }), + }; } export function toNumber(value?: string) { 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 6d9f982f92751..0596d649116a0 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 @@ -11,8 +11,7 @@ import { pickKeys } from '../../../common/utils/pick_keys'; import { localUIFilterNames } from '../../../server/lib/ui_filters/local_ui_filters/config'; import { toQuery } from '../../components/shared/Links/url_helpers'; import { - getEnd, - getStart, + getDateRange, removeUndefinedProps, toBoolean, toNumber, @@ -56,8 +55,7 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) { return removeUndefinedProps({ // date params - start: getStart(state, rangeFrom), - end: getEnd(state, rangeTo), + ...getDateRange({ state, rangeFrom, rangeTo }), rangeFrom, rangeTo, refreshPaused: refreshPaused ? toBoolean(refreshPaused) : undefined, diff --git a/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts b/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts index 551e92f8ba034..dabdf41c63f04 100644 --- a/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts +++ b/x-pack/plugins/apm/public/hooks/useLocalUIFilters.ts @@ -34,10 +34,12 @@ export function useLocalUIFilters({ projection, filterNames, params, + shouldFetch, }: { projection: Projection; filterNames: LocalUIFilterName[]; params?: Record; + shouldFetch: boolean; }) { const history = useHistory(); const { uiFilters, urlParams } = useUrlParams(); @@ -68,17 +70,19 @@ export function useLocalUIFilters({ }; const { data = getInitialData(filterNames), status } = useFetcher(() => { - return callApi({ - method: 'GET', - pathname: `/api/apm/ui_filters/local_filters/${projection}`, - query: { - uiFilters: JSON.stringify(uiFilters), - start: urlParams.start, - end: urlParams.end, - filterNames: JSON.stringify(filterNames), - ...params, - }, - }); + if (shouldFetch) { + return callApi({ + method: 'GET', + pathname: `/api/apm/ui_filters/local_filters/${projection}`, + query: { + uiFilters: JSON.stringify(uiFilters), + start: urlParams.start, + end: urlParams.end, + filterNames: JSON.stringify(filterNames), + ...params, + }, + }); + } }, [ callApi, projection, @@ -87,6 +91,7 @@ export function useLocalUIFilters({ urlParams.end, filterNames, params, + shouldFetch, ]); const filters = data.map((filter) => ({ diff --git a/x-pack/plugins/apm/public/hooks/use_fetcher.tsx b/x-pack/plugins/apm/public/hooks/use_fetcher.tsx index a9a4871dc8707..8174f06e06b8b 100644 --- a/x-pack/plugins/apm/public/hooks/use_fetcher.tsx +++ b/x-pack/plugins/apm/public/hooks/use_fetcher.tsx @@ -15,7 +15,7 @@ export enum FETCH_STATUS { LOADING = 'loading', SUCCESS = 'success', FAILURE = 'failure', - PENDING = 'pending', + NOT_INITIATED = 'not_initiated', } export interface FetcherResult { @@ -46,7 +46,7 @@ export function useFetcher( FetcherResult> >({ data: undefined, - status: FETCH_STATUS.PENDING, + status: FETCH_STATUS.NOT_INITIATED, }); const [counter, setCounter] = useState(0); diff --git a/x-pack/plugins/apm/public/selectors/latency_chart_selector.test.ts b/x-pack/plugins/apm/public/selectors/latency_chart_selector.test.ts index 40157aff3c129..7b0826fa76883 100644 --- a/x-pack/plugins/apm/public/selectors/latency_chart_selector.test.ts +++ b/x-pack/plugins/apm/public/selectors/latency_chart_selector.test.ts @@ -25,7 +25,7 @@ const latencyChartData = { latencyTimeseries: [{ x: 1, y: 10 }], anomalyTimeseries: { jobId: '1', - anomalyBoundaries: [{ x: 1, y: 2 }], + anomalyBoundaries: [{ x: 1, y: 2, y0: 1 }], anomalyScore: [{ x: 1, x0: 2 }], }, } as LatencyChartsResponse; @@ -110,30 +110,76 @@ describe('getLatencyChartSelector', () => { latencyAggregationType: LatencyAggregationType.p99, }); expect(latencyTimeseries).toEqual({ + anomalyTimeseries: { + boundaries: [ + { + color: 'rgba(0,0,0,0)', + areaSeriesStyle: { + point: { + opacity: 0, + }, + }, + data: [ + { + x: 1, + y: 1, + }, + ], + fit: 'lookahead', + hideLegend: true, + hideTooltipValue: true, + stackAccessors: ['y'], + title: 'anomalyBoundariesLower', + type: 'area', + }, + { + color: 'rgba(0,0,255,0.5)', + areaSeriesStyle: { + point: { + opacity: 0, + }, + }, + data: [ + { + x: 1, + y: 1, + }, + ], + fit: 'lookahead', + hideLegend: true, + hideTooltipValue: true, + stackAccessors: ['y'], + title: 'anomalyBoundariesUpper', + type: 'area', + }, + ], + scores: { + color: 'yellow', + data: [ + { + x: 1, + x0: 2, + }, + ], + title: 'anomalyScores', + type: 'rectAnnotation', + }, + }, latencyTimeseries: [ { + color: 'black', + data: [ + { + x: 1, + y: 10, + }, + ], title: '99th percentile', titleShort: '99th', - data: [{ x: 1, y: 10 }], type: 'linemark', - color: 'black', }, ], mlJobId: '1', - anomalyTimeseries: { - bounderies: { - title: 'Anomaly Boundaries', - data: [{ x: 1, y: 2 }], - type: 'area', - color: 'rgba(0,0,255,0.5)', - }, - scores: { - title: 'Anomaly score', - data: [{ x: 1, x0: 2 }], - type: 'rectAnnotation', - color: 'yellow', - }, - }, }); }); }); diff --git a/x-pack/plugins/apm/public/selectors/latency_chart_selectors.ts b/x-pack/plugins/apm/public/selectors/latency_chart_selectors.ts index a5c25cfa3e07c..dba698ffb1bc1 100644 --- a/x-pack/plugins/apm/public/selectors/latency_chart_selectors.ts +++ b/x-pack/plugins/apm/public/selectors/latency_chart_selectors.ts @@ -3,26 +3,20 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { Fit } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; import { rgba } from 'polished'; import { EuiTheme } from '../../../observability/public'; import { asDuration } from '../../common/utils/formatters'; -import { - Coordinate, - RectCoordinate, - TimeSeries, -} from '../../typings/timeseries'; +import { APMChartSpec, Coordinate } from '../../typings/timeseries'; import { APIReturnType } from '../services/rest/createCallApmApi'; export type LatencyChartsResponse = APIReturnType<'GET /api/apm/services/{serviceName}/transactions/charts/latency'>; -interface LatencyChart { - latencyTimeseries: Array>; +interface LatencyChartData { + latencyTimeseries: Array>; mlJobId?: string; - anomalyTimeseries?: { - bounderies: TimeSeries; - scores: TimeSeries; - }; + anomalyTimeseries?: { boundaries: APMChartSpec[]; scores: APMChartSpec }; } export function getLatencyChartSelector({ @@ -33,7 +27,7 @@ export function getLatencyChartSelector({ latencyChart?: LatencyChartsResponse; theme: EuiTheme; latencyAggregationType?: string; -}): LatencyChart { +}): LatencyChartData { if (!latencyChart?.latencyTimeseries || !latencyAggregationType) { return { latencyTimeseries: [], @@ -48,7 +42,7 @@ export function getLatencyChartSelector({ latencyAggregationType, }), mlJobId: latencyChart.anomalyTimeseries?.jobId, - anomalyTimeseries: getAnnomalyTimeseries({ + anomalyTimeseries: getAnomalyTimeseries({ anomalyTimeseries: latencyChart.anomalyTimeseries, theme, }), @@ -114,45 +108,57 @@ function getLatencyTimeseries({ return []; } -function getAnnomalyTimeseries({ +function getAnomalyTimeseries({ anomalyTimeseries, theme, }: { anomalyTimeseries: LatencyChartsResponse['anomalyTimeseries']; theme: EuiTheme; -}) { - if (anomalyTimeseries) { - return { - bounderies: getAnomalyBoundariesSeries( - anomalyTimeseries.anomalyBoundaries, - theme - ), - scores: getAnomalyScoreSeries(anomalyTimeseries.anomalyScore, theme), - }; +}): { boundaries: APMChartSpec[]; scores: APMChartSpec } | undefined { + if (!anomalyTimeseries) { + return undefined; } -} -export function getAnomalyScoreSeries(data: RectCoordinate[], theme: EuiTheme) { - return { - title: i18n.translate('xpack.apm.transactions.chart.anomalyScoreLabel', { - defaultMessage: 'Anomaly score', - }), - data, + const boundariesConfigBase = { + type: 'area', + fit: Fit.Lookahead, + hideLegend: true, + hideTooltipValue: true, + stackAccessors: ['y'], + areaSeriesStyle: { + point: { + opacity: 0, + }, + }, + }; + + const boundaries = [ + { + ...boundariesConfigBase, + title: 'anomalyBoundariesLower', + data: anomalyTimeseries.anomalyBoundaries.map((coord) => ({ + x: coord.x, + y: coord.y0, + })), + color: rgba(0, 0, 0, 0), + }, + { + ...boundariesConfigBase, + title: 'anomalyBoundariesUpper', + data: anomalyTimeseries.anomalyBoundaries.map((coord) => ({ + x: coord.x, + y: coord.y - coord.y0, + })), + color: rgba(theme.eui.euiColorVis1, 0.5), + }, + ]; + + const scores = { + title: 'anomalyScores', type: 'rectAnnotation', + data: anomalyTimeseries.anomalyScore, color: theme.eui.euiColorVis9, }; -} -function getAnomalyBoundariesSeries(data: Coordinate[], theme: EuiTheme) { - return { - title: i18n.translate( - 'xpack.apm.transactions.chart.anomalyBoundariesLabel', - { - defaultMessage: 'Anomaly Boundaries', - } - ), - data, - type: 'area', - color: rgba(theme.eui.euiColorVis1, 0.5), - }; + return { boundaries, scores }; } diff --git a/x-pack/plugins/apm/public/setHelpExtension.ts b/x-pack/plugins/apm/public/setHelpExtension.ts index f895fbc36ed03..156207f9c15d9 100644 --- a/x-pack/plugins/apm/public/setHelpExtension.ts +++ b/x-pack/plugins/apm/public/setHelpExtension.ts @@ -6,6 +6,7 @@ import { i18n } from '@kbn/i18n'; import { CoreStart } from 'kibana/public'; +import { getUpgradeAssistantHref } from './components/shared/Links/kibana'; export function setHelpExtension({ chrome, http }: CoreStart) { chrome.setHelpExtension({ @@ -19,7 +20,7 @@ export function setHelpExtension({ chrome, http }: CoreStart) { }, { linkType: 'custom', - href: http.basePath.prepend('/app/management/stack/upgrade_assistant'), + href: getUpgradeAssistantHref(http.basePath), content: i18n.translate('xpack.apm.helpMenu.upgradeAssistantLink', { defaultMessage: 'Upgrade assistant', }), diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index 7e128493c8739..47529de1042a1 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -6,11 +6,11 @@ import { Logger } from 'kibana/server'; import moment from 'moment'; +import { isActivePlatinumLicense } from '../../../common/license_check'; import { APMConfig } from '../..'; import { KibanaRequest } from '../../../../../../src/core/server'; import { UI_SETTINGS } from '../../../../../../src/plugins/data/common'; import { ESFilter } from '../../../../../typings/elasticsearch'; -import { isActivePlatinumLicense } from '../../../common/service_map'; import { UIFilters } from '../../../typings/ui_filters'; import { APMRequestHandlerContext } from '../../routes/typings'; import { 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 8f8d7763970b7..f0882eee92aa6 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 @@ -4,13 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ import Boom from '@hapi/boom'; +import { sortBy, uniqBy } from 'lodash'; +import { ESSearchResponse } from '../../../../../typings/elasticsearch'; import { MlPluginSetup } from '../../../../ml/server'; import { PromiseReturnType } from '../../../../observability/typings/common'; -import { - getSeverity, - ML_ERRORS, - ServiceAnomalyStats, -} from '../../../common/anomaly_detection'; +import { getSeverity, ML_ERRORS } from '../../../common/anomaly_detection'; import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; import { getServiceHealthStatus } from '../../../common/service_health_status'; import { @@ -20,7 +18,10 @@ import { import { getMlJobsWithAPMGroup } from '../anomaly_detection/get_ml_jobs_with_apm_group'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; -export const DEFAULT_ANOMALIES = { mlJobIds: [], serviceAnomalies: {} }; +export const DEFAULT_ANOMALIES: ServiceAnomaliesResponse = { + mlJobIds: [], + serviceAnomalies: [], +}; export type ServiceAnomaliesResponse = PromiseReturnType< typeof getServiceAnomalies @@ -39,21 +40,6 @@ export async function getServiceAnomalies({ throw Boom.notImplemented(ML_ERRORS.ML_NOT_AVAILABLE); } - const mlCapabilities = await ml.mlSystem.mlCapabilities(); - - if (!mlCapabilities.mlFeatureEnabledInSpace) { - throw Boom.forbidden(ML_ERRORS.ML_NOT_AVAILABLE_IN_SPACE); - } - - const mlJobIds = await getMLJobIds(ml.anomalyDetectors, environment); - - if (!mlJobIds.length) { - return { - mlJobIds: [], - serviceAnomalies: {}, - }; - } - const params = { body: { size: 0, @@ -61,7 +47,6 @@ export async function getServiceAnomalies({ bool: { filter: [ { terms: { result_type: ['model_plot', 'record'] } }, - { terms: { job_id: mlJobIds } }, { range: { timestamp: { @@ -83,19 +68,25 @@ export async function getServiceAnomalies({ }, aggs: { services: { - terms: { field: 'partition_field_value' }, + composite: { + size: 5000, + sources: [ + { serviceName: { terms: { field: 'partition_field_value' } } }, + { jobId: { terms: { field: 'job_id' } } }, + ], + }, aggs: { - top_score: { - top_hits: { - sort: { record_score: 'desc' }, - _source: [ - 'actual', - 'job_id', - 'by_field_value', - 'result_type', - 'record_score', - ], - size: 1, + metrics: { + top_metrics: { + metrics: [ + { field: 'actual' }, + { field: 'by_field_value' }, + { field: 'result_type' }, + { field: 'record_score' }, + ] as const, + sort: { + record_score: 'desc' as const, + }, }, }, }, @@ -104,80 +95,54 @@ export async function getServiceAnomalies({ }, }; - const response = await ml.mlSystem.mlAnomalySearch(params, mlJobIds); - - return { - mlJobIds, - serviceAnomalies: transformResponseToServiceAnomalies( - response as ServiceAnomaliesAggResponse + const [anomalyResponse, jobIds] = await Promise.all([ + // pass an empty array of job ids to anomaly search + // so any validation is skipped + ml.mlSystem.mlAnomalySearch(params, []), + getMLJobIds(ml.anomalyDetectors, environment), + ]); + + const typedAnomalyResponse: ESSearchResponse< + unknown, + typeof params + > = anomalyResponse as any; + const relevantBuckets = uniqBy( + sortBy( + // make sure we only return data for jobs that are available in this space + typedAnomalyResponse.aggregations?.services.buckets.filter((bucket) => + jobIds.includes(bucket.key.jobId as string) + ) ?? [], + // sort by job ID in case there are multiple jobs for one service to + // ensure consistent results + (bucket) => bucket.key.jobId ), - }; -} - -interface ServiceAnomaliesAggResponse { - aggregations: { - services: { - buckets: Array<{ - key: string; - top_score: { - hits: { - hits: Array<{ - sort: [number]; - _source: { - job_id: string; - by_field_value: string; - } & ( - | { - record_score: number | null; - result_type: 'record'; - actual: number[]; - } - | { - result_type: 'model_plot'; - actual?: number; - } - ); - }>; - }; - }; - }>; - }; - }; -} + // return one bucket per service + (bucket) => bucket.key.serviceName + ); -function transformResponseToServiceAnomalies( - response: ServiceAnomaliesAggResponse -) { - const serviceAnomaliesMap = ( - response.aggregations?.services.buckets ?? [] - ).reduce>( - (statsByServiceName, { key: serviceName, top_score: topScoreAgg }) => { - const mlResult = topScoreAgg.hits.hits[0]._source; + return { + mlJobIds: jobIds, + serviceAnomalies: relevantBuckets.map((bucket) => { + const metrics = bucket.metrics.top[0].metrics; const anomalyScore = - (mlResult.result_type === 'record' && mlResult.record_score) || 0; + metrics.result_type === 'record' && metrics.record_score + ? (metrics.record_score as number) + : 0; const severity = getSeverity(anomalyScore); const healthStatus = getServiceHealthStatus({ severity }); return { - ...statsByServiceName, - [serviceName]: { - transactionType: mlResult.by_field_value, - jobId: mlResult.job_id, - actualValue: - mlResult.result_type === 'record' - ? mlResult.actual[0] - : mlResult.actual, - anomalyScore, - healthStatus, - }, + serviceName: bucket.key.serviceName as string, + jobId: bucket.key.jobId as string, + transactionType: metrics.by_field_value as string, + actualValue: metrics.actual as number | null, + anomalyScore, + healthStatus, }; - }, - {} - ); - - return serviceAnomaliesMap; + }), + }; } export async function getMLJobs( 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 ccebbfa44538a..257158d3b4734 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 @@ -18,7 +18,6 @@ import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { DEFAULT_ANOMALIES, getServiceAnomalies, - ServiceAnomaliesResponse, } from './get_service_anomalies'; import { getServiceMapFromTraceIds } from './get_service_map_from_trace_ids'; import { getTraceSampleIds } from './get_trace_sample_ids'; @@ -149,7 +148,7 @@ export type ServiceMapAPIResponse = PromiseReturnType; export async function getServiceMap(options: IEnvOptions) { const { logger } = options; - const anomaliesPromise: Promise = getServiceAnomalies( + const anomaliesPromise = getServiceAnomalies( options // always catch error to avoid breaking service maps if there is a problem with ML diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts index 7d832c91022e5..2b339f38a8f5b 100644 --- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts @@ -39,15 +39,16 @@ const javaService = { const anomalies = { mlJobIds: ['apm-test-1234-ml-module-name'], - serviceAnomalies: { - 'opbeans-test': { + serviceAnomalies: [ + { + serviceName: 'opbeans-test', transactionType: 'request', actualValue: 10000, anomalyScore: 50, jobId: 'apm-test-1234-ml-module-name', healthStatus: ServiceHealthStatus.warning, }, - }, + ], }; describe('transformServiceMapResponses', () => { diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts index e2af4315e41a1..5e552bec4ee29 100644 --- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts +++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts @@ -110,7 +110,9 @@ export function transformServiceMapResponses(response: ServiceMapResponse) { const mergedServiceNode = Object.assign({}, ...matchedServiceNodes); const serviceAnomalyStats = serviceName - ? anomalies.serviceAnomalies[serviceName] + ? anomalies.serviceAnomalies.find( + (item) => item.serviceName === serviceName + ) : null; if (matchedServiceNodes.length) { 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 4e3256f0fcf87..5880b5cbc9546 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 @@ -112,6 +112,8 @@ export async function getServiceInstanceTransactionStats({ }, }); + const deltaAsMinutes = (end - start) / 60 / 1000; + return ( response.aggregations?.[SERVICE_NODE_NAME].buckets.map( (serviceNodeBucket) => { @@ -133,10 +135,10 @@ export async function getServiceInstanceTransactionStats({ })), }, throughput: { - value: count.value, + value: count.value / deltaAsMinutes, timeseries: timeseries.buckets.map((dateBucket) => ({ x: dateBucket.key, - y: dateBucket.count.value, + y: dateBucket.count.value / deltaAsMinutes, })), }, latency: { diff --git a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_transaction_groups_for_page.ts b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_transaction_groups_for_page.ts index 9ea0b821256de..ccccf946512dd 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_transaction_groups_for_page.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_transaction_groups/get_transaction_groups_for_page.ts @@ -64,6 +64,8 @@ export async function getTransactionGroupsForPage({ transactionType: string; latencyAggregationType: LatencyAggregationType; }) { + const deltaAsMinutes = (end - start) / 1000 / 60; + const field = getTransactionDurationFieldForAggregatedTransactions( searchAggregatedTransactions ); @@ -122,7 +124,7 @@ export async function getTransactionGroupsForPage({ latencyAggregationType, aggregation: bucket.latency, }), - throughput: bucket.transaction_count.value, + throughput: bucket.transaction_count.value / deltaAsMinutes, errorRate, }; }) ?? []; 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 206827a744113..a54e498958039 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 @@ -6,10 +6,7 @@ import { getSeverity } from '../../../../common/anomaly_detection'; import { getServiceHealthStatus } from '../../../../common/service_health_status'; -import { - getMLJobIds, - getServiceAnomalies, -} from '../../service_map/get_service_anomalies'; +import { getServiceAnomalies } from '../../service_map/get_service_anomalies'; import { ServicesItemsProjection, ServicesItemsSetup, @@ -29,27 +26,16 @@ export const getHealthStatuses = async ( return []; } - const jobIds = await getMLJobIds( - setup.ml.anomalyDetectors, - mlAnomaliesEnvironment - ); - if (!jobIds.length) { - return []; - } - const anomalies = await getServiceAnomalies({ setup, environment: mlAnomaliesEnvironment, }); - return Object.keys(anomalies.serviceAnomalies).map((serviceName) => { - const stats = anomalies.serviceAnomalies[serviceName]; - - const severity = getSeverity(stats.anomalyScore); + return anomalies.serviceAnomalies.map((anomalyStats) => { + const severity = getSeverity(anomalyStats.anomalyScore); const healthStatus = getServiceHealthStatus({ severity }); - return { - serviceName, + serviceName: anomalyStats.serviceName, healthStatus, }; }); 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 0ac0ad17ef8fa..29071f96e3a06 100644 --- a/x-pack/plugins/apm/server/lib/services/get_throughput.ts +++ b/x-pack/plugins/apm/server/lib/services/get_throughput.ts @@ -28,7 +28,10 @@ interface Options { type ESResponse = PromiseReturnType; function transform(response: ESResponse) { - const buckets = response.aggregations?.throughput?.buckets ?? []; + if (response.hits.total.value === 0) { + return []; + } + const buckets = response.aggregations?.throughput.buckets ?? []; return buckets.map(({ key: x, doc_count: y }) => ({ x, y })); } 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 425250fd5a1d1..6cc5f6f608060 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 @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Logger } from 'kibana/server'; import { ESSearchResponse } from '../../../../../../typings/elasticsearch'; import { PromiseReturnType } from '../../../../../observability/typings/common'; -import { Setup, SetupTimeRange } from '../../helpers/setup_request'; +import { Setup } from '../../helpers/setup_request'; export type ESResponse = Exclude< PromiseReturnType, @@ -18,81 +17,74 @@ export async function anomalySeriesFetcher({ serviceName, transactionType, intervalString, - mlBucketSize, - setup, - jobId, - logger, + ml, + start, + end, }: { serviceName: string; transactionType: string; intervalString: string; - mlBucketSize: number; - setup: Setup & SetupTimeRange; - jobId: string; - logger: Logger; + ml: Required['ml']; + start: number; + end: number; }) { - const { ml, start, end } = setup; - if (!ml) { - return; - } - - // move the start back with one bucket size, to ensure to get anomaly data in the beginning - // this is required because ML has a minimum bucket size (default is 900s) so if our buckets are smaller, we might have several null buckets in the beginning - const newStart = start - mlBucketSize * 1000; - const params = { body: { size: 0, query: { bool: { filter: [ - { term: { job_id: jobId } }, - { exists: { field: 'bucket_span' } }, { terms: { result_type: ['model_plot', 'record'] } }, { term: { partition_field_value: serviceName } }, { term: { by_field_value: transactionType } }, { range: { - timestamp: { gte: newStart, lte: end, format: 'epoch_millis' }, + timestamp: { + gte: start, + lte: end, + format: 'epoch_millis', + }, }, }, ], }, }, aggs: { - ml_avg_response_times: { - date_histogram: { - field: 'timestamp', - fixed_interval: intervalString, - min_doc_count: 0, - extended_bounds: { min: newStart, max: end }, + job_id: { + terms: { + field: 'job_id', }, aggs: { - anomaly_score: { max: { field: 'record_score' } }, - lower: { min: { field: 'model_lower' } }, - upper: { max: { field: 'model_upper' } }, + ml_avg_response_times: { + date_histogram: { + field: 'timestamp', + fixed_interval: intervalString, + extended_bounds: { min: start, max: end }, + }, + aggs: { + anomaly_score: { + top_metrics: { + metrics: [ + { field: 'record_score' }, + { field: 'timestamp' }, + { field: 'bucket_span' }, + ] as const, + sort: { + record_score: 'desc' as const, + }, + }, + }, + lower: { min: { field: 'model_lower' } }, + upper: { max: { field: 'model_upper' } }, + }, + }, }, }, }, }, }; - try { - const response: ESSearchResponse< - unknown, - typeof params - > = (await ml.mlSystem.mlAnomalySearch(params, [jobId])) as any; - - return response; - } catch (err) { - const isHttpError = 'statusCode' in err; - if (isHttpError) { - logger.info( - `Status code "${err.statusCode}" while retrieving ML anomalies for APM` - ); - return; - } - logger.error('An error occurred while retrieving ML anomalies for APM'); - logger.error(err); - } + return (ml.mlSystem.mlAnomalySearch(params, []) as unknown) as Promise< + ESSearchResponse + >; } diff --git a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/get_ml_bucket_size.ts b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/get_ml_bucket_size.ts deleted file mode 100644 index 1bb9e8a9e77a9..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/get_ml_bucket_size.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Logger } from 'kibana/server'; -import { Setup, SetupTimeRange } from '../../helpers/setup_request'; - -interface IOptions { - setup: Setup & SetupTimeRange; - jobId: string; - logger: Logger; -} - -interface ESResponse { - bucket_span: number; -} - -export async function getMlBucketSize({ - setup, - jobId, - logger, -}: IOptions): Promise { - const { ml, start, end } = setup; - if (!ml) { - return; - } - - const params = { - body: { - _source: 'bucket_span', - size: 1, - terminate_after: 1, - query: { - bool: { - filter: [ - { term: { job_id: jobId } }, - { exists: { field: 'bucket_span' } }, - { - range: { - timestamp: { gte: start, lte: end, format: 'epoch_millis' }, - }, - }, - ], - }, - }, - }, - }; - - try { - const resp = await ml.mlSystem.mlAnomalySearch(params, [jobId]); - return resp.hits.hits[0]?._source.bucket_span; - } catch (err) { - const isHttpError = 'statusCode' in err; - if (isHttpError) { - return; - } - logger.error(err); - } -} 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 a4b9bf8dfc6a8..0c6ef7c1fcf23 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 @@ -3,53 +3,47 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Logger } from 'kibana/server'; -import { isNumber } from 'lodash'; +import { compact } from 'lodash'; +import { Logger } from 'src/core/server'; +import { isFiniteNumber } from '../../../../common/utils/is_finite_number'; +import { maybe } from '../../../../common/utils/maybe'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { getBucketSize } from '../../helpers/get_bucket_size'; import { Setup, SetupTimeRange } from '../../helpers/setup_request'; import { anomalySeriesFetcher } from './fetcher'; -import { getMlBucketSize } from './get_ml_bucket_size'; -import { anomalySeriesTransform } from './transform'; import { getMLJobIds } from '../../service_map/get_service_anomalies'; -import { getLatencyTimeseries } from '../get_latency_charts'; -import { PromiseReturnType } from '../../../../../observability/typings/common'; +import { ANOMALY_THRESHOLD } from '../../../../../ml/common'; export async function getAnomalySeries({ serviceName, transactionType, transactionName, - latencyTimeseries, setup, logger, }: { serviceName: string; - transactionType: string | undefined; - transactionName: string | undefined; - latencyTimeseries: PromiseReturnType< - typeof getLatencyTimeseries - >['latencyTimeseries']; + transactionType: string; + transactionName?: string; setup: Setup & SetupTimeRange; logger: Logger; }) { - const timeseriesDates = latencyTimeseries?.map(({ x }) => x); - - /* - * don't fetch: - * - anomalies for transaction details page - * - anomalies without a type - * - timeseries is empty - */ - if (transactionName || !transactionType || !timeseriesDates?.length) { - return; + const { uiFilters, start, end, ml } = setup; + const { environment } = uiFilters; + + // don't fetch anomalies if the ML plugin is not setup + if (!ml) { + return undefined; } - const { uiFilters, start, end } = setup; - const { environment } = uiFilters; + // don't fetch anomalies if requested for a specific transaction name + // as ML results are not partitioned by transaction name + if (!!transactionName) { + return undefined; + } // don't fetch anomalies when no specific environment is selected if (environment === ENVIRONMENT_ALL.value) { - return; + return undefined; } // don't fetch anomalies if unknown uiFilters are applied @@ -60,48 +54,77 @@ export async function getAnomalySeries({ .some((uiFilterName) => !knownFilters.includes(uiFilterName)); if (hasUnknownFiltersApplied) { - return; + return undefined; } - // don't fetch anomalies if the ML plugin is not setup - if (!setup.ml) { - return; - } + const { intervalString } = getBucketSize({ start, end }); - // don't fetch anomalies if required license is not satisfied - const mlCapabilities = await setup.ml.mlSystem.mlCapabilities(); - if (!mlCapabilities.isPlatinumOrTrialLicense) { - return; - } + // move the start back with one bucket size, to ensure to get anomaly data in the beginning + // this is required because ML has a minimum bucket size (default is 900s) so if our buckets + // are smaller, we might have several null buckets in the beginning + const mlStart = start - 900 * 1000; - const mlJobIds = await getMLJobIds(setup.ml.anomalyDetectors, environment); + const [anomaliesResponse, jobIds] = await Promise.all([ + anomalySeriesFetcher({ + serviceName, + transactionType, + intervalString, + ml, + start: mlStart, + end, + }), + getMLJobIds(ml.anomalyDetectors, environment), + ]); - const jobId = mlJobIds[0]; + const scoreSeriesCollection = anomaliesResponse?.aggregations?.job_id.buckets + .filter((bucket) => jobIds.includes(bucket.key as string)) + .map((bucket) => { + const dateBuckets = bucket.ml_avg_response_times.buckets; - const mlBucketSize = await getMlBucketSize({ setup, jobId, logger }); - if (!isNumber(mlBucketSize)) { - return; - } + return { + jobId: bucket.key as string, + anomalyScore: compact( + dateBuckets.map((dateBucket) => { + const metrics = maybe(dateBucket.anomaly_score.top[0])?.metrics; + const score = metrics?.record_score; + + if ( + !metrics || + !isFiniteNumber(score) || + score < ANOMALY_THRESHOLD.CRITICAL + ) { + return null; + } - const { intervalString, bucketSize } = getBucketSize({ start, end }); - - const esResponse = await anomalySeriesFetcher({ - serviceName, - transactionType, - intervalString, - mlBucketSize, - setup, - jobId, - logger, - }); - - if (esResponse && mlBucketSize > 0) { - return anomalySeriesTransform( - esResponse, - mlBucketSize, - bucketSize, - timeseriesDates, - jobId + const anomalyStart = Date.parse(metrics.timestamp as string); + const anomalyEnd = + anomalyStart + (metrics.bucket_span as number) * 1000; + + return { + x0: anomalyStart, + x: anomalyEnd, + y: score, + }; + }) + ), + anomalyBoundaries: dateBuckets + .filter( + (dateBucket) => + dateBucket.lower.value !== null && dateBucket.upper.value !== null + ) + .map((dateBucket) => ({ + x: dateBucket.key, + y0: dateBucket.lower.value as number, + y: dateBucket.upper.value as number, + })), + }; + }); + + if ((scoreSeriesCollection?.length ?? 0) > 1) { + logger.warn( + `More than one ML job was found for ${serviceName} for environment ${environment}. Only showing results from ${scoreSeriesCollection?.[0].jobId}` ); } + + return scoreSeriesCollection?.[0]; } diff --git a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/transform.ts b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/transform.ts deleted file mode 100644 index 3bc9fdbabf9b8..0000000000000 --- a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/transform.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { first, last } from 'lodash'; -import { Coordinate, RectCoordinate } from '../../../../typings/timeseries'; -import { ESResponse } from './fetcher'; - -type IBucket = ReturnType; -function getBucket( - bucket: Required['aggregations']['ml_avg_response_times']['buckets'][0] -) { - return { - x: bucket.key, - anomalyScore: bucket.anomaly_score.value, - lower: bucket.lower.value, - upper: bucket.upper.value, - }; -} - -export type AnomalyTimeSeriesResponse = ReturnType< - typeof anomalySeriesTransform ->; -export function anomalySeriesTransform( - response: ESResponse, - mlBucketSize: number, - bucketSize: number, - timeSeriesDates: number[], - jobId: string -) { - const buckets = - response.aggregations?.ml_avg_response_times.buckets.map(getBucket) || []; - - const bucketSizeInMillis = Math.max(bucketSize, mlBucketSize) * 1000; - - return { - jobId, - anomalyScore: getAnomalyScoreDataPoints( - buckets, - timeSeriesDates, - bucketSizeInMillis - ), - anomalyBoundaries: getAnomalyBoundaryDataPoints(buckets, timeSeriesDates), - }; -} - -export function getAnomalyScoreDataPoints( - buckets: IBucket[], - timeSeriesDates: number[], - bucketSizeInMillis: number -): RectCoordinate[] { - const ANOMALY_THRESHOLD = 75; - const firstDate = first(timeSeriesDates); - const lastDate = last(timeSeriesDates); - - if (firstDate === undefined || lastDate === undefined) { - return []; - } - - return buckets - .filter( - (bucket) => - bucket.anomalyScore !== null && bucket.anomalyScore > ANOMALY_THRESHOLD - ) - .filter(isInDateRange(firstDate, lastDate)) - .map((bucket) => { - return { - x0: bucket.x, - x: Math.min(bucket.x + bucketSizeInMillis, lastDate), // don't go beyond last date - }; - }); -} - -export function getAnomalyBoundaryDataPoints( - buckets: IBucket[], - timeSeriesDates: number[] -): Coordinate[] { - return replaceFirstAndLastBucket(buckets, timeSeriesDates) - .filter((bucket) => bucket.lower !== null) - .map((bucket) => { - return { - x: bucket.x, - y0: bucket.lower, - y: bucket.upper, - }; - }); -} - -export function replaceFirstAndLastBucket( - buckets: IBucket[], - timeSeriesDates: number[] -) { - const firstDate = first(timeSeriesDates); - const lastDate = last(timeSeriesDates); - - if (firstDate === undefined || lastDate === undefined) { - return buckets; - } - - const preBucketWithValue = buckets - .filter((p) => p.x <= firstDate) - .reverse() - .find((p) => p.lower !== null); - - const bucketsInRange = buckets.filter(isInDateRange(firstDate, lastDate)); - - // replace first bucket if it is null - const firstBucket = first(bucketsInRange); - if (preBucketWithValue && firstBucket && firstBucket.lower === null) { - firstBucket.lower = preBucketWithValue.lower; - firstBucket.upper = preBucketWithValue.upper; - } - - const lastBucketWithValue = [...buckets] - .reverse() - .find((p) => p.lower !== null); - - // replace last bucket if it is null - const lastBucket = last(bucketsInRange); - if (lastBucketWithValue && lastBucket && lastBucket.lower === null) { - lastBucket.lower = lastBucketWithValue.lower; - lastBucket.upper = lastBucketWithValue.upper; - } - - return bucketsInRange; -} - -// anomaly time series contain one or more buckets extra in the beginning -// these extra buckets should be removed -function isInDateRange(firstDate: number, lastDate: number) { - return (p: IBucket) => p.x >= firstDate && p.x <= lastDate; -} diff --git a/x-pack/plugins/apm/server/routes/correlations.ts b/x-pack/plugins/apm/server/routes/correlations.ts index 6d1aead9292e3..d9431b6f8c0be 100644 --- a/x-pack/plugins/apm/server/routes/correlations.ts +++ b/x-pack/plugins/apm/server/routes/correlations.ts @@ -4,12 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ +import Boom from '@hapi/boom'; +import { i18n } from '@kbn/i18n'; import * as t from 'io-ts'; -import { rangeRt } from './default_api_types'; -import { getCorrelationsForSlowTransactions } from '../lib/correlations/get_correlations_for_slow_transactions'; +import { isActivePlatinumLicense } from '../../common/license_check'; import { getCorrelationsForFailedTransactions } from '../lib/correlations/get_correlations_for_failed_transactions'; -import { createRoute } from './create_route'; +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'; + +const INVALID_LICENSE = i18n.translate( + 'xpack.apm.significanTerms.license.text', + { + defaultMessage: + 'To use the correlations API, you must be subscribed to an Elastic Platinum license.', + } +); export const correlationsForSlowTransactionsRoute = createRoute({ endpoint: 'GET /api/apm/correlations/slow_transactions', @@ -30,6 +41,9 @@ export const correlationsForSlowTransactionsRoute = createRoute({ }), options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { + if (!isActivePlatinumLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } const setup = await setupRequest(context, request); const { serviceName, @@ -68,6 +82,9 @@ export const correlationsForFailedTransactionsRoute = createRoute({ }), options: { tags: ['access:apm'] }, handler: async ({ context, request }) => { + if (!isActivePlatinumLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } const setup = await setupRequest(context, request); const { serviceName, diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts index 452b00a7ae320..1192291de7b58 100644 --- a/x-pack/plugins/apm/server/routes/service_map.ts +++ b/x-pack/plugins/apm/server/routes/service_map.ts @@ -6,10 +6,7 @@ import Boom from '@hapi/boom'; import * as t from 'io-ts'; -import { - invalidLicenseMessage, - isActivePlatinumLicense, -} from '../../common/service_map'; +import { invalidLicenseMessage } from '../../common/service_map'; 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'; @@ -17,6 +14,7 @@ import { createRoute } from './create_route'; import { rangeRt, uiFiltersRt } from './default_api_types'; import { notifyFeatureUsage } from '../feature'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; +import { isActivePlatinumLicense } from '../../common/license_check'; export const serviceMapRoute = createRoute({ endpoint: 'GET /api/apm/service-map', diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts index 49708e4edb732..b5727862a898b 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -6,7 +6,7 @@ import * as t from 'io-ts'; import Boom from '@hapi/boom'; -import { isActivePlatinumLicense } from '../../../common/service_map'; +import { isActivePlatinumLicense } from '../../../common/license_check'; import { ML_ERRORS } from '../../../common/anomaly_detection'; import { createRoute } from '../create_route'; import { getAnomalyDetectionJobs } from '../../lib/anomaly_detection/get_anomaly_detection_jobs'; diff --git a/x-pack/plugins/apm/server/routes/settings/custom_link.ts b/x-pack/plugins/apm/server/routes/settings/custom_link.ts index 70755540721dd..89ac698e9208c 100644 --- a/x-pack/plugins/apm/server/routes/settings/custom_link.ts +++ b/x-pack/plugins/apm/server/routes/settings/custom_link.ts @@ -7,8 +7,8 @@ import Boom from '@hapi/boom'; import * as t from 'io-ts'; import { pick } from 'lodash'; +import { isActiveGoldLicense } from '../../../common/license_check'; import { INVALID_LICENSE } from '../../../common/custom_link'; -import { ILicense } from '../../../../licensing/common/types'; import { FILTER_OPTIONS } from '../../../common/custom_link/custom_link_filter_options'; import { notifyFeatureUsage } from '../../feature'; import { setupRequest } from '../../lib/helpers/setup_request'; @@ -22,10 +22,6 @@ import { getTransaction } from '../../lib/settings/custom_link/get_transaction'; import { listCustomLinks } from '../../lib/settings/custom_link/list_custom_links'; import { createRoute } from '../create_route'; -function isActiveGoldLicense(license: ILicense) { - return license.isActive && license.hasAtLeast('gold'); -} - export const customLinkTransactionRoute = createRoute({ endpoint: 'GET /api/apm/settings/custom_links/transaction', options: { tags: ['access:apm'] }, diff --git a/x-pack/plugins/apm/server/routes/transactions.ts b/x-pack/plugins/apm/server/routes/transactions.ts index 50510fba78512..57eb66cbcc7e4 100644 --- a/x-pack/plugins/apm/server/routes/transactions.ts +++ b/x-pack/plugins/apm/server/routes/transactions.ts @@ -169,23 +169,28 @@ export const transactionLatencyChatsRoute = createRoute({ transactionName, setup, searchAggregatedTransactions, + logger, }; - const { - latencyTimeseries, - overallAvgDuration, - } = await getLatencyTimeseries({ - ...options, - latencyAggregationType: latencyAggregationType as LatencyAggregationType, - }); + const [latencyData, anomalyTimeseries] = await Promise.all([ + getLatencyTimeseries({ + ...options, + latencyAggregationType: latencyAggregationType as LatencyAggregationType, + }), + getAnomalySeries(options).catch((error) => { + logger.warn(`Unable to retrieve anomalies for latency charts.`); + logger.error(error); + return undefined; + }), + ]); - const anomalyTimeseries = await getAnomalySeries({ - ...options, - logger, - latencyTimeseries, - }); + const { latencyTimeseries, overallAvgDuration } = latencyData; - return { latencyTimeseries, overallAvgDuration, anomalyTimeseries }; + return { + latencyTimeseries, + overallAvgDuration, + anomalyTimeseries, + }; }, }); diff --git a/x-pack/plugins/apm/server/ui_settings.ts b/x-pack/plugins/apm/server/ui_settings.ts index 80225de5195f8..c86fb636b5a1a 100644 --- a/x-pack/plugins/apm/server/ui_settings.ts +++ b/x-pack/plugins/apm/server/ui_settings.ts @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { UiSettingsParams } from '../../../../src/core/types'; import { - enableCorrelations, + enableSignificantTerms, enableServiceOverview, } from '../common/ui_settings_keys'; @@ -16,10 +16,10 @@ import { * uiSettings definitions for APM. */ export const uiSettings: Record> = { - [enableCorrelations]: { + [enableSignificantTerms]: { category: ['observability'], name: i18n.translate('xpack.apm.enableCorrelationsExperimentName', { - defaultMessage: 'APM Significant terms', + defaultMessage: 'APM Significant terms (Platinum required)', }), value: false, description: i18n.translate( diff --git a/x-pack/plugins/apm/typings/timeseries.ts b/x-pack/plugins/apm/typings/timeseries.ts index 600be15ea229f..3db37729962d7 100644 --- a/x-pack/plugins/apm/typings/timeseries.ts +++ b/x-pack/plugins/apm/typings/timeseries.ts @@ -3,6 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { + AccessorFn, + AreaSeriesStyle, + Fit, + FitConfig, + LineSeriesStyle, +} from '@elastic/charts'; +import { DeepPartial } from 'utility-types'; import { Maybe } from '../typings/common'; export interface Coordinate { @@ -15,7 +23,13 @@ export interface RectCoordinate { x0: number; } -export interface TimeSeries< +type Accessor = Array; + +export type TimeSeries< + TCoordinate extends { x: number } = Coordinate | RectCoordinate +> = APMChartSpec; + +export interface APMChartSpec< TCoordinate extends { x: number } = Coordinate | RectCoordinate > { title: string; @@ -27,6 +41,11 @@ export interface TimeSeries< type: string; color: string; areaColor?: string; + fit?: Exclude | FitConfig; + stackAccessors?: Accessor; + splitSeriesAccessors?: Accessor; + lineSeriesStyle?: DeepPartial; + areaSeriesStyle?: DeepPartial; } export type ChartType = 'area' | 'linemark'; diff --git a/x-pack/plugins/console_extensions/tsconfig.json b/x-pack/plugins/console_extensions/tsconfig.json new file mode 100644 index 0000000000000..5ad28f230a0bb --- /dev/null +++ b/x-pack/plugins/console_extensions/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "server/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/console/tsconfig.json" } + ] +} diff --git a/x-pack/plugins/data_enhanced/common/search/poll_search.test.ts b/x-pack/plugins/data_enhanced/common/search/poll_search.test.ts new file mode 100644 index 0000000000000..ddb595c90444f --- /dev/null +++ b/x-pack/plugins/data_enhanced/common/search/poll_search.test.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { pollSearch } from './poll_search'; +import { AbortError } from '../../../../../src/plugins/kibana_utils/common'; + +describe('pollSearch', () => { + function getMockedSearch$(resolveOnI = 1, finishWithError = false) { + let counter = 0; + return jest.fn().mockImplementation(() => { + counter++; + const lastCall = counter === resolveOnI; + return new Promise((resolve) => { + if (lastCall) { + resolve({ + isRunning: false, + isPartial: finishWithError, + }); + } else { + resolve({ + isRunning: true, + isPartial: true, + }); + } + }); + }); + } + + test('Defers execution', async () => { + const searchFn = getMockedSearch$(1); + const cancelFn = jest.fn(); + pollSearch(searchFn, cancelFn); + expect(searchFn).toBeCalledTimes(0); + expect(cancelFn).toBeCalledTimes(0); + }); + + test('Resolves immediatelly', async () => { + const searchFn = getMockedSearch$(1); + const cancelFn = jest.fn(); + await pollSearch(searchFn, cancelFn).toPromise(); + expect(searchFn).toBeCalledTimes(1); + expect(cancelFn).toBeCalledTimes(0); + }); + + test('Resolves when complete', async () => { + const searchFn = getMockedSearch$(3); + const cancelFn = jest.fn(); + await pollSearch(searchFn, cancelFn).toPromise(); + expect(searchFn).toBeCalledTimes(3); + expect(cancelFn).toBeCalledTimes(0); + }); + + test('Throws Error on ES error response', async () => { + const searchFn = getMockedSearch$(2, true); + const cancelFn = jest.fn(); + const poll = pollSearch(searchFn, cancelFn).toPromise(); + await expect(poll).rejects.toThrow(Error); + expect(searchFn).toBeCalledTimes(2); + expect(cancelFn).toBeCalledTimes(0); + }); + + test('Throws AbortError on empty response', async () => { + const searchFn = jest.fn().mockResolvedValue(undefined); + const cancelFn = jest.fn(); + const poll = pollSearch(searchFn, cancelFn).toPromise(); + await expect(poll).rejects.toThrow(AbortError); + expect(searchFn).toBeCalledTimes(1); + expect(cancelFn).toBeCalledTimes(0); + }); + + test('Throws AbortError and cancels on abort', async () => { + const searchFn = getMockedSearch$(20); + const cancelFn = jest.fn(); + const abortController = new AbortController(); + const poll = pollSearch(searchFn, cancelFn, { + abortSignal: abortController.signal, + }).toPromise(); + + await new Promise((resolve) => setTimeout(resolve, 500)); + abortController.abort(); + + await expect(poll).rejects.toThrow(AbortError); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + + expect(searchFn).toBeCalledTimes(1); + expect(cancelFn).toBeCalledTimes(1); + }); + + test("Stops, but doesn't cancel on unsubscribe", async () => { + const searchFn = getMockedSearch$(20); + const cancelFn = jest.fn(); + const subscription = pollSearch(searchFn, cancelFn).subscribe(() => {}); + + await new Promise((resolve) => setTimeout(resolve, 500)); + subscription.unsubscribe(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + + expect(searchFn).toBeCalledTimes(1); + expect(cancelFn).toBeCalledTimes(0); + }); + + test('Calls cancel even when consumer unsubscribes', async () => { + const searchFn = getMockedSearch$(20); + const cancelFn = jest.fn(); + const abortController = new AbortController(); + const subscription = pollSearch(searchFn, cancelFn, { + abortSignal: abortController.signal, + }).subscribe(() => {}); + subscription.unsubscribe(); + abortController.abort(); + + expect(searchFn).toBeCalledTimes(1); + expect(cancelFn).toBeCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/data_enhanced/common/search/poll_search.ts b/x-pack/plugins/data_enhanced/common/search/poll_search.ts index c0e289c691cfd..2cc55836f8cc1 100644 --- a/x-pack/plugins/data_enhanced/common/search/poll_search.ts +++ b/x-pack/plugins/data_enhanced/common/search/poll_search.ts @@ -4,28 +4,42 @@ * you may not use this file except in compliance with the Elastic License. */ -import { from, NEVER, Observable, timer } from 'rxjs'; -import { expand, finalize, switchMap, takeUntil, takeWhile, tap } from 'rxjs/operators'; +import { from, Observable, timer, defer, fromEvent, EMPTY } from 'rxjs'; +import { expand, map, switchMap, takeUntil, takeWhile, tap } from 'rxjs/operators'; import type { IKibanaSearchResponse } from '../../../../../src/plugins/data/common'; import { isErrorResponse, isPartialResponse } from '../../../../../src/plugins/data/common'; -import { AbortError, abortSignalToPromise } from '../../../../../src/plugins/kibana_utils/common'; +import { AbortError } from '../../../../../src/plugins/kibana_utils/common'; import type { IAsyncSearchOptions } from './types'; export const pollSearch = ( search: () => Promise, - { pollInterval = 1000, ...options }: IAsyncSearchOptions = {} + cancel?: () => void, + { pollInterval = 1000, abortSignal }: IAsyncSearchOptions = {} ): Observable => { - const aborted = options?.abortSignal - ? abortSignalToPromise(options?.abortSignal) - : { promise: NEVER, cleanup: () => {} }; + return defer(() => { + if (abortSignal?.aborted) { + throw new AbortError(); + } - return from(search()).pipe( - expand(() => timer(pollInterval).pipe(switchMap(search))), - tap((response) => { - if (isErrorResponse(response)) throw new AbortError(); - }), - takeWhile(isPartialResponse, true), - takeUntil(from(aborted.promise)), - finalize(aborted.cleanup) - ); + if (cancel) { + abortSignal?.addEventListener('abort', cancel, { once: true }); + } + + const aborted$ = (abortSignal ? fromEvent(abortSignal, 'abort') : EMPTY).pipe( + map(() => { + throw new AbortError(); + }) + ); + + return from(search()).pipe( + expand(() => timer(pollInterval).pipe(switchMap(search))), + tap((response) => { + if (isErrorResponse(response)) { + throw response ? new Error('Received partial response') : new AbortError(); + } + }), + takeWhile(isPartialResponse, true), + takeUntil(aborted$) + ); + }); }; diff --git a/x-pack/plugins/data_enhanced/config.ts b/x-pack/plugins/data_enhanced/config.ts index 9838f0959ef19..4c90b1fb4c81d 100644 --- a/x-pack/plugins/data_enhanced/config.ts +++ b/x-pack/plugins/data_enhanced/config.ts @@ -8,8 +8,13 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const configSchema = schema.object({ search: schema.object({ - sendToBackground: schema.object({ + sessions: schema.object({ enabled: schema.boolean({ defaultValue: false }), + pageSize: schema.number({ defaultValue: 10000 }), + trackingInterval: schema.duration({ defaultValue: '10s' }), + inMemTimeout: schema.duration({ defaultValue: '1m' }), + maxUpdateRetries: schema.number({ defaultValue: 3 }), + defaultExpiration: schema.duration({ defaultValue: '7d' }), }), }), }); diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts index c7d1c8624cb1f..fed2b4e71ab50 100644 --- a/x-pack/plugins/data_enhanced/public/plugin.ts +++ b/x-pack/plugins/data_enhanced/public/plugin.ts @@ -62,7 +62,7 @@ export class DataEnhancedPlugin public start(core: CoreStart, plugins: DataEnhancedStartDependencies) { setAutocompleteService(plugins.data.autocomplete); - if (this.initializerContext.config.get().search.sendToBackground.enabled) { + if (this.initializerContext.config.get().search.sessions.enabled) { core.chrome.setBreadcrumbsAppendExtension({ content: toMountPoint( React.createElement( diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts index fc6c860f907f6..1a6fc724e2cf2 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts @@ -192,7 +192,7 @@ describe('EnhancedSearchInterceptor', () => { await timeTravel(10); expect(error).toHaveBeenCalled(); - expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError); + expect(error.mock.calls[0][0]).toBeInstanceOf(Error); }); test('should abort on user abort', async () => { @@ -262,7 +262,7 @@ describe('EnhancedSearchInterceptor', () => { expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError); expect(fetchMock).toHaveBeenCalledTimes(2); - expect(mockCoreSetup.http.delete).toHaveBeenCalled(); + expect(mockCoreSetup.http.delete).toHaveBeenCalledTimes(1); }); test('should not DELETE a running async search on async timeout prior to first response', async () => { @@ -326,7 +326,7 @@ describe('EnhancedSearchInterceptor', () => { expect(error).toHaveBeenCalled(); expect(error.mock.calls[0][0]).toBeInstanceOf(SearchTimeoutError); expect(fetchMock).toHaveBeenCalledTimes(2); - expect(mockCoreSetup.http.delete).toHaveBeenCalled(); + expect(mockCoreSetup.http.delete).toHaveBeenCalledTimes(1); }); test('should DELETE a running async search on async timeout on error from fetch', async () => { @@ -343,8 +343,6 @@ describe('EnhancedSearchInterceptor', () => { time: 10, value: { error: 'oh no', - isPartial: false, - isRunning: false, id: 1, }, isError: true, @@ -368,12 +366,12 @@ describe('EnhancedSearchInterceptor', () => { expect(error).toHaveBeenCalled(); expect(error.mock.calls[0][0]).toBe(responses[1].value); expect(fetchMock).toHaveBeenCalledTimes(2); - expect(mockCoreSetup.http.delete).toHaveBeenCalled(); + expect(mockCoreSetup.http.delete).toHaveBeenCalledTimes(1); }); test('should NOT DELETE a running SAVED async search on abort', async () => { const sessionId = 'sessionId'; - sessionService.getSessionId.mockImplementation(() => sessionId); + sessionService.isCurrentSession.mockImplementation((_sessionId) => _sessionId === sessionId); const responses = [ { time: 10, @@ -479,6 +477,7 @@ describe('EnhancedSearchInterceptor', () => { test('should track searches', async () => { const sessionId = 'sessionId'; + sessionService.isCurrentSession.mockImplementation((_sessionId) => _sessionId === sessionId); sessionService.getSessionId.mockImplementation(() => sessionId); const untrack = jest.fn(); @@ -496,6 +495,7 @@ describe('EnhancedSearchInterceptor', () => { test('session service should be able to cancel search', async () => { const sessionId = 'sessionId'; + sessionService.isCurrentSession.mockImplementation((_sessionId) => _sessionId === sessionId); sessionService.getSessionId.mockImplementation(() => sessionId); const untrack = jest.fn(); @@ -519,6 +519,7 @@ describe('EnhancedSearchInterceptor', () => { test("don't track non current session searches", async () => { const sessionId = 'sessionId'; + sessionService.isCurrentSession.mockImplementation((_sessionId) => _sessionId === sessionId); sessionService.getSessionId.mockImplementation(() => sessionId); const untrack = jest.fn(); @@ -539,6 +540,7 @@ describe('EnhancedSearchInterceptor', () => { test("don't track if no current session", async () => { sessionService.getSessionId.mockImplementation(() => undefined); + sessionService.isCurrentSession.mockImplementation((_sessionId) => false); const untrack = jest.fn(); sessionService.trackSearch.mockImplementation(() => untrack); diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts index b0f194115f0b8..9145e35c4485f 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { once } from 'lodash'; import { throwError, Subscription } from 'rxjs'; import { tap, finalize, catchError, filter, take, skip } from 'rxjs/operators'; import { @@ -14,7 +15,6 @@ import { IKibanaSearchRequest, SearchSessionState, } from '../../../../../src/plugins/data/public'; -import { AbortError } from '../../../../../src/plugins/kibana_utils/common'; import { ENHANCED_ES_SEARCH_STRATEGY, IAsyncSearchOptions, pollSearch } from '../../common'; export class EnhancedSearchInterceptor extends SearchInterceptor { @@ -64,36 +64,45 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { const search = () => this.runSearch({ id, ...request }, searchOptions); this.pendingCount$.next(this.pendingCount$.getValue() + 1); - const isCurrentSession = () => - !!options.sessionId && options.sessionId === this.deps.session.getSessionId(); - const untrackSearch = isCurrentSession() && this.deps.session.trackSearch({ abort }); + const untrackSearch = + this.deps.session.isCurrentSession(options.sessionId) && + this.deps.session.trackSearch({ abort }); // track if this search's session will be send to background // if yes, then we don't need to cancel this search when it is aborted let isSavedToBackground = false; const savedToBackgroundSub = - isCurrentSession() && + this.deps.session.isCurrentSession(options.sessionId) && this.deps.session.state$ .pipe( skip(1), // ignore any state, we are only interested in transition x -> BackgroundLoading - filter((state) => isCurrentSession() && state === SearchSessionState.BackgroundLoading), + filter( + (state) => + this.deps.session.isCurrentSession(options.sessionId) && + state === SearchSessionState.BackgroundLoading + ), take(1) ) .subscribe(() => { isSavedToBackground = true; }); - return pollSearch(search, { ...options, abortSignal: combinedSignal }).pipe( + const cancel = once(() => { + if (id && !isSavedToBackground) this.deps.http.delete(`/internal/search/${strategy}/${id}`); + }); + + return pollSearch(search, cancel, { ...options, abortSignal: combinedSignal }).pipe( tap((response) => (id = response.id)), - catchError((e: AbortError) => { - if (id && !isSavedToBackground) this.deps.http.delete(`/internal/search/${strategy}/${id}`); + catchError((e: Error) => { + cancel(); return throwError(this.handleSearchError(e, timeoutSignal, options)); }), finalize(() => { this.pendingCount$.next(this.pendingCount$.getValue() - 1); cleanup(); - if (untrackSearch && isCurrentSession()) { + if (untrackSearch && this.deps.session.isCurrentSession(options.sessionId)) { + // untrack if this search still belongs to current session untrackSearch(); } if (savedToBackgroundSub) { diff --git a/x-pack/plugins/data_enhanced/server/plugin.ts b/x-pack/plugins/data_enhanced/server/plugin.ts index 592a5df1eee2f..18acd7f047e5d 100644 --- a/x-pack/plugins/data_enhanced/server/plugin.ts +++ b/x-pack/plugins/data_enhanced/server/plugin.ts @@ -61,7 +61,10 @@ export class EnhancedDataServerPlugin eqlSearchStrategyProvider(this.logger) ); - this.sessionService = new SearchSessionService(this.logger); + this.sessionService = new SearchSessionService( + this.logger, + this.initializerContext.config.create() + ); deps.data.__enhance({ search: { @@ -81,7 +84,6 @@ export class EnhancedDataServerPlugin public start(core: CoreStart, { taskManager }: StartDependencies) { this.sessionService.start(core, { taskManager, - config$: this.initializerContext.config.create(), }); } diff --git a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts index e615d9d2a660a..a0d4e9dcd19b9 100644 --- a/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/eql_search_strategy.ts @@ -5,7 +5,7 @@ */ import { tap } from 'rxjs/operators'; -import type { Logger } from 'kibana/server'; +import type { IScopedClusterClient, Logger } from 'kibana/server'; import type { ISearchStrategy } from '../../../../../src/plugins/data/server'; import type { EqlSearchStrategyRequest, @@ -21,10 +21,14 @@ import { EqlSearchResponse } from './types'; export const eqlSearchStrategyProvider = ( logger: Logger ): ISearchStrategy => { + async function cancelAsyncSearch(id: string, esClient: IScopedClusterClient) { + await esClient.asCurrentUser.asyncSearch.delete({ id }); + } + return { cancel: async (id, options, { esClient }) => { logger.debug(`_eql/delete ${id}`); - await esClient.asCurrentUser.eql.delete({ id }); + await cancelAsyncSearch(id, esClient); }, search: ({ id, ...request }, options: IAsyncSearchOptions, { esClient, uiSettingsClient }) => { @@ -54,7 +58,13 @@ export const eqlSearchStrategyProvider = ( return toEqlKibanaSearchResponse(response); }; - return pollSearch(search, options).pipe(tap((response) => (id = response.id))); + const cancel = async () => { + if (id) { + await cancelAsyncSearch(id, esClient); + } + }; + + return pollSearch(search, cancel, options).pipe(tap((response) => (id = response.id))); }, extend: async (id, keepAlive, options, { esClient }) => { diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index c1520d931c272..54ed59b30952a 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -5,7 +5,7 @@ */ import type { Observable } from 'rxjs'; -import type { Logger, SharedGlobalConfig } from 'kibana/server'; +import type { IScopedClusterClient, Logger, SharedGlobalConfig } from 'kibana/server'; import { first, tap } from 'rxjs/operators'; import { SearchResponse } from 'elasticsearch'; import { from } from 'rxjs'; @@ -40,6 +40,10 @@ export const enhancedEsSearchStrategyProvider = ( logger: Logger, usage?: SearchUsage ): ISearchStrategy => { + async function cancelAsyncSearch(id: string, esClient: IScopedClusterClient) { + await esClient.asCurrentUser.asyncSearch.delete({ id }); + } + function asyncSearch( { id, ...request }: IEsSearchRequest, options: IAsyncSearchOptions, @@ -58,7 +62,13 @@ export const enhancedEsSearchStrategyProvider = ( return toAsyncKibanaSearchResponse(body); }; - return pollSearch(search, options).pipe( + const cancel = async () => { + if (id) { + await cancelAsyncSearch(id, esClient); + } + }; + + return pollSearch(search, cancel, options).pipe( tap((response) => (id = response.id)), tap(searchUsageObserver(logger, usage)) ); @@ -109,7 +119,7 @@ export const enhancedEsSearchStrategyProvider = ( }, cancel: async (id, options, { esClient }) => { logger.debug(`cancel ${id}`); - await esClient.asCurrentUser.asyncSearch.delete({ id }); + await cancelAsyncSearch(id, esClient); }, extend: async (id, keepAlive, options, { esClient }) => { logger.debug(`extend ${id} by ${keepAlive}`); diff --git a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts b/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts index a7d57c94fa153..d32dcf72a4205 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/monitoring_task.ts @@ -4,6 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { Duration } from 'moment'; import { TaskManagerSetupContract, TaskManagerStartContract, @@ -12,15 +15,22 @@ import { import { checkRunningSessions } from './check_running_sessions'; import { CoreSetup, SavedObjectsClient, Logger } from '../../../../../../src/core/server'; import { SEARCH_SESSION_TYPE } from '../../saved_objects'; +import { ConfigSchema } from '../../../config'; export const SEARCH_SESSIONS_TASK_TYPE = 'bg_monitor'; export const SEARCH_SESSIONS_TASK_ID = `data_enhanced_${SEARCH_SESSIONS_TASK_TYPE}`; -export const MONITOR_INTERVAL = 15; // in seconds -function searchSessionRunner(core: CoreSetup, logger: Logger) { +interface SearchSessionTaskDeps { + taskManager: TaskManagerSetupContract; + logger: Logger; + config$: Observable; +} + +function searchSessionRunner(core: CoreSetup, { logger, config$ }: SearchSessionTaskDeps) { return ({ taskInstance }: RunContext) => { return { async run() { + const config = await config$.pipe(first()).toPromise(); const [coreStart] = await core.getStartServices(); const internalRepo = coreStart.savedObjects.createInternalRepository([SEARCH_SESSION_TYPE]); const internalSavedObjectsClient = new SavedObjectsClient(internalRepo); @@ -31,7 +41,7 @@ function searchSessionRunner(core: CoreSetup, logger: Logger) { ); return { - runAt: new Date(Date.now() + MONITOR_INTERVAL * 1000), + runAt: new Date(Date.now() + config.search.sessions.trackingInterval.asMilliseconds()), state: {}, }; }, @@ -39,22 +49,19 @@ function searchSessionRunner(core: CoreSetup, logger: Logger) { }; } -export function registerSearchSessionsTask( - core: CoreSetup, - taskManager: TaskManagerSetupContract, - logger: Logger -) { - taskManager.registerTaskDefinitions({ +export function registerSearchSessionsTask(core: CoreSetup, deps: SearchSessionTaskDeps) { + deps.taskManager.registerTaskDefinitions({ [SEARCH_SESSIONS_TASK_TYPE]: { title: 'Search Sessions Monitor', - createTaskRunner: searchSessionRunner(core, logger), + createTaskRunner: searchSessionRunner(core, deps), }, }); } export async function scheduleSearchSessionsTasks( taskManager: TaskManagerStartContract, - logger: Logger + logger: Logger, + trackingInterval: Duration ) { await taskManager.removeIfExists(SEARCH_SESSIONS_TASK_ID); @@ -63,7 +70,7 @@ export async function scheduleSearchSessionsTasks( id: SEARCH_SESSIONS_TASK_ID, taskType: SEARCH_SESSIONS_TASK_TYPE, schedule: { - interval: `${MONITOR_INTERVAL}s`, + interval: `${trackingInterval.asSeconds()}s`, }, state: {}, params: {}, diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts index 3114e746d0453..f37aaf71fded5 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts @@ -17,9 +17,11 @@ import { coreMock } from 'src/core/server/mocks'; import { ConfigSchema } from '../../../config'; // @ts-ignore import { taskManagerMock } from '../../../../task_manager/server/mocks'; -import { INMEM_TRACKING_INTERVAL, MAX_UPDATE_RETRIES } from './constants'; import { SearchStatus } from './types'; +const INMEM_TRACKING_INTERVAL = 10000; +const MAX_UPDATE_RETRIES = 3; + const flushPromises = () => new Promise((resolve) => setImmediate(resolve)); describe('SearchSessionService', () => { @@ -103,12 +105,36 @@ describe('SearchSessionService', () => { beforeEach(async () => { savedObjectsClient = savedObjectsClientMock.create(); + const config$ = new BehaviorSubject({ + search: { + sessions: { + enabled: true, + pageSize: 10000, + inMemTimeout: moment.duration(1, 'm'), + maxUpdateRetries: 3, + defaultExpiration: moment.duration(7, 'd'), + trackingInterval: moment.duration(10, 's'), + }, + }, + }); const mockLogger: any = { debug: jest.fn(), warn: jest.fn(), error: jest.fn(), }; - service = new SearchSessionService(mockLogger); + service = new SearchSessionService(mockLogger, config$); + const coreStart = coreMock.createStart(); + const mockTaskManager = taskManagerMock.createStart(); + jest.useFakeTimers(); + await flushPromises(); + await service.start(coreStart, { + taskManager: mockTaskManager, + }); + }); + + afterEach(() => { + service.stop(); + jest.useRealTimers(); }); it('search throws if `name` is not provided', () => { @@ -411,28 +437,6 @@ describe('SearchSessionService', () => { }); describe('Monitor', () => { - beforeEach(async () => { - jest.useFakeTimers(); - const config$ = new BehaviorSubject({ - search: { - sendToBackground: { - enabled: true, - }, - }, - }); - const mockTaskManager = taskManagerMock.createStart(); - await service.start(coreMock.createStart(), { - config$, - taskManager: mockTaskManager, - }); - await flushPromises(); - }); - - afterEach(() => { - jest.useRealTimers(); - service.stop(); - }); - it('schedules the next iteration', async () => { const findSpy = jest.fn().mockResolvedValue({ saved_objects: [] }); createMockInternalSavedObjectClient(findSpy); diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts index 8c9e0dad4957e..68f180f723c81 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.ts @@ -44,13 +44,6 @@ import { SEARCH_SESSION_TYPE } from '../../saved_objects'; import { createRequestHash } from './utils'; import { ConfigSchema } from '../../../config'; import { registerSearchSessionsTask, scheduleSearchSessionsTasks } from './monitoring_task'; -import { - DEFAULT_EXPIRATION, - INMEM_MAX_SESSIONS, - INMEM_TRACKING_INTERVAL, - INMEM_TRACKING_TIMEOUT_SEC, - MAX_UPDATE_RETRIES, -} from './constants'; import { SearchStatus } from './types'; export interface SearchSessionDependencies { @@ -69,8 +62,10 @@ interface SetupDependencies { interface StartDependencies { taskManager: TaskManagerStartContract; - config$: Observable; } + +type SearchSessionsConfig = ConfigSchema['search']['sessions']; + export class SearchSessionService implements ISessionService { /** * Map of sessionId to { [requestHash]: searchId } @@ -79,14 +74,24 @@ export class SearchSessionService implements ISessionService { private sessionSearchMap = new Map(); private internalSavedObjectsClient!: SavedObjectsClientContract; private monitorTimer!: NodeJS.Timeout; + private config!: SearchSessionsConfig; - constructor(private readonly logger: Logger) {} + constructor( + private readonly logger: Logger, + private readonly config$: Observable + ) {} public setup(core: CoreSetup, deps: SetupDependencies) { - registerSearchSessionsTask(core, deps.taskManager, this.logger); + registerSearchSessionsTask(core, { + config$: this.config$, + taskManager: deps.taskManager, + logger: this.logger, + }); } public async start(core: CoreStart, deps: StartDependencies) { + const configPromise = await this.config$.pipe(first()).toPromise(); + this.config = (await configPromise).search.sessions; return this.setupMonitoring(core, deps); } @@ -96,9 +101,8 @@ export class SearchSessionService implements ISessionService { } private setupMonitoring = async (core: CoreStart, deps: StartDependencies) => { - const config = await deps.config$.pipe(first()).toPromise(); - if (config.search.sendToBackground.enabled) { - scheduleSearchSessionsTasks(deps.taskManager, this.logger); + if (this.config.enabled) { + scheduleSearchSessionsTasks(deps.taskManager, this.logger, this.config.trackingInterval); this.logger.debug(`setupMonitoring | Enabling monitoring`); const internalRepo = core.savedObjects.createInternalRepository([SEARCH_SESSION_TYPE]); this.internalSavedObjectsClient = new SavedObjectsClient(internalRepo); @@ -129,7 +133,7 @@ export class SearchSessionService implements ISessionService { private async getAllMappedSavedObjects() { const filter = this.sessionIdsAsFilters(Array.from(this.sessionSearchMap.keys())); const res = await this.internalSavedObjectsClient.find({ - perPage: INMEM_MAX_SESSIONS, // If there are more sessions in memory, they will be synced when some items are cleared out. + perPage: this.config.pageSize, // If there are more sessions in memory, they will be synced when some items are cleared out. type: SEARCH_SESSION_TYPE, filter, namespaces: ['*'], @@ -138,17 +142,17 @@ export class SearchSessionService implements ISessionService { return res.saved_objects; } - private clearSessions = () => { + private clearSessions = async () => { const curTime = moment(); this.sessionSearchMap.forEach((sessionInfo, sessionId) => { if ( - moment.duration(curTime.diff(sessionInfo.insertTime)).asSeconds() > - INMEM_TRACKING_TIMEOUT_SEC + moment.duration(curTime.diff(sessionInfo.insertTime)).asMilliseconds() > + this.config.inMemTimeout.asMilliseconds() ) { this.logger.debug(`clearSessions | Deleting expired session ${sessionId}`); this.sessionSearchMap.delete(sessionId); - } else if (sessionInfo.retryCount >= MAX_UPDATE_RETRIES) { + } else if (sessionInfo.retryCount >= this.config.maxUpdateRetries) { this.logger.warn(`clearSessions | Deleting failed session ${sessionId}`); this.sessionSearchMap.delete(sessionId); } @@ -192,7 +196,7 @@ export class SearchSessionService implements ISessionService { } finally { this.monitorMappedIds(); } - }, INMEM_TRACKING_INTERVAL); + }, this.config.trackingInterval.asMilliseconds()); } private async updateAllSavedObjects( @@ -256,7 +260,7 @@ export class SearchSessionService implements ISessionService { name, appId, created = new Date().toISOString(), - expires = new Date(Date.now() + DEFAULT_EXPIRATION).toISOString(), + expires = new Date(Date.now() + this.config.defaultExpiration.asMilliseconds()).toISOString(), status = SearchSessionStatus.IN_PROGRESS, urlGeneratorId, initialState = {}, diff --git a/x-pack/plugins/discover_enhanced/tsconfig.json b/x-pack/plugins/discover_enhanced/tsconfig.json new file mode 100644 index 0000000000000..38a55e557909b --- /dev/null +++ b/x-pack/plugins/discover_enhanced/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["*.ts", "common/**/*", "public/**/*", "server/**/*"], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/discover/tsconfig.json" }, + { "path": "../../../src/plugins/share/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_legacy/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../../src/plugins/url_forwarding/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../../src/plugins/visualizations/tsconfig.json" }, + { "path": "../../../src/plugins/ui_actions/tsconfig.json" } + ] +} diff --git a/x-pack/plugins/encrypted_saved_objects/tsconfig.json b/x-pack/plugins/encrypted_saved_objects/tsconfig.json new file mode 100644 index 0000000000000..2b51b313d34fc --- /dev/null +++ b/x-pack/plugins/encrypted_saved_objects/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["server/**/*"], + "references": [ + { "path": "../security/tsconfig.json" }, + ] +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.test.ts new file mode 100644 index 0000000000000..62e241df01361 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.test.ts @@ -0,0 +1,308 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LogicMounter } from '../../../__mocks__/kea.mock'; + +jest.mock('../../../shared/kibana', () => ({ + KibanaLogic: { values: { history: { location: { search: '' } } } }, +})); +import { KibanaLogic } from '../../../shared/kibana'; + +jest.mock('../../../shared/http', () => ({ + HttpLogic: { values: { http: { get: jest.fn() } } }, +})); +import { HttpLogic } from '../../../shared/http'; + +jest.mock('../../../shared/flash_messages', () => ({ + flashAPIErrors: jest.fn(), +})); +import { flashAPIErrors } from '../../../shared/flash_messages'; + +jest.mock('../engine', () => ({ + EngineLogic: { values: { engineName: 'test-engine' } }, +})); + +import { AnalyticsLogic } from './'; + +describe('AnalyticsLogic', () => { + const DEFAULT_VALUES = { + dataLoading: true, + analyticsUnavailable: false, + }; + + const MOCK_TOP_QUERIES = [ + { + doc_count: 5, + key: 'some-key', + }, + { + doc_count: 0, + key: 'another-key', + }, + ]; + const MOCK_RECENT_QUERIES = [ + { + document_ids: ['1', '2'], + query_string: 'some-query', + tags: ['some-tag'], + timestamp: 'some-timestamp', + }, + ]; + const MOCK_TOP_CLICKS = [ + { + key: 'highly-clicked-query', + doc_count: 1, + document: { + id: 'some-id', + engine: 'some-engine', + tags: [], + }, + clicks: { + doc_count: 100, + }, + }, + ]; + const MOCK_ANALYTICS_RESPONSE = { + analyticsUnavailable: false, + allTags: ['some-tag'], + recentQueries: MOCK_RECENT_QUERIES, + topQueries: MOCK_TOP_QUERIES, + topQueriesNoResults: MOCK_TOP_QUERIES, + topQueriesNoClicks: MOCK_TOP_QUERIES, + topQueriesWithClicks: MOCK_TOP_QUERIES, + totalClicks: 1000, + totalQueries: 5000, + totalQueriesNoResults: 500, + clicksPerDay: [0, 10, 50], + queriesPerDay: [10, 50, 100], + queriesNoResultsPerDay: [1, 2, 3], + }; + const MOCK_QUERY_RESPONSE = { + analyticsUnavailable: false, + allTags: ['some-tag'], + totalQueriesForQuery: 50, + queriesPerDayForQuery: [25, 0, 25], + topClicksForQuery: MOCK_TOP_CLICKS, + }; + + const { mount } = new LogicMounter(AnalyticsLogic); + + beforeEach(() => { + jest.clearAllMocks(); + KibanaLogic.values.history.location.search = ''; + }); + + it('has expected default values', () => { + mount(); + expect(AnalyticsLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('onAnalyticsUnavailable', () => { + it('should set state', () => { + mount(); + AnalyticsLogic.actions.onAnalyticsUnavailable(); + + expect(AnalyticsLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: false, + analyticsUnavailable: true, + }); + }); + }); + + describe('onAnalyticsDataLoad', () => { + it('should set state', () => { + mount(); + AnalyticsLogic.actions.onAnalyticsDataLoad(MOCK_ANALYTICS_RESPONSE); + + expect(AnalyticsLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: false, + analyticsUnavailable: false, + // TODO: more state will get set here in future PRs + }); + }); + }); + + describe('onQueryDataLoad', () => { + it('should set state', () => { + mount(); + AnalyticsLogic.actions.onQueryDataLoad(MOCK_QUERY_RESPONSE); + + expect(AnalyticsLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: false, + analyticsUnavailable: false, + // TODO: more state will get set here in future PRs + }); + }); + }); + }); + + describe('listeners', () => { + describe('loadAnalyticsData', () => { + it('should set state', () => { + mount({ dataLoading: false }); + + AnalyticsLogic.actions.loadAnalyticsData(); + + expect(AnalyticsLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: true, + }); + }); + + it('should make an API call and set state based on the response', async () => { + const promise = Promise.resolve(MOCK_ANALYTICS_RESPONSE); + (HttpLogic.values.http.get as jest.Mock).mockReturnValueOnce(promise); + mount(); + jest.spyOn(AnalyticsLogic.actions, 'onAnalyticsDataLoad'); + + AnalyticsLogic.actions.loadAnalyticsData(); + await promise; + + expect(HttpLogic.values.http.get).toHaveBeenCalledWith( + '/api/app_search/engines/test-engine/analytics/queries', + { + query: { size: 20 }, + } + ); + expect(AnalyticsLogic.actions.onAnalyticsDataLoad).toHaveBeenCalledWith( + MOCK_ANALYTICS_RESPONSE + ); + }); + + it('parses and passes the current search query string', async () => { + (HttpLogic.values.http.get as jest.Mock).mockReturnValueOnce({}); + KibanaLogic.values.history.location.search = + '?start=1970-01-01&end=1970-01-02&&tag=some_tag'; + mount(); + + AnalyticsLogic.actions.loadAnalyticsData(); + + expect(HttpLogic.values.http.get).toHaveBeenCalledWith( + '/api/app_search/engines/test-engine/analytics/queries', + { + query: { + start: '1970-01-01', + end: '1970-01-02', + tag: 'some_tag', + size: 20, + }, + } + ); + }); + + it('calls onAnalyticsUnavailable if analyticsUnavailable is in response', async () => { + const promise = Promise.resolve({ analyticsUnavailable: true }); + (HttpLogic.values.http.get as jest.Mock).mockReturnValueOnce(promise); + mount(); + jest.spyOn(AnalyticsLogic.actions, 'onAnalyticsUnavailable'); + + AnalyticsLogic.actions.loadAnalyticsData(); + await promise; + + expect(AnalyticsLogic.actions.onAnalyticsUnavailable).toHaveBeenCalled(); + }); + + it('handles errors', async () => { + const promise = Promise.reject('error'); + (HttpLogic.values.http.get as jest.Mock).mockReturnValueOnce(promise); + mount(); + jest.spyOn(AnalyticsLogic.actions, 'onAnalyticsUnavailable'); + + try { + AnalyticsLogic.actions.loadAnalyticsData(); + await promise; + } catch { + // Do nothing + } + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + expect(AnalyticsLogic.actions.onAnalyticsUnavailable).toHaveBeenCalled(); + }); + }); + + describe('loadQueryData', () => { + it('should set state', () => { + mount({ dataLoading: false }); + + AnalyticsLogic.actions.loadQueryData('some-query'); + + expect(AnalyticsLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: true, + }); + }); + + it('should make an API call and set state based on the response', async () => { + const promise = Promise.resolve(MOCK_QUERY_RESPONSE); + (HttpLogic.values.http.get as jest.Mock).mockReturnValueOnce(promise); + mount(); + jest.spyOn(AnalyticsLogic.actions, 'onQueryDataLoad'); + + AnalyticsLogic.actions.loadQueryData('some-query'); + await promise; + + expect(HttpLogic.values.http.get).toHaveBeenCalledWith( + '/api/app_search/engines/test-engine/analytics/queries/some-query', + expect.any(Object) // empty query obj + ); + expect(AnalyticsLogic.actions.onQueryDataLoad).toHaveBeenCalledWith(MOCK_QUERY_RESPONSE); + }); + + it('parses and passes the current search query string', async () => { + (HttpLogic.values.http.get as jest.Mock).mockReturnValueOnce({}); + KibanaLogic.values.history.location.search = + '?start=1970-12-30&end=1970-12-31&&tag=another_tag'; + mount(); + + AnalyticsLogic.actions.loadQueryData('some-query'); + + expect(HttpLogic.values.http.get).toHaveBeenCalledWith( + '/api/app_search/engines/test-engine/analytics/queries/some-query', + { + query: { + start: '1970-12-30', + end: '1970-12-31', + tag: 'another_tag', + }, + } + ); + }); + + it('calls onAnalyticsUnavailable if analyticsUnavailable is in response', async () => { + const promise = Promise.resolve({ analyticsUnavailable: true }); + (HttpLogic.values.http.get as jest.Mock).mockReturnValueOnce(promise); + mount(); + jest.spyOn(AnalyticsLogic.actions, 'onAnalyticsUnavailable'); + + AnalyticsLogic.actions.loadQueryData('some-query'); + await promise; + + expect(AnalyticsLogic.actions.onAnalyticsUnavailable).toHaveBeenCalled(); + }); + + it('handles errors', async () => { + const promise = Promise.reject('error'); + (HttpLogic.values.http.get as jest.Mock).mockReturnValueOnce(promise); + mount(); + jest.spyOn(AnalyticsLogic.actions, 'onAnalyticsUnavailable'); + + try { + AnalyticsLogic.actions.loadQueryData('some-query'); + await promise; + } catch { + // Do nothing + } + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + expect(AnalyticsLogic.actions.onAnalyticsUnavailable).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.ts new file mode 100644 index 0000000000000..1e26acfc39670 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { kea, MakeLogicType } from 'kea'; +import queryString from 'query-string'; + +import { KibanaLogic } from '../../../shared/kibana'; +import { HttpLogic } from '../../../shared/http'; +import { flashAPIErrors } from '../../../shared/flash_messages'; +import { EngineLogic } from '../engine'; + +import { AnalyticsData, QueryDetails } from './types'; + +interface AnalyticsValues extends AnalyticsData, QueryDetails { + dataLoading: boolean; +} + +interface AnalyticsActions { + onAnalyticsUnavailable(): void; + onAnalyticsDataLoad(data: AnalyticsData): AnalyticsData; + onQueryDataLoad(data: QueryDetails): QueryDetails; + loadAnalyticsData(): void; + loadQueryData(query: string): string; +} + +export const AnalyticsLogic = kea>({ + path: ['enterprise_search', 'app_search', 'analytics_logic'], + actions: () => ({ + onAnalyticsUnavailable: true, + onAnalyticsDataLoad: (data) => data, + onQueryDataLoad: (data) => data, + loadAnalyticsData: true, + loadQueryData: (query) => query, + }), + reducers: () => ({ + dataLoading: [ + true, + { + loadAnalyticsData: () => true, + loadQueryData: () => true, + onAnalyticsUnavailable: () => false, + onAnalyticsDataLoad: () => false, + onQueryDataLoad: () => false, + }, + ], + analyticsUnavailable: [ + false, + { + onAnalyticsUnavailable: () => true, + onAnalyticsDataLoad: () => false, + onQueryDataLoad: () => false, + }, + ], + }), + listeners: ({ actions }) => ({ + loadAnalyticsData: async () => { + const { history } = KibanaLogic.values; + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + try { + const { start, end, tag } = queryString.parse(history.location.search); + const query = { start, end, tag, size: 20 }; + const url = `/api/app_search/engines/${engineName}/analytics/queries`; + + const response = await http.get(url, { query }); + + if (response.analyticsUnavailable) { + actions.onAnalyticsUnavailable(); + } else { + actions.onAnalyticsDataLoad(response); + } + } catch (e) { + flashAPIErrors(e); + actions.onAnalyticsUnavailable(); + } + }, + loadQueryData: async (query) => { + const { history } = KibanaLogic.values; + const { http } = HttpLogic.values; + const { engineName } = EngineLogic.values; + + try { + const { start, end, tag } = queryString.parse(history.location.search); + const queryParams = { start, end, tag }; + const url = `/api/app_search/engines/${engineName}/analytics/queries/${query}`; + + const response = await http.get(url, { query: queryParams }); + + if (response.analyticsUnavailable) { + actions.onAnalyticsUnavailable(); + } else { + actions.onQueryDataLoad(response); + } + } catch (e) { + flashAPIErrors(e); + actions.onAnalyticsUnavailable(); + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.test.tsx new file mode 100644 index 0000000000000..6b04a668b1489 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.test.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { Route, Switch } from 'react-router-dom'; + +import { AnalyticsRouter } from './'; + +describe('AnalyticsRouter', () => { + // Detailed route testing is better done via E2E tests + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(Switch)).toHaveLength(1); + expect(wrapper.find(Route)).toHaveLength(8); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx new file mode 100644 index 0000000000000..117d14f7ca836 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import { APP_SEARCH_PLUGIN } from '../../../../../common/constants'; +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { NotFound } from '../../../shared/not_found'; +import { + ENGINE_PATH, + ENGINE_ANALYTICS_PATH, + ENGINE_ANALYTICS_TOP_QUERIES_PATH, + ENGINE_ANALYTICS_TOP_QUERIES_NO_RESULTS_PATH, + ENGINE_ANALYTICS_TOP_QUERIES_NO_CLICKS_PATH, + ENGINE_ANALYTICS_TOP_QUERIES_WITH_CLICKS_PATH, + ENGINE_ANALYTICS_RECENT_QUERIES_PATH, + ENGINE_ANALYTICS_QUERY_DETAIL_PATH, +} from '../../routes'; +import { + ANALYTICS_TITLE, + TOP_QUERIES, + TOP_QUERIES_NO_RESULTS, + TOP_QUERIES_NO_CLICKS, + TOP_QUERIES_WITH_CLICKS, + RECENT_QUERIES, +} from './constants'; + +interface Props { + engineBreadcrumb: string[]; +} +export const AnalyticsRouter: React.FC = ({ engineBreadcrumb }) => { + const ANALYTICS_BREADCRUMB = [...engineBreadcrumb, ANALYTICS_TITLE]; + + return ( + + + + TODO: Analytics overview + + + + TODO: Top queries + + + + TODO: Top queries with no results + + + + TODO: Top queries with no clicks + + + + TODO: Top queries with clicks + + + + TODO: Recent queries + + + TODO: Query detail page + + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts index 9985753d09700..c1087bf4cb2f0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts @@ -11,26 +11,46 @@ export const ANALYTICS_TITLE = i18n.translate( { defaultMessage: 'Analytics' } ); +// Total card titles export const TOTAL_DOCUMENTS = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.analytics.totalDocuments', { defaultMessage: 'Total documents' } ); - export const TOTAL_API_OPERATIONS = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.analytics.totalApiOperations', { defaultMessage: 'Total API operations' } ); - export const TOTAL_QUERIES = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.analytics.totalQueries', { defaultMessage: 'Total queries' } ); - export const TOTAL_CLICKS = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.analytics.totalClicks', { defaultMessage: 'Total clicks' } ); +// Queries sub-pages +export const TOP_QUERIES = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.topQueriesTitle', + { defaultMessage: 'Top queries' } +); +export const TOP_QUERIES_NO_RESULTS = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.topQueriesNoResultsTitle', + { defaultMessage: 'Top queries with no results' } +); +export const TOP_QUERIES_NO_CLICKS = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.topQueriesNoClicksTitle', + { defaultMessage: 'Top queries with no clicks' } +); +export const TOP_QUERIES_WITH_CLICKS = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.topQueriesWithClicksTitle', + { defaultMessage: 'Top queries with clicks' } +); +export const RECENT_QUERIES = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engine.analytics.recentQueriesTitle', + { defaultMessage: 'Recent queries' } +); + // Moment date format conversions export const SERVER_DATE_FORMAT = 'YYYY-MM-DD'; export const TOOLTIP_DATE_FORMAT = 'MMMM D, YYYY'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/index.ts index 3b201b38703d9..4dbc2d930ddf0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/index.ts @@ -5,5 +5,7 @@ */ export { ANALYTICS_TITLE } from './constants'; +export { AnalyticsLogic } from './analytics_logic'; +export { AnalyticsRouter } from './analytics_router'; export { AnalyticsChart } from './components'; export { convertToChartData } from './utils'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/types.ts new file mode 100644 index 0000000000000..5cc14038507b9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/types.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +interface Query { + doc_count: number; + key: string; + clicks?: { doc_count: number }; + searches?: { doc_count: number }; + tags?: string[]; +} + +interface QueryClick extends Query { + document?: { + id: string; + engine: string; + tags?: string[]; + }; +} + +interface RecentQuery { + document_ids: string[]; + query_string: string; + tags: string[]; + timestamp: string; +} + +/** + * API response data + */ + +interface BaseData { + analyticsUnavailable: boolean; + allTags: string[]; + // NOTE: The API sends us back even more data than this (e.g., + // startDate, endDate, currentTag, logRetentionSettings, query), + // but we currently don't need that data in our front-end code, + // so I'm opting not to list them in our types +} + +export interface AnalyticsData extends BaseData { + recentQueries: RecentQuery[]; + topQueries: Query[]; + topQueriesWithClicks: Query[]; + topQueriesNoClicks: Query[]; + topQueriesNoResults: Query[]; + totalClicks: number; + totalQueries: number; + totalQueriesNoResults: number; + clicksPerDay: number[]; + queriesPerDay: number[]; + queriesNoResultsPerDay: number[]; +} + +export interface QueryDetails extends BaseData { + totalQueriesForQuery: number; + queriesPerDayForQuery: number[]; + topClicksForQuery: QueryClick[]; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx index 418ab33457d0a..40ae2cef0acb8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx @@ -103,7 +103,11 @@ export const EngineNav: React.FC = () => { {OVERVIEW_TITLE} {canViewEngineAnalytics && ( - + {ANALYTICS_TITLE} )} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx index cbaa347d65732..26c7b3f677fc1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx @@ -19,6 +19,7 @@ import { setQueuedErrorMessage } from '../../../shared/flash_messages'; import { Loading } from '../../../shared/loading'; import { EngineOverview } from '../engine_overview'; +import { AnalyticsRouter } from '../analytics'; import { EngineRouter } from './'; @@ -93,6 +94,6 @@ describe('EngineRouter', () => { setMockValues({ ...values, myRole: { canViewEngineAnalytics: true } }); const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="AnalyticsTODO"]')).toHaveLength(1); + expect(wrapper.find(AnalyticsRouter)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx index 1d2f3f640f341..47fe302ac7014 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx @@ -33,13 +33,13 @@ import { } from '../../routes'; import { ENGINES_TITLE } from '../engines'; import { OVERVIEW_TITLE } from '../engine_overview'; -import { ANALYTICS_TITLE } from '../analytics'; import { Loading } from '../../../shared/loading'; import { EngineOverview } from '../engine_overview'; +import { AnalyticsRouter } from '../analytics'; +import { DocumentDetail, Documents } from '../documents'; import { EngineLogic } from './'; -import { DocumentDetail, Documents } from '../documents'; export const EngineRouter: React.FC = () => { const { @@ -87,8 +87,7 @@ export const EngineRouter: React.FC = () => { {canViewEngineAnalytics && ( - -
    Just testing right now
    +
    )} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/assets/icons.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/assets/icons.test.tsx new file mode 100644 index 0000000000000..027dea0aec77b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/assets/icons.test.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { EngineIcon } from './engine_icon'; +import { MetaEngineIcon } from './meta_engine_icon'; + +describe('Engines icons', () => { + it('renders an engine icon', () => { + const wrapper = shallow(); + expect(wrapper.hasClass('engineIcon')).toBe(true); + }); + + it('renders a meta engine icon', () => { + const wrapper = shallow(); + expect(wrapper.hasClass('engineIcon')).toBe(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.test.ts new file mode 100644 index 0000000000000..157ae396319ac --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.test.ts @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LogicMounter } from '../../../__mocks__/kea.mock'; + +jest.mock('../../../shared/http', () => ({ + HttpLogic: { values: { http: { get: jest.fn() } } }, +})); +import { HttpLogic } from '../../../shared/http'; + +import { EngineDetails } from '../engine/types'; +import { EnginesLogic } from './'; + +describe('EnginesLogic', () => { + const DEFAULT_VALUES = { + dataLoading: true, + engines: [], + enginesTotal: 0, + enginesPage: 1, + metaEngines: [], + metaEnginesTotal: 0, + metaEnginesPage: 1, + }; + + const MOCK_ENGINE = { + name: 'hello-world', + created_at: 'Fri, 1 Jan 1970 12:00:00 +0000', + document_count: 50, + field_count: 10, + } as EngineDetails; + const MOCK_ENGINES_API_RESPONSE = { + results: [MOCK_ENGINE], + meta: { + page: { + current: 1, + total_pages: 10, + total_results: 100, + size: 10, + }, + }, + }; + + const { mount } = new LogicMounter(EnginesLogic); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + expect(EnginesLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('onEnginesLoad', () => { + describe('dataLoading', () => { + it('should be set to false', () => { + mount(); + EnginesLogic.actions.onEnginesLoad({ engines: [], total: 0 }); + + expect(EnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: false, + }); + }); + }); + + describe('engines & enginesTotal', () => { + it('should be set to the provided value', () => { + mount(); + EnginesLogic.actions.onEnginesLoad({ engines: [MOCK_ENGINE], total: 100 }); + + expect(EnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + dataLoading: false, + engines: [MOCK_ENGINE], + enginesTotal: 100, + }); + }); + }); + }); + + describe('onMetaEnginesLoad', () => { + describe('engines & enginesTotal', () => { + it('should be set to the provided value', () => { + mount(); + EnginesLogic.actions.onMetaEnginesLoad({ engines: [MOCK_ENGINE], total: 1 }); + + expect(EnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + metaEngines: [MOCK_ENGINE], + metaEnginesTotal: 1, + }); + }); + }); + }); + + describe('onEnginesPagination', () => { + describe('enginesPage', () => { + it('should be set to the provided value', () => { + mount(); + EnginesLogic.actions.onEnginesPagination(2); + + expect(EnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + enginesPage: 2, + }); + }); + }); + }); + + describe('onMetaEnginesPagination', () => { + describe('metaEnginesPage', () => { + it('should be set to the provided value', () => { + mount(); + EnginesLogic.actions.onMetaEnginesPagination(99); + + expect(EnginesLogic.values).toEqual({ + ...DEFAULT_VALUES, + metaEnginesPage: 99, + }); + }); + }); + }); + + describe('loadEngines', () => { + it('should call the engines API endpoint and set state based on the results', async () => { + const promise = Promise.resolve(MOCK_ENGINES_API_RESPONSE); + (HttpLogic.values.http.get as jest.Mock).mockReturnValueOnce(promise); + mount({ enginesPage: 10 }); + jest.spyOn(EnginesLogic.actions, 'onEnginesLoad'); + + EnginesLogic.actions.loadEngines(); + await promise; + + expect(HttpLogic.values.http.get).toHaveBeenCalledWith('/api/app_search/engines', { + query: { type: 'indexed', pageIndex: 10 }, + }); + expect(EnginesLogic.actions.onEnginesLoad).toHaveBeenCalledWith({ + engines: [MOCK_ENGINE], + total: 100, + }); + }); + }); + + describe('loadMetaEngines', () => { + it('should call the engines API endpoint and set state based on the results', async () => { + const promise = Promise.resolve(MOCK_ENGINES_API_RESPONSE); + (HttpLogic.values.http.get as jest.Mock).mockReturnValueOnce(promise); + mount({ metaEnginesPage: 99 }); + jest.spyOn(EnginesLogic.actions, 'onMetaEnginesLoad'); + + EnginesLogic.actions.loadMetaEngines(); + await promise; + + expect(HttpLogic.values.http.get).toHaveBeenCalledWith('/api/app_search/engines', { + query: { type: 'meta', pageIndex: 99 }, + }); + expect(EnginesLogic.actions.onMetaEnginesLoad).toHaveBeenCalledWith({ + engines: [MOCK_ENGINE], + total: 100, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.ts new file mode 100644 index 0000000000000..097b759cf1505 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { HttpLogic } from '../../../shared/http'; + +import { EngineDetails } from '../engine/types'; + +interface EnginesValues { + dataLoading: boolean; + engines: EngineDetails[]; + enginesTotal: number; + enginesPage: number; + metaEngines: EngineDetails[]; + metaEnginesTotal: number; + metaEnginesPage: number; +} + +interface OnEnginesLoad { + engines: EngineDetails[]; + total: number; +} +interface EnginesActions { + onEnginesLoad({ engines, total }: OnEnginesLoad): OnEnginesLoad; + onMetaEnginesLoad({ engines, total }: OnEnginesLoad): OnEnginesLoad; + onEnginesPagination(page: number): { page: number }; + onMetaEnginesPagination(page: number): { page: number }; + loadEngines(): void; + loadMetaEngines(): void; +} + +export const EnginesLogic = kea>({ + path: ['enterprise_search', 'app_search', 'engines_logic'], + actions: { + onEnginesLoad: ({ engines, total }) => ({ engines, total }), + onMetaEnginesLoad: ({ engines, total }) => ({ engines, total }), + onEnginesPagination: (page) => ({ page }), + onMetaEnginesPagination: (page) => ({ page }), + loadEngines: true, + loadMetaEngines: true, + }, + reducers: { + dataLoading: [ + true, + { + onEnginesLoad: () => false, + }, + ], + engines: [ + [], + { + onEnginesLoad: (_, { engines }) => engines, + }, + ], + enginesTotal: [ + 0, + { + onEnginesLoad: (_, { total }) => total, + }, + ], + enginesPage: [ + 1, + { + onEnginesPagination: (_, { page }) => page, + }, + ], + metaEngines: [ + [], + { + onMetaEnginesLoad: (_, { engines }) => engines, + }, + ], + metaEnginesTotal: [ + 0, + { + onMetaEnginesLoad: (_, { total }) => total, + }, + ], + metaEnginesPage: [ + 1, + { + onMetaEnginesPagination: (_, { page }) => page, + }, + ], + }, + listeners: ({ actions, values }) => ({ + loadEngines: async () => { + const { http } = HttpLogic.values; + const { enginesPage } = values; + + const response = await http.get('/api/app_search/engines', { + query: { type: 'indexed', pageIndex: enginesPage }, + }); + actions.onEnginesLoad({ + engines: response.results, + total: response.meta.page.total_results, + }); + }, + loadMetaEngines: async () => { + const { http } = HttpLogic.values; + const { metaEnginesPage } = values; + + const response = await http.get('/api/app_search/engines', { + query: { type: 'meta', pageIndex: metaEnginesPage }, + }); + actions.onMetaEnginesLoad({ + engines: response.results, + total: response.meta.page.total_results, + }); + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx index 61f783a8b6c2e..2cedec3c670b5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx @@ -4,14 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../../__mocks__/kea.mock'; -import '../../../__mocks__/react_router_history.mock'; +import '../../../__mocks__/shallow_useeffect.mock'; +import { rerender } from '../../../__mocks__'; +import { setMockValues, setMockActions } from '../../../__mocks__/kea.mock'; import React from 'react'; -import { act } from 'react-dom/test-utils'; -import { shallow, ReactWrapper } from 'enzyme'; - -import { mountAsync, mockHttpValues, setMockValues } from '../../../__mocks__'; +import { shallow, ShallowWrapper } from 'enzyme'; import { LoadingState, EmptyState } from './components'; import { EnginesTable } from './engines_table'; @@ -19,91 +17,85 @@ import { EnginesTable } from './engines_table'; import { EnginesOverview } from './'; describe('EnginesOverview', () => { + const values = { + hasPlatinumLicense: false, + dataLoading: false, + engines: [], + enginesTotal: 0, + enginesPage: 1, + metaEngines: [], + metaEnginesTotal: 0, + metaEnginesPage: 1, + }; + const actions = { + loadEngines: jest.fn(), + loadMetaEngines: jest.fn(), + onEnginesPagination: jest.fn(), + onMetaEnginesPagination: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + describe('non-happy-path states', () => { it('isLoading', () => { + setMockValues({ ...values, dataLoading: true }); const wrapper = shallow(); expect(wrapper.find(LoadingState)).toHaveLength(1); }); - it('isEmpty', async () => { - setMockValues({ - http: { - ...mockHttpValues.http, - get: () => ({ - results: [], - meta: { page: { total_results: 0 } }, - }), - }, - }); - const wrapper = await mountAsync(, { i18n: true }); + it('isEmpty', () => { + setMockValues({ ...values, engines: [] }); + const wrapper = shallow(); expect(wrapper.find(EmptyState)).toHaveLength(1); }); }); describe('happy-path states', () => { - const mockedApiResponse = { - results: [ - { - name: 'hello-world', - created_at: 'Fri, 1 Jan 1970 12:00:00 +0000', - document_count: 50, - field_count: 10, - }, - ], - meta: { - page: { - current: 1, - total_pages: 10, - total_results: 100, - size: 10, - }, - }, + const valuesWithEngines = { + ...values, + dataLoading: false, + engines: ['dummy-engine'], + enginesTotal: 100, + enginesPage: 1, }; - const mockApi = jest.fn(() => mockedApiResponse); beforeEach(() => { - jest.clearAllMocks(); - setMockValues({ http: { ...mockHttpValues.http, get: mockApi } }); + setMockValues(valuesWithEngines); }); it('renders and calls the engines API', async () => { - const wrapper = await mountAsync(, { i18n: true }); + const wrapper = shallow(); expect(wrapper.find(EnginesTable)).toHaveLength(1); - expect(mockApi).toHaveBeenNthCalledWith(1, '/api/app_search/engines', { - query: { - type: 'indexed', - pageIndex: 1, - }, - }); + expect(actions.loadEngines).toHaveBeenCalled(); }); describe('when on a platinum license', () => { it('renders a 2nd meta engines table & makes a 2nd meta engines API call', async () => { setMockValues({ + ...valuesWithEngines, hasPlatinumLicense: true, - http: { ...mockHttpValues.http, get: mockApi }, + metaEngines: ['dummy-meta-engine'], }); - const wrapper = await mountAsync(, { i18n: true }); + const wrapper = shallow(); expect(wrapper.find(EnginesTable)).toHaveLength(2); - expect(mockApi).toHaveBeenNthCalledWith(2, '/api/app_search/engines', { - query: { - type: 'meta', - pageIndex: 1, - }, - }); + expect(actions.loadMetaEngines).toHaveBeenCalled(); }); }); describe('pagination', () => { - const getTablePagination = (wrapper: ReactWrapper) => + const getTablePagination = (wrapper: ShallowWrapper) => wrapper.find(EnginesTable).prop('pagination'); it('passes down page data from the API', async () => { - const wrapper = await mountAsync(, { i18n: true }); + const wrapper = shallow(); const pagination = getTablePagination(wrapper); expect(pagination.totalEngines).toEqual(100); @@ -111,17 +103,13 @@ describe('EnginesOverview', () => { }); it('re-polls the API on page change', async () => { - const wrapper = await mountAsync(, { i18n: true }); - await act(async () => getTablePagination(wrapper).onPaginate(5)); - wrapper.update(); - - expect(mockApi).toHaveBeenLastCalledWith('/api/app_search/engines', { - query: { - type: 'indexed', - pageIndex: 5, - }, - }); - expect(getTablePagination(wrapper).pageIndex).toEqual(4); + const wrapper = shallow(); + + setMockValues({ ...valuesWithEngines, enginesPage: 51 }); + rerender(wrapper); + + expect(actions.loadEngines).toHaveBeenCalledTimes(2); + expect(getTablePagination(wrapper).pageIndex).toEqual(50); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx index 0381c3806fec7..8a24ee746ed14 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useState } from 'react'; -import { useValues } from 'kea'; +import React, { useEffect } from 'react'; +import { useValues, useActions } from 'kea'; import { EuiPageContent, EuiPageContentHeader, @@ -17,7 +17,6 @@ import { import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { FlashMessages } from '../../../shared/flash_messages'; -import { HttpLogic } from '../../../shared/http'; import { LicensingLogic } from '../../../shared/licensing'; import { EngineIcon } from './assets/engine_icon'; @@ -25,61 +24,34 @@ import { MetaEngineIcon } from './assets/meta_engine_icon'; import { ENGINES_TITLE, META_ENGINES_TITLE } from './constants'; import { EnginesOverviewHeader, LoadingState, EmptyState } from './components'; import { EnginesTable } from './engines_table'; +import { EnginesLogic } from './engines_logic'; import './engines_overview.scss'; -interface GetEnginesParams { - type: string; - pageIndex: number; -} -interface SetEnginesCallbacks { - setResults: React.Dispatch>; - setResultsTotal: React.Dispatch>; -} - export const EnginesOverview: React.FC = () => { - const { http } = useValues(HttpLogic); const { hasPlatinumLicense } = useValues(LicensingLogic); - - const [isLoading, setIsLoading] = useState(true); - const [engines, setEngines] = useState([]); - const [enginesPage, setEnginesPage] = useState(1); - const [enginesTotal, setEnginesTotal] = useState(0); - const [metaEngines, setMetaEngines] = useState([]); - const [metaEnginesPage, setMetaEnginesPage] = useState(1); - const [metaEnginesTotal, setMetaEnginesTotal] = useState(0); - - const getEnginesData = async ({ type, pageIndex }: GetEnginesParams) => { - return await http.get('/api/app_search/engines', { - query: { type, pageIndex }, - }); - }; - const setEnginesData = async (params: GetEnginesParams, callbacks: SetEnginesCallbacks) => { - const response = await getEnginesData(params); - - callbacks.setResults(response.results); - callbacks.setResultsTotal(response.meta.page.total_results); - - setIsLoading(false); - }; + const { + dataLoading, + engines, + enginesTotal, + enginesPage, + metaEngines, + metaEnginesTotal, + metaEnginesPage, + } = useValues(EnginesLogic); + const { loadEngines, loadMetaEngines, onEnginesPagination, onMetaEnginesPagination } = useActions( + EnginesLogic + ); useEffect(() => { - const params = { type: 'indexed', pageIndex: enginesPage }; - const callbacks = { setResults: setEngines, setResultsTotal: setEnginesTotal }; - - setEnginesData(params, callbacks); + loadEngines(); }, [enginesPage]); useEffect(() => { - if (hasPlatinumLicense) { - const params = { type: 'meta', pageIndex: metaEnginesPage }; - const callbacks = { setResults: setMetaEngines, setResultsTotal: setMetaEnginesTotal }; - - setEnginesData(params, callbacks); - } + if (hasPlatinumLicense) loadMetaEngines(); }, [hasPlatinumLicense, metaEnginesPage]); - if (isLoading) return ; + if (dataLoading) return ; if (!engines.length) return ; return ( @@ -103,7 +75,7 @@ export const EnginesOverview: React.FC = () => { pagination={{ totalEngines: enginesTotal, pageIndex: enginesPage - 1, - onPaginate: setEnginesPage, + onPaginate: onEnginesPagination, }} /> @@ -124,7 +96,7 @@ export const EnginesOverview: React.FC = () => { pagination={{ totalEngines: metaEnginesTotal, pageIndex: metaEnginesPage - 1, - onPaginate: setMetaEnginesPage, + onPaginate: onMetaEnginesPagination, }} /> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.test.tsx index ea7eeea750cc4..1dde4db15a425 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.test.tsx @@ -12,6 +12,7 @@ import React from 'react'; import { EuiBasicTable, EuiPagination, EuiButtonEmpty } from '@elastic/eui'; import { EuiLinkTo } from '../../../shared/react_router_helpers'; +import { EngineDetails } from '../engine/types'; import { EnginesTable } from './engines_table'; describe('EnginesTable', () => { @@ -25,7 +26,7 @@ describe('EnginesTable', () => { isMeta: false, document_count: 99999, field_count: 10, - }, + } as EngineDetails, ]; const pagination = { totalEngines: 50, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx index e9805ab8f2711..e8944c37efa47 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx @@ -16,22 +16,15 @@ import { getEngineRoute } from '../../routes'; import { ENGINES_PAGE_SIZE } from '../../../../../common/constants'; import { UNIVERSAL_LANGUAGE } from '../../constants'; +import { EngineDetails } from '../engine/types'; -interface EnginesTableData { - name: string; - created_at: string; - document_count: number; - field_count: number; - language: string | null; - isMeta: boolean; -} interface EnginesTablePagination { totalEngines: number; pageIndex: number; onPaginate(pageIndex: number): void; } interface EnginesTableProps { - data: EnginesTableData[]; + data: EngineDetails[]; pagination: EnginesTablePagination; } interface OnChange { @@ -55,7 +48,7 @@ export const EnginesTable: React.FC = ({ }), }); - const columns: Array> = [ + const columns: Array> = [ { field: 'name', name: i18n.translate('xpack.enterpriseSearch.appSearch.enginesOverview.table.column.name', { @@ -100,7 +93,7 @@ export const EnginesTable: React.FC = ({ } ), dataType: 'string', - render: (language: string, engine: EnginesTableData) => + render: (language: string, engine: EngineDetails) => engine.isMeta ? '' : language || UNIVERSAL_LANGUAGE, }, { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/index.ts index 76ca9239a3c7e..e26a813553b83 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/index.ts @@ -4,5 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ +export { EnginesLogic } from './engines_logic'; export { EnginesOverview } from './engines_overview'; export { ENGINES_TITLE, META_ENGINES_TITLE } from './constants'; diff --git a/x-pack/plugins/data_enhanced/server/search/session/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/index.ts similarity index 51% rename from x-pack/plugins/data_enhanced/server/search/session/constants.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/index.ts index 4ac32938c4843..1dba9c94ed724 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/index.ts @@ -4,8 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export const INMEM_MAX_SESSIONS = 10000; -export const DEFAULT_EXPIRATION = 7 * 24 * 60 * 60 * 1000; -export const INMEM_TRACKING_INTERVAL = 10 * 1000; -export const INMEM_TRACKING_TIMEOUT_SEC = 60; -export const MAX_UPDATE_RETRIES = 3; +export { LogRetentionCallout } from './log_retention_callout'; +export { LogRetentionTooltip } from './log_retention_tooltip'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.test.tsx new file mode 100644 index 0000000000000..d990648682b78 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.test.tsx @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/shallow_useeffect.mock'; +import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; +import { mountWithIntl } from '../../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; +import { EuiCallOut, EuiLink } from '@elastic/eui'; + +import { LogRetentionOptions } from '../'; +import { LogRetentionCallout } from './'; + +describe('LogRetentionCallout', () => { + const actions = { fetchLogRetention: jest.fn() }; + const values = { myRole: { canManageLogSettings: true } }; + const DISABLED = { + disabledAt: '01 Jan 1970 12:00:00 +0000', + enabled: false, + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockActions(actions); + }); + + it('renders an analytics callout', () => { + setMockValues({ ...values, logRetention: { analytics: DISABLED } }); + const wrapper = mountWithIntl(); + + expect(wrapper.find(EuiCallOut)).toHaveLength(1); + expect(wrapper.find('.euiCallOutHeader__title').text()).toEqual( + 'Analytics have been disabled since January 1, 1970.' + ); + expect(wrapper.find(EuiLink)).toHaveLength(1); + expect(wrapper.find('p').text()).toEqual('To manage analytics & logging, visit your settings.'); + }); + + it('renders an API callout', () => { + setMockValues({ ...values, logRetention: { api: DISABLED } }); + const wrapper = mountWithIntl(); + + expect(wrapper.find(EuiCallOut)).toHaveLength(1); + expect(wrapper.find('.euiCallOutHeader__title').text()).toEqual( + 'API Logs have been disabled since January 1, 1970.' + ); + expect(wrapper.find(EuiLink)).toHaveLength(1); + expect(wrapper.find('p').text()).toEqual('To manage analytics & logging, visit your settings.'); + }); + + it('renders a generic title if no disabled date is present', () => { + setMockValues({ ...values, logRetention: { api: { enabled: false, disabledAt: null } } }); + const wrapper = mountWithIntl(); + + expect(wrapper.find(EuiCallOut)).toHaveLength(1); + expect(wrapper.find('.euiCallOutHeader__title').text()).toEqual('API Logs have been disabled.'); + }); + + it('does not render a settings link if the user cannot manage settings', () => { + setMockValues({ myRole: { canManageLogSettings: false }, logRetention: { api: DISABLED } }); + const wrapper = mountWithIntl(); + + expect(wrapper.find(EuiCallOut)).toHaveLength(1); + expect(wrapper.find(EuiLink)).toHaveLength(0); + expect(wrapper.find('p')).toHaveLength(0); + }); + + it('does not render if log retention is enabled', () => { + setMockValues({ ...values, logRetention: { api: { enabled: true } } }); + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('does not render if log retention is not available', () => { + setMockValues({ ...values, logRetention: null }); + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + describe('on mount', () => { + it('fetches log retention data when not already loaded', () => { + setMockValues({ ...values, logRetention: null }); + shallow(); + + expect(actions.fetchLogRetention).toHaveBeenCalled(); + }); + + it('does not fetch log retention data if it has already been loaded', () => { + setMockValues({ ...values, logRetention: {} }); + shallow(); + + expect(actions.fetchLogRetention).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.tsx new file mode 100644 index 0000000000000..f97b01602664f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.tsx @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect } from 'react'; +import { useValues, useActions } from 'kea'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; + +import { EuiLinkTo } from '../../../../shared/react_router_helpers'; + +import { AppLogic } from '../../../app_logic'; +import { SETTINGS_PATH } from '../../../routes'; +import { ANALYTICS_TITLE } from '../../analytics'; +import { API_LOGS_TITLE } from '../../api_logs'; + +import { LogRetentionLogic, LogRetentionOptions, renderLogRetentionDate } from '../'; + +const TITLE_MAP = { + [LogRetentionOptions.Analytics]: ANALYTICS_TITLE, + [LogRetentionOptions.API]: API_LOGS_TITLE, +}; + +interface Props { + type: LogRetentionOptions; +} +export const LogRetentionCallout: React.FC = ({ type }) => { + const { fetchLogRetention } = useActions(LogRetentionLogic); + const { logRetention } = useValues(LogRetentionLogic); + const { + myRole: { canManageLogSettings }, + } = useValues(AppLogic); + + const hasLogRetention = logRetention !== null; + + useEffect(() => { + if (!hasLogRetention) fetchLogRetention(); + }, []); + + const logRetentionSettings = logRetention?.[type]; + const title = TITLE_MAP[type]; + const hasLogRetentionDisabled = hasLogRetention && !logRetentionSettings?.enabled; + + return hasLogRetentionDisabled ? ( + <> + + ) : ( + i18n.translate('xpack.enterpriseSearch.appSearch.logRetention.callout.disabledTitle', { + defaultMessage: '{logsTitle} have been disabled.', + values: { + logsTitle: title, + }, + }) + ) + } + > + {canManageLogSettings && ( +

    + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.logRetention.callout.description.manageSettingsLinkText', + { defaultMessage: 'visit your settings' } + )} + + ), + }} + /> +

    + )} +
    + + + ) : null; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.test.tsx new file mode 100644 index 0000000000000..9b0143f6f6c6d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.test.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import '../../../../__mocks__/shallow_useeffect.mock'; +import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow, mount } from 'enzyme'; +import { EuiIconTip } from '@elastic/eui'; + +import { LogRetentionOptions, LogRetentionMessage } from '../'; +import { LogRetentionTooltip } from './'; + +describe('LogRetentionTooltip', () => { + const values = { logRetention: {} }; + const actions = { fetchLogRetention: jest.fn() }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders an analytics tooltip', () => { + const wrapper = shallow(); + const tooltipContent = wrapper.find(EuiIconTip).prop('content') as React.ReactElement; + + expect(tooltipContent.type).toEqual(LogRetentionMessage); + expect(tooltipContent.props.type).toEqual('analytics'); + }); + + it('renders an API tooltip', () => { + const wrapper = shallow(); + const tooltipContent = wrapper.find(EuiIconTip).prop('content') as React.ReactElement; + + expect(tooltipContent.type).toEqual(LogRetentionMessage); + expect(tooltipContent.props.type).toEqual('api'); + }); + + it('passes custom tooltip positions', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiIconTip).prop('position')).toEqual('bottom'); + + wrapper.setProps({ position: 'right' }); + expect(wrapper.find(EuiIconTip).prop('position')).toEqual('right'); + }); + + it('does not render if log retention is not available', () => { + setMockValues({ logRetention: null }); + const wrapper = mount(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + describe('on mount', () => { + it('fetches log retention data when not already loaded', () => { + setMockValues({ logRetention: null }); + shallow(); + + expect(actions.fetchLogRetention).toHaveBeenCalled(); + }); + + it('does not fetch log retention data if it has already been loaded', () => { + setMockValues({ logRetention: {} }); + shallow(); + + expect(actions.fetchLogRetention).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.tsx new file mode 100644 index 0000000000000..9c08043b5313f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect } from 'react'; +import { useValues, useActions } from 'kea'; + +import { i18n } from '@kbn/i18n'; +import { EuiIconTip } from '@elastic/eui'; + +import { LogRetentionLogic, LogRetentionMessage, LogRetentionOptions } from '../'; + +interface Props { + type: LogRetentionOptions; + position?: 'top' | 'right' | 'bottom' | 'left'; +} +export const LogRetentionTooltip: React.FC = ({ type, position = 'bottom' }) => { + const { fetchLogRetention } = useActions(LogRetentionLogic); + const { logRetention } = useValues(LogRetentionLogic); + + const hasLogRetention = logRetention !== null; + + useEffect(() => { + if (!hasLogRetention) fetchLogRetention(); + }, []); + + return hasLogRetention ? ( + } + /> + ) : null; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/index.ts new file mode 100644 index 0000000000000..23db86f285d01 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/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; + * you may not use this file except in compliance with the Elastic License. + */ + +export { LogRetentionLogic } from './log_retention_logic'; +export * from './types'; +export * from './messaging'; +export * from './components'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts similarity index 92% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_logic.test.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts index 8310e2abe045b..004e5e9752d97 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts @@ -4,18 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LogicMounter } from '../../../../__mocks__/kea.mock'; +import { LogicMounter } from '../../../__mocks__/kea.mock'; -import { mockHttpValues } from '../../../../__mocks__'; -jest.mock('../../../../shared/http', () => ({ +import { mockHttpValues } from '../../../__mocks__'; +jest.mock('../../../shared/http', () => ({ HttpLogic: { values: mockHttpValues }, })); const { http } = mockHttpValues; -jest.mock('../../../../shared/flash_messages', () => ({ +jest.mock('../../../shared/flash_messages', () => ({ flashAPIErrors: jest.fn(), })); -import { flashAPIErrors } from '../../../../shared/flash_messages'; +import { flashAPIErrors } from '../../../shared/flash_messages'; import { LogRetentionOptions } from './types'; import { LogRetentionLogic } from './log_retention_logic'; @@ -112,6 +112,21 @@ describe('LogRetentionLogic', () => { }); }); + describe('setLogRetentionUpdating', () => { + describe('isLogRetentionUpdating', () => { + it('sets isLogRetentionUpdating to true', () => { + mount(); + + LogRetentionLogic.actions.setLogRetentionUpdating(); + + expect(LogRetentionLogic.values).toEqual({ + ...DEFAULT_VALUES, + isLogRetentionUpdating: true, + }); + }); + }); + }); + describe('clearLogRetentionUpdating', () => { describe('isLogRetentionUpdating', () => { it('resets isLogRetentionUpdating to false', () => { @@ -263,24 +278,8 @@ describe('LogRetentionLogic', () => { }); describe('fetchLogRetention', () => { - describe('isLogRetentionUpdating', () => { - it('sets isLogRetentionUpdating to true', () => { - mount({ - isLogRetentionUpdating: false, - }); - - LogRetentionLogic.actions.fetchLogRetention(); - - expect(LogRetentionLogic.values).toEqual({ - ...DEFAULT_VALUES, - isLogRetentionUpdating: true, - }); - }); - }); - it('will call an API endpoint and update log retention', async () => { mount(); - jest.spyOn(LogRetentionLogic.actions, 'clearLogRetentionUpdating'); jest .spyOn(LogRetentionLogic.actions, 'updateLogRetention') .mockImplementationOnce(() => {}); @@ -289,14 +288,14 @@ describe('LogRetentionLogic', () => { http.get.mockReturnValue(promise); LogRetentionLogic.actions.fetchLogRetention(); + expect(LogRetentionLogic.values.isLogRetentionUpdating).toBe(true); expect(http.get).toHaveBeenCalledWith('/api/app_search/log_settings'); await promise; expect(LogRetentionLogic.actions.updateLogRetention).toHaveBeenCalledWith( TYPICAL_CLIENT_LOG_RETENTION ); - - expect(LogRetentionLogic.actions.clearLogRetentionUpdating).toHaveBeenCalled(); + expect(LogRetentionLogic.values.isLogRetentionUpdating).toBe(false); }); it('handles errors', async () => { @@ -315,6 +314,14 @@ describe('LogRetentionLogic', () => { expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); expect(LogRetentionLogic.actions.clearLogRetentionUpdating).toHaveBeenCalled(); }); + + it('does not run if isLogRetentionUpdating is true, preventing duplicate fetches', async () => { + mount({ isLogRetentionUpdating: true }); + + LogRetentionLogic.actions.fetchLogRetention(); + + expect(http.get).not.toHaveBeenCalled(); + }); }); it('will call saveLogRetention if NOT already enabled', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.ts similarity index 88% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_logic.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.ts index 31fc41213492d..d0088fa1803dc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.ts @@ -6,15 +6,17 @@ import { kea, MakeLogicType } from 'kea'; +import { HttpLogic } from '../../../shared/http'; +import { flashAPIErrors } from '../../../shared/flash_messages'; + import { LogRetentionOptions, LogRetention, LogRetentionServer } from './types'; -import { HttpLogic } from '../../../../shared/http'; -import { flashAPIErrors } from '../../../../shared/flash_messages'; import { convertLogRetentionFromServerToClient } from './utils/convert_log_retention'; interface LogRetentionActions { - clearLogRetentionUpdating(): { value: boolean }; - closeModals(): { value: boolean }; - fetchLogRetention(): { value: boolean }; + setLogRetentionUpdating(): void; + clearLogRetentionUpdating(): void; + closeModals(): void; + fetchLogRetention(): void; saveLogRetention( option: LogRetentionOptions, enabled: boolean @@ -33,6 +35,7 @@ interface LogRetentionValues { export const LogRetentionLogic = kea>({ path: ['enterprise_search', 'app_search', 'log_retention_logic'], actions: () => ({ + setLogRetentionUpdating: true, clearLogRetentionUpdating: true, closeModals: true, fetchLogRetention: true, @@ -53,7 +56,7 @@ export const LogRetentionLogic = kea false, closeModals: () => false, - fetchLogRetention: () => true, + setLogRetentionUpdating: () => true, toggleLogRetention: () => true, }, ], @@ -68,9 +71,14 @@ export const LogRetentionLogic = kea ({ fetchLogRetention: async () => { + if (values.isLogRetentionUpdating) return; // Prevent duplicate calls to the API + try { + actions.setLogRetentionUpdating(); + const { http } = HttpLogic.values; const response = await http.get('/api/app_search/log_settings'); + actions.updateLogRetention( convertLogRetentionFromServerToClient(response as LogRetentionServer) ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/constants.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/constants.tsx new file mode 100644 index 0000000000000..1f5dd7ed369c5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/constants.tsx @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { FormattedDate, FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { LogRetentionOptions, LogRetentionSettings, LogRetentionPolicy } from '../types'; + +export const renderLogRetentionDate = (dateString: string) => ( + +); + +const CAPITALIZATION_MAP = { + [LogRetentionOptions.Analytics]: { + capitalized: i18n.translate( + 'xpack.enterpriseSearch.appSearch.logRetention.type.analytics.title.capitalized', + { defaultMessage: 'Analytics' } + ), + lowercase: i18n.translate( + 'xpack.enterpriseSearch.appSearch.logRetention.type.analytics.title.lowercase', + { defaultMessage: 'analytics' } + ), + }, + [LogRetentionOptions.API]: { + capitalized: i18n.translate( + 'xpack.enterpriseSearch.appSearch.logRetention.type.api.title.capitalized', + { defaultMessage: 'API' } + ), + lowercase: i18n.translate( + 'xpack.enterpriseSearch.appSearch.logRetention.type.api.title.lowercase', + { defaultMessage: 'API' } + ), + }, +}; + +interface Props { + type: LogRetentionOptions; + disabledAt?: LogRetentionSettings['disabledAt']; + minAgeDays?: LogRetentionPolicy['minAgeDays']; +} + +export const NoLogging: React.FC = ({ type, disabledAt }) => { + return ( + <> + {' '} + {disabledAt ? ( + + ) : ( + + )} + + ); +}; + +export const ILMDisabled: React.FC = ({ type }) => ( + +); + +export const CustomPolicy: React.FC = ({ type }) => ( + +); + +export const DefaultPolicy: React.FC = ({ type, minAgeDays }) => ( + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/index.ts new file mode 100644 index 0000000000000..3e5154be25b57 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { LogRetentionMessage } from './log_retention_message'; +export { renderLogRetentionDate } from './constants'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.test.tsx new file mode 100644 index 0000000000000..435b28c9d6dc8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.test.tsx @@ -0,0 +1,172 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { setMockValues } from '../../../../__mocks__/kea.mock'; +import { mountWithIntl } from '../../../../__mocks__'; + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { LogRetentionOptions } from '../types'; +import { LogRetentionMessage } from './'; + +describe('LogRetentionMessage', () => { + const analytics = LogRetentionOptions.Analytics; + const api = LogRetentionOptions.API; + + const setLogRetention = (logRetention: object, ilmEnabled: boolean = true) => { + const logRetentionSettings = { + disabledAt: null, + enabled: true, + retentionPolicy: null, + ...logRetention, + }; + + setMockValues({ + ilmEnabled, + logRetention: { + [LogRetentionOptions.API]: logRetentionSettings, + [LogRetentionOptions.Analytics]: logRetentionSettings, + }, + }); + }; + + it('does not render if ILM is unavailable', () => { + setMockValues({ logRetention: null }); + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + it('does not render if log retention settings are empty', () => { + setMockValues({ logRetention: { api: null } }); + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); + + describe('when logs are enabled', () => { + describe("and they're using the default policy", () => { + describe('a retention policy message renders', () => { + beforeEach(() => { + setLogRetention({ + enabled: true, + retentionPolicy: { + isDefault: true, + minAgeDays: 7, + }, + }); + }); + + it('for analytics', () => { + const wrapper = mountWithIntl(); + expect(wrapper.text()).toEqual( + 'Your analytics logs are being stored for at least 7 days.' + ); + }); + + it('for api', () => { + const wrapper = mountWithIntl(); + expect(wrapper.text()).toEqual('Your API logs are being stored for at least 7 days.'); + }); + }); + }); + + describe('and there is a custom policy', () => { + describe('a retention policy message renders', () => { + beforeEach(() => { + setLogRetention({ + enabled: true, + retentionPolicy: { + isDefault: false, + minAgeDays: 7, + }, + }); + }); + + it('for analytics', () => { + const wrapper = mountWithIntl(); + expect(wrapper.text()).toEqual('You have a custom analytics log retention policy.'); + }); + + it('for api', () => { + const wrapper = mountWithIntl(); + expect(wrapper.text()).toEqual('You have a custom API log retention policy.'); + }); + }); + }); + }); + + describe('when logs are disabled', () => { + describe('and there is no disabledAt date', () => { + describe('a no logging message renders', () => { + beforeEach(() => { + setLogRetention({ + enabled: false, + disabledAt: null, + }); + }); + + it('for api', () => { + const wrapper = mountWithIntl(); + expect(wrapper.text()).toEqual( + 'API logging has been disabled for all engines. There are no API logs collected.' + ); + }); + + it('for analytics', () => { + const wrapper = mountWithIntl(); + expect(wrapper.text()).toEqual( + 'Analytics logging has been disabled for all engines. There are no analytics logs collected.' + ); + }); + }); + }); + + describe('and there is a disabledAt date', () => { + describe('a no logging message renders with a date', () => { + beforeEach(() => { + setLogRetention({ + enabled: false, + disabledAt: 'Thu, 05 Nov 2020 18:57:28 +0000', + }); + }); + + it('for analytics', () => { + const wrapper = mountWithIntl(); + expect(wrapper.text()).toEqual( + 'Analytics logging has been disabled for all engines. The last date analytics logs were collected was November 5, 2020.' + ); + }); + + it('for api', () => { + const wrapper = mountWithIntl(); + expect(wrapper.text()).toEqual( + 'API logging has been disabled for all engines. The last date API logs were collected was November 5, 2020.' + ); + }); + }); + }); + }); + + describe('when ILM is disabled entirely', () => { + describe('an ILM disabled message renders', () => { + beforeEach(() => { + setLogRetention({}, false); + }); + + it('for analytics', () => { + const wrapper = mountWithIntl(); + expect(wrapper.text()).toEqual("App Search isn't managing analytics log retention."); + }); + + it('for api', () => { + const wrapper = mountWithIntl(); + expect(wrapper.text()).toEqual("App Search isn't managing API log retention."); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.tsx new file mode 100644 index 0000000000000..2c213cfc43e92 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { useValues } from 'kea'; + +import { AppLogic } from '../../../app_logic'; +import { LogRetentionLogic } from '../log_retention_logic'; +import { LogRetentionOptions } from '../types'; + +import { NoLogging, ILMDisabled, CustomPolicy, DefaultPolicy } from './constants'; + +interface Props { + type: LogRetentionOptions; +} +export const LogRetentionMessage: React.FC = ({ type }) => { + const { ilmEnabled } = useValues(AppLogic); + + const { logRetention } = useValues(LogRetentionLogic); + if (!logRetention) return null; + + const logRetentionSettings = logRetention[type]; + if (!logRetentionSettings) return null; + + if (!logRetentionSettings.enabled) { + return ; + } + if (!ilmEnabled) { + return ; + } + if (!logRetentionSettings.retentionPolicy?.isDefault) { + return ; + } else { + return ( + + ); + } +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/types.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/types.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/types.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/utils/convert_log_retention.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/utils/convert_log_retention.test.ts similarity index 99% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/utils/convert_log_retention.test.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/utils/convert_log_retention.test.ts index b49b2afe50831..88090dbb5ea1f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/utils/convert_log_retention.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/utils/convert_log_retention.test.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { convertLogRetentionFromServerToClient } from './convert_log_retention'; describe('convertLogRetentionFromServerToClient', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/utils/convert_log_retention.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/utils/convert_log_retention.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/utils/convert_log_retention.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/utils/convert_log_retention.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/generic_confirmation_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/generic_confirmation_modal.tsx index 6d802b0c5cfaf..5b2e439436b55 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/generic_confirmation_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/generic_confirmation_modal.tsx @@ -55,7 +55,7 @@ export const GenericConfirmationModal: React.FC =

    {subheading}

    -

    {description}

    + {description} ', () => { const actions = { 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 67421bb78fa71..25f90df4e541b 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 @@ -10,10 +10,8 @@ import { i18n } from '@kbn/i18n'; import { EuiTextColor, EuiOverlayMask } from '@elastic/eui'; import { useActions, useValues } from 'kea'; +import { LogRetentionLogic, LogRetentionOptions } from '../../log_retention'; import { GenericConfirmationModal } from './generic_confirmation_modal'; -import { LogRetentionLogic } from './log_retention_logic'; - -import { LogRetentionOptions } from './types'; export const LogRetentionConfirmationModal: React.FC = () => { const CANNOT_BE_RECOVERED_TEXT = i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.test.tsx index c44418a1eb5a2..9140704ece3f8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.test.tsx @@ -12,8 +12,8 @@ import { setMockActions, setMockValues } from '../../../../__mocks__'; import React from 'react'; import { shallow } from 'enzyme'; +import { LogRetention } from '../../log_retention/types'; import { LogRetentionPanel } from './log_retention_panel'; -import { LogRetention } from './types'; describe('', () => { const actions = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx index 3297f0df4d7bd..873ff58be8373 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx @@ -12,9 +12,7 @@ import { useActions, useValues } from 'kea'; import { DOCS_PREFIX } from '../../../routes'; -import { LogRetentionLogic } from './log_retention_logic'; -import { AnalyticsLogRetentionMessage, ApiLogRetentionMessage } from './messaging'; -import { LogRetentionOptions } from './types'; +import { LogRetentionLogic, LogRetentionOptions, LogRetentionMessage } from '../../log_retention'; export const LogRetentionPanel: React.FC = () => { const { toggleLogRetention, fetchLogRetention } = useActions(LogRetentionLogic); @@ -66,7 +64,7 @@ export const LogRetentionPanel: React.FC = () => { {': '} {hasILM && ( - + )} @@ -93,7 +91,7 @@ export const LogRetentionPanel: React.FC = () => { {': '} {hasILM && ( - + )} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/constants.ts deleted file mode 100644 index 6db087e092697..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/constants.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -import { LogRetentionMessages } from './types'; -import { renderLogRetentionDate } from '.'; - -const ANALYTICS_NO_LOGGING = i18n.translate( - 'xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.noLogging', - { - defaultMessage: 'Analytics collection has been disabled for all engines.', - } -); - -const ANALYTICS_NO_LOGGING_COLLECTED = (disabledAt: string) => - i18n.translate( - 'xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.noLogging.collected', - { - defaultMessage: 'The last date analytics were collected was {disabledAt}.', - values: { disabledAt }, - } - ); - -const ANALYTICS_NO_LOGGING_NOT_COLLECTED = i18n.translate( - 'xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.noLogging.notCollected', - { - defaultMessage: 'There are no analytics collected.', - } -); - -const ANALYTICS_ILM_DISABLED = i18n.translate( - 'xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.ilmDisabled', - { - defaultMessage: "App Search isn't managing analytics retention.", - } -); - -const ANALYTICS_CUSTOM_POLICY = i18n.translate( - 'xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.customPolicy', - { - defaultMessage: 'You have a custom analytics retention policy.', - } -); - -const ANALYTICS_STORED = (minAgeDays: number | null | undefined) => - i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.stored', { - defaultMessage: 'Your analytics are being stored for at least {minAgeDays} days.', - values: { minAgeDays }, - }); - -const API_NO_LOGGING = i18n.translate( - 'xpack.enterpriseSearch.appSearch.settings.logRetention.api.noLogging', - { - defaultMessage: 'API logging has been disabled for all engines.', - } -); - -const API_NO_LOGGING_COLLECTED = (disabledAt: string) => - i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.api.noLogging.collected', { - defaultMessage: 'The last date logs were collected was {disabledAt}.', - values: { disabledAt }, - }); - -const API_NO_LOGGING_NOT_COLLECTED = i18n.translate( - 'xpack.enterpriseSearch.appSearch.settings.logRetention.api.noLogging.notCollected', - { - defaultMessage: 'There are no logs collected.', - } -); - -const API_ILM_DISABLED = i18n.translate( - 'xpack.enterpriseSearch.appSearch.settings.logRetention.api.ilmDisabled', - { - defaultMessage: "App Search isn't managing API log retention.", - } -); - -const API_CUSTOM_POLICY = i18n.translate( - 'xpack.enterpriseSearch.appSearch.settings.logRetention.api.customPolicy', - { - defaultMessage: 'You have a custom API log retention policy.', - } -); - -const API_STORED = (minAgeDays: number | null | undefined) => - i18n.translate('xpack.enterpriseSearch.appSearch.settings.logRetention.api.stored', { - defaultMessage: 'Your logs are being stored for at least {minAgeDays} days.', - values: { minAgeDays }, - }); - -export const ANALYTICS_MESSAGES: LogRetentionMessages = { - noLogging: (_, logRetentionSettings) => - `${ANALYTICS_NO_LOGGING} ${ - logRetentionSettings.disabledAt - ? ANALYTICS_NO_LOGGING_COLLECTED(renderLogRetentionDate(logRetentionSettings.disabledAt)) - : ANALYTICS_NO_LOGGING_NOT_COLLECTED - }`, - ilmDisabled: ANALYTICS_ILM_DISABLED, - customPolicy: ANALYTICS_CUSTOM_POLICY, - defaultPolicy: (_, logRetentionSettings) => - ANALYTICS_STORED(logRetentionSettings.retentionPolicy?.minAgeDays), -}; - -export const API_MESSAGES: LogRetentionMessages = { - noLogging: (_, logRetentionSettings) => - `${API_NO_LOGGING} ${ - logRetentionSettings.disabledAt - ? API_NO_LOGGING_COLLECTED(renderLogRetentionDate(logRetentionSettings.disabledAt)) - : API_NO_LOGGING_NOT_COLLECTED - }`, - ilmDisabled: API_ILM_DISABLED, - customPolicy: API_CUSTOM_POLICY, - defaultPolicy: (_, logRetentionSettings) => - API_STORED(logRetentionSettings.retentionPolicy?.minAgeDays), -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/determine_tooltip_content.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/determine_tooltip_content.test.ts deleted file mode 100644 index fbc2ccfbc8a52..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/determine_tooltip_content.test.ts +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { determineTooltipContent } from './determine_tooltip_content'; -import { ANALYTICS_MESSAGES, API_MESSAGES } from './constants'; - -describe('determineTooltipContent', () => { - const BASE_SETTINGS = { - disabledAt: null, - enabled: true, - retentionPolicy: null, - }; - - it('will return nothing if settings are not provided', () => { - expect(determineTooltipContent(ANALYTICS_MESSAGES, true)).toBeUndefined(); - }); - - describe('analytics messages', () => { - describe('when analytics logs are enabled', () => { - describe("and they're using the default policy", () => { - it('will render a retention policy message', () => { - expect( - determineTooltipContent(ANALYTICS_MESSAGES, true, { - ...BASE_SETTINGS, - enabled: true, - retentionPolicy: { - isDefault: true, - minAgeDays: 7, - }, - }) - ).toEqual('Your analytics are being stored for at least 7 days.'); - }); - }); - - describe('and there is a custom policy', () => { - it('will render a retention policy message', () => { - expect( - determineTooltipContent(ANALYTICS_MESSAGES, true, { - ...BASE_SETTINGS, - enabled: true, - retentionPolicy: { - isDefault: false, - minAgeDays: 7, - }, - }) - ).toEqual('You have a custom analytics retention policy.'); - }); - }); - }); - - describe('when analytics logs are disabled', () => { - describe('and there is no disabledAt date', () => { - it('will render a no logging message', () => { - expect( - determineTooltipContent(ANALYTICS_MESSAGES, true, { - ...BASE_SETTINGS, - enabled: false, - disabledAt: null, - }) - ).toEqual( - 'Analytics collection has been disabled for all engines. There are no analytics collected.' - ); - }); - }); - - describe('and there is a disabledAt date', () => { - it('will render a no logging message', () => { - expect( - determineTooltipContent(ANALYTICS_MESSAGES, true, { - ...BASE_SETTINGS, - enabled: false, - disabledAt: 'Thu, 05 Nov 2020 18:57:28 +0000', - }) - ).toEqual( - 'Analytics collection has been disabled for all engines. The last date analytics were collected was November 5, 2020.' - ); - }); - }); - }); - - describe('when ilm is disabled entirely', () => { - it('will render a no logging message', () => { - expect( - determineTooltipContent(ANALYTICS_MESSAGES, false, { - ...BASE_SETTINGS, - enabled: true, - }) - ).toEqual("App Search isn't managing analytics retention."); - }); - }); - }); - - describe('api messages', () => { - describe('when analytics logs are enabled', () => { - describe("and they're using the default policy", () => { - it('will render a retention policy message', () => { - expect( - determineTooltipContent(API_MESSAGES, true, { - ...BASE_SETTINGS, - enabled: true, - retentionPolicy: { - isDefault: true, - minAgeDays: 7, - }, - }) - ).toEqual('Your logs are being stored for at least 7 days.'); - }); - }); - - describe('and there is a custom policy', () => { - it('will render a retention policy message', () => { - expect( - determineTooltipContent(API_MESSAGES, true, { - ...BASE_SETTINGS, - enabled: true, - retentionPolicy: { - isDefault: false, - minAgeDays: 7, - }, - }) - ).toEqual('You have a custom API log retention policy.'); - }); - }); - }); - - describe('when analytics logs are disabled', () => { - describe('and there is no disabledAt date', () => { - it('will render a no logging message', () => { - expect( - determineTooltipContent(API_MESSAGES, true, { - ...BASE_SETTINGS, - enabled: false, - disabledAt: null, - }) - ).toEqual('API logging has been disabled for all engines. There are no logs collected.'); - }); - }); - - describe('and there is a disabledAt date', () => { - it('will render a no logging message', () => { - expect( - determineTooltipContent(API_MESSAGES, true, { - ...BASE_SETTINGS, - enabled: false, - disabledAt: 'Thu, 05 Nov 2020 18:57:28 +0000', - }) - ).toEqual( - 'API logging has been disabled for all engines. The last date logs were collected was November 5, 2020.' - ); - }); - }); - }); - - describe('when ilm is disabled entirely', () => { - it('will render a no logging message', () => { - expect( - determineTooltipContent(API_MESSAGES, false, { - ...BASE_SETTINGS, - enabled: true, - }) - ).toEqual("App Search isn't managing API log retention."); - }); - }); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/determine_tooltip_content.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/determine_tooltip_content.ts deleted file mode 100644 index b1476dbd171ad..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/determine_tooltip_content.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { LogRetentionSettings } from '../types'; -import { TMessageStringOrFunction, LogRetentionMessages } from './types'; - -export const determineTooltipContent = ( - messages: LogRetentionMessages, - ilmEnabled: boolean, - logRetentionSettings?: LogRetentionSettings -) => { - if (typeof logRetentionSettings === 'undefined') { - return; - } - - const renderOrReturnMessage = (message: TMessageStringOrFunction) => { - if (typeof message === 'function') { - return message(ilmEnabled, logRetentionSettings); - } - return message; - }; - - if (!logRetentionSettings.enabled) { - return renderOrReturnMessage(messages.noLogging); - } - if (!ilmEnabled) { - return renderOrReturnMessage(messages.ilmDisabled); - } - if (!logRetentionSettings.retentionPolicy?.isDefault) { - return renderOrReturnMessage(messages.customPolicy); - } else { - return renderOrReturnMessage(messages.defaultPolicy); - } -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/index.test.tsx deleted file mode 100644 index b65ffc04ad700..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/index.test.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import '../../../../../__mocks__/kea.mock'; -import { setMockValues } from '../../../../../__mocks__'; - -import React from 'react'; -import { shallow } from 'enzyme'; - -import { AnalyticsLogRetentionMessage, ApiLogRetentionMessage, renderLogRetentionDate } from '.'; - -describe('LogRetentionMessaging', () => { - const LOG_RETENTION = { - analytics: { - disabledAt: null, - enabled: true, - retentionPolicy: { isDefault: true, minAgeDays: 180 }, - }, - api: { - disabledAt: null, - enabled: true, - retentionPolicy: { isDefault: true, minAgeDays: 180 }, - }, - }; - - describe('renderLogRetentionDate', () => { - it('renders a formatted date', () => { - expect(renderLogRetentionDate('Thu, 05 Nov 2020 18:57:28 +0000')).toEqual('November 5, 2020'); - }); - }); - - describe('AnalyticsLogRetentionMessage', () => { - it('renders', () => { - setMockValues({ - ilmEnabled: true, - logRetention: LOG_RETENTION, - }); - const wrapper = shallow(); - expect(wrapper.text()).toEqual('Your analytics are being stored for at least 180 days.'); - }); - - it('renders nothing if logRetention is null', () => { - setMockValues({ - ilmEnabled: true, - logRetention: null, - }); - const wrapper = shallow(); - expect(wrapper.isEmptyRender()).toEqual(true); - }); - }); - - describe('ApiLogRetentionMessage', () => { - it('renders', () => { - setMockValues({ - ilmEnabled: true, - logRetention: LOG_RETENTION, - }); - const wrapper = shallow(); - expect(wrapper.text()).toEqual('Your logs are being stored for at least 180 days.'); - }); - - it('renders nothing if logRetention is null', () => { - setMockValues({ - ilmEnabled: true, - logRetention: null, - }); - const wrapper = shallow(); - expect(wrapper.isEmptyRender()).toEqual(true); - }); - }); -}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/index.tsx deleted file mode 100644 index 21267738f61ad..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/index.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; - -import { useValues } from 'kea'; -import moment from 'moment'; - -import { AppLogic } from '../../../../app_logic'; -import { LogRetentionLogic } from '../log_retention_logic'; -import { LogRetentionOptions } from '../types'; - -import { determineTooltipContent } from './determine_tooltip_content'; -import { ANALYTICS_MESSAGES, API_MESSAGES } from './constants'; - -export const renderLogRetentionDate = (dateString: string) => - moment(dateString).format('MMMM D, YYYY'); - -export const AnalyticsLogRetentionMessage: React.FC = () => { - const { ilmEnabled } = useValues(AppLogic); - const { logRetention } = useValues(LogRetentionLogic); - if (!logRetention) return null; - - return ( - <> - {determineTooltipContent( - ANALYTICS_MESSAGES, - ilmEnabled, - logRetention[LogRetentionOptions.Analytics] - )} - - ); -}; - -export const ApiLogRetentionMessage: React.FC = () => { - const { ilmEnabled } = useValues(AppLogic); - const { logRetention } = useValues(LogRetentionLogic); - if (!logRetention) return null; - - return ( - <>{determineTooltipContent(API_MESSAGES, ilmEnabled, logRetention[LogRetentionOptions.API])} - ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/types.ts deleted file mode 100644 index 8f86e397dfe26..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/messaging/types.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { LogRetentionSettings } from '../types'; - -export type TMessageStringOrFunction = - | string - | ((ilmEnabled: boolean, logRetentionSettings: LogRetentionSettings) => string); - -export interface LogRetentionMessages { - noLogging: TMessageStringOrFunction; - ilmDisabled: TMessageStringOrFunction; - customPolicy: TMessageStringOrFunction; - defaultPolicy: TMessageStringOrFunction; -} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx index dbd6627a3b9ce..993da9e563185 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx @@ -16,9 +16,8 @@ import { import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { FlashMessages } from '../../../shared/flash_messages'; -import { LogRetentionPanel } from './log_retention/log_retention_panel'; -import { LogRetentionConfirmationModal } from './log_retention/log_retention_confirmation_modal'; +import { LogRetentionPanel, LogRetentionConfirmationModal } from './log_retention'; import { SETTINGS_TITLE } from './'; export const Settings: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts index ca2ce177617b8..e8c6948438fe0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts @@ -25,7 +25,12 @@ export const SAMPLE_ENGINE_PATH = '/engines/national-parks-demo'; export const getEngineRoute = (engineName: string) => generatePath(ENGINE_PATH, { engineName }); export const ENGINE_ANALYTICS_PATH = '/analytics'; -// TODO: Analytics sub-pages +export const ENGINE_ANALYTICS_TOP_QUERIES_PATH = `${ENGINE_ANALYTICS_PATH}/top_queries`; +export const ENGINE_ANALYTICS_TOP_QUERIES_NO_CLICKS_PATH = `${ENGINE_ANALYTICS_PATH}/top_queries_no_clicks`; +export const ENGINE_ANALYTICS_TOP_QUERIES_NO_RESULTS_PATH = `${ENGINE_ANALYTICS_PATH}/top_queries_no_results`; +export const ENGINE_ANALYTICS_TOP_QUERIES_WITH_CLICKS_PATH = `${ENGINE_ANALYTICS_PATH}/top_queries_with_clicks`; +export const ENGINE_ANALYTICS_RECENT_QUERIES_PATH = `${ENGINE_ANALYTICS_PATH}/recent_queries`; +export const ENGINE_ANALYTICS_QUERY_DETAIL_PATH = `${ENGINE_ANALYTICS_PATH}/query_detail/:query`; export const ENGINE_DOCUMENTS_PATH = '/documents'; export const ENGINE_DOCUMENT_DETAIL_PATH = `${ENGINE_DOCUMENTS_PATH}/:documentId`; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx index 62c0af31cffd9..083173c8e7a4c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx @@ -13,6 +13,7 @@ import { shallow } from 'enzyme'; import { EuiButton as EuiButtonExternal, EuiEmptyPrompt } from '@elastic/eui'; import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../../common/constants'; +import { SetAppSearchChrome } from '../kibana_chrome'; import { AppSearchLogo } from './assets/app_search_logo'; import { WorkplaceSearchLogo } from './assets/workplace_search_logo'; @@ -51,6 +52,14 @@ describe('NotFound', () => { expect(prompt.find(EuiButtonExternal).prop('href')).toEqual('https://support.elastic.co'); }); + it('passes down optional custom breadcrumbs', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(SetAppSearchChrome).prop('trail')).toEqual(['Hello', 'World']); + }); + it('does not render anything without a valid product', () => { const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx index d0140b8730229..5c76b5dae1e43 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx @@ -23,6 +23,7 @@ import { } from '../../../../common/constants'; import { EuiButtonTo } from '../react_router_helpers'; +import { BreadcrumbTrail } from '../kibana_chrome/generate_breadcrumbs'; import { SetAppSearchChrome, SetWorkplaceSearchChrome } from '../kibana_chrome'; import { SendAppSearchTelemetry, SendWorkplaceSearchTelemetry } from '../telemetry'; import { LicensingLogic } from '../licensing'; @@ -37,9 +38,11 @@ interface NotFoundProps { ID: string; SUPPORT_URL: string; }; + // Optional breadcrumbs + breadcrumbs?: BreadcrumbTrail; } -export const NotFound: React.FC = ({ product = {} }) => { +export const NotFound: React.FC = ({ product = {}, breadcrumbs }) => { const { hasGoldLicense } = useValues(LicensingLogic); const supportUrl = hasGoldLicense ? LICENSED_SUPPORT_URL : product.SUPPORT_URL; @@ -64,7 +67,7 @@ export const NotFound: React.FC = ({ product = {} }) => { return ( <> - + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx index 0677d46839af0..8d6e421005df7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx @@ -9,7 +9,6 @@ import React from 'react'; import { useValues } from 'kea'; import { - EuiAvatar, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, @@ -40,7 +39,6 @@ import { } from '../../../routes'; import { AppLogic } from '../../../app_logic'; -import { User } from '../../../types'; import { ComponentLoader } from '../../../components/shared/component_loader'; import { CredentialItem } from '../../../components/shared/credential_item'; @@ -223,31 +221,6 @@ export const Overview: React.FC = () => { }; const GroupsSummary = () => { - const GroupAvatars = ({ users }: { users: User[] }) => { - const MAX_USERS = 4; - return ( - - {users.slice(0, MAX_USERS).map((user) => ( - - - - ))} - {users.slice(MAX_USERS).length > 0 && ( - - - +{users.slice(MAX_USERS).length} - - - )} - - ); - }; - return !groups.length ? null : ( <> @@ -262,16 +235,9 @@ export const Overview: React.FC = () => { data-test-subj="SourceGroupLink" className="euiPanel--inset" > - - - - {group.name} - - - - - - + + {group.name} +
    ))} diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/analytics.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/analytics.test.ts new file mode 100644 index 0000000000000..9ede6989052b2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/analytics.test.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; + +import { registerAnalyticsRoutes } from './analytics'; + +describe('analytics routes', () => { + describe('GET /api/app_search/engines/{engineName}/analytics/queries', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/app_search/engines/{engineName}/analytics/queries', + payload: 'query', + }); + + registerAnalyticsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + mockRouter.callRoute({ + params: { engineName: 'some-engine' }, + }); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/some-engine/analytics/queries', + }); + }); + + describe('validates', () => { + it('correctly without optional query params', () => { + const request = { query: {} }; + mockRouter.shouldValidate(request); + }); + + it('correctly with all optional query params', () => { + const request = { + query: { + size: 20, + start: '1970-01-01', + end: '1970-01-02', + tag: 'some-tag', + }, + }; + mockRouter.shouldValidate(request); + }); + + it('incorrect types', () => { + const request = { + query: { + start: 100, + size: '100', + }, + }; + mockRouter.shouldThrow(request); + }); + }); + }); + + describe('GET /api/app_search/engines/{engineName}/analytics/queries/{query}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/app_search/engines/{engineName}/analytics/queries/{query}', + payload: 'query', + }); + + registerAnalyticsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + mockRouter.callRoute({ + params: { engineName: 'some-engine', query: 'some-query' }, + }); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/some-engine/analytics/query/some-query', + }); + }); + + describe('validates', () => { + it('correctly without optional query params', () => { + const request = { query: {} }; + mockRouter.shouldValidate(request); + }); + + it('correctly with all optional query params', () => { + const request = { + query: { + start: '1970-01-01', + end: '1970-01-02', + tag: 'some-tag', + }, + }; + mockRouter.shouldValidate(request); + }); + + it('incorrect types', () => { + const request = { + query: { + start: 100, + tag: false, + }, + }; + mockRouter.shouldThrow(request); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/analytics.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/analytics.ts new file mode 100644 index 0000000000000..f7d0786b27fd4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/analytics.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../plugin'; + +const querySchema = { + start: schema.maybe(schema.string()), // Date string, expected format 'YYYY-MM-DD' + end: schema.maybe(schema.string()), // Date string, expected format 'YYYY-MM-DD' + tag: schema.maybe(schema.string()), +}; +const queriesSchema = { + ...querySchema, + size: schema.maybe(schema.number()), +}; + +export function registerAnalyticsRoutes({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/app_search/engines/{engineName}/analytics/queries', + validate: { + params: schema.object({ + engineName: schema.string(), + }), + query: schema.object(queriesSchema), + }, + }, + async (context, request, response) => { + const { engineName } = request.params; + + return enterpriseSearchRequestHandler.createRequest({ + path: `/as/engines/${engineName}/analytics/queries`, + })(context, request, response); + } + ); + + router.get( + { + path: '/api/app_search/engines/{engineName}/analytics/queries/{query}', + validate: { + params: schema.object({ + engineName: schema.string(), + query: schema.string(), + }), + query: schema.object(querySchema), + }, + }, + async (context, request, response) => { + const { engineName, query } = request.params; + + return enterpriseSearchRequestHandler.createRequest({ + path: `/as/engines/${engineName}/analytics/query/${query}`, + })(context, request, response); + } + ); +} diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts index 67dcbfdc4f4d5..a20e7854db171 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts @@ -9,12 +9,14 @@ import { RouteDependencies } from '../../plugin'; import { registerEnginesRoutes } from './engines'; import { registerCredentialsRoutes } from './credentials'; import { registerSettingsRoutes } from './settings'; +import { registerAnalyticsRoutes } from './analytics'; import { registerDocumentsRoutes, registerDocumentRoutes } from './documents'; export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { registerEnginesRoutes(dependencies); registerCredentialsRoutes(dependencies); registerSettingsRoutes(dependencies); + registerAnalyticsRoutes(dependencies); registerDocumentsRoutes(dependencies); registerDocumentRoutes(dependencies); }; diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.mock.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.mock.ts index c1f60f2d63049..55871c975d353 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.mock.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.mock.ts @@ -16,7 +16,7 @@ const createClusterClientMock = () => { createIndexTemplate: jest.fn(), doesAliasExist: jest.fn(), createIndex: jest.fn(), - queryEventsBySavedObject: jest.fn(), + queryEventsBySavedObjects: jest.fn(), shutdown: jest.fn(), }; return mock; diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts index 2b1d89f12be56..545b3b1517145 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts @@ -327,11 +327,11 @@ describe('queryEventsBySavedObject', () => { total: { value: 0 }, }, }); - await clusterClientAdapter.queryEventsBySavedObject( + await clusterClientAdapter.queryEventsBySavedObjects( 'index-name', 'namespace', 'saved-object-type', - 'saved-object-id', + ['saved-object-id'], DEFAULT_OPTIONS ); @@ -365,10 +365,10 @@ describe('queryEventsBySavedObject', () => { }, }, Object { - "term": Object { - "kibana.saved_objects.id": Object { - "value": "saved-object-id", - }, + "terms": Object { + "kibana.saved_objects.id": Array [ + "saved-object-id", + ], }, }, Object { @@ -406,11 +406,11 @@ describe('queryEventsBySavedObject', () => { total: { value: 0 }, }, }); - await clusterClientAdapter.queryEventsBySavedObject( + await clusterClientAdapter.queryEventsBySavedObjects( 'index-name', undefined, 'saved-object-type', - 'saved-object-id', + ['saved-object-id'], DEFAULT_OPTIONS ); @@ -444,10 +444,10 @@ describe('queryEventsBySavedObject', () => { }, }, Object { - "term": Object { - "kibana.saved_objects.id": Object { - "value": "saved-object-id", - }, + "terms": Object { + "kibana.saved_objects.id": Array [ + "saved-object-id", + ], }, }, Object { @@ -487,11 +487,11 @@ describe('queryEventsBySavedObject', () => { total: { value: 0 }, }, }); - await clusterClientAdapter.queryEventsBySavedObject( + await clusterClientAdapter.queryEventsBySavedObjects( 'index-name', 'namespace', 'saved-object-type', - 'saved-object-id', + ['saved-object-id'], { ...DEFAULT_OPTIONS, sort_field: 'event.end', sort_order: 'desc' } ); @@ -515,11 +515,11 @@ describe('queryEventsBySavedObject', () => { const start = '2020-07-08T00:52:28.350Z'; - await clusterClientAdapter.queryEventsBySavedObject( + await clusterClientAdapter.queryEventsBySavedObjects( 'index-name', 'namespace', 'saved-object-type', - 'saved-object-id', + ['saved-object-id'], { ...DEFAULT_OPTIONS, start } ); @@ -553,10 +553,10 @@ describe('queryEventsBySavedObject', () => { }, }, Object { - "term": Object { - "kibana.saved_objects.id": Object { - "value": "saved-object-id", - }, + "terms": Object { + "kibana.saved_objects.id": Array [ + "saved-object-id", + ], }, }, Object { @@ -605,11 +605,11 @@ describe('queryEventsBySavedObject', () => { const start = '2020-07-08T00:52:28.350Z'; const end = '2020-07-08T00:00:00.000Z'; - await clusterClientAdapter.queryEventsBySavedObject( + await clusterClientAdapter.queryEventsBySavedObjects( 'index-name', 'namespace', 'saved-object-type', - 'saved-object-id', + ['saved-object-id'], { ...DEFAULT_OPTIONS, start, end } ); @@ -643,10 +643,10 @@ describe('queryEventsBySavedObject', () => { }, }, Object { - "term": Object { - "kibana.saved_objects.id": Object { - "value": "saved-object-id", - }, + "terms": Object { + "kibana.saved_objects.id": Array [ + "saved-object-id", + ], }, }, Object { diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts index 0ac1193998cef..5d4c33f319fcc 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts @@ -194,11 +194,11 @@ export class ClusterClientAdapter { } } - public async queryEventsBySavedObject( + public async queryEventsBySavedObjects( index: string, namespace: string | undefined, type: string, - id: string, + ids: string[], // eslint-disable-next-line @typescript-eslint/naming-convention { page, per_page: perPage, start, end, sort_field, sort_order }: FindOptionsType ): Promise { @@ -249,10 +249,9 @@ export class ClusterClientAdapter { }, }, { - term: { - 'kibana.saved_objects.id': { - value: id, - }, + terms: { + // default maximum of 65,536 terms, configurable by index.max_terms_count + 'kibana.saved_objects.id': ids, }, }, namespaceQuery, @@ -298,7 +297,7 @@ export class ClusterClientAdapter { }; } catch (err) { throw new Error( - `querying for Event Log by for type "${type}" and id "${id}" failed with: ${err.message}` + `querying for Event Log by for type "${type}" and ids "${ids}" failed with: ${err.message}` ); } } diff --git a/x-pack/plugins/event_log/server/event_log_client.mock.ts b/x-pack/plugins/event_log/server/event_log_client.mock.ts index 31cab802555d0..65934d62a5a9a 100644 --- a/x-pack/plugins/event_log/server/event_log_client.mock.ts +++ b/x-pack/plugins/event_log/server/event_log_client.mock.ts @@ -8,7 +8,7 @@ import { IEventLogClient } from './types'; const createEventLogClientMock = () => { const mock: jest.Mocked = { - findEventsBySavedObject: jest.fn(), + findEventsBySavedObjectIds: jest.fn(), }; return mock; }; diff --git a/x-pack/plugins/event_log/server/event_log_client.test.ts b/x-pack/plugins/event_log/server/event_log_client.test.ts index d6793be425585..b9beebdc76db4 100644 --- a/x-pack/plugins/event_log/server/event_log_client.test.ts +++ b/x-pack/plugins/event_log/server/event_log_client.test.ts @@ -11,7 +11,7 @@ import { merge } from 'lodash'; import moment from 'moment'; describe('EventLogStart', () => { - describe('findEventsBySavedObject', () => { + describe('findEventsBySavedObjectIds', () => { test('verifies that the user can access the specified saved object', async () => { const esContext = contextMock.create(); const savedObjectGetter = jest.fn(); @@ -29,9 +29,9 @@ describe('EventLogStart', () => { references: [], }); - await eventLogClient.findEventsBySavedObject('saved-object-type', 'saved-object-id'); + await eventLogClient.findEventsBySavedObjectIds('saved-object-type', ['saved-object-id']); - expect(savedObjectGetter).toHaveBeenCalledWith('saved-object-type', 'saved-object-id'); + expect(savedObjectGetter).toHaveBeenCalledWith('saved-object-type', ['saved-object-id']); }); test('throws when the user doesnt have permission to access the specified saved object', async () => { @@ -48,7 +48,7 @@ describe('EventLogStart', () => { savedObjectGetter.mockRejectedValue(new Error('Fail')); expect( - eventLogClient.findEventsBySavedObject('saved-object-type', 'saved-object-id') + eventLogClient.findEventsBySavedObjectIds('saved-object-type', ['saved-object-id']) ).rejects.toMatchInlineSnapshot(`[Error: Fail]`); }); @@ -107,17 +107,17 @@ describe('EventLogStart', () => { total: expectedEvents.length, data: expectedEvents, }; - esContext.esAdapter.queryEventsBySavedObject.mockResolvedValue(result); + esContext.esAdapter.queryEventsBySavedObjects.mockResolvedValue(result); expect( - await eventLogClient.findEventsBySavedObject('saved-object-type', 'saved-object-id') + await eventLogClient.findEventsBySavedObjectIds('saved-object-type', ['saved-object-id']) ).toEqual(result); - expect(esContext.esAdapter.queryEventsBySavedObject).toHaveBeenCalledWith( + expect(esContext.esAdapter.queryEventsBySavedObjects).toHaveBeenCalledWith( esContext.esNames.indexPattern, undefined, 'saved-object-type', - 'saved-object-id', + ['saved-object-id'], { page: 1, per_page: 10, @@ -182,23 +182,23 @@ describe('EventLogStart', () => { total: expectedEvents.length, data: expectedEvents, }; - esContext.esAdapter.queryEventsBySavedObject.mockResolvedValue(result); + esContext.esAdapter.queryEventsBySavedObjects.mockResolvedValue(result); const start = moment().subtract(1, 'days').toISOString(); const end = moment().add(1, 'days').toISOString(); expect( - await eventLogClient.findEventsBySavedObject('saved-object-type', 'saved-object-id', { + await eventLogClient.findEventsBySavedObjectIds('saved-object-type', ['saved-object-id'], { start, end, }) ).toEqual(result); - expect(esContext.esAdapter.queryEventsBySavedObject).toHaveBeenCalledWith( + expect(esContext.esAdapter.queryEventsBySavedObjects).toHaveBeenCalledWith( esContext.esNames.indexPattern, undefined, 'saved-object-type', - 'saved-object-id', + ['saved-object-id'], { page: 1, per_page: 10, @@ -228,7 +228,7 @@ describe('EventLogStart', () => { references: [], }); - esContext.esAdapter.queryEventsBySavedObject.mockResolvedValue({ + esContext.esAdapter.queryEventsBySavedObjects.mockResolvedValue({ page: 0, per_page: 0, total: 0, @@ -236,7 +236,7 @@ describe('EventLogStart', () => { }); expect( - eventLogClient.findEventsBySavedObject('saved-object-type', 'saved-object-id', { + eventLogClient.findEventsBySavedObjectIds('saved-object-type', ['saved-object-id'], { start: 'not a date string', }) ).rejects.toMatchInlineSnapshot(`[Error: [start]: Invalid Date]`); @@ -260,7 +260,7 @@ describe('EventLogStart', () => { references: [], }); - esContext.esAdapter.queryEventsBySavedObject.mockResolvedValue({ + esContext.esAdapter.queryEventsBySavedObjects.mockResolvedValue({ page: 0, per_page: 0, total: 0, @@ -268,7 +268,7 @@ describe('EventLogStart', () => { }); expect( - eventLogClient.findEventsBySavedObject('saved-object-type', 'saved-object-id', { + eventLogClient.findEventsBySavedObjectIds('saved-object-type', ['saved-object-id'], { end: 'not a date string', }) ).rejects.toMatchInlineSnapshot(`[Error: [end]: Invalid Date]`); diff --git a/x-pack/plugins/event_log/server/event_log_client.ts b/x-pack/plugins/event_log/server/event_log_client.ts index 9b7d4e00b2761..63453c6327da2 100644 --- a/x-pack/plugins/event_log/server/event_log_client.ts +++ b/x-pack/plugins/event_log/server/event_log_client.ts @@ -12,7 +12,7 @@ import { SpacesServiceStart } from '../../spaces/server'; import { EsContext } from './es'; import { IEventLogClient } from './types'; import { QueryEventsBySavedObjectResult } from './es/cluster_client_adapter'; -import { SavedObjectGetter } from './saved_object_provider_registry'; +import { SavedObjectBulkGetterResult } from './saved_object_provider_registry'; export type PluginClusterClient = Pick; export type AdminClusterClient$ = Observable; @@ -59,7 +59,7 @@ export type FindOptionsType = Pick< interface EventLogServiceCtorParams { esContext: EsContext; - savedObjectGetter: SavedObjectGetter; + savedObjectGetter: SavedObjectBulkGetterResult; spacesService?: SpacesServiceStart; request: KibanaRequest; } @@ -67,7 +67,7 @@ interface EventLogServiceCtorParams { // note that clusterClient may be null, indicating we can't write to ES export class EventLogClient implements IEventLogClient { private esContext: EsContext; - private savedObjectGetter: SavedObjectGetter; + private savedObjectGetter: SavedObjectBulkGetterResult; private spacesService?: SpacesServiceStart; private request: KibanaRequest; @@ -78,9 +78,9 @@ export class EventLogClient implements IEventLogClient { this.request = request; } - async findEventsBySavedObject( + async findEventsBySavedObjectIds( type: string, - id: string, + ids: string[], options?: Partial ): Promise { const findOptions = findOptionsSchema.validate(options ?? {}); @@ -88,14 +88,14 @@ export class EventLogClient implements IEventLogClient { const space = await this.spacesService?.getActiveSpace(this.request); const namespace = space && this.spacesService?.spaceIdToNamespace(space.id); - // verify the user has the required permissions to view this saved object - await this.savedObjectGetter(type, id); + // verify the user has the required permissions to view this saved objects + await this.savedObjectGetter(type, ids); - return await this.esContext.esAdapter.queryEventsBySavedObject( + return await this.esContext.esAdapter.queryEventsBySavedObjects( this.esContext.esNames.indexPattern, namespace, type, - id, + ids, findOptions ); } diff --git a/x-pack/plugins/event_log/server/plugin.ts b/x-pack/plugins/event_log/server/plugin.ts index d85de565b4d8e..6471db7d5dd69 100644 --- a/x-pack/plugins/event_log/server/plugin.ts +++ b/x-pack/plugins/event_log/server/plugin.ts @@ -31,6 +31,7 @@ import { EventLogService } from './event_log_service'; import { createEsContext, EsContext } from './es'; import { EventLogClientService } from './event_log_start_service'; import { SavedObjectProviderRegistry } from './saved_object_provider_registry'; +import { findByIdsRoute } from './routes/find_by_ids'; export type PluginClusterClient = Pick; @@ -99,6 +100,7 @@ export class Plugin implements CorePlugin { const client = core.savedObjects.getScopedClient(request); - return client.get.bind(client); + return client.bulkGet.bind(client); }); this.eventLogClientService = new EventLogClientService({ diff --git a/x-pack/plugins/event_log/server/routes/find.test.ts b/x-pack/plugins/event_log/server/routes/find.test.ts index 07bb8329f78ec..639995a7c3793 100644 --- a/x-pack/plugins/event_log/server/routes/find.test.ts +++ b/x-pack/plugins/event_log/server/routes/find.test.ts @@ -34,7 +34,7 @@ describe('find', () => { total: events.length, data: events, }; - eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(result); + eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(result); const [context, req, res] = mockHandlerArguments( eventLogClient, @@ -46,11 +46,11 @@ describe('find', () => { await handler(context, req, res); - expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); - const [type, id] = eventLogClient.findEventsBySavedObject.mock.calls[0]; + const [type, ids] = eventLogClient.findEventsBySavedObjectIds.mock.calls[0]; expect(type).toEqual(`action`); - expect(id).toEqual(`1`); + expect(ids).toEqual(['1']); expect(res.ok).toHaveBeenCalledWith({ body: result, @@ -63,7 +63,7 @@ describe('find', () => { findRoute(router, systemLogger); const [, handler] = router.get.mock.calls[0]; - eventLogClient.findEventsBySavedObject.mockResolvedValueOnce({ + eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce({ page: 0, per_page: 10, total: 0, @@ -81,11 +81,11 @@ describe('find', () => { await handler(context, req, res); - expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1); + expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); - const [type, id, options] = eventLogClient.findEventsBySavedObject.mock.calls[0]; + const [type, ids, options] = eventLogClient.findEventsBySavedObjectIds.mock.calls[0]; expect(type).toEqual(`action`); - expect(id).toEqual(`1`); + expect(ids).toEqual(['1']); expect(options).toMatchObject({}); expect(res.ok).toHaveBeenCalledWith({ @@ -104,7 +104,7 @@ describe('find', () => { findRoute(router, systemLogger); const [, handler] = router.get.mock.calls[0]; - eventLogClient.findEventsBySavedObject.mockRejectedValueOnce(new Error('oof!')); + eventLogClient.findEventsBySavedObjectIds.mockRejectedValueOnce(new Error('oof!')); const [context, req, res] = mockHandlerArguments( eventLogClient, @@ -119,7 +119,7 @@ describe('find', () => { expect(systemLogger.debug).toHaveBeenCalledTimes(1); expect(systemLogger.debug).toHaveBeenCalledWith( - 'error calling eventLog findEventsBySavedObject(action, 1, {"page":3,"per_page":10}): oof!' + 'error calling eventLog findEventsBySavedObjectIds(action, [1], {"page":3,"per_page":10}): oof!' ); }); }); diff --git a/x-pack/plugins/event_log/server/routes/find.ts b/x-pack/plugins/event_log/server/routes/find.ts index 3880ac2c10129..50785de72cfc5 100644 --- a/x-pack/plugins/event_log/server/routes/find.ts +++ b/x-pack/plugins/event_log/server/routes/find.ts @@ -47,10 +47,10 @@ export const findRoute = (router: IRouter, systemLogger: Logger) => { try { return res.ok({ - body: await eventLogClient.findEventsBySavedObject(type, id, query), + body: await eventLogClient.findEventsBySavedObjectIds(type, [id], query), }); } catch (err) { - const call = `findEventsBySavedObject(${type}, ${id}, ${JSON.stringify(query)})`; + const call = `findEventsBySavedObjectIds(${type}, [${id}], ${JSON.stringify(query)})`; systemLogger.debug(`error calling eventLog ${call}: ${err.message}`); return res.notFound(); } diff --git a/x-pack/plugins/event_log/server/routes/find_by_ids.test.ts b/x-pack/plugins/event_log/server/routes/find_by_ids.test.ts new file mode 100644 index 0000000000000..f28c36164e95e --- /dev/null +++ b/x-pack/plugins/event_log/server/routes/find_by_ids.test.ts @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { httpServiceMock } from 'src/core/server/mocks'; +import { mockHandlerArguments, fakeEvent } from './_mock_handler_arguments'; +import { eventLogClientMock } from '../event_log_client.mock'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { findByIdsRoute } from './find_by_ids'; + +const eventLogClient = eventLogClientMock.create(); +const systemLogger = loggingSystemMock.createLogger(); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('find_by_ids', () => { + it('finds events with proper parameters', async () => { + const router = httpServiceMock.createRouter(); + + findByIdsRoute(router, systemLogger); + + const [config, handler] = router.post.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/event_log/{type}/_find"`); + + const events = [fakeEvent(), fakeEvent()]; + const result = { + page: 0, + per_page: 10, + total: events.length, + data: events, + }; + eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce(result); + + const [context, req, res] = mockHandlerArguments( + eventLogClient, + { + params: { type: 'action' }, + body: { ids: ['1'] }, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); + + const [type, ids] = eventLogClient.findEventsBySavedObjectIds.mock.calls[0]; + expect(type).toEqual(`action`); + expect(ids).toEqual(['1']); + + expect(res.ok).toHaveBeenCalledWith({ + body: result, + }); + }); + + it('supports optional pagination parameters', async () => { + const router = httpServiceMock.createRouter(); + + findByIdsRoute(router, systemLogger); + + const [, handler] = router.post.mock.calls[0]; + eventLogClient.findEventsBySavedObjectIds.mockResolvedValueOnce({ + page: 0, + per_page: 10, + total: 0, + data: [], + }); + + const [context, req, res] = mockHandlerArguments( + eventLogClient, + { + params: { type: 'action' }, + body: { ids: ['1'] }, + query: { page: 3, per_page: 10 }, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(eventLogClient.findEventsBySavedObjectIds).toHaveBeenCalledTimes(1); + + const [type, id, options] = eventLogClient.findEventsBySavedObjectIds.mock.calls[0]; + expect(type).toEqual(`action`); + expect(id).toEqual(['1']); + expect(options).toMatchObject({}); + + expect(res.ok).toHaveBeenCalledWith({ + body: { + page: 0, + per_page: 10, + total: 0, + data: [], + }, + }); + }); + + it('logs a warning when the query throws an error', async () => { + const router = httpServiceMock.createRouter(); + + findByIdsRoute(router, systemLogger); + + const [, handler] = router.post.mock.calls[0]; + eventLogClient.findEventsBySavedObjectIds.mockRejectedValueOnce(new Error('oof!')); + + const [context, req, res] = mockHandlerArguments( + eventLogClient, + { + params: { type: 'action' }, + body: { ids: ['1'] }, + query: { page: 3, per_page: 10 }, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(systemLogger.debug).toHaveBeenCalledTimes(1); + expect(systemLogger.debug).toHaveBeenCalledWith( + 'error calling eventLog findEventsBySavedObjectIds(action, [1], {"page":3,"per_page":10}): oof!' + ); + }); +}); diff --git a/x-pack/plugins/event_log/server/routes/find_by_ids.ts b/x-pack/plugins/event_log/server/routes/find_by_ids.ts new file mode 100644 index 0000000000000..a7ee0f35ac59e --- /dev/null +++ b/x-pack/plugins/event_log/server/routes/find_by_ids.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; +import { + IRouter, + RequestHandlerContext, + KibanaRequest, + IKibanaResponse, + KibanaResponseFactory, + Logger, +} from 'src/core/server'; + +import { BASE_EVENT_LOG_API_PATH } from '../../common'; +import { findOptionsSchema, FindOptionsType } from '../event_log_client'; + +const paramSchema = schema.object({ + type: schema.string(), +}); + +const bodySchema = schema.object({ + ids: schema.arrayOf(schema.string(), { defaultValue: [] }), +}); + +export const findByIdsRoute = (router: IRouter, systemLogger: Logger) => { + router.post( + { + path: `${BASE_EVENT_LOG_API_PATH}/{type}/_find`, + validate: { + params: paramSchema, + query: findOptionsSchema, + body: bodySchema, + }, + }, + router.handleLegacyErrors(async function ( + context: RequestHandlerContext, + req: KibanaRequest, FindOptionsType, TypeOf>, + res: KibanaResponseFactory + ): Promise { + if (!context.eventLog) { + return res.badRequest({ body: 'RouteHandlerContext is not registered for eventLog' }); + } + const eventLogClient = context.eventLog.getEventLogClient(); + const { + params: { type }, + body: { ids }, + query, + } = req; + + try { + return res.ok({ + body: await eventLogClient.findEventsBySavedObjectIds(type, ids, query), + }); + } catch (err) { + const call = `findEventsBySavedObjectIds(${type}, [${ids}], ${JSON.stringify(query)})`; + systemLogger.debug(`error calling eventLog ${call}: ${err.message}`); + return res.notFound(); + } + }) + ); +}; diff --git a/x-pack/plugins/event_log/server/saved_object_provider_registry.test.ts b/x-pack/plugins/event_log/server/saved_object_provider_registry.test.ts index 6a02d54c87514..f6d42a6113518 100644 --- a/x-pack/plugins/event_log/server/saved_object_provider_registry.test.ts +++ b/x-pack/plugins/event_log/server/saved_object_provider_registry.test.ts @@ -45,10 +45,10 @@ describe('SavedObjectProviderRegistry', () => { getter.mockResolvedValue(alert); - expect(await registry.getProvidersClient(request)('alert', alert.id)).toMatchObject(alert); + expect(await registry.getProvidersClient(request)('alert', [alert.id])).toMatchObject(alert); expect(provider).toHaveBeenCalledWith(request); - expect(getter).toHaveBeenCalledWith('alert', alert.id); + expect(getter).toHaveBeenCalledWith([{ id: alert.id, type: 'alert' }]); }); test('should get SavedObject using the default provider for unregistered types', async () => { @@ -70,9 +70,11 @@ describe('SavedObjectProviderRegistry', () => { defaultProvider.mockReturnValue(getter); getter.mockResolvedValue(action); - expect(await registry.getProvidersClient(request)('action', action.id)).toMatchObject(action); + expect(await registry.getProvidersClient(request)('action', [action.id])).toMatchObject( + action + ); - expect(getter).toHaveBeenCalledWith('action', action.id); + expect(getter).toHaveBeenCalledWith([{ id: action.id, type: 'action' }]); expect(defaultProvider).toHaveBeenCalledWith(request); }); }); diff --git a/x-pack/plugins/event_log/server/saved_object_provider_registry.ts b/x-pack/plugins/event_log/server/saved_object_provider_registry.ts index 87a1da5dd6f4a..3c3e93afec394 100644 --- a/x-pack/plugins/event_log/server/saved_object_provider_registry.ts +++ b/x-pack/plugins/event_log/server/saved_object_provider_registry.ts @@ -13,7 +13,14 @@ import { pipe } from 'fp-ts/lib/pipeable'; export type SavedObjectGetter = ( ...params: Parameters ) => Promise; -export type SavedObjectProvider = (request: KibanaRequest) => SavedObjectGetter; + +export type SavedObjectBulkGetter = ( + ...params: Parameters +) => Promise; + +export type SavedObjectBulkGetterResult = (type: string, ids: string[]) => Promise; + +export type SavedObjectProvider = (request: KibanaRequest) => SavedObjectBulkGetter; export class SavedObjectProviderRegistry { private providers = new Map(); @@ -34,7 +41,7 @@ export class SavedObjectProviderRegistry { this.providers.set(type, provider); } - public getProvidersClient(request: KibanaRequest): SavedObjectGetter { + public getProvidersClient(request: KibanaRequest): SavedObjectBulkGetterResult { if (!this.defaultProvider) { throw new Error( i18n.translate( @@ -49,9 +56,13 @@ export class SavedObjectProviderRegistry { // `scopedProviders` is a cache of providers which are scoped t othe current request. // The client will only instantiate a provider on-demand and it will cache each // one to enable the request to reuse each provider. - const scopedProviders = new Map(); + + // would be nice to have a simple version support in API: + // curl -X GET "localhost:9200/my-index-000001/_mget?pretty" -H 'Content-Type: application/json' -d' { "ids" : ["1", "2"] } ' + const scopedProviders = new Map(); const defaultGetter = this.defaultProvider(request); - return (type: string, id: string) => { + return (type: string, ids: string[]) => { + const objects = ids.map((id: string) => ({ type, id })); const getter = pipe( fromNullable(scopedProviders.get(type)), getOrElse(() => { @@ -62,7 +73,7 @@ export class SavedObjectProviderRegistry { return client; }) ); - return getter(type, id); + return getter(objects); }; } } diff --git a/x-pack/plugins/event_log/server/types.ts b/x-pack/plugins/event_log/server/types.ts index 66030ee3910dc..ff2ae81632923 100644 --- a/x-pack/plugins/event_log/server/types.ts +++ b/x-pack/plugins/event_log/server/types.ts @@ -51,9 +51,9 @@ export interface IEventLogClientService { } export interface IEventLogClient { - findEventsBySavedObject( + findEventsBySavedObjectIds( type: string, - id: string, + ids: string[], options?: Partial ): Promise; } diff --git a/x-pack/plugins/fleet/common/services/agent_status.ts b/x-pack/plugins/fleet/common/services/agent_status.ts index 30b52bcb28748..4cf35398bab24 100644 --- a/x-pack/plugins/fleet/common/services/agent_status.ts +++ b/x-pack/plugins/fleet/common/services/agent_status.ts @@ -49,7 +49,7 @@ export function buildKueryForUnenrollingAgents() { } export function buildKueryForOnlineAgents() { - return `not (${buildKueryForOfflineAgents()}) AND not (${buildKueryForErrorAgents()}) AND not (${buildKueryForEnrollingAgents()}) AND not (${buildKueryForUnenrollingAgents()})`; + return `not (${buildKueryForOfflineAgents()}) AND not (${buildKueryForErrorAgents()}) AND not (${buildKueryForUpdatingAgents()})`; } export function buildKueryForErrorAgents() { @@ -59,11 +59,11 @@ export function buildKueryForErrorAgents() { export function buildKueryForOfflineAgents() { return `${AGENT_SAVED_OBJECT_TYPE}.last_checkin < now-${ (4 * AGENT_POLLING_THRESHOLD_MS) / 1000 - }s AND not (${buildKueryForErrorAgents()})`; + }s AND not (${buildKueryForErrorAgents()}) AND not ( ${buildKueryForUpdatingAgents()} )`; } export function buildKueryForUpgradingAgents() { - return `${AGENT_SAVED_OBJECT_TYPE}.upgrade_started_at:*`; + return `(${AGENT_SAVED_OBJECT_TYPE}.upgrade_started_at:*) and not (${AGENT_SAVED_OBJECT_TYPE}.upgraded_at:*)`; } export function buildKueryForUpdatingAgents() { diff --git a/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts b/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts index 36972270de011..1192f0a3e8e52 100644 --- a/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts +++ b/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts @@ -26,6 +26,15 @@ describe('Fleet - packageToPackagePolicy', () => { search: [], index_pattern: [], map: [], + lens: [], + }, + elasticsearch: { + ingest_pipeline: [], + component_template: [], + index_template: [], + transform: [], + ilm_policy: [], + data_stream_ilm_policy: [], }, }, status: 'not_installed', diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 08f92f406b90f..e09fbfc80b196 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -46,6 +46,7 @@ export enum KibanaAssetType { search = 'search', indexPattern = 'index_pattern', map = 'map', + lens = 'lens', } /* @@ -57,6 +58,7 @@ export enum KibanaSavedObjectType { search = 'search', indexPattern = 'index-pattern', map = 'map', + lens = 'lens', } export enum ElasticsearchAssetType { @@ -187,8 +189,8 @@ export type AssetTypeToParts = KibanaAssetTypeToParts & ElasticsearchAssetTypeTo export type AssetsGroupedByServiceByType = Record< Extract, KibanaAssetTypeToParts ->; -// & Record, ElasticsearchAssetTypeToParts>; +> & + Record, ElasticsearchAssetTypeToParts>; export type KibanaAssetParts = AssetParts & { service: Extract; diff --git a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts index 236fc586bf528..f758ca0921a08 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts @@ -219,5 +219,6 @@ export interface GetAgentStatusResponse { error: number; offline: number; other: number; + updating: number; }; } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_package.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_package.tsx index 3bcafaecbf8d9..ef8dba4447c31 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_package.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_select_package.tsx @@ -141,15 +141,7 @@ export const StepSelectPackage: React.FunctionComponent<{ return { label: title || name, key: pkgkey, - prepend: ( - - ), + prepend: , checked: selectedPkgKey === pkgkey ? 'on' : undefined, }; })} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx index 81195bdeaa9e2..b62c5b0ca8744 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { memo } from 'react'; +import styled from 'styled-components'; import { EuiDescriptionList, EuiDescriptionListTitle, @@ -22,6 +23,11 @@ import { isAgentUpgradeable } from '../../../../../services'; import { AgentPolicyPackageBadges } from '../../../components/agent_policy_package_badges'; import { LinkAndRevision } from '../../../../../components'; +// Allows child text to be truncated +const FlexItemWithMinWidth = styled(EuiFlexItem)` + min-width: 0px; +`; + export const AgentDetailsOverviewSection: React.FunctionComponent<{ agent: Agent; agentPolicy?: AgentPolicy; @@ -30,7 +36,7 @@ export const AgentDetailsOverviewSection: React.FunctionComponent<{ const kibanaVersion = useKibanaVersion(); return ( - + {[ { title: i18n.translate('xpack.fleet.agentDetails.hostIdLabel', { @@ -161,16 +167,14 @@ export const AgentDetailsOverviewSection: React.FunctionComponent<{ }, ].map(({ title, description }) => { return ( - - - - {title} - - - {description} - - - + + + {title} + + + {description} + + ); })} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/index.tsx index 0b83fb4cc64e1..0dc16e75ff131 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/index.tsx @@ -4,12 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { memo } from 'react'; +import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { Agent, AgentPolicy } from '../../../../../types'; import { AgentDetailsOverviewSection } from './agent_details_overview'; import { AgentDetailsIntegrationsSection } from './agent_details_integrations'; +// Allows child text to be truncated +const FlexItemWithMinWidth = styled(EuiFlexItem)` + min-width: 0px; +`; + export const AgentDetailsContent: React.FunctionComponent<{ agent: Agent; agentPolicy?: AgentPolicy; @@ -17,7 +23,7 @@ export const AgentDetailsContent: React.FunctionComponent<{ return ( <> - +

    - - + +

    - + ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx index 95c630e3b3686..7326d2efb8565 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/agent_logs.tsx @@ -15,6 +15,8 @@ import { EuiFilterGroup, EuiPanel, EuiButtonEmpty, + EuiCallOut, + EuiLink, } from '@elastic/eui'; import useMeasure from 'react-use/lib/useMeasure'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -184,7 +186,7 @@ export const AgentLogsUI: React.FunctionComponent = memo(({ agen const [logsPanelRef, { height: logPanelHeight }] = useMeasure(); const agentVersion = agent.local_metadata?.elastic?.agent?.version; - const isLogLevelSelectionAvailable = useMemo(() => { + const isLogFeatureAvailable = useMemo(() => { if (!agentVersion) { return false; } @@ -195,6 +197,31 @@ export const AgentLogsUI: React.FunctionComponent = memo(({ agen return semverGte(agentVersionWithPrerelease, '7.11.0'); }, [agentVersion]); + if (!isLogFeatureAvailable) { + return ( + + + + ), + }} + /> + } + /> + ); + } + return ( @@ -271,11 +298,9 @@ export const AgentLogsUI: React.FunctionComponent = memo(({ agen /> - {isLogLevelSelectionAvailable && ( - - - - )} + + + ); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx index 34893dccd93a4..f3e9125aae9f5 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/index.tsx @@ -90,7 +90,7 @@ export const AgentDetailsPage: React.FunctionComponent = () => { - +

    {isLoading && isInitialRequest ? ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx index 2067a2bd91c58..fb7dc78e85770 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/index.tsx @@ -47,7 +47,7 @@ import { AgentTableHeader } from './components/table_header'; import { SelectionMode } from './components/bulk_actions'; import { SearchAndFilterBar } from './components/search_and_filter_bar'; -const REFRESH_INTERVAL_MS = 10000; +const REFRESH_INTERVAL_MS = 30000; const RowActions = React.memo<{ agent: Agent; @@ -282,7 +282,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { healthy: agentsStatusRequest.data.results.online, unhealthy: agentsStatusRequest.data.results.error, offline: agentsStatusRequest.data.results.offline, - updating: agentsStatusRequest.data.results.other, + updating: agentsStatusRequest.data.results.updating, inactive: agentsRequest.data.totalInactive, }); 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 45fd380a06f34..a87dd0c6d35eb 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 @@ -37,7 +37,7 @@ export const ConfirmEnrollmentTokenDelete = (props: Props) => { = { input: 'Agent input', map: 'Map', data_stream_ilm_policy: 'Data Stream ILM Policy', + lens: 'Lens', }; -export const ServiceTitleMap: Record, string> = { +export const ServiceTitleMap: Record = { kibana: 'Kibana', + elasticsearch: 'Elasticsearch', }; export const AssetIcons: Record = { @@ -39,7 +41,8 @@ export const AssetIcons: Record = { index_pattern: 'indexPatternApp', search: 'searchProfilerApp', visualization: 'visualizeApp', - map: 'mapApp', + map: 'emsApp', + lens: 'lensApp', }; export const ServiceIcons: Record = { diff --git a/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts b/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts index f42f5da2695d0..4820f25c05f96 100644 --- a/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/data_streams/handlers.ts @@ -9,7 +9,7 @@ import { GetDataStreamsResponse, KibanaAssetType, KibanaSavedObjectType } from ' import { getPackageSavedObjects, getKibanaSavedObject } from '../../services/epm/packages/get'; import { defaultIngestErrorHandler } from '../../errors'; -const DATA_STREAM_INDEX_PATTERN = 'logs-*-*,metrics-*-*'; +const DATA_STREAM_INDEX_PATTERN = 'logs-*-*,metrics-*-*,traces-*-*'; export const getListHandler: RequestHandler = async (context, request, response) => { const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; diff --git a/x-pack/plugins/fleet/server/services/agents/status.ts b/x-pack/plugins/fleet/server/services/agents/status.ts index 0dfa6db7df9be..74faedc8e2931 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.ts @@ -5,6 +5,7 @@ */ import { SavedObjectsClientContract } from 'src/core/server'; +import pMap from 'p-map'; import { getAgent, listAgents } from './crud'; import { AGENT_EVENT_SAVED_OBJECT_TYPE, AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { AgentStatus } from '../../types'; @@ -21,18 +22,32 @@ export async function getAgentStatusById( export const getAgentStatus = AgentStatusKueryHelper.getAgentStatus; +function joinKuerys(...kuerys: Array) { + return kuerys + .filter((kuery) => kuery !== undefined) + .reduce((acc, kuery) => { + if (acc === '') { + return `(${kuery})`; + } + + return `${acc} and (${kuery})`; + }, ''); +} + export async function getAgentStatusForAgentPolicy( soClient: SavedObjectsClientContract, agentPolicyId?: string, filterKuery?: string ) { - const [all, online, error, offline] = await Promise.all( + const [all, online, error, offline, updating] = await pMap( [ undefined, AgentStatusKueryHelper.buildKueryForOnlineAgents(), AgentStatusKueryHelper.buildKueryForErrorAgents(), AgentStatusKueryHelper.buildKueryForOfflineAgents(), - ].map((kuery) => + AgentStatusKueryHelper.buildKueryForUpdatingAgents(), + ], + (kuery) => listAgents(soClient, { showInactive: false, perPage: 0, @@ -44,28 +59,19 @@ export async function getAgentStatusForAgentPolicy( agentPolicyId ? `${AGENT_SAVED_OBJECT_TYPE}.policy_id:"${agentPolicyId}"` : undefined, ] ), - }) - ) + }), + { + concurrency: 1, + } ); - function joinKuerys(...kuerys: Array) { - return kuerys - .filter((kuery) => kuery !== undefined) - .reduce((acc, kuery) => { - if (acc === '') { - return `(${kuery})`; - } - - return `${acc} and (${kuery})`; - }, ''); - } - return { events: await getEventsCount(soClient, agentPolicyId), total: all.total, online: online.total, error: error.total, offline: offline.total, + updating: updating.total, other: all.total - online.total - error.total - offline.total, }; } diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts index fe93ed84b32f2..eec055e251347 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts @@ -39,6 +39,7 @@ const KibanaSavedObjectTypeMapping: Record { diff --git a/x-pack/plugins/fleet/server/services/epm/registry/index.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.ts index dc4f02c94acde..61bbb4cddd085 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.ts @@ -208,7 +208,10 @@ export function groupPathsByService(paths: string[]): AssetsGroupedByServiceByTy // ASK: best way, if any, to avoid `any`? const assets = paths.reduce((map: any, path) => { const parts = getPathParts(path.replace(/^\/package\//, '')); - if (parts.service === 'kibana' && kibanaAssetTypes.includes(parts.type)) { + if ( + (parts.service === 'kibana' && kibanaAssetTypes.includes(parts.type)) || + parts.service === 'elasticsearch' + ) { if (!map[parts.service]) map[parts.service] = {}; if (!map[parts.service][parts.type]) map[parts.service][parts.type] = []; map[parts.service][parts.type].push(parts); @@ -219,6 +222,6 @@ export function groupPathsByService(paths: string[]): AssetsGroupedByServiceByTy return { kibana: assets.kibana, - // elasticsearch: assets.elasticsearch, + elasticsearch: assets.elasticsearch, }; } diff --git a/x-pack/plugins/fleet/server/services/package_policy.test.ts b/x-pack/plugins/fleet/server/services/package_policy.test.ts index c6bfb53812c70..5e295c1576705 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts @@ -295,6 +295,36 @@ describe('Package policy service', () => { }, ]); }); + + it('should work with a package without input', async () => { + const inputs = await packagePolicyService.compilePackagePolicyInputs( + ({ + policy_templates: [ + { + inputs: undefined, + }, + ], + } as unknown) as PackageInfo, + [] + ); + + expect(inputs).toEqual([]); + }); + + it('should work with a package with a empty inputs array', async () => { + const inputs = await packagePolicyService.compilePackagePolicyInputs( + ({ + policy_templates: [ + { + inputs: [], + }, + ], + } as unknown) as PackageInfo, + [] + ); + + expect(inputs).toEqual([]); + }); }); describe('update', () => { diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 5a6b48b952836..95b1a43ec2e5e 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -437,7 +437,7 @@ async function _compilePackagePolicyInput( pkgInfo: PackageInfo, input: PackagePolicyInput ) { - if (!input.enabled || !pkgInfo.policy_templates?.[0].inputs) { + if ((!input.enabled || !pkgInfo.policy_templates?.[0]?.inputs?.length) ?? 0 > 0) { return undefined; } diff --git a/x-pack/plugins/global_search_providers/tsconfig.json b/x-pack/plugins/global_search_providers/tsconfig.json new file mode 100644 index 0000000000000..381d314b2e530 --- /dev/null +++ b/x-pack/plugins/global_search_providers/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "public/**/*", + "server/**/*", + "../../typings/**/*", + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../global_search/tsconfig.json" } + ] +} diff --git a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts index 58468f06e3b2d..1f4b06e80c49f 100644 --- a/x-pack/plugins/index_lifecycle_management/common/types/policies.ts +++ b/x-pack/plugins/index_lifecycle_management/common/types/policies.ts @@ -154,25 +154,6 @@ export interface PhaseWithMinAge { selectedMinimumAgeUnits: string; } -/** - * Different types of allocation markers we use in deserialized policies. - * - * default - use data tier based data allocation based on node roles -- this is ES best practice mode. - * custom - use node_attrs to allocate data to specific nodes - * none - do not move data anywhere when entering a phase - */ -export type DataTierAllocationType = 'default' | 'custom' | 'none'; - -export interface PhaseWithAllocationAction { - selectedNodeAttrs: string; - selectedReplicaCount: string; - /** - * A string value indicating allocation type. If unspecified we assume the user - * wants to use default allocation. - */ - dataTierAllocationType: DataTierAllocationType; -} - export interface PhaseWithIndexPriority { phaseIndexPriority: string; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/determine_allocation_type.ts b/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/determine_allocation_type.ts index 20ac439e9964f..6dde03ec4593f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/determine_allocation_type.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/lib/data_tiers/determine_allocation_type.ts @@ -4,39 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DataTierAllocationType, AllocateAction, MigrateAction } from '../../../../common/types'; - -/** - * Determine what deserialized state the policy config represents. - * - * See {@DataTierAllocationType} for more information. - */ -export const determineDataTierAllocationTypeLegacy = ( - actions: { - allocate?: AllocateAction; - migrate?: MigrateAction; - } = {} -): DataTierAllocationType => { - const { allocate, migrate } = actions; - - if (migrate?.enabled === false) { - return 'none'; - } - - if (!allocate) { - return 'default'; - } - - if ( - (allocate.require && Object.keys(allocate.require).length) || - (allocate.include && Object.keys(allocate.include).length) || - (allocate.exclude && Object.keys(allocate.exclude).length) - ) { - return 'custom'; - } - - return 'default'; -}; +import { AllocateAction, MigrateAction } from '../../../../common/types'; export const determineDataTierAllocationType = ( actions: { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts new file mode 100644 index 0000000000000..28910871fa33b --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts @@ -0,0 +1,507 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { deserializer } from '../form'; + +import { + absoluteTimingToRelativeTiming, + calculateRelativeTimingMs, +} from './absolute_timing_to_relative_timing'; + +describe('Conversion of absolute policy timing to relative timing', () => { + describe('calculateRelativeTimingMs', () => { + describe('policy that never deletes data (keep forever)', () => { + test('always hot', () => { + expect( + calculateRelativeTimingMs( + deserializer({ + name: 'test', + phases: { + hot: { + min_age: '0ms', + actions: {}, + }, + }, + }) + ) + ).toEqual({ total: Infinity, phases: { hot: Infinity, warm: undefined, cold: undefined } }); + }); + + test('hot, then always warm', () => { + expect( + calculateRelativeTimingMs( + deserializer({ + name: 'test', + phases: { + hot: { + min_age: '0ms', + actions: {}, + }, + warm: { + actions: {}, + }, + }, + }) + ) + ).toEqual({ total: Infinity, phases: { hot: 0, warm: Infinity, cold: undefined } }); + }); + + test('hot, then warm, then always cold', () => { + expect( + calculateRelativeTimingMs( + deserializer({ + name: 'test', + phases: { + hot: { + min_age: '0ms', + actions: {}, + }, + warm: { + min_age: '1M', + actions: {}, + }, + cold: { + min_age: '34d', + actions: {}, + }, + }, + }) + ) + ).toEqual({ + total: Infinity, + phases: { + hot: 2592000000, + warm: 345600000, + cold: Infinity, + }, + }); + }); + + test('hot, then always cold', () => { + expect( + calculateRelativeTimingMs( + deserializer({ + name: 'test', + phases: { + hot: { + min_age: '0ms', + actions: {}, + }, + cold: { + min_age: '34d', + actions: {}, + }, + }, + }) + ) + ).toEqual({ + total: Infinity, + phases: { hot: 2937600000, warm: undefined, cold: Infinity }, + }); + }); + }); + + describe('policy that deletes data', () => { + test('hot, then delete', () => { + expect( + calculateRelativeTimingMs( + deserializer({ + name: 'test', + phases: { + hot: { + min_age: '0ms', + actions: {}, + }, + delete: { + min_age: '1M', + actions: {}, + }, + }, + }) + ) + ).toEqual({ + total: 2592000000, + phases: { + hot: 2592000000, + warm: undefined, + cold: undefined, + }, + }); + }); + + test('hot, then warm, then delete', () => { + expect( + calculateRelativeTimingMs( + deserializer({ + name: 'test', + phases: { + hot: { + min_age: '0ms', + actions: {}, + }, + warm: { + min_age: '24d', + actions: {}, + }, + delete: { + min_age: '1M', + actions: {}, + }, + }, + }) + ) + ).toEqual({ + total: 2592000000, + phases: { + hot: 2073600000, + warm: 518400000, + cold: undefined, + }, + }); + }); + + test('hot, then warm, then cold, then delete', () => { + expect( + calculateRelativeTimingMs( + deserializer({ + name: 'test', + phases: { + hot: { + min_age: '0ms', + actions: {}, + }, + warm: { + min_age: '24d', + actions: {}, + }, + cold: { + min_age: '2M', + actions: {}, + }, + delete: { + min_age: '2d', + actions: {}, + }, + }, + }) + ) + ).toEqual({ + total: 5270400000, + phases: { + hot: 2073600000, + warm: 3196800000, + cold: 0, + }, + }); + }); + + test('hot, then cold, then delete', () => { + expect( + calculateRelativeTimingMs( + deserializer({ + name: 'test', + phases: { + hot: { + min_age: '0ms', + actions: {}, + }, + cold: { + min_age: '2M', + actions: {}, + }, + delete: { + min_age: '2d', + actions: {}, + }, + }, + }) + ) + ).toEqual({ + total: 5270400000, + phases: { + hot: 5270400000, + warm: undefined, + cold: 0, + }, + }); + }); + + test('hot, then long warm, then short cold, then delete', () => { + expect( + calculateRelativeTimingMs( + deserializer({ + name: 'test', + phases: { + hot: { + min_age: '0ms', + actions: {}, + }, + warm: { + min_age: '2M', + actions: {}, + }, + cold: { + min_age: '1d', + actions: {}, + }, + delete: { + min_age: '2d', + actions: {}, + }, + }, + }) + ) + ).toEqual({ + total: 5270400000, + phases: { + hot: 5270400000, + warm: 0, + cold: 0, + }, + }); + }); + }); + }); + + describe('absoluteTimingToRelativeTiming', () => { + describe('policy that never deletes data (keep forever)', () => { + test('always hot', () => { + expect( + absoluteTimingToRelativeTiming( + deserializer({ + name: 'test', + phases: { + hot: { + min_age: '0ms', + actions: {}, + }, + }, + }) + ) + ).toEqual({ total: 'Forever', hot: 'Forever', warm: undefined, cold: undefined }); + }); + + test('hot, then always warm', () => { + expect( + absoluteTimingToRelativeTiming( + deserializer({ + name: 'test', + phases: { + hot: { + min_age: '0ms', + actions: {}, + }, + warm: { + actions: {}, + }, + }, + }) + ) + ).toEqual({ total: 'Forever', hot: 'Less than a day', warm: 'Forever', cold: undefined }); + }); + + test('hot, then warm, then always cold', () => { + expect( + absoluteTimingToRelativeTiming( + deserializer({ + name: 'test', + phases: { + hot: { + min_age: '0ms', + actions: {}, + }, + warm: { + min_age: '1M', + actions: {}, + }, + cold: { + min_age: '34d', + actions: {}, + }, + }, + }) + ) + ).toEqual({ + total: 'Forever', + hot: '30 days', + warm: '4 days', + cold: 'Forever', + }); + }); + + test('hot, then always cold', () => { + expect( + absoluteTimingToRelativeTiming( + deserializer({ + name: 'test', + phases: { + hot: { + min_age: '0ms', + actions: {}, + }, + cold: { + min_age: '34d', + actions: {}, + }, + }, + }) + ) + ).toEqual({ total: 'Forever', hot: '34 days', warm: undefined, cold: 'Forever' }); + }); + }); + + describe('policy that deletes data', () => { + test('hot, then delete', () => { + expect( + absoluteTimingToRelativeTiming( + deserializer({ + name: 'test', + phases: { + hot: { + min_age: '0ms', + actions: {}, + }, + delete: { + min_age: '1M', + actions: {}, + }, + }, + }) + ) + ).toEqual({ + total: '30 days', + hot: '30 days', + warm: undefined, + cold: undefined, + }); + }); + + test('hot, then warm, then delete', () => { + expect( + absoluteTimingToRelativeTiming( + deserializer({ + name: 'test', + phases: { + hot: { + min_age: '0ms', + actions: {}, + }, + warm: { + min_age: '24d', + actions: {}, + }, + delete: { + min_age: '1M', + actions: {}, + }, + }, + }) + ) + ).toEqual({ + total: '30 days', + hot: '24 days', + warm: '6 days', + cold: undefined, + }); + }); + + test('hot, then warm, then cold, then delete', () => { + expect( + absoluteTimingToRelativeTiming( + deserializer({ + name: 'test', + phases: { + hot: { + min_age: '0ms', + actions: {}, + }, + warm: { + min_age: '24d', + actions: {}, + }, + cold: { + min_age: '2M', + actions: {}, + }, + delete: { + min_age: '2d', + actions: {}, + }, + }, + }) + ) + ).toEqual({ + total: '61 days', + hot: '24 days', + warm: '37 days', + cold: 'Less than a day', + }); + }); + + test('hot, then cold, then delete', () => { + expect( + absoluteTimingToRelativeTiming( + deserializer({ + name: 'test', + phases: { + hot: { + min_age: '0ms', + actions: {}, + }, + cold: { + min_age: '2M', + actions: {}, + }, + delete: { + min_age: '2d', + actions: {}, + }, + }, + }) + ) + ).toEqual({ + total: '61 days', + hot: '61 days', + warm: undefined, + cold: 'Less than a day', + }); + }); + + test('hot, then long warm, then short cold, then delete', () => { + expect( + absoluteTimingToRelativeTiming( + deserializer({ + name: 'test', + phases: { + hot: { + min_age: '0ms', + actions: {}, + }, + warm: { + min_age: '2M', + actions: {}, + }, + cold: { + min_age: '1d', + actions: {}, + }, + delete: { + min_age: '2d', + actions: {}, + }, + }, + }) + ) + ).toEqual({ + total: '61 days', + hot: '61 days', + warm: 'Less than a day', + cold: 'Less than a day', + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts new file mode 100644 index 0000000000000..c77b171a56bed --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts @@ -0,0 +1,185 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * READ ME: + * + * ILM policies express data age thresholds as minimum age from an absolute point of reference. + * The absolute point of reference could be when data was created, but it could also be when + * rollover has occurred. This is useful for configuring a policy, but when trying to understand + * how long data will be in a specific phase, when thinking of data tiers, it is not as useful. + * + * This code converts the absolute timings to _relative_ timings of the form: 30 days in hot phase, + * 40 days in warm phase then forever in cold phase. + */ + +import moment from 'moment'; +import { flow } from 'fp-ts/lib/function'; +import { i18n } from '@kbn/i18n'; + +import { splitSizeAndUnits } from '../../../lib/policies'; + +import { FormInternal } from '../types'; + +type MinAgePhase = 'warm' | 'cold' | 'delete'; + +type Phase = 'hot' | MinAgePhase; + +const i18nTexts = { + forever: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.Forever', { + defaultMessage: 'Forever', + }), + lessThanADay: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.lessThanADay', { + defaultMessage: 'Less than a day', + }), + day: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.day', { + defaultMessage: 'day', + }), + days: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.days', { + defaultMessage: 'days', + }), +}; + +interface AbsoluteTimings { + hot: { + min_age: undefined; + }; + warm?: { + min_age: string; + }; + cold?: { + min_age: string; + }; + delete?: { + min_age: string; + }; +} + +export interface PhaseAgeInMilliseconds { + total: number; + phases: { + hot: number; + warm?: number; + cold?: number; + }; +} + +const phaseOrder: Phase[] = ['hot', 'warm', 'cold', 'delete']; + +const getMinAge = (phase: MinAgePhase, formData: FormInternal) => ({ + min_age: formData.phases[phase]?.min_age + ? formData.phases[phase]!.min_age! + formData._meta[phase].minAgeUnit + : '0ms', +}); + +const formDataToAbsoluteTimings = (formData: FormInternal): AbsoluteTimings => { + const { _meta } = formData; + if (!_meta) { + return { hot: { min_age: undefined } }; + } + return { + hot: { min_age: undefined }, + warm: _meta.warm.enabled ? getMinAge('warm', formData) : undefined, + cold: _meta.cold.enabled ? getMinAge('cold', formData) : undefined, + delete: _meta.delete.enabled ? getMinAge('delete', formData) : undefined, + }; +}; + +/** + * See https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#date-math + * for all date math values. ILM policies also support "micros" and "nanos". + */ +const getPhaseMinAgeInMilliseconds = (phase: { min_age: string }): number => { + let milliseconds: number; + const { units, size } = splitSizeAndUnits(phase.min_age); + if (units === 'micros') { + milliseconds = parseInt(size, 10) / 1e3; + } else if (units === 'nanos') { + milliseconds = parseInt(size, 10) / 1e6; + } else { + milliseconds = moment.duration(size, units as any).asMilliseconds(); + } + return milliseconds; +}; + +/** + * Given a set of phase minimum age absolute timings, like hot phase 0ms and warm phase 3d, work out + * the number of milliseconds data will reside in phase. + */ +const calculateMilliseconds = (inputs: AbsoluteTimings): PhaseAgeInMilliseconds => { + return phaseOrder.reduce( + (acc, phaseName, idx) => { + // Delete does not have an age associated with it + if (phaseName === 'delete') { + return acc; + } + const phase = inputs[phaseName]; + if (!phase) { + return acc; + } + const nextPhase = phaseOrder + .slice(idx + 1) + .find((nextPhaseName) => Boolean(inputs[nextPhaseName])); // find the next existing phase + + let nextPhaseMinAge = Infinity; + + // If we have a next phase, calculate the timing between this phase and the next + if (nextPhase && inputs[nextPhase]?.min_age) { + nextPhaseMinAge = getPhaseMinAgeInMilliseconds(inputs[nextPhase] as { min_age: string }); + } + + return { + // data will be the age of the phase with the highest min age requirement + total: Math.max(acc.total, nextPhaseMinAge), + phases: { + ...acc.phases, + [phaseName]: Math.max(nextPhaseMinAge - acc.total, 0), // get the max age for the current phase, take 0 if negative number + }, + }; + }, + { + total: 0, + phases: { + hot: 0, + warm: inputs.warm ? 0 : undefined, + cold: inputs.cold ? 0 : undefined, + }, + } + ); +}; + +const millisecondsToDays = (milliseconds?: number): string | undefined => { + if (milliseconds == null) { + return; + } + if (!isFinite(milliseconds)) { + return i18nTexts.forever; + } + const days = milliseconds / 8.64e7; + return days < 1 + ? i18nTexts.lessThanADay + : `${Math.floor(days)} ${days === 1 ? i18nTexts.day : i18nTexts.days}`; +}; + +export const normalizeTimingsToHumanReadable = ({ + total, + phases, +}: PhaseAgeInMilliseconds): { total?: string; hot?: string; warm?: string; cold?: string } => { + return { + total: millisecondsToDays(total), + hot: millisecondsToDays(phases.hot), + warm: millisecondsToDays(phases.warm), + cold: millisecondsToDays(phases.cold), + }; +}; + +export const calculateRelativeTimingMs = flow(formDataToAbsoluteTimings, calculateMilliseconds); + +export const absoluteTimingToRelativeTiming = flow( + formDataToAbsoluteTimings, + calculateMilliseconds, + normalizeTimingsToHumanReadable +); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts new file mode 100644 index 0000000000000..9593fcc810a6f --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + absoluteTimingToRelativeTiming, + calculateRelativeTimingMs, + normalizeTimingsToHumanReadable, + PhaseAgeInMilliseconds, +} from './absolute_timing_to_relative_timing'; diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts index fc5f369e588f1..1cf84c70ff907 100644 --- a/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts +++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/policies/register_fetch_route.ts @@ -43,7 +43,7 @@ async function addLinkedIndices(client: ElasticsearchClient, policiesMap: Polici const response = await client.ilm.explainLifecycle<{ indices: { [indexName: string]: IndexLifecyclePolicy }; - }>({ index: '*' }, options); + }>({ index: '*,.*' }, options); // '*,.*' will include hidden indices const policyExplanation = response.body; Object.entries(policyExplanation.indices).forEach(([indexName, { policy }]) => { if (policy && policiesMap[policy]) { diff --git a/x-pack/plugins/index_management/kibana.json b/x-pack/plugins/index_management/kibana.json index af3d61c8808ef..5dcff0ba942e1 100644 --- a/x-pack/plugins/index_management/kibana.json +++ b/x-pack/plugins/index_management/kibana.json @@ -9,6 +9,6 @@ "requiredBundles": [ "kibanaReact", "esUiShared", - "runtimeFieldEditor" + "runtimeFields" ] } diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts index 652925a977fa0..36f7fecbcff21 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/shared_imports.ts @@ -58,7 +58,7 @@ export { RuntimeField, RuntimeFieldEditorFlyoutContent, RuntimeFieldEditorFlyoutContentProps, -} from '../../../../../runtime_field_editor/public'; +} from '../../../../../runtime_fields/public'; export { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; diff --git a/x-pack/plugins/infra/common/typed_json.ts b/x-pack/plugins/infra/common/typed_json.ts index 0ff9e42942ef2..f3e7608910e09 100644 --- a/x-pack/plugins/infra/common/typed_json.ts +++ b/x-pack/plugins/infra/common/typed_json.ts @@ -20,5 +20,3 @@ export const jsonArrayRT: rt.Type = rt.recursion('JsonArray', () => export const jsonObjectRT: rt.Type = rt.recursion('JsonObject', () => rt.record(rt.string, jsonValueRT) ); - -export { JsonValue, JsonArray, JsonObject }; diff --git a/x-pack/plugins/infra/public/components/log_stream/index.tsx b/x-pack/plugins/infra/public/components/log_stream/index.tsx index 997f81583fb64..3d69b6a022987 100644 --- a/x-pack/plugins/infra/public/components/log_stream/index.tsx +++ b/x-pack/plugins/infra/public/components/log_stream/index.tsx @@ -16,7 +16,7 @@ import { useLogStream } from '../../containers/logs/log_stream'; import { ScrollableLogTextStreamView } from '../logging/log_text_stream'; import { LogColumnRenderConfiguration } from '../../utils/log_column_render_configuration'; -import { JsonValue } from '../../../common/typed_json'; +import { JsonValue } from '../../../../../../src/plugins/kibana_utils/common'; const PAGE_THRESHOLD = 2; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/field_value.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/field_value.tsx index 4047e80846ca6..b13e3569c1e5b 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/field_value.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/field_value.tsx @@ -7,7 +7,7 @@ import stringify from 'json-stable-stringify'; import React from 'react'; import { euiStyled } from '../../../../../observability/public'; -import { JsonArray, JsonValue } from '../../../../common/typed_json'; +import { JsonArray, JsonValue } from '../../../../../../../src/plugins/kibana_utils/common'; import { ActiveHighlightMarker, highlightFieldValue, HighlightMarker } from './highlighting'; export const FieldValue: React.FC<{ diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx index a8165463d7ee6..fe5e7f305f60c 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { JsonValue } from '../../../../common/typed_json'; +import { JsonValue } from '../../../../../../../src/plugins/kibana_utils/common'; import { euiStyled } from '../../../../../observability/public'; import { LogColumn } from '../../../../common/http_api'; import { isFieldColumn, isHighlightFieldColumn } from '../../../utils/log_entry'; diff --git a/x-pack/plugins/infra/public/utils/log_column_render_configuration.tsx b/x-pack/plugins/infra/public/utils/log_column_render_configuration.tsx index 7ffbfcace88ce..95ebe37b64c97 100644 --- a/x-pack/plugins/infra/public/utils/log_column_render_configuration.tsx +++ b/x-pack/plugins/infra/public/utils/log_column_render_configuration.tsx @@ -5,7 +5,7 @@ */ import { ReactNode } from 'react'; -import { JsonValue } from '../../common/typed_json'; +import { JsonValue } from '../../../../../src/plugins/kibana_utils/common'; /** * Interface for common configuration properties, regardless of the column type. diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts index 93a7bc9a0830b..705d7bf34c1c6 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/adapter_types.ts @@ -19,7 +19,7 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../pl import { SpacesPluginSetup } from '../../../../../../plugins/spaces/server'; import { PluginSetupContract as AlertingPluginContract } from '../../../../../alerts/server'; import { MlPluginSetup } from '../../../../../ml/server'; -import { JsonArray, JsonValue } from '../../../../common/typed_json'; +import { JsonArray, JsonValue } from '../../../../../../../src/plugins/kibana_utils/common'; export interface InfraServerPluginSetupDeps { data: DataPluginSetup; diff --git a/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts index 4637f3ab41782..98c42ab7d98ab 100644 --- a/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts @@ -11,7 +11,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import * as runtimeTypes from 'io-ts'; import { compact } from 'lodash'; import { RequestHandlerContext } from 'src/core/server'; -import { JsonArray } from '../../../../common/typed_json'; +import { JsonArray } from '../../../../../../../src/plugins/kibana_utils/common'; import { LogEntriesAdapter, LogEntriesParams, diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts index 52cf6f46716b3..d9f125908b32d 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts @@ -5,7 +5,7 @@ */ import { RequestHandlerContext } from 'src/core/server'; -import { JsonObject } from '../../../../common/typed_json'; +import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/common'; import { LogEntriesSummaryBucket, LogEntriesSummaryHighlightsBucket, diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/message.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/message.ts index f9775e127088a..19ab82c9c5ac1 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/message.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/message.ts @@ -5,7 +5,7 @@ */ import { LogMessagePart } from '../../../../common/http_api/log_entries'; -import { JsonArray, JsonValue } from '../../../../common/typed_json'; +import { JsonArray, JsonValue } from '../../../../../../../src/plugins/kibana_utils/common'; import { LogMessageFormattingCondition, LogMessageFormattingFieldValueConditionValue, diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/rule_types.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/rule_types.ts index 4569f4b8e8a91..e2368d7475d48 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/rule_types.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/rule_types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { JsonValue } from '../../../../common/typed_json'; +import { JsonValue } from '../../../../../../../src/plugins/kibana_utils/common'; export interface LogMessageFormattingRule { when: LogMessageFormattingCondition; diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/get_metrics_aggregations.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/get_metrics_aggregations.ts index 2421469eb1bdd..578ebaffc8369 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/get_metrics_aggregations.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/get_metrics_aggregations.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { JsonObject } from '../../../../common/typed_json'; +import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/common'; import { InventoryItemType, MetricsUIAggregation, diff --git a/x-pack/plugins/infra/server/utils/serialized_query.ts b/x-pack/plugins/infra/server/utils/serialized_query.ts index 932df847e65d0..8dfe00fcd380e 100644 --- a/x-pack/plugins/infra/server/utils/serialized_query.ts +++ b/x-pack/plugins/infra/server/utils/serialized_query.ts @@ -6,7 +6,7 @@ import { UserInputError } from 'apollo-server-errors'; -import { JsonObject } from '../../common/typed_json'; +import { JsonObject } from '../../../../../src/plugins/kibana_utils/common'; export const parseFilterQuery = ( filterQuery: string | null | undefined diff --git a/x-pack/plugins/infra/server/utils/typed_search_strategy.ts b/x-pack/plugins/infra/server/utils/typed_search_strategy.ts index 1234aea507f3f..b7132fa5dc3aa 100644 --- a/x-pack/plugins/infra/server/utils/typed_search_strategy.ts +++ b/x-pack/plugins/infra/server/utils/typed_search_strategy.ts @@ -6,7 +6,8 @@ import * as rt from 'io-ts'; import stringify from 'json-stable-stringify'; -import { JsonValue, jsonValueRT } from '../../common/typed_json'; +import { JsonValue } from '../../../../../src/plugins/kibana_utils/common'; +import { jsonValueRT } from '../../common/typed_json'; import { SearchStrategyError } from '../../common/search_strategies/common/errors'; import { ShardFailure } from './elasticsearch_runtime_types'; diff --git a/x-pack/plugins/infra/types/eui.d.ts b/x-pack/plugins/infra/types/eui.d.ts index 802e11dd8fc84..e3009820d1624 100644 --- a/x-pack/plugins/infra/types/eui.d.ts +++ b/x-pack/plugins/infra/types/eui.d.ts @@ -11,9 +11,6 @@ import { IconType, ToolTipPositions } from '@elastic/eui'; import { CommonProps } from '@elastic/eui/src/components/common'; -import moment from 'moment'; -import { MouseEventHandler, ReactType, Ref } from 'react'; -import { JsonObject } from '../common/typed_json'; declare module '@elastic/eui' { interface EuiFormControlLayoutIconProps { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index 1144a1043c5b1..fd62b8ca962ab 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -220,6 +220,7 @@ export function DimensionEditor(props: DimensionEditorProps) { 'data-test-subj': `lns-indexPatternDimension-${operationType}${ compatibleWithCurrentField ? '' : ' incompatible' }`, + [`aria-pressed`]: isActive, onClick() { if ( operationDefinitionMap[operationType].input === 'none' || diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index 625084000fa93..fae086506f015 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -259,7 +259,7 @@ export const termsOperation: OperationDefinition { - setPopoverOpen(true); + setPopoverOpen(!popoverOpen); }} > {i18n.translate('xpack.lens.indexPattern.terms.advancedSettings', { diff --git a/x-pack/plugins/lens/public/persistence/filter_references.test.ts b/x-pack/plugins/lens/public/persistence/filter_references.test.ts index 23c0cd1d11f1b..02b99c1b63ee5 100644 --- a/x-pack/plugins/lens/public/persistence/filter_references.test.ts +++ b/x-pack/plugins/lens/public/persistence/filter_references.test.ts @@ -56,6 +56,18 @@ describe('filter saved object references', () => { `); }); + it('should remove index and value from persistable filter', () => { + const { persistableFilters } = extractFilterReferences([ + { ...filters[0], meta: { ...filters[0].meta, value: 'CN' } }, + { ...filters[1], meta: { ...filters[1].meta, value: 'US' } }, + ]); + expect(persistableFilters.length).toBe(2); + persistableFilters.forEach((filter) => { + expect(filter.meta.hasOwnProperty('index')).toBe(false); + expect(filter.meta.hasOwnProperty('value')).toBe(false); + }); + }); + it('should restore the same filter after extracting and injecting', () => { const { persistableFilters, references } = extractFilterReferences(filters); expect(injectFilterReferences(persistableFilters, references)).toEqual(filters); diff --git a/x-pack/plugins/lens/public/persistence/filter_references.ts b/x-pack/plugins/lens/public/persistence/filter_references.ts index 47564e510ce9c..0dca0941662c7 100644 --- a/x-pack/plugins/lens/public/persistence/filter_references.ts +++ b/x-pack/plugins/lens/public/persistence/filter_references.ts @@ -22,14 +22,18 @@ export function extractFilterReferences( type: 'index-pattern', id: filterRow.meta.index, }); - return { + const newFilter = { ...filterRow, meta: { ...filterRow.meta, indexRefName: refName, - index: undefined, }, }; + // remove index because it's specified by indexRefName + delete newFilter.meta.index; + // remove value because it can't be persisted + delete newFilter.meta.value; + return newFilter; }); return { persistableFilters, references }; diff --git a/x-pack/plugins/lists/server/routes/read_privileges_route.mock.ts b/x-pack/plugins/lists/server/routes/read_privileges_route.mock.ts index cef6233440db6..a0dc76b488e47 100644 --- a/x-pack/plugins/lists/server/routes/read_privileges_route.mock.ts +++ b/x-pack/plugins/lists/server/routes/read_privileges_route.mock.ts @@ -49,6 +49,7 @@ interface Index { create: boolean; manage_follow_index: boolean; manage_leader_index: boolean; + maintenance: boolean; write: boolean; }; } @@ -113,6 +114,7 @@ export const getReadPrivilegeMock = ( delete: booleanValues, delete_index: booleanValues, index: booleanValues, + maintenance: booleanValues, manage: booleanValues, manage_follow_index: booleanValues, manage_ilm: booleanValues, @@ -165,6 +167,7 @@ export const getReadPrivilegeMock = ( delete: booleanValues, delete_index: booleanValues, index: booleanValues, + maintenance: booleanValues, manage: booleanValues, manage_follow_index: booleanValues, manage_ilm: booleanValues, diff --git a/x-pack/plugins/maps/public/api/create_layer_descriptors.ts b/x-pack/plugins/maps/public/api/create_layer_descriptors.ts index 5d3eca2200ad5..fbe8da55ed702 100644 --- a/x-pack/plugins/maps/public/api/create_layer_descriptors.ts +++ b/x-pack/plugins/maps/public/api/create_layer_descriptors.ts @@ -6,6 +6,7 @@ import { LayerDescriptor } from '../../common/descriptor_types'; import { lazyLoadMapModules } from '../lazy_load_bundle'; +import type { CreateLayerDescriptorParams } from '../classes/sources/es_search_source'; export const createLayerDescriptors = { async createSecurityLayerDescriptors( @@ -19,4 +20,10 @@ export const createLayerDescriptors = { const mapModules = await lazyLoadMapModules(); return mapModules.createBasemapLayerDescriptor(); }, + async createESSearchSourceLayerDescriptor( + params: CreateLayerDescriptorParams + ): Promise { + const mapModules = await lazyLoadMapModules(); + return mapModules.createESSearchSourceLayerDescriptor(params); + }, }; diff --git a/x-pack/plugins/maps/public/api/start_api.ts b/x-pack/plugins/maps/public/api/start_api.ts index 7109ca8ce5c9d..5877520e9227b 100644 --- a/x-pack/plugins/maps/public/api/start_api.ts +++ b/x-pack/plugins/maps/public/api/start_api.ts @@ -7,6 +7,7 @@ import { LayerDescriptor } from '../../common/descriptor_types'; import { SourceRegistryEntry } from '../classes/sources/source_registry'; import { LayerWizard } from '../classes/layers/layer_wizard_registry'; +import type { CreateLayerDescriptorParams } from '../classes/sources/es_search_source'; export interface MapsStartApi { createLayerDescriptors: { @@ -15,6 +16,9 @@ export interface MapsStartApi { indexPatternTitle: string ) => Promise; createBasemapLayerDescriptor: () => Promise; + createESSearchSourceLayerDescriptor: ( + params: CreateLayerDescriptorParams + ) => Promise; }; registerLayerWizard(layerWizard: LayerWizard): Promise; registerSource(entry: SourceRegistryEntry): Promise; diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.test.tsx b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.test.tsx index 5d234f5be44af..1321593f015c0 100644 --- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.test.tsx +++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.test.tsx @@ -6,7 +6,6 @@ import { SCALING_TYPES, SOURCE_TYPES } from '../../../../common/constants'; import { BlendedVectorLayer } from './blended_vector_layer'; -// @ts-expect-error import { ESSearchSource } from '../../sources/es_search_source'; import { ESGeoGridSourceDescriptor } from '../../../../common/descriptor_types'; diff --git a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx index 2db12db33ff27..a0029c5c64e0b 100644 --- a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx +++ b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx @@ -16,7 +16,6 @@ import { import { getFileUploadComponent } from '../../../kibana_services'; import { GeojsonFileSource } from '../../sources/geojson_file_source'; import { VectorLayer } from '../../layers/vector_layer/vector_layer'; -// @ts-expect-error import { createDefaultLayerDescriptor } from '../../sources/es_search_source'; import { RenderWizardArguments } from '../../layers/layer_wizard_registry'; import { FileUploadComponentProps } from '../../../../../file_upload/public'; diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/create_layer_descriptor.test.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/create_layer_descriptor.test.ts new file mode 100644 index 0000000000000..b734186b52c93 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/create_layer_descriptor.test.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createLayerDescriptor } from './create_layer_descriptor'; +import { ES_GEO_FIELD_TYPE } from '../../../../common/constants'; + +jest.mock('../../../kibana_services', () => { + return { + getIsDarkMode() { + return false; + }, + }; +}); +jest.mock('../../../licensed_features', () => { + return { + getIsGoldPlus() { + return true; + }, + }; +}); +jest.mock('uuid/v4', () => { + return function () { + return '12345'; + }; +}); + +test('Should create layer descriptor', () => { + const layerDescriptor = createLayerDescriptor({ + indexPatternId: 'myIndexPattern', + geoFieldName: 'myGeoField', + geoFieldType: ES_GEO_FIELD_TYPE.GEO_POINT, + }); + expect(layerDescriptor.sourceDescriptor).toEqual({ + applyGlobalQuery: true, + applyGlobalTime: true, + filterByMapBounds: true, + geoField: 'myGeoField', + id: '12345', + indexPatternId: 'myIndexPattern', + scalingType: 'CLUSTERS', + sortField: '', + sortOrder: 'desc', + tooltipProperties: [], + topHitsSize: 1, + topHitsSplitField: '', + type: 'ES_SEARCH', + }); +}); diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/create_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/create_layer_descriptor.ts new file mode 100644 index 0000000000000..4d5d14ef3b2a3 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/create_layer_descriptor.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Query } from 'src/plugins/data/public'; +import { LayerDescriptor } from '../../../../common/descriptor_types'; +import { ES_GEO_FIELD_TYPE, SCALING_TYPES } from '../../../../common/constants'; +import { ESSearchSource } from './es_search_source'; +import { VectorLayer } from '../../layers/vector_layer/vector_layer'; +import { getIsGoldPlus } from '../../../licensed_features'; + +export interface CreateLayerDescriptorParams { + indexPatternId: string; + geoFieldName: string; + geoFieldType: ES_GEO_FIELD_TYPE; + query?: Query; +} + +export function createLayerDescriptor({ + indexPatternId, + geoFieldName, + geoFieldType, + query, +}: CreateLayerDescriptorParams): LayerDescriptor { + // Prefer clusters for geo_shapes if liscensing is enabled. + const scalingType = + geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT || + (geoFieldType === ES_GEO_FIELD_TYPE.GEO_SHAPE && getIsGoldPlus()) + ? SCALING_TYPES.CLUSTERS + : SCALING_TYPES.LIMIT; + const sourceDescriptor = ESSearchSource.createDescriptor({ + indexPatternId, + geoField: geoFieldName, + scalingType, + }); + + return VectorLayer.createDescriptor({ sourceDescriptor, query }); +} diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/index.js b/x-pack/plugins/maps/public/classes/sources/es_search_source/index.ts similarity index 80% rename from x-pack/plugins/maps/public/classes/sources/es_search_source/index.js rename to x-pack/plugins/maps/public/classes/sources/es_search_source/index.ts index 6ae327a18b7c2..44e39b7c637d8 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/index.js +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +export { createLayerDescriptor, CreateLayerDescriptorParams } from './create_layer_descriptor'; export { ESSearchSource } from './es_search_source'; export { createDefaultLayerDescriptor, diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.js.snap b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.js.snap index ad00889b87e46..a9a1afabfc193 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.js.snap +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.js.snap @@ -97,13 +97,32 @@ exports[`Should render metrics expression for metrics 1`] = ` values={Object {}} /> - - - +

    `; diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.js index a66236dc9dbf2..e52d9ece5333e 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.js @@ -53,6 +53,7 @@ export class MetricsExpression extends Component { fields={this.props.rightFields} metrics={this.props.metrics} onChange={this.props.onChange} + allowMultipleMetrics={true} /> ); }; diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js index e0e1556ecde06..48923773f5701 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js @@ -22,6 +22,7 @@ test('Should render metrics expression for metrics', () => { const component = shallow( ; @@ -71,6 +72,7 @@ interface LazyLoadedMapModules { metricFieldName?: string; }) => LayerDescriptor | null; createBasemapLayerDescriptor: () => LayerDescriptor | null; + createESSearchSourceLayerDescriptor: (params: CreateLayerDescriptorParams) => LayerDescriptor; } export async function lazyLoadMapModules(): Promise { @@ -90,6 +92,7 @@ export async function lazyLoadMapModules(): Promise { createTileMapLayerDescriptor, createRegionMapLayerDescriptor, createBasemapLayerDescriptor, + createESSearchSourceLayerDescriptor, } = await import('./lazy'); resolve({ @@ -103,6 +106,7 @@ export async function lazyLoadMapModules(): Promise { createTileMapLayerDescriptor, createRegionMapLayerDescriptor, createBasemapLayerDescriptor, + createESSearchSourceLayerDescriptor, }); }); return loadModulesPromise; diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts index 4d816fd2f2c61..4e78db9cbde23 100644 --- a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts +++ b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts @@ -4,9 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -// These are map-dependencies of the embeddable. -// By lazy-loading these, the Maps-app can register the embeddable when the plugin mounts, without actually pulling all the code. - export * from '../../embeddable/map_embeddable'; export * from '../../kibana_services'; export { renderApp } from '../../render_app'; @@ -16,3 +13,4 @@ export { registerSource } from '../../classes/sources/source_registry'; export { createTileMapLayerDescriptor } from '../../classes/layers/create_tile_map_layer_descriptor'; export { createRegionMapLayerDescriptor } from '../../classes/layers/create_region_map_layer_descriptor'; export { createBasemapLayerDescriptor } from '../../classes/layers/create_basemap_layer_descriptor'; +export { createLayerDescriptor as createESSearchSourceLayerDescriptor } from '../../classes/sources/es_search_source'; diff --git a/x-pack/plugins/ml/common/types/job_service.ts b/x-pack/plugins/ml/common/types/job_service.ts new file mode 100644 index 0000000000000..3121c3a82a387 --- /dev/null +++ b/x-pack/plugins/ml/common/types/job_service.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Job, JobStats } from './anomaly_detection_jobs'; + +export interface MlJobsResponse { + jobs: Job[]; + count: number; +} + +export interface MlJobsStatsResponse { + jobs: JobStats[]; + count: number; +} + +export interface JobsExistResponse { + [jobId: string]: { + exists: boolean; + isGroup: boolean; + }; +} diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_chart_config_builder.test.js.snap b/x-pack/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_chart_config_builder.test.js.snap index 3713b28a103ec..e2e1140f5c5b4 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_chart_config_builder.test.js.snap +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_chart_config_builder.test.js.snap @@ -47,6 +47,7 @@ Object { "jobId": "mock-job-id", "metricFieldName": "responsetime", "metricFunction": "avg", + "summaryCountFieldName": undefined, "timeField": "@timestamp", } `; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap b/x-pack/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap index ecb7fc07711e8..80ad6ed6bfab0 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/__snapshots__/explorer_charts_container_service.test.js.snap @@ -53,6 +53,7 @@ Object { "loading": true, "metricFieldName": "responsetime", "metricFunction": "avg", + "summaryCountFieldName": undefined, "timeField": "@timestamp", }, ], @@ -600,6 +601,7 @@ Object { "plotLatest": 1486783800000, "selectedEarliest": 1486656000000, "selectedLatest": 1486670399999, + "summaryCountFieldName": undefined, "timeField": "@timestamp", }, ], diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js index a2c530c9ca494..3dc1c0234584d 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js @@ -126,6 +126,7 @@ export const anomalyDataChange = function ( datafeedQuery, config.metricFunction, config.metricFieldName, + config.summaryCountFieldName, config.timeField, range.min, range.max, 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 bd781d32a6b06..97f9515810a60 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 @@ -29,6 +29,7 @@ import { saveJob } from './edit_utils'; import { loadFullJob } from '../utils'; import { validateModelMemoryLimit, validateGroupNames, isValidCustomUrls } from '../validate_job'; import { toastNotificationServiceProvider } from '../../../../services/toast_notification_service'; +import { ml } from '../../../../services/ml_api_service'; import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; import { collapseLiteralStrings } from '../../../../../../shared_imports'; import { DATAFEED_STATE } from '../../../../../../common/constants/states'; @@ -195,16 +196,24 @@ export class EditJobFlyoutUI extends Component { } if (jobDetails.jobGroups !== undefined) { - if (jobDetails.jobGroups.some((j) => this.props.allJobIds.includes(j))) { - jobGroupsValidationError = i18n.translate( - 'xpack.ml.jobsList.editJobFlyout.groupsAndJobsHasSameIdErrorMessage', - { - defaultMessage: - 'A job with this ID already exists. Groups and jobs cannot use the same ID.', + jobGroupsValidationError = validateGroupNames(jobDetails.jobGroups).message; + if (jobGroupsValidationError === '') { + ml.jobs.jobsExist(jobDetails.jobGroups, true).then((resp) => { + const groups = Object.values(resp); + const valid = groups.some((g) => g.exists === true && g.isGroup === false) === false; + if (valid === false) { + this.setState({ + jobGroupsValidationError: i18n.translate( + 'xpack.ml.jobsList.editJobFlyout.groupsAndJobsHasSameIdErrorMessage', + { + defaultMessage: + 'A job with this ID already exists. Groups and jobs cannot use the same ID.', + } + ), + isValidJobDetails: false, + }); } - ); - } else { - jobGroupsValidationError = validateGroupNames(jobDetails.jobGroups).message; + }); } } 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 c3b4471269bb8..c66527f1fb213 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 @@ -28,7 +28,7 @@ import { CREATED_BY_LABEL, SHARED_RESULTS_INDEX_NAME, } from '../../../../../../common/constants/new_job'; -import { isSparseDataJob, collectAggs } from './util/general'; +import { collectAggs } from './util/general'; import { parseInterval } from '../../../../../../common/util/parse_interval'; import { Calendar } from '../../../../../../common/types/calendars'; import { mlCalendarService } from '../../../../services/calendar_service'; @@ -681,7 +681,6 @@ export class JobCreator { ) { this.useDedicatedIndex = true; } - this._sparseData = isSparseDataJob(job, datafeed); this._scriptFields = []; if (this._datafeed_config.script_fields !== undefined) { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts index 95141f31cdea6..a476d72851dbb 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/multi_metric_job_creator.ts @@ -17,6 +17,7 @@ import { createBasicDetector } from './util/default_configs'; import { JOB_TYPE, CREATED_BY_LABEL } from '../../../../../../common/constants/new_job'; import { getRichDetectors } from './util/general'; import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; +import { isSparseDataJob } from './util/general'; export class MultiMetricJobCreator extends JobCreator { // a multi-metric job has one optional overall partition field @@ -92,6 +93,7 @@ export class MultiMetricJobCreator extends JobCreator { public cloneFromExistingJob(job: Job, datafeed: Datafeed) { this._overrideConfigs(job, datafeed); this.createdBy = CREATED_BY_LABEL.MULTI_METRIC; + this._sparseData = isSparseDataJob(job, datafeed); const detectors = getRichDetectors(job, datafeed, this.additionalFields, false); if (datafeed.aggregations !== undefined) { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts index e884da5470cc5..58d192732b13d 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/single_metric_job_creator.ts @@ -22,6 +22,7 @@ import { import { JOB_TYPE, CREATED_BY_LABEL } from '../../../../../../common/constants/new_job'; import { getRichDetectors } from './util/general'; import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; +import { isSparseDataJob } from './util/general'; export class SingleMetricJobCreator extends JobCreator { protected _type: JOB_TYPE = JOB_TYPE.SINGLE_METRIC; @@ -196,6 +197,7 @@ export class SingleMetricJobCreator extends JobCreator { public cloneFromExistingJob(job: Job, datafeed: Datafeed) { this._overrideConfigs(job, datafeed); this.createdBy = CREATED_BY_LABEL.SINGLE_METRIC; + this._sparseData = isSparseDataJob(job, datafeed); const detectors = getRichDetectors(job, datafeed, this.additionalFields, false); this.removeAllDetectors(); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts index 1c012033e97c8..cf08de196a7d8 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/job_validator.ts @@ -14,9 +14,15 @@ import { } from '../../../../../../common/util/job_utils'; import { getNewJobLimits } from '../../../../services/ml_server_info'; import { JobCreator, JobCreatorType, isCategorizationJobCreator } from '../job_creator'; -import { populateValidationMessages, checkForExistingJobAndGroupIds } from './util'; -import { ExistingJobsAndGroups } from '../../../../services/job_service'; -import { cardinalityValidator, CardinalityValidatorResult } from './validators'; +import { populateValidationMessages } from './util'; +import { + cardinalityValidator, + CardinalityValidatorResult, + jobIdValidator, + groupIdsValidator, + JobExistsResult, + GroupsExistResult, +} from './validators'; import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../../../common/constants/categorization_job'; import { JOB_TYPE } from '../../../../../../common/constants/new_job'; @@ -25,7 +31,9 @@ import { JOB_TYPE } from '../../../../../../common/constants/new_job'; // after every keystroke export const VALIDATION_DELAY_MS = 500; -type AsyncValidatorsResult = Partial; +type AsyncValidatorsResult = Partial< + CardinalityValidatorResult & JobExistsResult & GroupsExistResult +>; /** * Union of possible validation results. @@ -69,7 +77,6 @@ export class JobValidator { private _validateTimeout: ReturnType | null = null; private _asyncValidators$: Array> = []; private _asyncValidatorsResult$: Observable; - private _existingJobsAndGroups: ExistingJobsAndGroups; private _basicValidations: BasicValidations = { jobId: { valid: true }, groupIds: { valid: true }, @@ -97,7 +104,7 @@ export class JobValidator { */ public validationResult$: Observable; - constructor(jobCreator: JobCreatorType, existingJobsAndGroups: ExistingJobsAndGroups) { + constructor(jobCreator: JobCreatorType) { this._jobCreator = jobCreator; this._lastJobConfig = this._jobCreator.formattedJobJson; this._lastDatafeedConfig = this._jobCreator.formattedDatafeedJson; @@ -105,9 +112,12 @@ export class JobValidator { basic: false, advanced: false, }; - this._existingJobsAndGroups = existingJobsAndGroups; - this._asyncValidators$ = [cardinalityValidator(this._jobCreatorSubject$)]; + this._asyncValidators$ = [ + cardinalityValidator(this._jobCreatorSubject$), + jobIdValidator(this._jobCreatorSubject$), + groupIdsValidator(this._jobCreatorSubject$), + ]; this._asyncValidatorsResult$ = combineLatest(this._asyncValidators$).pipe( map((res) => { @@ -208,14 +218,6 @@ export class JobValidator { datafeedConfig ); - // run addition job and group id validation - const idResults = checkForExistingJobAndGroupIds( - this._jobCreator.jobId, - this._jobCreator.groups, - this._existingJobsAndGroups - ); - populateValidationMessages(idResults, this._basicValidations, jobConfig, datafeedConfig); - this._validationSummary.basic = this._isOverallBasicValid(); // Update validation results subject this._basicValidationResult$.next(this._basicValidations); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts index 04be935ed4399..9bdb4c7708b40 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/util.ts @@ -13,8 +13,6 @@ import { } from '../../../../../../common/constants/validation'; import { getNewJobLimits } from '../../../../services/ml_server_info'; import { ValidationResults } from '../../../../../../common/util/job_utils'; -import { ExistingJobsAndGroups } from '../../../../services/job_service'; -import { JobValidationMessage } from '../../../../../../common/constants/messages'; export function populateValidationMessages( validationResults: ValidationResults, @@ -204,36 +202,6 @@ export function populateValidationMessages( } } -export function checkForExistingJobAndGroupIds( - jobId: string, - groupIds: string[], - existingJobsAndGroups: ExistingJobsAndGroups -): ValidationResults { - const messages: JobValidationMessage[] = []; - - // check that job id does not already exist as a job or group or a newly created group - if ( - existingJobsAndGroups.jobIds.includes(jobId) || - existingJobsAndGroups.groupIds.includes(jobId) || - groupIds.includes(jobId) - ) { - messages.push({ id: 'job_id_already_exists' }); - } - - // check that groups that have been newly added in this job do not already exist as job ids - const newGroups = groupIds.filter((g) => !existingJobsAndGroups.groupIds.includes(g)); - if (existingJobsAndGroups.jobIds.some((g) => newGroups.includes(g))) { - messages.push({ id: 'job_group_id_already_exists' }); - } - - return { - messages, - valid: messages.length === 0, - contains: (id: string) => messages.some((m) => id === m.id), - find: (id: string) => messages.find((m) => id === m.id), - }; -} - function invalidTimeIntervalMessage(value: string | undefined) { return i18n.translate( 'xpack.ml.newJob.wizard.validateJob.frequencyInvalidTimeIntervalFormatErrorMessage', diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/validators.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/validators.ts index eabf5588579c5..d17c5507722f4 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/validators.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_validator/validators.ts @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators'; -import { Observable, Subject } from 'rxjs'; +import { i18n } from '@kbn/i18n'; +import { distinctUntilChanged, filter, map, pluck, switchMap, startWith } from 'rxjs/operators'; +import { combineLatest, Observable, Subject } from 'rxjs'; import { CardinalityModelPlotHigh, CardinalityValidationResult, @@ -13,6 +14,7 @@ import { } from '../../../../services/ml_api_service'; import { JobCreator } from '../job_creator'; import { CombinedJob } from '../../../../../../common/types/anomaly_detection_jobs'; +import { BasicValidations } from './job_validator'; export enum VALIDATOR_SEVERITY { ERROR, @@ -26,8 +28,30 @@ export interface CardinalityValidatorError { }; } +const jobExistsErrorMessage = i18n.translate( + 'xpack.ml.newJob.wizard.validateJob.asyncJobNameAlreadyExists', + { + defaultMessage: + 'Job ID already exists. A job ID cannot be the same as an existing job or group.', + } +); +const groupExistsErrorMessage = i18n.translate( + 'xpack.ml.newJob.wizard.validateJob.asyncGroupNameAlreadyExists', + { + defaultMessage: + 'Group ID already exists. A group ID cannot be the same as an existing group or job.', + } +); + export type CardinalityValidatorResult = CardinalityValidatorError | null; +export type JobExistsResult = { + jobIdExists: BasicValidations['jobId']; +} | null; +export type GroupsExistResult = { + groupIdsExist: BasicValidations['groupIds']; +} | null; + export function isCardinalityModelPlotHigh( cardinalityValidationResult: CardinalityValidationResult ): cardinalityValidationResult is CardinalityModelPlotHigh { @@ -39,39 +63,95 @@ export function isCardinalityModelPlotHigh( export function cardinalityValidator( jobCreator$: Subject ): Observable { + return combineLatest([ + jobCreator$.pipe(pluck('modelPlot')), + jobCreator$.pipe( + filter((jobCreator) => { + return jobCreator?.modelPlot; + }), + map((jobCreator) => { + return { + jobCreator, + analysisConfigString: JSON.stringify(jobCreator.jobConfig.analysis_config, null, 2), + }; + }), + distinctUntilChanged((prev, curr) => { + return prev.analysisConfigString === curr.analysisConfigString; + }), + switchMap(({ jobCreator }) => { + // Perform a cardinality check only with enabled model plot. + return ml + .validateCardinality$({ + ...jobCreator.jobConfig, + datafeed_config: jobCreator.datafeedConfig, + } as CombinedJob) + .pipe( + map((validationResults) => { + for (const validationResult of validationResults) { + if (isCardinalityModelPlotHigh(validationResult)) { + return { + highCardinality: { + value: validationResult.modelPlotCardinality, + severity: VALIDATOR_SEVERITY.WARNING, + }, + }; + } + } + return null; + }) + ); + }), + startWith(null) + ), + ]).pipe( + map(([isModelPlotEnabled, cardinalityValidationResult]) => { + return isModelPlotEnabled ? cardinalityValidationResult : null; + }) + ); +} + +export function jobIdValidator(jobCreator$: Subject): Observable { return jobCreator$.pipe( - // Perform a cardinality check only with enabled model plot. - filter((jobCreator) => { - return jobCreator?.modelPlot; - }), map((jobCreator) => { + return jobCreator.jobId; + }), + // No need to perform an API call if the analysis configuration hasn't been changed + distinctUntilChanged((prevJobId, currJobId) => prevJobId === currJobId), + switchMap((jobId) => ml.jobs.jobsExist$([jobId], true)), + map((jobExistsResults) => { + const jobs = Object.values(jobExistsResults); + const valid = jobs?.[0].exists === false; return { - jobCreator, - analysisConfigString: JSON.stringify(jobCreator.jobConfig.analysis_config), + jobIdExists: { + valid, + ...(valid ? {} : { message: jobExistsErrorMessage }), + }, }; - }), + }) + ); +} + +export function groupIdsValidator(jobCreator$: Subject): Observable { + return jobCreator$.pipe( + map((jobCreator) => jobCreator.groups), // No need to perform an API call if the analysis configuration hasn't been changed - distinctUntilChanged((prev, curr) => { - return prev.analysisConfigString === curr.analysisConfigString; - }), - switchMap(({ jobCreator }) => { - return ml.validateCardinality$({ - ...jobCreator.jobConfig, - datafeed_config: jobCreator.datafeedConfig, - } as CombinedJob); + distinctUntilChanged( + (prevGroups, currGroups) => JSON.stringify(prevGroups) === JSON.stringify(currGroups) + ), + switchMap((groups) => { + return ml.jobs.jobsExist$(groups, true); }), - map((validationResults) => { - for (const validationResult of validationResults) { - if (isCardinalityModelPlotHigh(validationResult)) { - return { - highCardinality: { - value: validationResult.modelPlotCardinality, - severity: VALIDATOR_SEVERITY.WARNING, - }, - }; - } - } - return null; + map((jobExistsResults) => { + const groups = Object.values(jobExistsResults); + // only match jobs that exist but aren't groups. + // as we should allow existing groups to be reused. + const valid = groups.some((g) => g.exists === true && g.isGroup === false) === false; + return { + groupIdsExist: { + valid, + ...(valid ? {} : { message: groupExistsErrorMessage }), + }, + }; }) ); } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx index a693127e07f48..3fd191d653826 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/job_details_step/components/groups/groups_input.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useState, useContext, useEffect } from 'react'; +import React, { FC, useState, useContext, useEffect, useMemo } from 'react'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { JobCreatorContext } from '../../../job_creator_context'; @@ -17,7 +17,19 @@ export const GroupsInput: FC = () => { ); const { existingJobsAndGroups } = useContext(JobCreatorContext); const [selectedGroups, setSelectedGroups] = useState(jobCreator.groups); - const [validation, setValidation] = useState(jobValidator.groupIds); + + const validation = useMemo(() => { + const valid = + jobValidator.groupIds.valid === true && + jobValidator.latestValidationResult.groupIdsExist?.valid === true; + const message = + jobValidator.groupIds.message ?? jobValidator.latestValidationResult.groupIdsExist?.message; + + return { + valid, + message, + }; + }, [jobValidatorUpdated]); useEffect(() => { jobCreator.groups = selectedGroups; @@ -61,10 +73,6 @@ export const GroupsInput: FC = () => { setSelectedGroups([...selectedOptions, newGroup].map((g) => g.label)); } - useEffect(() => { - setValidation(jobValidator.groupIds); - }, [jobValidatorUpdated]); - return ( { JobCreatorContext ); const [jobId, setJobId] = useState(jobCreator.jobId); - const [validation, setValidation] = useState(jobValidator.jobId); + + const validation = useMemo(() => { + const isEmptyId = jobId === ''; + const valid = + isEmptyId === true || + (jobValidator.jobId.valid === true && + jobValidator.latestValidationResult.jobIdExists?.valid === true); + + const message = + jobValidator.jobId.message ?? jobValidator.latestValidationResult.jobIdExists?.message; + + return { + valid, + message, + }; + }, [jobValidatorUpdated]); useEffect(() => { jobCreator.jobId = jobId; jobCreatorUpdate(); }, [jobId]); - useEffect(() => { - const isEmptyId = jobId === ''; - setValidation({ - valid: isEmptyId === true || jobValidator.jobId.valid, - message: isEmptyId === false ? jobValidator.jobId.message : '', - }); - }, [jobValidatorUpdated]); - return ( = ({ jobValidator.jobId.valid && jobValidator.modelMemoryLimit.valid && jobValidator.groupIds.valid && + jobValidator.latestValidationResult.jobIdExists?.valid === true && + jobValidator.latestValidationResult.groupIdsExist?.valid === true && jobValidator.validating === false; setNextActive(active); }, [jobValidatorUpdated]); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx index 8e223b69b00e8..8f7f93763fdd6 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/new_job/page.tsx @@ -182,7 +182,7 @@ export const Page: FC = ({ existingJobsAndGroups, jobType }) => { const chartLoader = new ChartLoader(mlContext.currentIndexPattern, mlContext.combinedQuery); - const jobValidator = new JobValidator(jobCreator, existingJobsAndGroups); + const jobValidator = new JobValidator(jobCreator); const resultsLoader = new ResultsLoader(jobCreator, chartInterval, chartLoader); diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts index d356fc0ef339b..10e035103dbec 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/jobs.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Observable } from 'rxjs'; import { HttpService } from '../http_service'; import { basePath } from './index'; @@ -23,6 +24,7 @@ import { } from '../../../../common/types/categories'; import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '../../../../common/constants/categorization_job'; import { Category } from '../../../../common/types/categories'; +import { JobsExistResponse } from '../../../../common/types/job_service'; export const jobsApiProvider = (httpService: HttpService) => ({ jobsSummary(jobIds: string[]) { @@ -138,9 +140,18 @@ export const jobsApiProvider = (httpService: HttpService) => ({ }); }, - jobsExist(jobIds: string[]) { - const body = JSON.stringify({ jobIds }); - return httpService.http({ + jobsExist(jobIds: string[], allSpaces: boolean = false) { + const body = JSON.stringify({ jobIds, allSpaces }); + return httpService.http({ + path: `${basePath()}/jobs/jobs_exist`, + method: 'POST', + body, + }); + }, + + jobsExist$(jobIds: string[], allSpaces: boolean = false): Observable { + const body = JSON.stringify({ jobIds, allSpaces }); + return httpService.http$({ path: `${basePath()}/jobs/jobs_exist`, method: 'POST', body, 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 79afe2ba5a0ad..514449385bf0b 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 @@ -22,6 +22,7 @@ import { CriteriaField } from './index'; 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'; interface ResultResponse { success: boolean; @@ -68,6 +69,7 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) { query: object | undefined, metricFunction: string, // ES aggregation name metricFieldName: string, + summaryCountFieldName: string | undefined, timeFieldName: string, earliestMs: number, latestMs: number, @@ -153,9 +155,8 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) { body.query.bool.minimum_should_match = shouldCriteria.length / 2; } + body.aggs.byTime.aggs = {}; if (metricFieldName !== undefined && metricFieldName !== '') { - body.aggs.byTime.aggs = {}; - const metricAgg: any = { [metricFunction]: {}, }; @@ -186,8 +187,23 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) { } else { body.aggs.byTime.aggs.metric = metricAgg; } + } else { + // if metricFieldName is not defined, it's probably a variation of the non zero count function + // refer to buildConfigFromDetector + if (summaryCountFieldName !== undefined && metricFunction === ES_AGGREGATION.CARDINALITY) { + // if so, check if summaryCountFieldName is an aggregation field + if (typeof aggFields === 'object' && Object.keys(aggFields).length > 0) { + // 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) }; + const foundCardinalityField = findAggField(tempAggs, summaryCountFieldName); + if (foundCardinalityField !== undefined) { + tempAggs.metric = foundCardinalityField; + } + body.aggs.byTime.aggs = tempAggs; + } + } } - return mlApiServices.esSearch$({ index, body }).pipe( map((resp: any) => { const obj: MetricData = { success: true, results: {} }; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts index 90c39497a9a18..0fbac571d1bdf 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts @@ -91,6 +91,7 @@ function getMetricData( chartConfig.datafeedConfig.query, esMetricFunction ?? chartConfig.metricFunction, chartConfig.metricFieldName, + chartConfig.summaryCountFieldName, chartConfig.timeField, earliestMs, latestMs, diff --git a/x-pack/plugins/ml/public/application/util/chart_config_builder.js b/x-pack/plugins/ml/public/application/util/chart_config_builder.js index 2fa869b058aa2..a30280f1220c0 100644 --- a/x-pack/plugins/ml/public/application/util/chart_config_builder.js +++ b/x-pack/plugins/ml/public/application/util/chart_config_builder.js @@ -28,6 +28,7 @@ export function buildConfigFromDetector(job, detectorIndex) { timeField: job.data_description.time_field, interval: job.analysis_config.bucket_span, datafeedConfig: job.datafeed_config, + summaryCountFieldName: job.analysis_config.summary_count_field_name, }; if (detector.field_name !== undefined) { @@ -63,10 +64,17 @@ export function buildConfigFromDetector(job, detectorIndex) { 'field', ]); } - - if (detector.function === ML_JOB_AGGREGATION.NON_ZERO_COUNT && cardinalityField !== undefined) { + if ( + (detector.function === ML_JOB_AGGREGATION.NON_ZERO_COUNT || + detector.function === ML_JOB_AGGREGATION.LOW_NON_ZERO_COUNT || + detector.function === ML_JOB_AGGREGATION.HIGH_NON_ZERO_COUNT || + detector.function === ML_JOB_AGGREGATION.COUNT || + detector.function === ML_JOB_AGGREGATION.HIGH_COUNT || + detector.function === ML_JOB_AGGREGATION.LOW_COUNT) && + cardinalityField !== undefined + ) { config.metricFunction = ES_AGGREGATION.CARDINALITY; - config.metricFieldName = cardinalityField; + config.metricFieldName = undefined; } else { // For count detectors using summary_count_field, plot sum(summary_count_field_name) config.metricFunction = ES_AGGREGATION.SUM; diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts index 67a95de3b3d71..c93af249be203 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -43,7 +43,7 @@ import { fieldsServiceProvider } from '../fields_service'; import { jobServiceProvider } from '../job_service'; import { resultsServiceProvider } from '../results_service'; import { JobExistResult, JobStat } from '../../../common/types/data_recognizer'; -import { MlJobsStatsResponse } from '../job_service/jobs'; +import { MlJobsStatsResponse } from '../../../common/types/job_service'; import { JobSavedObjectService } from '../../saved_objects'; const ML_DIR = 'ml'; @@ -533,7 +533,7 @@ export class DataRecognizer { const jobInfo = await this._jobsService.jobsExist(jobIds); // Check if the value for any of the jobs is false. - const doJobsExist = Object.values(jobInfo).includes(false) === false; + const doJobsExist = Object.values(jobInfo).every((j) => j.exists === true); results.jobsExist = doJobsExist; if (doJobsExist === true) { diff --git a/x-pack/plugins/ml/server/models/job_service/groups.ts b/x-pack/plugins/ml/server/models/job_service/groups.ts index f6073ae7071b0..81b0494cbdf27 100644 --- a/x-pack/plugins/ml/server/models/job_service/groups.ts +++ b/x-pack/plugins/ml/server/models/job_service/groups.ts @@ -7,7 +7,7 @@ import { CalendarManager } from '../calendar'; import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; import { Job } from '../../../common/types/anomaly_detection_jobs'; -import { MlJobsResponse } from './jobs'; +import { MlJobsResponse } from '../../../common/types/job_service'; import type { MlClient } from '../../lib/ml_client'; interface Group { 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 9abc34dfbb5d9..d47a1d4b4892d 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.ts +++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts @@ -16,11 +16,14 @@ import { JOB_STATE, DATAFEED_STATE } from '../../../common/constants/states'; import { MlSummaryJob, AuditMessage, - Job, - JobStats, DatafeedWithStats, CombinedJobWithStats, } from '../../../common/types/anomaly_detection_jobs'; +import { + MlJobsResponse, + MlJobsStatsResponse, + JobsExistResponse, +} from '../../../common/types/job_service'; import { GLOBAL_CALENDAR } from '../../../common/constants/calendars'; import { datafeedsProvider, MlDatafeedsResponse, MlDatafeedsStatsResponse } from './datafeeds'; import { jobAuditMessagesProvider } from '../job_audit_messages'; @@ -34,16 +37,6 @@ import { import { groupsProvider } from './groups'; import type { MlClient } from '../../lib/ml_client'; -export interface MlJobsResponse { - jobs: Job[]; - count: number; -} - -export interface MlJobsStatsResponse { - jobs: JobStats[]; - count: number; -} - interface Results { [id: string]: { [status: string]: boolean; @@ -420,10 +413,18 @@ export function jobsProvider(client: IScopedClusterClient, mlClient: MlClient) { // Checks if each of the jobs in the specified list of IDs exist. // Job IDs in supplied array may contain wildcard '*' characters // e.g. *_low_request_rate_ecs - async function jobsExist(jobIds: string[] = [], allSpaces: boolean = false) { - const results: { [id: string]: boolean } = {}; + async function jobsExist( + jobIds: string[] = [], + allSpaces: boolean = false + ): Promise { + const results: JobsExistResponse = {}; for (const jobId of jobIds) { try { + if (jobId === '') { + results[jobId] = { exists: false, isGroup: false }; + continue; + } + const { body } = allSpaces ? await client.asInternalUser.ml.getJobs({ job_id: jobId, @@ -431,13 +432,15 @@ export function jobsProvider(client: IScopedClusterClient, mlClient: MlClient) { : await mlClient.getJobs({ job_id: jobId, }); - results[jobId] = body.count > 0; + + const isGroup = body.jobs.some((j) => j.groups !== undefined && j.groups.includes(jobId)); + results[jobId] = { exists: body.count > 0, isGroup }; } catch (e) { // if a non-wildcarded job id is supplied, the get jobs endpoint will 404 if (e.statusCode !== 404) { throw e; } - results[jobId] = false; + results[jobId] = { exists: false, isGroup: false }; } } return results; diff --git a/x-pack/plugins/ml/server/models/results_service/results_service.ts b/x-pack/plugins/ml/server/models/results_service/results_service.ts index a196f1034fdd3..a153944f37bef 100644 --- a/x-pack/plugins/ml/server/models/results_service/results_service.ts +++ b/x-pack/plugins/ml/server/models/results_service/results_service.ts @@ -17,7 +17,7 @@ import { } from '../../../common/types/anomalies'; import { JOB_ID, PARTITION_FIELD_VALUE } from '../../../common/constants/anomalies'; import { GetStoppedPartitionResult } from '../../../common/types/results'; -import { MlJobsResponse } from '../job_service/jobs'; +import { MlJobsResponse } from '../../../common/types/job_service'; import type { MlClient } from '../../lib/ml_client'; // Service for carrying out Elasticsearch queries to obtain data for the diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx index 9fdc59d61257e..b0fedf5a44cba 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx @@ -65,7 +65,7 @@ describe('APMSection', () => { expect(getByText('APM')).toBeInTheDocument(); expect(getByText('View in app')).toBeInTheDocument(); expect(getByText('Services 11')).toBeInTheDocument(); - expect(getByText('Transactions per minute 312.00k')).toBeInTheDocument(); + expect(getByText('Throughput 312.00k tpm')).toBeInTheDocument(); expect(queryAllByTestId('loading')).toEqual([]); }); it('shows loading state', () => { @@ -80,6 +80,6 @@ describe('APMSection', () => { expect(getByTestId('loading')).toBeInTheDocument(); expect(queryAllByText('View in app')).toEqual([]); expect(queryAllByText('Services 11')).toEqual([]); - expect(queryAllByText('Transactions per minute 312.00k')).toEqual([]); + expect(queryAllByText('Throughput 312.00k tpm')).toEqual([]); }); }); diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx index 91d20d3478960..620604562cc13 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx @@ -92,9 +92,9 @@ export function APMSection({ bucketSize }: Props) { /x-pack/plugins/runtime_field_editor'], + roots: ['/x-pack/plugins/runtime_fields'], }; diff --git a/x-pack/plugins/runtime_field_editor/kibana.json b/x-pack/plugins/runtime_fields/kibana.json similarity index 88% rename from x-pack/plugins/runtime_field_editor/kibana.json rename to x-pack/plugins/runtime_fields/kibana.json index 3270ada544ba4..65932c723c474 100644 --- a/x-pack/plugins/runtime_field_editor/kibana.json +++ b/x-pack/plugins/runtime_fields/kibana.json @@ -1,5 +1,5 @@ { - "id": "runtimeFieldEditor", + "id": "runtimeFields", "version": "kibana", "server": false, "ui": true, diff --git a/x-pack/plugins/runtime_field_editor/public/__jest__/setup_environment.tsx b/x-pack/plugins/runtime_fields/public/__jest__/setup_environment.tsx similarity index 100% rename from x-pack/plugins/runtime_field_editor/public/__jest__/setup_environment.tsx rename to x-pack/plugins/runtime_fields/public/__jest__/setup_environment.tsx diff --git a/x-pack/plugins/runtime_field_editor/public/components/index.ts b/x-pack/plugins/runtime_fields/public/components/index.ts similarity index 100% rename from x-pack/plugins/runtime_field_editor/public/components/index.ts rename to x-pack/plugins/runtime_fields/public/components/index.ts diff --git a/x-pack/plugins/runtime_field_editor/public/components/runtime_field_editor/index.ts b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/index.ts similarity index 100% rename from x-pack/plugins/runtime_field_editor/public/components/runtime_field_editor/index.ts rename to x-pack/plugins/runtime_fields/public/components/runtime_field_editor/index.ts diff --git a/x-pack/plugins/runtime_field_editor/public/components/runtime_field_editor/runtime_field_editor.test.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.test.tsx similarity index 100% rename from x-pack/plugins/runtime_field_editor/public/components/runtime_field_editor/runtime_field_editor.test.tsx rename to x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.test.tsx diff --git a/x-pack/plugins/runtime_field_editor/public/components/runtime_field_editor/runtime_field_editor.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.tsx similarity index 100% rename from x-pack/plugins/runtime_field_editor/public/components/runtime_field_editor/runtime_field_editor.tsx rename to x-pack/plugins/runtime_fields/public/components/runtime_field_editor/runtime_field_editor.tsx diff --git a/x-pack/plugins/runtime_field_editor/public/components/runtime_field_editor_flyout_content/index.ts b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/index.ts similarity index 100% rename from x-pack/plugins/runtime_field_editor/public/components/runtime_field_editor_flyout_content/index.ts rename to x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/index.ts diff --git a/x-pack/plugins/runtime_field_editor/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.test.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.test.tsx similarity index 100% rename from x-pack/plugins/runtime_field_editor/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.test.tsx rename to x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.test.tsx diff --git a/x-pack/plugins/runtime_field_editor/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.tsx similarity index 100% rename from x-pack/plugins/runtime_field_editor/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.tsx rename to x-pack/plugins/runtime_fields/public/components/runtime_field_editor_flyout_content/runtime_field_editor_flyout_content.tsx diff --git a/x-pack/plugins/runtime_field_editor/public/components/runtime_field_form/index.ts b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/index.ts similarity index 100% rename from x-pack/plugins/runtime_field_editor/public/components/runtime_field_form/index.ts rename to x-pack/plugins/runtime_fields/public/components/runtime_field_form/index.ts diff --git a/x-pack/plugins/runtime_field_editor/public/components/runtime_field_form/runtime_field_form.test.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.test.tsx similarity index 100% rename from x-pack/plugins/runtime_field_editor/public/components/runtime_field_form/runtime_field_form.test.tsx rename to x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.test.tsx diff --git a/x-pack/plugins/runtime_field_editor/public/components/runtime_field_form/runtime_field_form.tsx b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx similarity index 100% rename from x-pack/plugins/runtime_field_editor/public/components/runtime_field_form/runtime_field_form.tsx rename to x-pack/plugins/runtime_fields/public/components/runtime_field_form/runtime_field_form.tsx diff --git a/x-pack/plugins/runtime_field_editor/public/components/runtime_field_form/schema.ts b/x-pack/plugins/runtime_fields/public/components/runtime_field_form/schema.ts similarity index 100% rename from x-pack/plugins/runtime_field_editor/public/components/runtime_field_form/schema.ts rename to x-pack/plugins/runtime_fields/public/components/runtime_field_form/schema.ts diff --git a/x-pack/plugins/runtime_field_editor/public/constants.ts b/x-pack/plugins/runtime_fields/public/constants.ts similarity index 75% rename from x-pack/plugins/runtime_field_editor/public/constants.ts rename to x-pack/plugins/runtime_fields/public/constants.ts index eebc3007d79d5..017b58c246afe 100644 --- a/x-pack/plugins/runtime_field_editor/public/constants.ts +++ b/x-pack/plugins/runtime_fields/public/constants.ts @@ -3,7 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ComboBoxOption, RuntimeType } from './types'; +import { ComboBoxOption } from './types'; + +export const RUNTIME_FIELD_TYPES = ['keyword', 'long', 'double', 'date', 'ip', 'boolean'] as const; + +type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; export const RUNTIME_FIELD_OPTIONS: Array> = [ { diff --git a/x-pack/plugins/runtime_field_editor/public/index.ts b/x-pack/plugins/runtime_fields/public/index.ts similarity index 100% rename from x-pack/plugins/runtime_field_editor/public/index.ts rename to x-pack/plugins/runtime_fields/public/index.ts diff --git a/x-pack/plugins/runtime_field_editor/public/lib/documentation.ts b/x-pack/plugins/runtime_fields/public/lib/documentation.ts similarity index 100% rename from x-pack/plugins/runtime_field_editor/public/lib/documentation.ts rename to x-pack/plugins/runtime_fields/public/lib/documentation.ts diff --git a/x-pack/plugins/runtime_field_editor/public/lib/index.ts b/x-pack/plugins/runtime_fields/public/lib/index.ts similarity index 100% rename from x-pack/plugins/runtime_field_editor/public/lib/index.ts rename to x-pack/plugins/runtime_fields/public/lib/index.ts diff --git a/x-pack/plugins/runtime_field_editor/public/load_editor.tsx b/x-pack/plugins/runtime_fields/public/load_editor.tsx similarity index 100% rename from x-pack/plugins/runtime_field_editor/public/load_editor.tsx rename to x-pack/plugins/runtime_fields/public/load_editor.tsx diff --git a/x-pack/plugins/runtime_field_editor/public/plugin.test.ts b/x-pack/plugins/runtime_fields/public/plugin.test.ts similarity index 100% rename from x-pack/plugins/runtime_field_editor/public/plugin.test.ts rename to x-pack/plugins/runtime_fields/public/plugin.test.ts diff --git a/x-pack/plugins/runtime_field_editor/public/plugin.ts b/x-pack/plugins/runtime_fields/public/plugin.ts similarity index 100% rename from x-pack/plugins/runtime_field_editor/public/plugin.ts rename to x-pack/plugins/runtime_fields/public/plugin.ts diff --git a/x-pack/plugins/runtime_field_editor/public/shared_imports.ts b/x-pack/plugins/runtime_fields/public/shared_imports.ts similarity index 100% rename from x-pack/plugins/runtime_field_editor/public/shared_imports.ts rename to x-pack/plugins/runtime_fields/public/shared_imports.ts diff --git a/x-pack/plugins/runtime_field_editor/public/test_utils.ts b/x-pack/plugins/runtime_fields/public/test_utils.ts similarity index 100% rename from x-pack/plugins/runtime_field_editor/public/test_utils.ts rename to x-pack/plugins/runtime_fields/public/test_utils.ts diff --git a/x-pack/plugins/runtime_field_editor/public/types.ts b/x-pack/plugins/runtime_fields/public/types.ts similarity index 80% rename from x-pack/plugins/runtime_field_editor/public/types.ts rename to x-pack/plugins/runtime_fields/public/types.ts index 984d6ab9f8655..b1bbb06d79655 100644 --- a/x-pack/plugins/runtime_field_editor/public/types.ts +++ b/x-pack/plugins/runtime_fields/public/types.ts @@ -4,12 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { DataPublicPluginStart } from 'src/plugins/data/public'; -export type { - RuntimeField, - RuntimeType, - RUNTIME_FIELD_TYPES, -} from 'src/plugins/runtime_fields/common'; +import { RUNTIME_FIELD_TYPES } from './constants'; import { OpenRuntimeFieldEditorProps } from './load_editor'; export interface LoadEditorResponse { @@ -30,6 +26,16 @@ export interface StartPlugins { data: DataPublicPluginStart; } +export type RuntimeType = typeof RUNTIME_FIELD_TYPES[number]; + +export interface RuntimeField { + name: string; + type: RuntimeType; + script: { + source: string; + }; +} + export interface ComboBoxOption { label: string; value?: T; diff --git a/x-pack/plugins/searchprofiler/kibana.json b/x-pack/plugins/searchprofiler/kibana.json index a5e42f20b5c7a..6c94701c0ec09 100644 --- a/x-pack/plugins/searchprofiler/kibana.json +++ b/x-pack/plugins/searchprofiler/kibana.json @@ -2,9 +2,9 @@ "id": "searchprofiler", "version": "8.0.0", "kibanaVersion": "kibana", - "requiredPlugins": ["devTools", "home", "licensing"], "configPath": ["xpack", "searchprofiler"], "server": true, "ui": true, + "requiredPlugins": ["devTools", "home", "licensing"], "requiredBundles": ["esUiShared"] } diff --git a/x-pack/plugins/searchprofiler/tsconfig.json b/x-pack/plugins/searchprofiler/tsconfig.json new file mode 100644 index 0000000000000..f8ac3a61f7812 --- /dev/null +++ b/x-pack/plugins/searchprofiler/tsconfig.json @@ -0,0 +1,22 @@ +{ + "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/es_ui_shared/tsconfig.json" }, + { "path": "../../../src/plugins/dev_tools/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json" }, + { "path": "../licensing/tsconfig.json" }, + ] +} diff --git a/x-pack/plugins/security/common/model/authenticated_user.ts b/x-pack/plugins/security/common/model/authenticated_user.ts index 491ceb6845e28..5513fa27fa178 100644 --- a/x-pack/plugins/security/common/model/authenticated_user.ts +++ b/x-pack/plugins/security/common/model/authenticated_user.ts @@ -8,7 +8,7 @@ import type { AuthenticationProvider, User } from '.'; const REALMS_ELIGIBLE_FOR_PASSWORD_CHANGE = ['reserved', 'native']; -interface UserRealm { +export interface UserRealm { name: string; type: string; } diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap index 3c6458a6d2467..44b6c081bcfba 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap @@ -1,132 +1,123 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`APIKeysGridPage renders a callout when API keys are not enabled 1`] = ` - } > - - } +
    - - + + -
    - - - , - } - } - > - Contact your system administrator and refer to the - - + +
    + + -
    -
    - - + (opens in a new tab or window) + + + + + + to enable API keys. + +
    + +
    + `; exports[`APIKeysGridPage renders permission denied if user does not have required permissions 1`] = ` diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx index 68daf427677ff..a55d872ca60f9 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx @@ -10,10 +10,10 @@ import { ReactWrapper } from 'enzyme'; import { EuiCallOut } from '@elastic/eui'; import type { PublicMethodsOf } from '@kbn/utility-types'; +import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; import { NotEnabled } from './not_enabled'; import { PermissionDenied } from './permission_denied'; import { APIKeysAPIClient } from '../api_keys_api_client'; -import { DocumentationLinksService } from '../documentation_links'; import { APIKeysGridPage } from './api_keys_grid_page'; import { coreMock } from '../../../../../../../src/core/public/mocks'; @@ -66,21 +66,16 @@ describe('APIKeysGridPage', () => { }); const coreStart = coreMock.createStart(); - - const getViewProperties = () => { - const { docLinks, notifications, application } = coreStart; - return { - docLinks: new DocumentationLinksService(docLinks), - navigateToApp: application.navigateToApp, - notifications, - apiKeysAPIClient: apiClientMock, - }; + const renderView = () => { + return mountWithIntl( + + + + ); }; it('renders a loading state when fetching API keys', async () => { - const wrapper = mountWithIntl(); - - expect(wrapper.find('[data-test-subj="apiKeysSectionLoading"]')).toHaveLength(1); + expect(renderView().find('[data-test-subj="apiKeysSectionLoading"]')).toHaveLength(1); }); it('renders a callout when API keys are not enabled', async () => { @@ -90,13 +85,12 @@ describe('APIKeysGridPage', () => { areApiKeysEnabled: false, }); - const wrapper = mountWithIntl(); - + const wrapper = renderView(); await waitForRender(wrapper, (updatedWrapper) => { return updatedWrapper.find(NotEnabled).length > 0; }); - expect(wrapper.find(NotEnabled)).toMatchSnapshot(); + expect(wrapper.find(NotEnabled).find(EuiCallOut)).toMatchSnapshot(); }); it('renders permission denied if user does not have required permissions', async () => { @@ -106,8 +100,7 @@ describe('APIKeysGridPage', () => { areApiKeysEnabled: true, }); - const wrapper = mountWithIntl(); - + const wrapper = renderView(); await waitForRender(wrapper, (updatedWrapper) => { return updatedWrapper.find(PermissionDenied).length > 0; }); @@ -118,8 +111,7 @@ describe('APIKeysGridPage', () => { it('renders error callout if error fetching API keys', async () => { apiClientMock.getApiKeys.mockRejectedValue(mock500()); - const wrapper = mountWithIntl(); - + const wrapper = renderView(); await waitForRender(wrapper, (updatedWrapper) => { return updatedWrapper.find(EuiCallOut).length > 0; }); @@ -130,7 +122,7 @@ describe('APIKeysGridPage', () => { describe('Admin view', () => { let wrapper: ReactWrapper; beforeEach(() => { - wrapper = mountWithIntl(); + wrapper = renderView(); }); it('renders a callout indicating the user is an administrator', async () => { @@ -165,7 +157,7 @@ describe('APIKeysGridPage', () => { areApiKeysEnabled: true, }); - wrapper = mountWithIntl(); + wrapper = renderView(); }); it('does NOT render a callout indicating the user is an administrator', async () => { diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx index b4ea91ea024f9..c208502fc6a37 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx @@ -28,11 +28,10 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import type { PublicMethodsOf } from '@kbn/utility-types'; import moment from 'moment-timezone'; -import { ApplicationStart, NotificationsStart } from 'src/core/public'; +import type { NotificationsStart } from 'src/core/public'; import { SectionLoading } from '../../../../../../../src/plugins/es_ui_shared/public'; import { ApiKey, ApiKeyToInvalidate } from '../../../../common/model'; import { APIKeysAPIClient } from '../api_keys_api_client'; -import { DocumentationLinksService } from '../documentation_links'; import { PermissionDenied } from './permission_denied'; import { EmptyPrompt } from './empty_prompt'; import { NotEnabled } from './not_enabled'; @@ -40,9 +39,7 @@ import { InvalidateProvider } from './invalidate_provider'; interface Props { notifications: NotificationsStart; - docLinks: DocumentationLinksService; apiKeysAPIClient: PublicMethodsOf; - navigateToApp: ApplicationStart['navigateToApp']; } interface State { @@ -132,7 +129,7 @@ export class APIKeysGridPage extends Component { if (!areApiKeysEnabled) { return ( - + ); } @@ -140,11 +137,7 @@ export class APIKeysGridPage extends Component { if (!isLoadingTable && apiKeys && apiKeys.length === 0) { return ( - + ); } diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx index 9b2ccfcb99ef3..39a34efb9f934 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx @@ -5,72 +5,70 @@ */ import React, { Fragment } from 'react'; -import { ApplicationStart } from 'kibana/public'; import { EuiEmptyPrompt, EuiButton, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { DocumentationLinksService } from '../../documentation_links'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; interface Props { isAdmin: boolean; - docLinks: DocumentationLinksService; - navigateToApp: ApplicationStart['navigateToApp']; } -export const EmptyPrompt: React.FunctionComponent = ({ - isAdmin, - docLinks, - navigateToApp, -}) => ( - - {isAdmin ? ( +export const EmptyPrompt: React.FunctionComponent = ({ isAdmin }) => { + const { services } = useKibana(); + const application = services.application!; + const docLinks = services.docLinks!; + return ( + + {isAdmin ? ( + + ) : ( + + )} + + } + body={ + +

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

    +
    + } + actions={ + application.navigateToApp('dev_tools')} + data-test-subj="goToConsoleButton" + > - ) : ( - - )} - - } - body={ - -

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

    -
    - } - actions={ - navigateToApp('dev_tools')} - data-test-subj="goToConsoleButton" - > - - - } - data-test-subj="emptyPrompt" - /> -); +
    + } + data-test-subj="emptyPrompt" + /> + ); +}; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/not_enabled/not_enabled.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/not_enabled/not_enabled.tsx index 08fe542557757..54ea724cfef1d 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/not_enabled/not_enabled.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/not_enabled/not_enabled.tsx @@ -7,36 +7,35 @@ import React from 'react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { DocumentationLinksService } from '../../documentation_links'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -interface Props { - docLinks: DocumentationLinksService; -} - -export const NotEnabled: React.FunctionComponent = ({ docLinks }) => ( - { + const docLinks = useKibana().services.docLinks!; + return ( + + } + color="danger" + iconType="alert" + > + + + ), + }} /> - } - color="danger" - iconType="alert" - > - - - - ), - }} - /> - -); + + ); +}; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.test.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.test.tsx index 2b43bc7ebc20d..e9901b8f7847c 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.test.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.test.tsx @@ -43,7 +43,7 @@ describe('apiKeysManagementApp', () => { expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: '/', text: 'API Keys' }]); expect(container).toMatchInlineSnapshot(`
    - Page: {"notifications":{"toasts":{}},"docLinks":{"apiKeySettings":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-settings.html#api-key-service-settings","createApiKey":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-api-create-api-key.html"},"apiKeysAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}}} + Page: {"notifications":{"toasts":{}},"apiKeysAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}}}
    `); diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx index 6ff91852d0a3e..241bb43828814 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx @@ -9,8 +9,8 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { i18n } from '@kbn/i18n'; import { StartServicesAccessor } from 'src/core/public'; import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { PluginStartDependencies } from '../../plugin'; -import { DocumentationLinksService } from './documentation_links'; interface CreateParams { getStartServices: StartServicesAccessor; @@ -35,25 +35,21 @@ export const apiKeysManagementApp = Object.freeze({ }, ]); - const [ - [{ docLinks, http, notifications, i18n: i18nStart, application }], - { APIKeysGridPage }, - { APIKeysAPIClient }, - ] = await Promise.all([ + const [[core], { APIKeysGridPage }, { APIKeysAPIClient }] = await Promise.all([ getStartServices(), import('./api_keys_grid'), import('./api_keys_api_client'), ]); render( - - - , + + + + + , element ); diff --git a/x-pack/plugins/security/public/management/api_keys/documentation_links.ts b/x-pack/plugins/security/public/management/api_keys/documentation_links.ts deleted file mode 100644 index 66a54f1cb7b72..0000000000000 --- a/x-pack/plugins/security/public/management/api_keys/documentation_links.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { DocLinksStart } from 'src/core/public'; - -export class DocumentationLinksService { - private readonly apiKeySettings: string; - private readonly createApiKey: string; - - constructor(docLinks: DocLinksStart) { - this.apiKeySettings = `${docLinks.links.security.apiKeyServiceSettings}`; - this.createApiKey = `${docLinks.links.apis.createApiKey}`; - } - - public getApiKeyServiceSettingsDocUrl() { - return `${this.apiKeySettings}`; - } - - public getCreateApiKeyDocUrl() { - return `${this.createApiKey}`; - } -} diff --git a/x-pack/plugins/security/public/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx b/x-pack/plugins/security/public/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx index 5e14b0c179bfd..a3a7377b0d395 100644 --- a/x-pack/plugins/security/public/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx @@ -7,36 +7,35 @@ import React from 'react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { DocumentationLinksService } from '../../documentation_links'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -interface Props { - docLinks: DocumentationLinksService; -} - -export const NoCompatibleRealms: React.FunctionComponent = ({ docLinks }: Props) => ( - { + const docLinks = useKibana().services.docLinks!; + return ( + + } + color="warning" + iconType="alert" + > + + + ), + }} /> - } - color="warning" - iconType="alert" - > - - - - ), - }} - /> - -); + + ); +}; diff --git a/x-pack/plugins/security/public/management/role_mappings/documentation_links.ts b/x-pack/plugins/security/public/management/role_mappings/documentation_links.ts deleted file mode 100644 index 2098d5c71ee7a..0000000000000 --- a/x-pack/plugins/security/public/management/role_mappings/documentation_links.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { DocLinksStart } from 'src/core/public'; - -export class DocumentationLinksService { - private readonly mappingRoles: string; - private readonly createRoleMapping: string; - private readonly createRoleMappingTemplates: string; - private readonly roleMappingFieldRules: string; - - constructor(docLinks: DocLinksStart) { - this.mappingRoles = `${docLinks.links.security.mappingRoles}`; - this.createRoleMapping = `${docLinks.links.apis.createRoleMapping}`; - this.createRoleMappingTemplates = `${docLinks.links.apis.createRoleMappingTemplates}`; - this.roleMappingFieldRules = `${docLinks.links.security.mappingRolesFieldRules}`; - } - - public getRoleMappingDocUrl() { - return `${this.mappingRoles}`; - } - - public getRoleMappingAPIDocUrl() { - return `${this.createRoleMapping}`; - } - - public getRoleMappingTemplateDocUrl() { - return `${this.createRoleMappingTemplates}`; - } - - public getRoleMappingFieldRulesDocUrl() { - return `${this.roleMappingFieldRules}`; - } -} diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx index c7c40cea63e2e..64ef1459a5246 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.test.tsx @@ -20,7 +20,7 @@ import { VisualRuleEditor } from './rule_editor_panel/visual_rule_editor'; import { JSONRuleEditor } from './rule_editor_panel/json_rule_editor'; import { RolesAPIClient } from '../../roles'; import { Role } from '../../../../common/model'; -import { DocumentationLinksService } from '../documentation_links'; +import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; import { coreMock, scopedHistoryMock } from '../../../../../../../src/core/public/mocks'; import { roleMappingsAPIClientMock } from '../role_mappings_api_client.mock'; @@ -31,6 +31,25 @@ describe('EditRoleMappingPage', () => { const history = scopedHistoryMock.create(); let rolesAPI: PublicMethodsOf; + const renderView = ( + roleMappingsAPI: ReturnType, + name?: string + ) => { + const coreStart = coreMock.createStart(); + return mountWithIntl( + + + + ); + }; + beforeEach(() => { rolesAPI = rolesAPIClientMock.create(); (rolesAPI as jest.Mocked).getRoles.mockResolvedValue([ @@ -50,17 +69,7 @@ describe('EditRoleMappingPage', () => { canUseStoredScripts: true, }); - const { docLinks, notifications } = coreMock.createStart(); - const wrapper = mountWithIntl( - - ); - + const wrapper = renderView(roleMappingsAPI); await nextTick(); wrapper.update(); @@ -112,18 +121,7 @@ describe('EditRoleMappingPage', () => { canUseStoredScripts: true, }); - const { docLinks, notifications } = coreMock.createStart(); - const wrapper = mountWithIntl( - - ); - + const wrapper = renderView(roleMappingsAPI, 'foo'); await nextTick(); wrapper.update(); @@ -161,16 +159,7 @@ describe('EditRoleMappingPage', () => { hasCompatibleRealms: true, }); - const { docLinks, notifications } = coreMock.createStart(); - const wrapper = mountWithIntl( - - ); + const wrapper = renderView(roleMappingsAPI); expect(wrapper.find(SectionLoading)).toHaveLength(1); expect(wrapper.find(PermissionDenied)).toHaveLength(0); @@ -189,16 +178,7 @@ describe('EditRoleMappingPage', () => { hasCompatibleRealms: false, }); - const { docLinks, notifications } = coreMock.createStart(); - const wrapper = mountWithIntl( - - ); + const wrapper = renderView(roleMappingsAPI); expect(wrapper.find(SectionLoading)).toHaveLength(1); expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); @@ -226,18 +206,7 @@ describe('EditRoleMappingPage', () => { canUseStoredScripts: true, }); - const { docLinks, notifications } = coreMock.createStart(); - const wrapper = mountWithIntl( - - ); - + const wrapper = renderView(roleMappingsAPI, 'foo'); expect(findTestSubject(wrapper, 'deprecatedRolesAssigned')).toHaveLength(0); await nextTick(); @@ -267,18 +236,7 @@ describe('EditRoleMappingPage', () => { canUseStoredScripts: false, }); - const { docLinks, notifications } = coreMock.createStart(); - const wrapper = mountWithIntl( - - ); - + const wrapper = renderView(roleMappingsAPI, 'foo'); expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); @@ -310,18 +268,7 @@ describe('EditRoleMappingPage', () => { canUseStoredScripts: true, }); - const { docLinks, notifications } = coreMock.createStart(); - const wrapper = mountWithIntl( - - ); - + const wrapper = renderView(roleMappingsAPI, 'foo'); expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); @@ -365,18 +312,7 @@ describe('EditRoleMappingPage', () => { canUseStoredScripts: true, }); - const { docLinks, notifications } = coreMock.createStart(); - const wrapper = mountWithIntl( - - ); - + const wrapper = renderView(roleMappingsAPI, 'foo'); await nextTick(); wrapper.update(); @@ -421,18 +357,7 @@ describe('EditRoleMappingPage', () => { canUseStoredScripts: true, }); - const { docLinks, notifications } = coreMock.createStart(); - const wrapper = mountWithIntl( - - ); - + const wrapper = renderView(roleMappingsAPI, 'foo'); await nextTick(); wrapper.update(); diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.tsx index 30584348960a5..3de11e84e042b 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.tsx @@ -20,7 +20,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import type { PublicMethodsOf } from '@kbn/utility-types'; -import { NotificationsStart, ScopedHistory } from 'src/core/public'; +import type { NotificationsStart, ScopedHistory, DocLinksStart } from 'src/core/public'; import { RoleMapping } from '../../../../common/model'; import { RuleEditorPanel } from './rule_editor_panel'; import { @@ -32,7 +32,6 @@ import { import { RolesAPIClient } from '../../roles'; import { validateRoleMappingForSave } from './services/role_mapping_validation'; import { MappingInfoPanel } from './mapping_info_panel'; -import { DocumentationLinksService } from '../documentation_links'; import { RoleMappingsAPIClient } from '../role_mappings_api_client'; interface State { @@ -54,7 +53,7 @@ interface Props { roleMappingsAPI: PublicMethodsOf; rolesAPIClient: PublicMethodsOf; notifications: NotificationsStart; - docLinks: DocumentationLinksService; + docLinks: DocLinksStart; history: ScopedHistory; } @@ -163,7 +162,7 @@ export class EditRoleMappingPage extends Component { values={{ learnMoreLink: ( @@ -180,7 +179,7 @@ export class EditRoleMappingPage extends Component { {!this.state.hasCompatibleRealms && ( <> - + )} diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.test.tsx index dbd034ff3f764..f9201a8dbf249 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.test.tsx @@ -10,7 +10,6 @@ import { mountWithIntl } from '@kbn/test/jest'; import { findTestSubject } from '@kbn/test/jest'; import { Role, RoleMapping } from '../../../../../common/model'; import { RolesAPIClient } from '../../../roles'; -import { DocumentationLinksService } from '../../documentation_links'; import { RoleSelector } from '../role_selector'; import { RoleTemplateEditor } from '../role_selector/role_template_editor'; import { MappingInfoPanel } from '.'; @@ -39,7 +38,7 @@ describe('MappingInfoPanel', () => { metadata: {}, } as RoleMapping, mode: 'create', - docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + docLinks: coreMock.createStart().docLinks, rolesAPIClient: rolesAPI, } as MappingInfoPanel['props']; @@ -86,7 +85,7 @@ describe('MappingInfoPanel', () => { metadata: {}, } as RoleMapping, mode: 'edit', - docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + docLinks: coreMock.createStart().docLinks, rolesAPIClient: rolesAPI, } as MappingInfoPanel['props']; @@ -112,7 +111,7 @@ describe('MappingInfoPanel', () => { canUseInlineScripts: true, canUseStoredScripts: false, validateForm: false, - docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + docLinks: coreMock.createStart().docLinks, rolesAPIClient: rolesAPI, }; @@ -153,7 +152,7 @@ describe('MappingInfoPanel', () => { canUseInlineScripts: false, canUseStoredScripts: true, validateForm: false, - docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + docLinks: coreMock.createStart().docLinks, rolesAPIClient: rolesAPI, }; @@ -194,7 +193,7 @@ describe('MappingInfoPanel', () => { canUseInlineScripts: false, canUseStoredScripts: false, validateForm: false, - docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + docLinks: coreMock.createStart().docLinks, rolesAPIClient: rolesAPI, }; @@ -219,7 +218,7 @@ describe('MappingInfoPanel', () => { metadata: {}, } as RoleMapping, mode: 'edit', - docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + docLinks: coreMock.createStart().docLinks, rolesAPIClient: rolesAPI, } as MappingInfoPanel['props']; diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.tsx index faf0278e5d8f3..8176950a46339 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.tsx @@ -20,6 +20,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import type { PublicMethodsOf } from '@kbn/utility-types'; +import type { DocLinksStart } from 'src/core/public'; import { RoleMapping } from '../../../../../common/model'; import { RolesAPIClient } from '../../../roles'; import { @@ -28,7 +29,6 @@ import { validateRoleMappingRoleTemplates, } from '../services/role_mapping_validation'; import { RoleSelector } from '../role_selector'; -import { DocumentationLinksService } from '../../documentation_links'; interface Props { roleMapping: RoleMapping; @@ -38,7 +38,7 @@ interface Props { canUseInlineScripts: boolean; canUseStoredScripts: boolean; rolesAPIClient: PublicMethodsOf; - docLinks: DocumentationLinksService; + docLinks: DocLinksStart; } interface State { @@ -205,7 +205,7 @@ export class MappingInfoPanel extends Component { defaultMessage="Create templates that describe the roles to assign to your users." />{' '} diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx index bae41b31cdcc1..5c7c6ed5b3ca1 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx @@ -17,20 +17,24 @@ import { act } from 'react-dom/test-utils'; import { mountWithIntl } from '@kbn/test/jest'; import { JSONRuleEditor } from './json_rule_editor'; import { EuiCodeEditor } from '@elastic/eui'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; import { AllRule, AnyRule, FieldRule, ExceptAnyRule, ExceptAllRule } from '../../model'; -import { DocumentationLinksService } from '../../documentation_links'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; describe('JSONRuleEditor', () => { + const renderView = (props: React.ComponentProps) => { + const coreStart = coreMock.createStart(); + return mountWithIntl( + + + + ); + }; + it('renders an empty rule set', () => { - const props = { - rules: null, - onChange: jest.fn(), - onValidityChange: jest.fn(), - docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), - }; - const wrapper = mountWithIntl(); + const props = { rules: null, onChange: jest.fn(), onValidityChange: jest.fn() }; + const wrapper = renderView(props); expect(props.onChange).not.toHaveBeenCalled(); expect(props.onValidityChange).not.toHaveBeenCalled(); @@ -50,9 +54,8 @@ describe('JSONRuleEditor', () => { ]), onChange: jest.fn(), onValidityChange: jest.fn(), - docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; - const wrapper = mountWithIntl(); + const wrapper = renderView(props); const { value } = wrapper.find(EuiCodeEditor).props(); expect(JSON.parse(value as string)).toEqual({ @@ -80,13 +83,8 @@ describe('JSONRuleEditor', () => { }); it('notifies when input contains invalid JSON', () => { - const props = { - rules: null, - onChange: jest.fn(), - onValidityChange: jest.fn(), - docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), - }; - const wrapper = mountWithIntl(); + const props = { rules: null, onChange: jest.fn(), onValidityChange: jest.fn() }; + const wrapper = renderView(props); const allRule = JSON.stringify(new AllRule().toRaw()); act(() => { @@ -99,13 +97,8 @@ describe('JSONRuleEditor', () => { }); it('notifies when input contains an invalid rule set, even if it is valid JSON', () => { - const props = { - rules: null, - onChange: jest.fn(), - onValidityChange: jest.fn(), - docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), - }; - const wrapper = mountWithIntl(); + const props = { rules: null, onChange: jest.fn(), onValidityChange: jest.fn() }; + const wrapper = renderView(props); const invalidRule = JSON.stringify({ all: [ @@ -127,13 +120,8 @@ describe('JSONRuleEditor', () => { }); it('fires onChange when a valid rule set is provided after being previously invalidated', () => { - const props = { - rules: null, - onChange: jest.fn(), - onValidityChange: jest.fn(), - docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), - }; - const wrapper = mountWithIntl(); + const props = { rules: null, onChange: jest.fn(), onValidityChange: jest.fn() }; + const wrapper = renderView(props); const allRule = JSON.stringify(new AllRule().toRaw()); act(() => { diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.tsx index e7a9149513d20..931803ddc6ffe 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.tsx @@ -11,17 +11,17 @@ import 'brace/theme/github'; import { EuiCodeEditor, EuiFormRow, EuiButton, EuiSpacer, EuiLink, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { DocumentationLinksService } from '../../documentation_links'; import { Rule, RuleBuilderError, generateRulesFromRaw } from '../../model'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; interface Props { rules: Rule | null; onChange: (updatedRules: Rule | null) => void; onValidityChange: (isValid: boolean) => void; - docLinks: DocumentationLinksService; } export const JSONRuleEditor = (props: Props) => { + const docLinks = useKibana().services.docLinks!; const [rawRules, setRawRules] = useState( JSON.stringify(props.rules ? props.rules.toRaw() : {}, null, 2) ); @@ -108,7 +108,7 @@ export const JSONRuleEditor = (props: Props) => { values={{ roleMappingAPI: ( diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx index ac31900b11a67..5a44a7c05c193 100644 --- a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx @@ -17,20 +17,28 @@ import '@kbn/test/target/jest/utils/stub_web_worker'; import { AllRule, FieldRule } from '../../model'; import { EuiErrorBoundary } from '@elastic/eui'; -import { DocumentationLinksService } from '../../documentation_links'; +import { KibanaContextProvider } from '../../../../../../../../src/plugins/kibana_react/public'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; describe('RuleEditorPanel', () => { + const renderView = (props: Omit, 'docLinks'>) => { + const coreStart = coreMock.createStart(); + const viewProps = { ...props, docLinks: coreStart.docLinks }; + return mountWithIntl( + + + + ); + }; it('renders the visual editor when no rules are defined', () => { const props = { rawRules: {}, onChange: jest.fn(), onValidityChange: jest.fn(), validateForm: false, - docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; - const wrapper = mountWithIntl(); + const wrapper = renderView(props); expect(wrapper.find(VisualRuleEditor)).toHaveLength(1); expect(wrapper.find(JSONRuleEditor)).toHaveLength(0); }); @@ -49,9 +57,9 @@ describe('RuleEditorPanel', () => { onChange: jest.fn(), onValidityChange: jest.fn(), validateForm: false, - docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + docLinks: coreMock.createStart().docLinks, }; - const wrapper = mountWithIntl(); + const wrapper = renderView(props); expect(wrapper.find(VisualRuleEditor)).toHaveLength(1); expect(wrapper.find(JSONRuleEditor)).toHaveLength(0); @@ -73,9 +81,8 @@ describe('RuleEditorPanel', () => { onChange: jest.fn(), onValidityChange: jest.fn(), validateForm: false, - docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; - const wrapper = mountWithIntl(); + const wrapper = renderView(props); findTestSubject(wrapper, 'roleMappingsJSONRuleEditorButton').simulate('click'); @@ -109,9 +116,8 @@ describe('RuleEditorPanel', () => { onChange: jest.fn(), onValidityChange: jest.fn(), validateForm: false, - docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; - const wrapper = mountWithIntl(); + const wrapper = renderView(props); wrapper.find(VisualRuleEditor).simulateError(new Error('Something awful happened here.')); 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 6e6641caa1f39..1b5cda1df5b52 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 @@ -22,12 +22,12 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import type { DocLinksStart } from 'src/core/public'; import { RoleMapping } from '../../../../../common/model'; import { VisualRuleEditor } from './visual_rule_editor'; import { JSONRuleEditor } from './json_rule_editor'; import { VISUAL_MAX_RULE_DEPTH } from '../services/role_mapping_constants'; import { Rule, generateRulesFromRaw } from '../../model'; -import { DocumentationLinksService } from '../../documentation_links'; import { validateRoleMappingRules } from '../services/role_mapping_validation'; interface Props { @@ -35,7 +35,7 @@ interface Props { onChange: (rawRules: RoleMapping['rules']) => void; onValidityChange: (isValid: boolean) => void; validateForm: boolean; - docLinks: DocumentationLinksService; + docLinks: DocLinksStart; } interface State { @@ -92,7 +92,7 @@ export class RuleEditorPanel extends Component { values={{ learnMoreLink: ( @@ -215,7 +215,6 @@ export class RuleEditorPanel extends Component { rules={this.state.rules} onChange={this.onRuleChange} onValidityChange={this.onValidityChange} - docLinks={this.props.docLinks} /> ); default: diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx index a8001c1b775ea..05edd3a370092 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx @@ -13,7 +13,7 @@ import { EmptyPrompt } from './empty_prompt'; import { findTestSubject } from '@kbn/test/jest'; import { EuiLink } from '@elastic/eui'; import { act } from '@testing-library/react'; -import { DocumentationLinksService } from '../documentation_links'; +import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; import { coreMock, scopedHistoryMock } from '../../../../../../../src/core/public/mocks'; import { roleMappingsAPIClientMock } from '../role_mappings_api_client.mock'; @@ -23,6 +23,24 @@ describe('RoleMappingsGridPage', () => { let history: ScopedHistory; let coreStart: CoreStart; + const renderView = ( + roleMappingsAPI: ReturnType, + rolesAPI: ReturnType = rolesAPIClientMock.create() + ) => { + return mountWithIntl( + + + + ); + }; + beforeEach(() => { history = scopedHistoryMock.create(); coreStart = coreMock.createStart(); @@ -36,17 +54,7 @@ describe('RoleMappingsGridPage', () => { hasCompatibleRealms: true, }); - const { docLinks, notifications } = coreMock.createStart(); - const wrapper = mountWithIntl( - - ); + const wrapper = renderView(roleMappingsAPI); expect(wrapper.find(SectionLoading)).toHaveLength(1); expect(wrapper.find(EmptyPrompt)).toHaveLength(0); @@ -65,17 +73,7 @@ describe('RoleMappingsGridPage', () => { hasCompatibleRealms: true, }); - const { docLinks, notifications } = coreMock.createStart(); - const wrapper = mountWithIntl( - - ); + const wrapper = renderView(roleMappingsAPI); expect(wrapper.find(SectionLoading)).toHaveLength(1); expect(wrapper.find(PermissionDenied)).toHaveLength(0); @@ -102,17 +100,7 @@ describe('RoleMappingsGridPage', () => { hasCompatibleRealms: false, }); - const { docLinks, notifications } = coreMock.createStart(); - const wrapper = mountWithIntl( - - ); + const wrapper = renderView(roleMappingsAPI); expect(wrapper.find(SectionLoading)).toHaveLength(1); expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); @@ -138,17 +126,7 @@ describe('RoleMappingsGridPage', () => { hasCompatibleRealms: true, }); - const { docLinks, notifications } = coreMock.createStart(); - const wrapper = mountWithIntl( - - ); + const wrapper = renderView(roleMappingsAPI); await nextTick(); wrapper.update(); @@ -172,17 +150,7 @@ describe('RoleMappingsGridPage', () => { hasCompatibleRealms: true, }); - const { docLinks, notifications } = coreMock.createStart(); - const wrapper = mountWithIntl( - - ); + const wrapper = renderView(roleMappingsAPI); await nextTick(); wrapper.update(); @@ -212,17 +180,7 @@ describe('RoleMappingsGridPage', () => { }, ]); - const { docLinks, notifications } = coreMock.createStart(); - const wrapper = mountWithIntl( - - ); + const wrapper = renderView(roleMappingsAPI); await nextTick(); wrapper.update(); @@ -275,17 +233,7 @@ describe('RoleMappingsGridPage', () => { }, ]); - const { docLinks, notifications } = coreMock.createStart(); - const wrapper = mountWithIntl( - - ); + const wrapper = renderView(roleMappingsAPI, roleAPIClient); await nextTick(); wrapper.update(); diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx index 7e83c3654cac3..2d225f5ebd084 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx @@ -25,7 +25,12 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import type { PublicMethodsOf } from '@kbn/utility-types'; -import { NotificationsStart, ApplicationStart, ScopedHistory } from 'src/core/public'; +import type { + NotificationsStart, + ApplicationStart, + DocLinksStart, + ScopedHistory, +} from 'src/core/public'; import { RoleMapping, Role } from '../../../../common/model'; import { EmptyPrompt } from './empty_prompt'; import { @@ -35,7 +40,6 @@ import { SectionLoading, } from '../components'; import { EDIT_ROLE_MAPPING_PATH, getEditRoleMappingHref } from '../../management_urls'; -import { DocumentationLinksService } from '../documentation_links'; import { RoleMappingsAPIClient } from '../role_mappings_api_client'; import { RoleTableDisplay } from '../../role_table_display'; import { RolesAPIClient } from '../../roles'; @@ -46,7 +50,7 @@ interface Props { rolesAPIClient: PublicMethodsOf; roleMappingsAPI: PublicMethodsOf; notifications: NotificationsStart; - docLinks: DocumentationLinksService; + docLinks: DocLinksStart; history: ScopedHistory; navigateToApp: ApplicationStart['navigateToApp']; } @@ -148,7 +152,7 @@ export class RoleMappingsGridPage extends Component { values={{ learnMoreLink: ( @@ -179,7 +183,7 @@ export class RoleMappingsGridPage extends Component { {!this.state.hasCompatibleRealms && ( <> - + )} diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.test.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.test.tsx index c72aeac5ba6f2..0d7443bad5f32 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.test.tsx @@ -5,11 +5,21 @@ */ jest.mock('./role_mappings_grid', () => ({ - RoleMappingsGridPage: (props: any) => `Role Mappings Page: ${JSON.stringify(props)}`, + RoleMappingsGridPage: (props: any) => + // `docLinks` object is too big to include into test snapshot, so we just check its existence. + `Role Mappings Page: ${JSON.stringify({ + ...props, + docLinks: props.docLinks ? {} : undefined, + })}`, })); jest.mock('./edit_role_mapping', () => ({ - EditRoleMappingPage: (props: any) => `Role Mapping Edit Page: ${JSON.stringify(props)}`, + EditRoleMappingPage: (props: any) => + // `docLinks` object is too big to include into test snapshot, so we just check its existence. + `Role Mapping Edit Page: ${JSON.stringify({ + ...props, + docLinks: props.docLinks ? {} : undefined, + })}`, })); import { roleMappingsManagementApp } from './role_mappings_management_app'; @@ -54,7 +64,7 @@ describe('roleMappingsManagementApp', () => { expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `/`, text: 'Role Mappings' }]); expect(container).toMatchInlineSnapshot(`
    - Role Mappings Page: {"notifications":{"toasts":{}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"docLinks":{"mappingRoles":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/mapping-roles.html","createRoleMapping":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-api-put-role-mapping.html","createRoleMappingTemplates":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-api-put-role-mapping.html#_role_templates","roleMappingFieldRules":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/role-mapping-resources.html#mapping-roles-rule-field"},"history":{"action":"PUSH","length":1,"location":{"pathname":"/","search":"","hash":""}}} + Role Mappings Page: {"notifications":{"toasts":{}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"docLinks":{},"history":{"action":"PUSH","length":1,"location":{"pathname":"/","search":"","hash":""}}}
    `); @@ -73,7 +83,7 @@ describe('roleMappingsManagementApp', () => { ]); expect(container).toMatchInlineSnapshot(`
    - Role Mapping Edit Page: {"roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"notifications":{"toasts":{}},"docLinks":{"mappingRoles":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/mapping-roles.html","createRoleMapping":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-api-put-role-mapping.html","createRoleMappingTemplates":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-api-put-role-mapping.html#_role_templates","roleMappingFieldRules":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/role-mapping-resources.html#mapping-roles-rule-field"},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit","search":"","hash":""}}} + Role Mapping Edit Page: {"roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"notifications":{"toasts":{}},"docLinks":{},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit","search":"","hash":""}}}
    `); @@ -94,7 +104,7 @@ describe('roleMappingsManagementApp', () => { ]); expect(container).toMatchInlineSnapshot(`
    - Role Mapping Edit Page: {"name":"role@mapping","roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"notifications":{"toasts":{}},"docLinks":{"mappingRoles":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/mapping-roles.html","createRoleMapping":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-api-put-role-mapping.html","createRoleMappingTemplates":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-api-put-role-mapping.html#_role_templates","roleMappingFieldRules":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/role-mapping-resources.html#mapping-roles-rule-field"},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/role@mapping","search":"","hash":""}}} + Role Mapping Edit Page: {"name":"role@mapping","roleMappingsAPI":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"notifications":{"toasts":{}},"docLinks":{},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/role@mapping","search":"","hash":""}}}
    `); diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx index ce4ded5a9acbc..28b452c10c237 100644 --- a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx @@ -11,8 +11,8 @@ import { i18n } from '@kbn/i18n'; import { StartServicesAccessor } from 'src/core/public'; import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; import { PluginStartDependencies } from '../../plugin'; -import { DocumentationLinksService } from './documentation_links'; import { tryDecodeURIComponent } from '../url_utils'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; interface CreateParams { getStartServices: StartServicesAccessor; @@ -39,7 +39,7 @@ export const roleMappingsManagementApp = Object.freeze({ ]; const [ - [{ docLinks, http, notifications, i18n: i18nStart }], + [core], { RoleMappingsGridPage }, { EditRoleMappingPage }, { RoleMappingsAPIClient }, @@ -52,16 +52,15 @@ export const roleMappingsManagementApp = Object.freeze({ import('../roles'), ]); - const roleMappingsAPIClient = new RoleMappingsAPIClient(http); - const dockLinksService = new DocumentationLinksService(docLinks); + const roleMappingsAPIClient = new RoleMappingsAPIClient(core.http); const RoleMappingsGridPageWithBreadcrumbs = () => { setBreadcrumbs(roleMappingsBreadcrumbs); return ( @@ -90,27 +89,29 @@ export const roleMappingsManagementApp = Object.freeze({ ); }; render( - - - - - - - - - - - - , + + + + + + + + + + + + + + , element ); diff --git a/x-pack/plugins/security/public/management/roles/documentation_links.ts b/x-pack/plugins/security/public/management/roles/documentation_links.ts deleted file mode 100644 index aa19fbecb9c7b..0000000000000 --- a/x-pack/plugins/security/public/management/roles/documentation_links.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { DocLinksStart } from 'src/core/public'; - -export class DocumentationLinksService { - private readonly esClusterPrivileges: string; - private readonly esRunAsPrivilege: string; - private readonly esIndicesPrivileges: string; - - constructor(docLinks: DocLinksStart) { - this.esClusterPrivileges = `${docLinks.links.security.clusterPrivileges}`; - this.esRunAsPrivilege = `${docLinks.links.security.runAsPrivilege}`; - this.esIndicesPrivileges = `${docLinks.links.security.indicesPrivileges}`; - } - - public getESClusterPrivilegesDocUrl() { - return `${this.esClusterPrivileges}`; - } - - public getESRunAsPrivilegesDocUrl() { - return `${this.esRunAsPrivilege}`; - } - - public getESIndicesPrivilegesDocUrl() { - return `${this.esIndicesPrivileges}`; - } -} diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx index b86fa1f175e96..e431b49bf2f84 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx @@ -11,7 +11,6 @@ import { mountWithIntl, nextTick } from '@kbn/test/jest'; import { Capabilities } from 'src/core/public'; import { KibanaFeature } from '../../../../../features/public'; import { Role } from '../../../../common/model'; -import { DocumentationLinksService } from '../documentation_links'; import { EditRolePage } from './edit_role_page'; import { SimplePrivilegeSection } from './privileges/kibana/simple_privilege_section'; @@ -184,7 +183,7 @@ function getProps({ userAPIClient, getFeatures: () => Promise.resolve(buildFeatures()), notifications, - docLinks: new DocumentationLinksService(docLinks), + docLinks, fatalErrors, uiCapabilities: buildUICapabilities(canManageSpaces), history: scopedHistoryMock.create(), diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx index d24191c54bd94..c750ec373b9f7 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx @@ -38,7 +38,7 @@ import { IHttpFetchError, NotificationsStart, } from 'src/core/public'; -import { ScopedHistory } from 'kibana/public'; +import type { DocLinksStart, ScopedHistory } from 'kibana/public'; import { FeaturesPluginStart } from '../../../../../features/public'; import { KibanaFeature } from '../../../../../features/common'; import { IndexPatternsContract } from '../../../../../../../src/plugins/data/public'; @@ -61,7 +61,6 @@ import { ElasticsearchPrivileges, KibanaPrivilegesRegion } from './privileges'; import { ReservedRoleBadge } from './reserved_role_badge'; import { SecurityLicense } from '../../../../common/licensing'; import { UserAPIClient } from '../../users'; -import { DocumentationLinksService } from '../documentation_links'; import { IndicesAPIClient } from '../indices_api_client'; import { RolesAPIClient } from '../roles_api_client'; import { PrivilegesAPIClient } from '../privileges_api_client'; @@ -77,7 +76,7 @@ interface Props { rolesAPIClient: PublicMethodsOf; privilegesAPIClient: PublicMethodsOf; getFeatures: FeaturesPluginStart['getFeatures']; - docLinks: DocumentationLinksService; + docLinks: DocLinksStart; http: HttpStart; license: SecurityLicense; uiCapabilities: Capabilities; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/index_privilege_form.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/index_privilege_form.test.tsx.snap index d23a6da13a3bb..4dd032f15e8d8 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/index_privilege_form.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/index_privilege_form.test.tsx.snap @@ -65,6 +65,7 @@ exports[`it renders without crashing 1`] = ` isClearable={true} isDisabled={false} onChange={[Function]} + onCreateOption={[Function]} options={ Array [ Object { diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.test.tsx index 316822f7db024..4a29c5a5b134c 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.test.tsx @@ -6,7 +6,6 @@ import React from 'react'; import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest'; -import { DocumentationLinksService } from '../../../documentation_links'; import { RoleValidator } from '../../validate_role'; import { ClusterPrivileges } from './cluster_privileges'; import { ElasticsearchPrivileges } from './elasticsearch_privileges'; @@ -45,7 +44,7 @@ function getProps() { index: ['all', 'read', 'write', 'index'], }, indicesAPIClient: indicesAPIClientMock.create(), - docLinks: new DocumentationLinksService(docLinks), + docLinks, license, }; } diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx index 8fc09ce167400..ca7a086639051 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx @@ -19,6 +19,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import type { PublicMethodsOf } from '@kbn/utility-types'; import React, { Component, Fragment } from 'react'; +import type { DocLinksStart } from 'src/core/public'; import { Role, BuiltinESPrivileges } from '../../../../../../common/model'; import { SecurityLicense } from '../../../../../../common/licensing'; import { IndicesAPIClient } from '../../../indices_api_client'; @@ -26,13 +27,12 @@ import { RoleValidator } from '../../validate_role'; import { CollapsiblePanel } from '../../collapsible_panel'; import { ClusterPrivileges } from './cluster_privileges'; import { IndexPrivileges } from './index_privileges'; -import { DocumentationLinksService } from '../../../documentation_links'; interface Props { role: Role; editable: boolean; indicesAPIClient: PublicMethodsOf; - docLinks: DocumentationLinksService; + docLinks: DocLinksStart; license: SecurityLicense; onChange: (role: Role) => void; runAsUsers: string[]; @@ -90,7 +90,7 @@ export class ElasticsearchPrivileges extends Component { id="xpack.security.management.editRole.elasticSearchPrivileges.manageRoleActionsDescription" defaultMessage="Manage the actions this role can perform against your cluster. " /> - {this.learnMore(docLinks.getESClusterPrivilegesDocUrl())} + {this.learnMore(docLinks.links.security.clusterPrivileges)}

    } > @@ -120,7 +120,7 @@ export class ElasticsearchPrivileges extends Component { id="xpack.security.management.editRole.elasticSearchPrivileges.howToBeSubmittedOnBehalfOfOtherUsersDescription" defaultMessage="Allow requests to be submitted on the behalf of other users. " /> - {this.learnMore(docLinks.getESRunAsPrivilegesDocUrl())} + {this.learnMore(docLinks.links.security.runAsPrivilege)}

    } > @@ -164,7 +164,7 @@ export class ElasticsearchPrivileges extends Component { id="xpack.security.management.editRole.elasticSearchPrivileges.controlAccessToClusterDataDescription" defaultMessage="Control access to the data in your cluster. " /> - {this.learnMore(docLinks.getESIndicesPrivilegesDocUrl())} + {this.learnMore(docLinks.links.security.indicesPrivileges)}

    diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx index 889b72d0b65dd..f5eb2eadf8c55 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx @@ -36,6 +36,40 @@ test('it renders without crashing', () => { expect(wrapper).toMatchSnapshot(); }); +test('it allows for custom index privileges', () => { + const props = { + indexPrivilege: { + names: ['foo'], + privileges: ['existing-custom', 'read'], + query: '', + field_security: { + grant: [], + }, + }, + formIndex: 0, + indexPatterns: [], + availableFields: [], + availableIndexPrivileges: ['all', 'read', 'write', 'index'], + isRoleReadOnly: false, + allowDocumentLevelSecurity: true, + allowFieldLevelSecurity: true, + validator: new RoleValidator(), + onChange: jest.fn(), + onDelete: jest.fn(), + intl: {} as any, + }; + + const wrapper = mountWithIntl(); + + const indexPrivsSelect = wrapper.find('EuiComboBox[data-test-subj="privilegesInput0"]'); + + (indexPrivsSelect.props() as any).onCreateOption('custom-index-privilege'); + + expect(props.onChange).toHaveBeenCalledWith( + expect.objectContaining({ privileges: ['existing-custom', 'read', 'custom-index-privilege'] }) + ); +}); + describe('delete button', () => { const props = { indexPrivilege: { diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx index ed5ade0d23bf3..19a17daf07927 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx @@ -128,6 +128,7 @@ export class IndexPrivilegeForm extends Component { options={this.props.availableIndexPrivileges.map(toOption)} selectedOptions={this.props.indexPrivilege.privileges.map(toOption)} onChange={this.onPrivilegeChange} + onCreateOption={this.onCreateCustomPrivilege} isDisabled={this.props.isRoleReadOnly} /> @@ -390,6 +391,13 @@ export class IndexPrivilegeForm extends Component { }); }; + private onCreateCustomPrivilege = (customPrivilege: string) => { + this.props.onChange({ + ...this.props.indexPrivilege, + privileges: [...this.props.indexPrivilege.privileges, customPrivilege], + }); + }; + private onQueryChange = (e: ChangeEvent) => { this.props.onChange({ ...this.props.indexPrivilege, diff --git a/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx b/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx index 8003b21f5d906..12c1951fc60f0 100644 --- a/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx @@ -11,7 +11,9 @@ jest.mock('./roles_grid', () => ({ })); jest.mock('./edit_role', () => ({ - EditRolePage: (props: any) => `Role Edit Page: ${JSON.stringify(props)}`, + // `docLinks` object is too big to include into test snapshot, so we just check its existence. + EditRolePage: (props: any) => + `Role Edit Page: ${JSON.stringify({ ...props, docLinks: props.docLinks ? {} : undefined })}`, })); import { rolesManagementApp } from './roles_management_app'; @@ -87,7 +89,7 @@ describe('rolesManagementApp', () => { expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `/`, text: 'Roles' }, { text: 'Create' }]); expect(container).toMatchInlineSnapshot(`
    - Role Edit Page: {"action":"edit","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{"esClusterPrivileges":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-privileges.html#privileges-list-cluster","esRunAsPrivilege":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-privileges.html#_run_as_privilege","esIndicesPrivileges":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-privileges.html#privileges-list-indices"},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit","search":"","hash":""}}} + Role Edit Page: {"action":"edit","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit","search":"","hash":""}}}
    `); @@ -108,7 +110,7 @@ describe('rolesManagementApp', () => { ]); expect(container).toMatchInlineSnapshot(`
    - Role Edit Page: {"action":"edit","roleName":"role@name","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{"esClusterPrivileges":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-privileges.html#privileges-list-cluster","esRunAsPrivilege":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-privileges.html#_run_as_privilege","esIndicesPrivileges":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-privileges.html#privileges-list-indices"},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/role@name","search":"","hash":""}}} + Role Edit Page: {"action":"edit","roleName":"role@name","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/edit/role@name","search":"","hash":""}}}
    `); @@ -126,7 +128,7 @@ describe('rolesManagementApp', () => { expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `/`, text: 'Roles' }, { text: 'Create' }]); expect(container).toMatchInlineSnapshot(`
    - Role Edit Page: {"action":"clone","roleName":"someRoleName","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{"esClusterPrivileges":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-privileges.html#privileges-list-cluster","esRunAsPrivilege":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-privileges.html#_run_as_privilege","esIndicesPrivileges":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/security-privileges.html#privileges-list-indices"},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/clone/someRoleName","search":"","hash":""}}} + Role Edit Page: {"action":"clone","roleName":"someRoleName","rolesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"userAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}}},"http":{"basePath":{"basePath":"","serverBasePath":""},"anonymousPaths":{},"externalUrl":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}},"history":{"action":"PUSH","length":1,"location":{"pathname":"/clone/someRoleName","search":"","hash":""}}}
    `); diff --git a/x-pack/plugins/security/public/management/roles/roles_management_app.tsx b/x-pack/plugins/security/public/management/roles/roles_management_app.tsx index d5b3b4998a09d..26c679516b46e 100644 --- a/x-pack/plugins/security/public/management/roles/roles_management_app.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_management_app.tsx @@ -12,7 +12,6 @@ import { StartServicesAccessor, FatalErrorsSetup } from 'src/core/public'; import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; import { SecurityLicense } from '../../../common/licensing'; import { PluginStartDependencies } from '../../plugin'; -import { DocumentationLinksService } from './documentation_links'; import { tryDecodeURIComponent } from '../url_utils'; interface CreateParams { @@ -97,7 +96,7 @@ export const rolesManagementApp = Object.freeze({ notifications={notifications} fatalErrors={fatalErrors} license={license} - docLinks={new DocumentationLinksService(docLinks)} + docLinks={docLinks} uiCapabilities={application.capabilities} indexPatterns={data.indexPatterns} history={history} diff --git a/x-pack/plugins/security/public/security_checkup/components/insecure_cluster_alert.tsx b/x-pack/plugins/security/public/security_checkup/components/insecure_cluster_alert.tsx index 310caeac91dc1..844444c0e64d5 100644 --- a/x-pack/plugins/security/public/security_checkup/components/insecure_cluster_alert.tsx +++ b/x-pack/plugins/security/public/security_checkup/components/insecure_cluster_alert.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { I18nProvider, FormattedMessage } from '@kbn/i18n/react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { MountPoint } from 'kibana/public'; +import type { DocLinksStart, MountPoint } from 'kibana/public'; import { EuiCheckbox, EuiText, @@ -16,7 +16,6 @@ import { EuiFlexItem, EuiButton, } from '@elastic/eui'; -import { DocumentationLinksService } from '../documentation_links'; export const insecureClusterAlertTitle = i18n.translate( 'xpack.security.checkup.insecureClusterTitle', @@ -24,12 +23,15 @@ export const insecureClusterAlertTitle = i18n.translate( ); export const insecureClusterAlertText = ( - getDocLinksService: () => DocumentationLinksService, + getDocLinks: () => DocLinksStart, onDismiss: (persist: boolean) => void ) => ((e) => { const AlertText = () => { const [persist, setPersist] = useState(false); + const enableSecurityDocLink = `${ + getDocLinks().links.security.elasticsearchEnableSecurity + }?blade=kibanasecuritymessage`; return ( @@ -56,7 +58,7 @@ export const insecureClusterAlertText = ( size="s" color="primary" fill - href={getDocLinksService().getEnableSecurityDocUrl()} + href={enableSecurityDocLink} target="_blank" data-test-subj="learnMoreButton" > diff --git a/x-pack/plugins/security/public/security_checkup/documentation_links.ts b/x-pack/plugins/security/public/security_checkup/documentation_links.ts deleted file mode 100644 index 4a2a2bc968cc6..0000000000000 --- a/x-pack/plugins/security/public/security_checkup/documentation_links.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { DocLinksStart } from 'src/core/public'; - -export class DocumentationLinksService { - private readonly esEnableSecurity: string; - - constructor(docLinks: DocLinksStart) { - this.esEnableSecurity = `${docLinks.links.security.elasticsearchEnableSecurity}`; - } - - public getEnableSecurityDocUrl() { - return `${this.esEnableSecurity}?blade=kibanasecuritymessage`; - } -} diff --git a/x-pack/plugins/security/public/security_checkup/security_checkup_service.tsx b/x-pack/plugins/security/public/security_checkup/security_checkup_service.tsx index a0ea194170dff..bda9f210b02ec 100644 --- a/x-pack/plugins/security/public/security_checkup/security_checkup_service.tsx +++ b/x-pack/plugins/security/public/security_checkup/security_checkup_service.tsx @@ -4,14 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DocLinksStart } from 'kibana/public'; +import type { DocLinksStart } from 'kibana/public'; import { SecurityOssPluginSetup, SecurityOssPluginStart, } from '../../../../../src/plugins/security_oss/public'; import { insecureClusterAlertTitle, insecureClusterAlertText } from './components'; -import { DocumentationLinksService } from './documentation_links'; interface SetupDeps { securityOssSetup: SecurityOssPluginSetup; @@ -25,13 +24,13 @@ interface StartDeps { export class SecurityCheckupService { private securityOssStart?: SecurityOssPluginStart; - private docLinksService?: DocumentationLinksService; + private docLinks?: DocLinksStart; public setup({ securityOssSetup }: SetupDeps) { securityOssSetup.insecureCluster.setAlertTitle(insecureClusterAlertTitle); securityOssSetup.insecureCluster.setAlertText( insecureClusterAlertText( - () => this.docLinksService!, + () => this.docLinks!, (persist: boolean) => this.onDismiss(persist) ) ); @@ -39,7 +38,7 @@ export class SecurityCheckupService { public start({ securityOssStart, docLinks }: StartDeps) { this.securityOssStart = securityOssStart; - this.docLinksService = new DocumentationLinksService(docLinks); + this.docLinks = docLinks; } private onDismiss(persist: boolean) { diff --git a/x-pack/plugins/security/server/anonymous_access/anonymous_access_service.mock.ts b/x-pack/plugins/security/server/anonymous_access/anonymous_access_service.mock.ts new file mode 100644 index 0000000000000..55509090af28a --- /dev/null +++ b/x-pack/plugins/security/server/anonymous_access/anonymous_access_service.mock.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import type { AnonymousAccessServiceStart } from './anonymous_access_service'; + +import { capabilitiesServiceMock } from '../../../../../src/core/server/mocks'; + +export const anonymousAccessServiceMock = { + createStart: (): jest.Mocked => ({ + isAnonymousAccessEnabled: false, + accessURLParameters: null, + getCapabilities: jest.fn().mockReturnValue(capabilitiesServiceMock.createCapabilities()), + }), +}; diff --git a/x-pack/plugins/security/server/anonymous_access/anonymous_access_service.test.ts b/x-pack/plugins/security/server/anonymous_access/anonymous_access_service.test.ts new file mode 100644 index 0000000000000..5aee78c368e64 --- /dev/null +++ b/x-pack/plugins/security/server/anonymous_access/anonymous_access_service.test.ts @@ -0,0 +1,255 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { errors } from '@elastic/elasticsearch'; +import type { Logger } from '../../../../../src/core/server'; +import { ConfigSchema, createConfig } from '../config'; +import { AnonymousAccessService } from './anonymous_access_service'; + +import { + coreMock, + httpServerMock, + loggingSystemMock, + elasticsearchServiceMock, +} from '../../../../../src/core/server/mocks'; +import { spacesMock } from '../../../spaces/server/mocks'; +import { securityMock } from '../mocks'; + +const createSecurityConfig = (config: Record = {}) => { + return createConfig(ConfigSchema.validate(config), loggingSystemMock.createLogger(), { + isTLSEnabled: true, + }); +}; + +describe('AnonymousAccessService', () => { + let service: AnonymousAccessService; + let logger: jest.Mocked; + let getConfigMock: jest.Mock; + beforeEach(() => { + logger = loggingSystemMock.createLogger(); + getConfigMock = jest.fn().mockReturnValue(createSecurityConfig()); + + service = new AnonymousAccessService(logger, getConfigMock); + }); + + describe('#setup()', () => { + it('returns proper contract', () => { + expect(service.setup()).toBeUndefined(); + }); + }); + + describe('#start()', () => { + const getStartParams = () => { + const mockCoreStart = coreMock.createStart(); + return { + spaces: spacesMock.createStart().spacesService, + basePath: mockCoreStart.http.basePath, + capabilities: mockCoreStart.capabilities, + clusterClient: elasticsearchServiceMock.createClusterClient(), + }; + }; + + it('returns proper contract', () => { + service.setup(); + expect(service.start(getStartParams())).toEqual({ + isAnonymousAccessEnabled: false, + accessURLParameters: null, + getCapabilities: expect.any(Function), + }); + }); + + it('returns `isAnonymousAccessEnabled == true` if anonymous provider is enabled', () => { + getConfigMock.mockReturnValue( + createSecurityConfig({ + authc: { + providers: { + anonymous: { + anonymous1: { order: 0, credentials: { username: 'user', password: 'password' } }, + }, + }, + }, + }) + ); + + service.setup(); + expect(service.start(getStartParams())).toEqual({ + isAnonymousAccessEnabled: true, + accessURLParameters: null, + getCapabilities: expect.any(Function), + }); + }); + + it('returns `isAnonymousAccessEnabled == true` and access URL parameters if anonymous provider is enabled, but not default', () => { + getConfigMock.mockReturnValue( + createSecurityConfig({ + authc: { + providers: { + anonymous: { + anonymous1: { order: 0, credentials: { username: 'user', password: 'password' } }, + }, + basic: { basic1: { order: 1 } }, + }, + }, + }) + ); + + service.setup(); + expect(service.start(getStartParams())).toEqual({ + isAnonymousAccessEnabled: true, + accessURLParameters: new Map([['auth_provider_hint', 'anonymous1']]), + getCapabilities: expect.any(Function), + }); + }); + + it('returns `isAnonymousAccessEnabled == false` if anonymous provider defined, but disabled', () => { + getConfigMock.mockReturnValue( + createSecurityConfig({ + authc: { + providers: { + anonymous: { + anonymous1: { + enabled: false, + order: 0, + credentials: { username: 'user', password: 'password' }, + }, + }, + }, + }, + }) + ); + + service.setup(); + expect(service.start(getStartParams())).toEqual({ + isAnonymousAccessEnabled: false, + accessURLParameters: null, + getCapabilities: expect.any(Function), + }); + }); + + describe('#getCapabilities', () => { + beforeEach(() => { + getConfigMock.mockReturnValue( + createSecurityConfig({ + authc: { + providers: { + anonymous: { + anonymous1: { order: 0, credentials: { username: 'user', password: 'password' } }, + }, + }, + }, + }) + ); + }); + + it('returns default capabilities if anonymous access is not enabled', async () => { + getConfigMock.mockReturnValue(createSecurityConfig()); + service.setup(); + + const defaultCapabilities = { navLinks: {}, management: {}, catalogue: {} }; + const startParams = getStartParams(); + startParams.capabilities.resolveCapabilities.mockResolvedValue(defaultCapabilities); + + const mockRequest = httpServerMock.createKibanaRequest({ + headers: { authorization: 'xxx' }, + }); + await expect(service.start(startParams).getCapabilities(mockRequest)).resolves.toEqual( + defaultCapabilities + ); + expect(startParams.capabilities.resolveCapabilities).toHaveBeenCalledTimes(1); + expect(startParams.capabilities.resolveCapabilities).toHaveBeenCalledWith( + expect.objectContaining({ headers: {} }), + { + useDefaultCapabilities: true, + } + ); + }); + + it('returns default capabilities if cannot authenticate anonymous service account', async () => { + service.setup(); + + const defaultCapabilities = { navLinks: {}, management: {}, catalogue: {} }; + const startParams = getStartParams(); + startParams.capabilities.resolveCapabilities.mockResolvedValue(defaultCapabilities); + + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue( + new errors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} })) + ); + startParams.clusterClient.asScoped.mockReturnValue(mockScopedClusterClient); + + const mockRequest = httpServerMock.createKibanaRequest({ + headers: { authorization: 'xxx' }, + }); + await expect(service.start(startParams).getCapabilities(mockRequest)).resolves.toEqual( + defaultCapabilities + ); + + expect(startParams.clusterClient.asScoped).toHaveBeenCalledTimes(1); + expect(startParams.clusterClient.asScoped).toHaveBeenCalledWith( + expect.objectContaining({ headers: { authorization: 'Basic dXNlcjpwYXNzd29yZA==' } }) + ); + expect(mockScopedClusterClient.asCurrentUser.security.authenticate).toHaveBeenCalledTimes( + 1 + ); + + expect(startParams.capabilities.resolveCapabilities).toHaveBeenCalledTimes(1); + expect(startParams.capabilities.resolveCapabilities).toHaveBeenCalledWith( + expect.objectContaining({ headers: {} }), + { + useDefaultCapabilities: true, + } + ); + }); + + it('fails if cannot retrieve capabilities because of unknown reason', async () => { + service.setup(); + + const failureReason = new errors.ResponseError( + securityMock.createApiResponse({ statusCode: 500, body: {} }) + ); + const startParams = getStartParams(); + startParams.capabilities.resolveCapabilities.mockRejectedValue(failureReason); + + const mockRequest = httpServerMock.createKibanaRequest({ + headers: { authorization: 'xxx' }, + }); + await expect(service.start(startParams).getCapabilities(mockRequest)).rejects.toBe( + failureReason + ); + + expect(startParams.capabilities.resolveCapabilities).toHaveBeenCalledTimes(1); + expect( + startParams.capabilities.resolveCapabilities + ).toHaveBeenCalledWith( + expect.objectContaining({ headers: { authorization: 'Basic dXNlcjpwYXNzd29yZA==' } }), + { useDefaultCapabilities: false } + ); + }); + + it('returns resolved capabilities', async () => { + service.setup(); + + const resolvedCapabilities = { navLinks: {}, management: {}, catalogue: {}, custom: {} }; + const startParams = getStartParams(); + startParams.capabilities.resolveCapabilities.mockResolvedValue(resolvedCapabilities); + + const mockRequest = httpServerMock.createKibanaRequest({ + headers: { authorization: 'xxx' }, + }); + await expect(service.start(startParams).getCapabilities(mockRequest)).resolves.toEqual( + resolvedCapabilities + ); + expect(startParams.capabilities.resolveCapabilities).toHaveBeenCalledTimes(1); + expect( + startParams.capabilities.resolveCapabilities + ).toHaveBeenCalledWith( + expect.objectContaining({ headers: { authorization: 'Basic dXNlcjpwYXNzd29yZA==' } }), + { useDefaultCapabilities: false } + ); + }); + }); + }); +}); diff --git a/x-pack/plugins/security/server/anonymous_access/anonymous_access_service.ts b/x-pack/plugins/security/server/anonymous_access/anonymous_access_service.ts new file mode 100644 index 0000000000000..66b1e91e12bce --- /dev/null +++ b/x-pack/plugins/security/server/anonymous_access/anonymous_access_service.ts @@ -0,0 +1,178 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import type { Request } from '@hapi/hapi'; +import { + CapabilitiesStart, + IBasePath, + KibanaRequest, + Logger, + Capabilities, + IClusterClient, +} from '../../../../../src/core/server'; +import { addSpaceIdToPath } from '../../../spaces/common'; +import type { SpacesServiceStart } from '../../../spaces/server'; +import { AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER } from '../../common/constants'; +import { AnonymousAuthenticationProvider, HTTPAuthorizationHeader } from '../authentication'; +import type { ConfigType } from '../config'; +import { getDetailedErrorMessage, getErrorStatusCode } from '../errors'; + +export interface AnonymousAccessServiceStart { + readonly isAnonymousAccessEnabled: boolean; + // We cannot use `ReadonlyMap` just yet: https://github.com/microsoft/TypeScript/issues/16840 + readonly accessURLParameters: Readonly> | null; + getCapabilities: (request: KibanaRequest) => Promise; +} + +interface AnonymousAccessServiceStartParams { + basePath: IBasePath; + capabilities: CapabilitiesStart; + clusterClient: IClusterClient; + spaces?: SpacesServiceStart; +} + +/** + * Service that manages various aspects of the anonymous access. + */ +export class AnonymousAccessService { + /** + * Indicates whether anonymous access is enabled. + */ + private isAnonymousAccessEnabled = false; + + /** + * Defines HTTP authorization header that should be used to authenticate request. + */ + private httpAuthorizationHeader: HTTPAuthorizationHeader | null = null; + + constructor(private readonly logger: Logger, private readonly getConfig: () => ConfigType) {} + + setup() { + const config = this.getConfig(); + const anonymousProvider = config.authc.sortedProviders.find( + ({ type }) => type === AnonymousAuthenticationProvider.type + ); + + this.isAnonymousAccessEnabled = !!anonymousProvider; + this.httpAuthorizationHeader = anonymousProvider + ? AnonymousAuthenticationProvider.createHTTPAuthorizationHeader( + (config.authc.providers.anonymous![anonymousProvider.name] as Record) + .credentials + ) + : null; + } + + start({ + basePath, + capabilities, + clusterClient, + spaces, + }: AnonymousAccessServiceStartParams): AnonymousAccessServiceStart { + const config = this.getConfig(); + const anonymousProvider = config.authc.sortedProviders.find( + ({ type }) => type === AnonymousAuthenticationProvider.type + ); + // We don't need to add any special parameters to the URL if any of the following is true: + // * anonymous provider isn't enabled + // * anonymous provider is enabled, but it's a default authentication mechanism + const anonymousIsDefault = + !config.authc.selector.enabled && anonymousProvider === config.authc.sortedProviders[0]; + + return { + isAnonymousAccessEnabled: this.isAnonymousAccessEnabled, + accessURLParameters: + anonymousProvider && !anonymousIsDefault + ? Object.freeze( + new Map([[AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER, anonymousProvider.name]]) + ) + : null, + getCapabilities: async (request) => { + this.logger.debug('Retrieving capabilities of the anonymous service account.'); + + let useDefaultCapabilities = false; + if (!this.isAnonymousAccessEnabled) { + this.logger.warn( + 'Default capabilities will be returned since anonymous access is not enabled.' + ); + useDefaultCapabilities = true; + } else if (!(await this.canAuthenticateAnonymousServiceAccount(clusterClient))) { + this.logger.warn( + `Default capabilities will be returned since anonymous service account cannot authenticate.` + ); + useDefaultCapabilities = true; + } + + // We should use credentials of the anonymous service account instead of credentials of the + // current user to get capabilities relevant to the anonymous access itself. + const fakeAnonymousRequest = this.createFakeAnonymousRequest({ + authenticateRequest: !useDefaultCapabilities, + }); + const spaceId = spaces?.getSpaceId(request); + if (spaceId) { + basePath.set(fakeAnonymousRequest, addSpaceIdToPath('/', spaceId)); + } + + try { + return await capabilities.resolveCapabilities(fakeAnonymousRequest, { + useDefaultCapabilities, + }); + } catch (err) { + this.logger.error( + `Failed to retrieve anonymous service account capabilities: ${getDetailedErrorMessage( + err + )}` + ); + throw err; + } + }, + }; + } + + /** + * Checks if anonymous service account can authenticate to Elasticsearch using currently configured credentials. + * @param clusterClient + */ + private async canAuthenticateAnonymousServiceAccount(clusterClient: IClusterClient) { + try { + await clusterClient + .asScoped(this.createFakeAnonymousRequest({ authenticateRequest: true })) + .asCurrentUser.security.authenticate(); + } catch (err) { + this.logger.warn( + `Failed to authenticate anonymous service account: ${getDetailedErrorMessage(err)}` + ); + + if (getErrorStatusCode(err) === 401) { + return false; + } + throw err; + } + + return true; + } + + /** + * Creates a fake Kibana request optionally attributed with the anonymous service account + * credentials to get the list of capabilities. + * @param authenticateRequest Indicates whether or not we should include authorization header with + * anonymous service account credentials. + */ + private createFakeAnonymousRequest({ authenticateRequest }: { authenticateRequest: boolean }) { + return KibanaRequest.from(({ + headers: + authenticateRequest && this.httpAuthorizationHeader + ? { authorization: this.httpAuthorizationHeader.toString() } + : {}, + // This flag is essential for the security capability switcher that relies on it to decide if + // it should perform a privileges check or automatically disable all capabilities. + auth: { isAuthenticated: authenticateRequest }, + path: '/', + route: { settings: {} }, + url: { href: '/' }, + raw: { req: { url: '/' } }, + } as unknown) as Request); + } +} diff --git a/x-pack/plugins/spaces/public/management/components/secure_space_message/index.ts b/x-pack/plugins/security/server/anonymous_access/index.ts similarity index 71% rename from x-pack/plugins/spaces/public/management/components/secure_space_message/index.ts rename to x-pack/plugins/security/server/anonymous_access/index.ts index 4526dc791a224..62a5c4f6b27ae 100644 --- a/x-pack/plugins/spaces/public/management/components/secure_space_message/index.ts +++ b/x-pack/plugins/security/server/anonymous_access/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { SecureSpaceMessage } from './secure_space_message'; +export { AnonymousAccessService, AnonymousAccessServiceStart } from './anonymous_access_service'; diff --git a/x-pack/plugins/security/server/authentication/index.ts b/x-pack/plugins/security/server/authentication/index.ts index 839596eafcc5b..c87a02c9545c1 100644 --- a/x-pack/plugins/security/server/authentication/index.ts +++ b/x-pack/plugins/security/server/authentication/index.ts @@ -19,6 +19,7 @@ export { TokenAuthenticationProvider, SAMLAuthenticationProvider, OIDCAuthenticationProvider, + AnonymousAuthenticationProvider, } from './providers'; export { BasicHTTPAuthorizationHeaderCredentials, diff --git a/x-pack/plugins/security/server/authentication/providers/anonymous.ts b/x-pack/plugins/security/server/authentication/providers/anonymous.ts index 4d1b5f4a74b2f..1585b0592b356 100644 --- a/x-pack/plugins/security/server/authentication/providers/anonymous.ts +++ b/x-pack/plugins/security/server/authentication/providers/anonymous.ts @@ -70,7 +70,42 @@ export class AnonymousAuthenticationProvider extends BaseAuthenticationProvider * Defines HTTP authorization header that should be used to authenticate request. It isn't defined * if provider should rely on Elasticsearch native anonymous access. */ - private readonly httpAuthorizationHeader?: HTTPAuthorizationHeader; + private readonly httpAuthorizationHeader: HTTPAuthorizationHeader | null; + + /** + * Create authorization header for the specified credentials. Returns `null` if credentials imply + * Elasticsearch anonymous user. + * @param credentials Credentials to create HTTP authorization header for. + */ + public static createHTTPAuthorizationHeader( + credentials: Readonly< + ElasticsearchAnonymousUserCredentials | UsernameAndPasswordCredentials | APIKeyCredentials + > + ) { + if (credentials === 'elasticsearch_anonymous_user') { + return null; + } + + if (isAPIKeyCredentials(credentials)) { + return new HTTPAuthorizationHeader( + 'ApiKey', + typeof credentials.apiKey === 'string' + ? credentials.apiKey + : new BasicHTTPAuthorizationHeaderCredentials( + credentials.apiKey.id, + credentials.apiKey.key + ).toString() + ); + } + + return new HTTPAuthorizationHeader( + 'Basic', + new BasicHTTPAuthorizationHeaderCredentials( + credentials.username, + credentials.password + ).toString() + ); + } constructor( protected readonly options: Readonly, @@ -93,25 +128,13 @@ export class AnonymousAuthenticationProvider extends BaseAuthenticationProvider ); } else if (isAPIKeyCredentials(credentials)) { this.logger.debug('Anonymous requests will be authenticated via API key.'); - this.httpAuthorizationHeader = new HTTPAuthorizationHeader( - 'ApiKey', - typeof credentials.apiKey === 'string' - ? credentials.apiKey - : new BasicHTTPAuthorizationHeaderCredentials( - credentials.apiKey.id, - credentials.apiKey.key - ).toString() - ); } else { this.logger.debug('Anonymous requests will be authenticated via username and password.'); - this.httpAuthorizationHeader = new HTTPAuthorizationHeader( - 'Basic', - new BasicHTTPAuthorizationHeaderCredentials( - credentials.username, - credentials.password - ).toString() - ); } + + this.httpAuthorizationHeader = AnonymousAuthenticationProvider.createHTTPAuthorizationHeader( + credentials + ); } /** diff --git a/x-pack/plugins/security/server/authentication/providers/pki.test.ts b/x-pack/plugins/security/server/authentication/providers/pki.test.ts index 5ccf2ead0a8c8..88753f8dc2ab1 100644 --- a/x-pack/plugins/security/server/authentication/providers/pki.test.ts +++ b/x-pack/plugins/security/server/authentication/providers/pki.test.ts @@ -31,7 +31,7 @@ interface MockPeerCertificate extends Partial { fingerprint256: string; } -function getMockPeerCertificate(chain: string[] | string) { +function getMockPeerCertificate(chain: string[] | string, isChainIncomplete = false) { const mockPeerCertificate = {} as MockPeerCertificate; (Array.isArray(chain) ? chain : [chain]).reduce( @@ -39,11 +39,16 @@ function getMockPeerCertificate(chain: string[] | string) { certificate.fingerprint256 = fingerprint; certificate.raw = { toString: (enc: string) => `fingerprint:${fingerprint}:${enc}` }; - // Imitate self-signed certificate that is issuer for itself. - certificate.issuerCertificate = index === fingerprintChain.length - 1 ? certificate : {}; + if (index === fingerprintChain.length - 1) { + // If the chain is incomplete, set the issuer to undefined. + // Otherwise, imitate self-signed certificate that is issuer for itself. + certificate.issuerCertificate = isChainIncomplete ? undefined : certificate; + } else { + certificate.issuerCertificate = {}; + } // Imitate other fields for logging assertions - certificate.subject = 'mock subject'; + certificate.subject = `mock subject(${fingerprint})`; certificate.issuer = 'mock issuer'; certificate.subjectaltname = 'mock subjectaltname'; certificate.valid_from = 'mock valid_from'; @@ -60,17 +65,25 @@ function getMockPeerCertificate(chain: string[] | string) { function getMockSocket({ authorized = false, peerCertificate = null, + protocol = 'TLSv1.2', + renegotiateError = null, }: { authorized?: boolean; peerCertificate?: MockPeerCertificate | null; + protocol?: string; + renegotiateError?: Error | null; } = {}) { const socket = new TLSSocket(new Socket()); socket.authorized = authorized; if (!authorized) { socket.authorizationError = new Error('mock authorization error'); } - socket.getPeerCertificate = jest.fn().mockReturnValue(peerCertificate); - return socket; + const mockGetPeerCertificate = jest.fn().mockReturnValue(peerCertificate); + const mockRenegotiate = jest.fn().mockImplementation((_, callback) => callback(renegotiateError)); + socket.getPeerCertificate = mockGetPeerCertificate; + socket.renegotiate = mockRenegotiate; + socket.getProtocol = jest.fn().mockReturnValue(protocol); + return { socket, mockGetPeerCertificate, mockRenegotiate }; } function expectAuthenticateCall( @@ -95,72 +108,192 @@ describe('PKIAuthenticationProvider', () => { afterEach(() => jest.clearAllMocks()); + function expectDebugLogs(...messages: string[]) { + for (const message of messages) { + expect(mockOptions.logger.debug).toHaveBeenCalledWith(message); + } + } + function defineCommonLoginAndAuthenticateTests( operation: (request: KibanaRequest) => Promise ) { it('does not handle unauthorized requests.', async () => { - const request = httpServerMock.createKibanaRequest({ - socket: getMockSocket({ - authorized: false, - peerCertificate: getMockPeerCertificate('2A:7A:C2:DD'), - }), - }); + const peerCertificate = getMockPeerCertificate('2A:7A:C2:DD'); + const { socket } = getMockSocket({ authorized: false, peerCertificate }); + const request = httpServerMock.createKibanaRequest({ socket }); await expect(operation(request)).resolves.toEqual(AuthenticationResult.notHandled()); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); - expect(mockOptions.logger.debug).toHaveBeenCalledWith( - 'Peer certificate chain: [{"subject":"mock subject","issuer":"mock issuer","issuerCertType":"object","subjectaltname":"mock subjectaltname","validFrom":"mock valid_from","validTo":"mock valid_to"}]' - ); - expect(mockOptions.logger.debug).toHaveBeenCalledWith( + expectDebugLogs( + 'Peer certificate chain: [{"subject":"mock subject(2A:7A:C2:DD)","issuer":"mock issuer","issuerCertType":"object","subjectaltname":"mock subjectaltname","validFrom":"mock valid_from","validTo":"mock valid_to"}]', 'Authentication is not possible since peer certificate was not authorized: Error: mock authorization error.' ); }); it('does not handle requests with a missing certificate chain.', async () => { - const request = httpServerMock.createKibanaRequest({ - socket: getMockSocket({ authorized: true, peerCertificate: null }), - }); + const { socket } = getMockSocket({ authorized: true, peerCertificate: null }); + const request = httpServerMock.createKibanaRequest({ socket }); await expect(operation(request)).resolves.toEqual(AuthenticationResult.notHandled()); expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); - expect(mockOptions.logger.debug).toHaveBeenCalledWith('Peer certificate chain: []'); - expect(mockOptions.logger.debug).toHaveBeenCalledWith( + expectDebugLogs( + 'Peer certificate chain: []', 'Authentication is not possible due to missing peer certificate chain.' ); }); - it('does not handle requests with an incomplete certificate chain.', async () => { - const peerCertificate = getMockPeerCertificate('2A:7A:C2:DD'); - (peerCertificate as any).issuerCertificate = undefined; // This behavior has been observed, even though it's not valid according to the type definition - const request = httpServerMock.createKibanaRequest({ - socket: getMockSocket({ authorized: true, peerCertificate }), - }); + describe('incomplete certificate chain', () => { + it('when the protocol does not allow renegotiation', async () => { + const peerCertificate = getMockPeerCertificate('2A:7A:C2:DD', true); + const { socket, mockGetPeerCertificate, mockRenegotiate } = getMockSocket({ + authorized: true, + peerCertificate, + protocol: 'TLSv1.3', + }); + const request = httpServerMock.createKibanaRequest({ socket }); + + await expect(operation(request)).resolves.toEqual(AuthenticationResult.notHandled()); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expectDebugLogs( + `Detected incomplete certificate chain with protocol 'TLSv1.3', cannot renegotiate connection.`, + 'Peer certificate chain: [{"subject":"mock subject(2A:7A:C2:DD)","issuer":"mock issuer","issuerCertType":"undefined","subjectaltname":"mock subjectaltname","validFrom":"mock valid_from","validTo":"mock valid_to"}]', + 'Authentication is not possible due to incomplete peer certificate chain.' + ); + expect(mockGetPeerCertificate).toHaveBeenCalledTimes(1); + expect(mockRenegotiate).not.toHaveBeenCalled(); + }); + + it('when renegotiation fails', async () => { + const peerCertificate = getMockPeerCertificate('2A:7A:C2:DD', true); + const { socket, mockGetPeerCertificate, mockRenegotiate } = getMockSocket({ + authorized: true, + peerCertificate, + renegotiateError: new Error('Oh no!'), + }); + const request = httpServerMock.createKibanaRequest({ socket }); + + await expect(operation(request)).resolves.toEqual(AuthenticationResult.notHandled()); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expectDebugLogs( + `Detected incomplete certificate chain with protocol 'TLSv1.2', attempting to renegotiate connection.`, + `Failed to renegotiate connection: Error: Oh no!.`, + 'Peer certificate chain: [{"subject":"mock subject(2A:7A:C2:DD)","issuer":"mock issuer","issuerCertType":"undefined","subjectaltname":"mock subjectaltname","validFrom":"mock valid_from","validTo":"mock valid_to"}]', + 'Authentication is not possible due to incomplete peer certificate chain.' + ); + expect(mockGetPeerCertificate).toHaveBeenCalledTimes(1); + expect(mockRenegotiate).toHaveBeenCalledTimes(1); + }); + + it('when renegotiation results in an incomplete cert chain', async () => { + const peerCertificate = getMockPeerCertificate('2A:7A:C2:DD', true); + const { socket, mockGetPeerCertificate, mockRenegotiate } = getMockSocket({ + authorized: true, + peerCertificate, + }); + const request = httpServerMock.createKibanaRequest({ socket }); - await expect(operation(request)).resolves.toEqual(AuthenticationResult.notHandled()); + await expect(operation(request)).resolves.toEqual(AuthenticationResult.notHandled()); - expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); - expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); - expect(mockOptions.logger.debug).toHaveBeenCalledWith( - 'Peer certificate chain: [{"subject":"mock subject","issuer":"mock issuer","issuerCertType":"undefined","subjectaltname":"mock subjectaltname","validFrom":"mock valid_from","validTo":"mock valid_to"}]' - ); - expect(mockOptions.logger.debug).toHaveBeenCalledWith( - 'Authentication is not possible due to incomplete peer certificate chain.' - ); + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expectDebugLogs( + `Detected incomplete certificate chain with protocol 'TLSv1.2', attempting to renegotiate connection.`, + 'Peer certificate chain: [{"subject":"mock subject(2A:7A:C2:DD)","issuer":"mock issuer","issuerCertType":"undefined","subjectaltname":"mock subjectaltname","validFrom":"mock valid_from","validTo":"mock valid_to"}]', + 'Authentication is not possible due to incomplete peer certificate chain.' + ); + expect(mockGetPeerCertificate).toHaveBeenCalledTimes(2); + expect(mockRenegotiate).toHaveBeenCalledTimes(1); + }); + + it('when renegotiation results in a complete cert chain with an unauthorized socket', async () => { + const { socket, mockGetPeerCertificate, mockRenegotiate } = getMockSocket({ + authorized: true, + }); + const peerCertificate1 = getMockPeerCertificate('2A:7A:C2:DD', true); // incomplete chain + const peerCertificate2 = getMockPeerCertificate(['2A:7A:C2:DD', '3B:8B:D3:EE']); // complete chain + mockGetPeerCertificate.mockReturnValue(peerCertificate2); + mockGetPeerCertificate.mockReturnValueOnce(peerCertificate1); + mockRenegotiate.mockImplementation((_, callback) => { + socket.authorized = false; + socket.authorizationError = new Error('Oh no!'); + callback(); + }); + const request = httpServerMock.createKibanaRequest({ socket }); + + await expect(operation(request)).resolves.toEqual(AuthenticationResult.notHandled()); + + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled(); + expectDebugLogs( + `Detected incomplete certificate chain with protocol 'TLSv1.2', attempting to renegotiate connection.`, + 'Self-signed certificate is detected in certificate chain', + 'Peer certificate chain: [{"subject":"mock subject(2A:7A:C2:DD)","issuer":"mock issuer","issuerCertType":"object","subjectaltname":"mock subjectaltname","validFrom":"mock valid_from","validTo":"mock valid_to"}, {"subject":"mock subject(3B:8B:D3:EE)","issuer":"mock issuer","issuerCertType":"object","subjectaltname":"mock subjectaltname","validFrom":"mock valid_from","validTo":"mock valid_to"}]', + 'Authentication is not possible since peer certificate was not authorized: Error: Oh no!.' + ); + expect(mockGetPeerCertificate).toHaveBeenCalledTimes(2); + expect(mockRenegotiate).toHaveBeenCalledTimes(1); + }); + + it('when renegotiation results in a complete cert chain with an authorized socket', async () => { + const user = mockAuthenticatedUser(); + const { socket, mockGetPeerCertificate, mockRenegotiate } = getMockSocket({ + authorized: true, + }); + const peerCertificate1 = getMockPeerCertificate('2A:7A:C2:DD', true); // incomplete chain + const peerCertificate2 = getMockPeerCertificate(['2A:7A:C2:DD', '3B:8B:D3:EE']); // complete chain + mockGetPeerCertificate.mockReturnValue(peerCertificate2); + mockGetPeerCertificate.mockReturnValueOnce(peerCertificate1); + const request = httpServerMock.createKibanaRequest({ socket }); + + mockOptions.client.callAsInternalUser.mockResolvedValue({ + authentication: user, + access_token: 'access-token', + }); + + await expect(operation(request)).resolves.toEqual( + AuthenticationResult.succeeded( + { ...user, authentication_provider: { type: 'pki', name: 'pki' } }, + { + authHeaders: { authorization: 'Bearer access-token' }, + state: { accessToken: 'access-token', peerCertificateFingerprint256: '2A:7A:C2:DD' }, + } + ) + ); + + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1); + expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', { + body: { + x509_certificate_chain: [ + 'fingerprint:2A:7A:C2:DD:base64', + 'fingerprint:3B:8B:D3:EE:base64', + ], + }, + }); + expect(mockOptions.client.asScoped).not.toHaveBeenCalled(); + expectDebugLogs( + `Detected incomplete certificate chain with protocol 'TLSv1.2', attempting to renegotiate connection.`, + 'Self-signed certificate is detected in certificate chain', + 'Peer certificate chain: [{"subject":"mock subject(2A:7A:C2:DD)","issuer":"mock issuer","issuerCertType":"object","subjectaltname":"mock subjectaltname","validFrom":"mock valid_from","validTo":"mock valid_to"}, {"subject":"mock subject(3B:8B:D3:EE)","issuer":"mock issuer","issuerCertType":"object","subjectaltname":"mock subjectaltname","validFrom":"mock valid_from","validTo":"mock valid_to"}]', + 'Successfully retrieved access token in exchange to peer certificate chain.' + ); + expect(mockGetPeerCertificate).toHaveBeenCalledTimes(2); + expect(mockRenegotiate).toHaveBeenCalledTimes(1); + }); }); it('gets an access token in exchange to peer certificate chain and stores it in the state.', async () => { const user = mockAuthenticatedUser(); - const request = httpServerMock.createKibanaRequest({ - headers: {}, - socket: getMockSocket({ - authorized: true, - peerCertificate: getMockPeerCertificate(['2A:7A:C2:DD', '3B:8B:D3:EE']), - }), - }); + const peerCertificate = getMockPeerCertificate(['2A:7A:C2:DD', '3B:8B:D3:EE']); + const { socket } = getMockSocket({ authorized: true, peerCertificate }); + const request = httpServerMock.createKibanaRequest({ socket, headers: {} }); mockOptions.client.callAsInternalUser.mockResolvedValue({ authentication: user, @@ -193,13 +326,9 @@ describe('PKIAuthenticationProvider', () => { it('gets an access token in exchange to a self-signed certificate and stores it in the state.', async () => { const user = mockAuthenticatedUser(); - const request = httpServerMock.createKibanaRequest({ - headers: {}, - socket: getMockSocket({ - authorized: true, - peerCertificate: getMockPeerCertificate('2A:7A:C2:DD'), - }), - }); + const peerCertificate = getMockPeerCertificate('2A:7A:C2:DD'); + const { socket } = getMockSocket({ authorized: true, peerCertificate }); + const request = httpServerMock.createKibanaRequest({ socket, headers: {} }); mockOptions.client.callAsInternalUser.mockResolvedValue({ authentication: user, @@ -226,12 +355,9 @@ describe('PKIAuthenticationProvider', () => { }); it('fails if could not retrieve an access token in exchange to peer certificate chain.', async () => { - const request = httpServerMock.createKibanaRequest({ - socket: getMockSocket({ - authorized: true, - peerCertificate: getMockPeerCertificate('2A:7A:C2:DD'), - }), - }); + const peerCertificate = getMockPeerCertificate('2A:7A:C2:DD'); + const { socket } = getMockSocket({ authorized: true, peerCertificate }); + const request = httpServerMock.createKibanaRequest({ socket, headers: {} }); const failureReason = LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()); mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason); @@ -287,13 +413,9 @@ describe('PKIAuthenticationProvider', () => { }); it('does not exchange peer certificate to access token if request does not require authentication.', async () => { - const request = httpServerMock.createKibanaRequest({ - routeAuthRequired: false, - socket: getMockSocket({ - authorized: true, - peerCertificate: getMockPeerCertificate(['2A:7A:C2:DD', '3B:8B:D3:EE']), - }), - }); + const peerCertificate = getMockPeerCertificate('2A:7A:C2:DD'); + const { socket } = getMockSocket({ authorized: true, peerCertificate }); + const request = httpServerMock.createKibanaRequest({ socket, routeAuthRequired: false }); await expect(provider.authenticate(request)).resolves.toEqual( AuthenticationResult.notHandled() ); @@ -303,12 +425,11 @@ describe('PKIAuthenticationProvider', () => { }); it('does not exchange peer certificate to access token for Ajax requests.', async () => { + const peerCertificate = getMockPeerCertificate('2A:7A:C2:DD'); + const { socket } = getMockSocket({ authorized: true, peerCertificate }); const request = httpServerMock.createKibanaRequest({ + socket, headers: { 'kbn-xsrf': 'xsrf' }, - socket: getMockSocket({ - authorized: true, - peerCertificate: getMockPeerCertificate(['2A:7A:C2:DD', '3B:8B:D3:EE']), - }), }); await expect(provider.authenticate(request)).resolves.toEqual( AuthenticationResult.notHandled() @@ -319,9 +440,8 @@ describe('PKIAuthenticationProvider', () => { }); it('fails with non-401 error if state is available, peer is authorized, but certificate is not available.', async () => { - const request = httpServerMock.createKibanaRequest({ - socket: getMockSocket({ authorized: true }), - }); + const { socket } = getMockSocket({ authorized: true }); + const request = httpServerMock.createKibanaRequest({ socket }); const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; @@ -333,7 +453,8 @@ describe('PKIAuthenticationProvider', () => { }); it('invalidates token and fails with 401 if state is present, but peer certificate is not.', async () => { - const request = httpServerMock.createKibanaRequest({ socket: getMockSocket() }); + const { socket } = getMockSocket(); + const request = httpServerMock.createKibanaRequest({ socket }); const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; await expect(provider.authenticate(request, state)).resolves.toEqual( @@ -347,9 +468,9 @@ describe('PKIAuthenticationProvider', () => { }); it('invalidates token and fails with 401 if new certificate is present, but not authorized.', async () => { - const request = httpServerMock.createKibanaRequest({ - socket: getMockSocket({ peerCertificate: getMockPeerCertificate('2A:7A:C2:DD') }), - }); + const peerCertificate = getMockPeerCertificate('2A:7A:C2:DD'); + const { socket } = getMockSocket({ peerCertificate }); + const request = httpServerMock.createKibanaRequest({ socket }); const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; await expect(provider.authenticate(request, state)).resolves.toEqual( @@ -364,12 +485,9 @@ describe('PKIAuthenticationProvider', () => { it('invalidates existing token and gets a new one if fingerprints do not match.', async () => { const user = mockAuthenticatedUser(); - const request = httpServerMock.createKibanaRequest({ - socket: getMockSocket({ - authorized: true, - peerCertificate: getMockPeerCertificate(['2A:7A:C2:DD', '3B:8B:D3:EE']), - }), - }); + const peerCertificate = getMockPeerCertificate(['2A:7A:C2:DD', '3B:8B:D3:EE']); + const { socket } = getMockSocket({ authorized: true, peerCertificate }); + const request = httpServerMock.createKibanaRequest({ socket }); const state = { accessToken: 'existing-token', peerCertificateFingerprint256: '3A:9A:C5:DD' }; mockOptions.client.callAsInternalUser.mockResolvedValue({ @@ -422,7 +540,7 @@ describe('PKIAuthenticationProvider', () => { socket: getMockSocket({ authorized: true, peerCertificate: getMockPeerCertificate(['2A:7A:C2:DD', '3B:8B:D3:EE']), - }), + }).socket, }); const nonAjaxState = { accessToken: 'existing-token', @@ -440,7 +558,7 @@ describe('PKIAuthenticationProvider', () => { socket: getMockSocket({ authorized: true, peerCertificate: getMockPeerCertificate(['3A:7A:C2:DD', '3B:8B:D3:EE']), - }), + }).socket, }); const ajaxState = { accessToken: 'existing-token', @@ -458,7 +576,7 @@ describe('PKIAuthenticationProvider', () => { socket: getMockSocket({ authorized: true, peerCertificate: getMockPeerCertificate(['4A:7A:C2:DD', '3B:8B:D3:EE']), - }), + }).socket, }); const optionalAuthState = { accessToken: 'existing-token', @@ -503,7 +621,8 @@ describe('PKIAuthenticationProvider', () => { }); it('fails with 401 if existing token is expired, but certificate is not present.', async () => { - const request = httpServerMock.createKibanaRequest({ socket: getMockSocket() }); + const { socket } = getMockSocket(); + const request = httpServerMock.createKibanaRequest({ socket }); const state = { accessToken: 'existing-token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); @@ -524,13 +643,9 @@ describe('PKIAuthenticationProvider', () => { it('succeeds if state contains a valid token.', async () => { const user = mockAuthenticatedUser(); const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; - const request = httpServerMock.createKibanaRequest({ - headers: {}, - socket: getMockSocket({ - authorized: true, - peerCertificate: getMockPeerCertificate(state.peerCertificateFingerprint256), - }), - }); + const peerCertificate = getMockPeerCertificate(state.peerCertificateFingerprint256); + const { socket } = getMockSocket({ authorized: true, peerCertificate }); + const request = httpServerMock.createKibanaRequest({ socket, headers: {} }); const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user); @@ -552,13 +667,9 @@ describe('PKIAuthenticationProvider', () => { it('fails if token from the state is rejected because of unknown reason.', async () => { const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' }; - const request = httpServerMock.createKibanaRequest({ - headers: {}, - socket: getMockSocket({ - authorized: true, - peerCertificate: getMockPeerCertificate(state.peerCertificateFingerprint256), - }), - }); + const peerCertificate = getMockPeerCertificate(state.peerCertificateFingerprint256); + const { socket } = getMockSocket({ authorized: true, peerCertificate }); + const request = httpServerMock.createKibanaRequest({ socket, headers: {} }); const failureReason = new errors.ServiceUnavailable(); const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); diff --git a/x-pack/plugins/security/server/authentication/providers/pki.ts b/x-pack/plugins/security/server/authentication/providers/pki.ts index 5642a6feac2b5..3171c5ff24abe 100644 --- a/x-pack/plugins/security/server/authentication/providers/pki.ts +++ b/x-pack/plugins/security/server/authentication/providers/pki.ts @@ -30,6 +30,17 @@ interface ProviderState { peerCertificateFingerprint256: string; } +interface CertificateChain { + peerCertificate: DetailedPeerCertificate | null; + certificateChain: string[]; + isChainIncomplete: boolean; +} + +/** + * List of protocols that can be renegotiated. Notably, TLSv1.3 is absent from this list, because it does not support renegotiation. + */ +const RENEGOTIATABLE_PROTOCOLS = ['TLSv1', 'TLSv1.1', 'TLSv1.2']; + /** * Checks whether current request can initiate new session. * @param request Request instance. @@ -238,8 +249,9 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider { this.logger.debug('Trying to authenticate request via peer certificate chain.'); // We should collect entire certificate chain as an ordered array of certificates encoded as base64 strings. - const peerCertificate = request.socket.getPeerCertificate(true); - const { certificateChain, isChainIncomplete } = this.getCertificateChain(peerCertificate); + const { peerCertificate, certificateChain, isChainIncomplete } = await this.getCertificateChain( + request + ); if (!request.socket.authorized) { this.logger.debug( @@ -286,17 +298,21 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider { } /** - * Starts from the leaf peer certificate and iterates up to the top-most available certificate - * authority using `issuerCertificate` certificate property. THe iteration is stopped only when - * we detect circular reference (root/self-signed certificate) or when `issuerCertificate` isn't - * available (null or empty object). - * @param peerCertificate Peer leaf certificate instance. + * Obtains the peer certificate chain. Starts from the leaf peer certificate and iterates up to the top-most available certificate + * authority using `issuerCertificate` certificate property. THe iteration is stopped only when we detect circular reference + * (root/self-signed certificate) or when `issuerCertificate` isn't available (null or empty object). Automatically attempts to + * renegotiate the TLS connection once if the peer certificate chain is incomplete. + * @param request Request instance. */ - private getCertificateChain(peerCertificate: DetailedPeerCertificate | null) { - const certificateChain = []; - const certificateStrings = []; + private async getCertificateChain( + request: KibanaRequest, + isRenegotiated = false + ): Promise { + const certificateChain: string[] = []; + const certificateStrings: string[] = []; let isChainIncomplete = false; - let certificate: DetailedPeerCertificate | null = peerCertificate; + const peerCertificate = request.socket.getPeerCertificate(true); + let certificate = peerCertificate; while (certificate && Object.keys(certificate).length > 0) { certificateChain.push(certificate.raw.toString('base64')); @@ -307,6 +323,24 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider { this.logger.debug('Self-signed certificate is detected in certificate chain'); break; } else if (certificate.issuerCertificate === undefined) { + const protocol = request.socket.getProtocol(); + if (!isRenegotiated && protocol && RENEGOTIATABLE_PROTOCOLS.includes(protocol)) { + this.logger.debug( + `Detected incomplete certificate chain with protocol '${protocol}', attempting to renegotiate connection.` + ); + try { + await request.socket.renegotiate({ requestCert: true, rejectUnauthorized: false }); + return this.getCertificateChain(request, true); + } catch (err) { + this.logger.debug(`Failed to renegotiate connection: ${err}.`); + } + } else if (!isRenegotiated) { + this.logger.debug( + `Detected incomplete certificate chain with protocol '${protocol}', cannot renegotiate connection.` + ); + } else { + this.logger.debug(`Detected incomplete certificate chain after renegotiation.`); + } // The chain is only considered to be incomplete if one or more issuerCertificate values is undefined; // this is not an expected return value from Node, but it can happen in some edge cases isChainIncomplete = true; @@ -319,6 +353,6 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider { this.logger.debug(`Peer certificate chain: [${certificateStrings.join(', ')}]`); - return { certificateChain, isChainIncomplete }; + return { peerCertificate, certificateChain, isChainIncomplete }; } } diff --git a/x-pack/plugins/security/server/errors.test.ts b/x-pack/plugins/security/server/errors.test.ts index 630ab5b9295db..2a6ee2dbcb325 100644 --- a/x-pack/plugins/security/server/errors.test.ts +++ b/x-pack/plugins/security/server/errors.test.ts @@ -5,8 +5,10 @@ */ import Boom from '@hapi/boom'; -import { errors as esErrors } from 'elasticsearch'; +import { errors as esErrors } from '@elastic/elasticsearch'; +import { errors as legacyESErrors } from 'elasticsearch'; import * as errors from './errors'; +import { securityMock } from './mocks'; describe('lib/errors', () => { describe('#wrapError', () => { @@ -55,9 +57,22 @@ describe('lib/errors', () => { expect(errors.getErrorStatusCode(Boom.unauthorized())).toBe(401); }); - it('extracts status code from Elasticsearch client error', () => { - expect(errors.getErrorStatusCode(new esErrors.BadRequest())).toBe(400); - expect(errors.getErrorStatusCode(new esErrors.AuthenticationException())).toBe(401); + it('extracts status code from Elasticsearch client response error', () => { + expect( + errors.getErrorStatusCode( + new esErrors.ResponseError(securityMock.createApiResponse({ statusCode: 400, body: {} })) + ) + ).toBe(400); + expect( + errors.getErrorStatusCode( + new esErrors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} })) + ) + ).toBe(401); + }); + + it('extracts status code from legacy Elasticsearch client error', () => { + expect(errors.getErrorStatusCode(new legacyESErrors.BadRequest())).toBe(400); + expect(errors.getErrorStatusCode(new legacyESErrors.AuthenticationException())).toBe(401); }); it('extracts status code from `status` property', () => { @@ -65,4 +80,53 @@ describe('lib/errors', () => { expect(errors.getErrorStatusCode({ statusText: 'Unauthorized', status: 401 })).toBe(401); }); }); + + describe('#getDetailedErrorMessage', () => { + it('extracts body payload from Boom error', () => { + expect(errors.getDetailedErrorMessage(Boom.badRequest())).toBe( + JSON.stringify({ statusCode: 400, error: 'Bad Request', message: 'Bad Request' }) + ); + expect(errors.getDetailedErrorMessage(Boom.unauthorized())).toBe( + JSON.stringify({ statusCode: 401, error: 'Unauthorized', message: 'Unauthorized' }) + ); + + const customBoomError = Boom.unauthorized(); + customBoomError.output.payload = { + statusCode: 401, + error: 'some-weird-error', + message: 'some-weird-message', + }; + expect(errors.getDetailedErrorMessage(customBoomError)).toBe( + JSON.stringify({ + statusCode: 401, + error: 'some-weird-error', + message: 'some-weird-message', + }) + ); + }); + + it('extracts body from Elasticsearch client response error', () => { + expect( + errors.getDetailedErrorMessage( + new esErrors.ResponseError( + securityMock.createApiResponse({ + statusCode: 401, + body: { field1: 'value-1', field2: 'value-2' }, + }) + ) + ) + ).toBe(JSON.stringify({ field1: 'value-1', field2: 'value-2' })); + }); + + it('extracts status code from legacy Elasticsearch client error', () => { + expect(errors.getDetailedErrorMessage(new legacyESErrors.BadRequest())).toBe('Bad Request'); + expect(errors.getDetailedErrorMessage(new legacyESErrors.AuthenticationException())).toBe( + 'Authentication Exception' + ); + }); + + it('extracts `message` property', () => { + expect(errors.getDetailedErrorMessage(new Error('some-message'))).toBe('some-message'); + }); + }); }); diff --git a/x-pack/plugins/security/server/errors.ts b/x-pack/plugins/security/server/errors.ts index 9c177c3916faf..3b49b40d26559 100644 --- a/x-pack/plugins/security/server/errors.ts +++ b/x-pack/plugins/security/server/errors.ts @@ -5,7 +5,8 @@ */ import Boom from '@hapi/boom'; -import { CustomHttpResponseOptions, ResponseError } from '../../../../src/core/server'; +import { errors } from '@elastic/elasticsearch'; +import type { CustomHttpResponseOptions, ResponseError } from '../../../../src/core/server'; export function wrapError(error: any) { return Boom.boomify(error, { statusCode: getErrorStatusCode(error) }); @@ -29,5 +30,27 @@ export function wrapIntoCustomErrorResponse(error: any) { * @param error Error instance to extract status code from. */ export function getErrorStatusCode(error: any): number { + if (error instanceof errors.ResponseError) { + return error.statusCode; + } + return Boom.isBoom(error) ? error.output.statusCode : error.statusCode || error.status; } + +/** + * Extracts detailed error message from Boom and Elasticsearch "native" errors. It's supposed to be + * only logged on the server side and never returned to the client as it may contain sensitive + * information. + * @param error Error instance to extract message from. + */ +export function getDetailedErrorMessage(error: any): string { + if (error instanceof errors.ResponseError) { + return JSON.stringify(error.body); + } + + if (Boom.isBoom(error)) { + return JSON.stringify(error.output.payload); + } + + return error.message; +} diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index b9615eed990f0..54efdbdccbb77 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -64,66 +64,65 @@ describe('Security Plugin', () => { }); describe('setup()', () => { - it('exposes proper contract', async () => { - await expect(plugin.setup(mockCoreSetup, mockSetupDependencies)).resolves - .toMatchInlineSnapshot(` - Object { - "audit": Object { - "asScoped": [Function], - "getLogger": [Function], - }, - "authc": Object { - "getCurrentUser": [Function], - }, - "authz": Object { - "actions": Actions { - "alerting": AlertingActions { - "prefix": "alerting:version:", - }, - "api": ApiActions { - "prefix": "api:version:", - }, - "app": AppActions { - "prefix": "app:version:", - }, - "login": "login:", - "savedObject": SavedObjectActions { - "prefix": "saved_object:version:", - }, - "space": SpaceActions { - "prefix": "space:version:", - }, - "ui": UIActions { - "prefix": "ui:version:", - }, - "version": "version:version", - "versionNumber": "version", - }, - "checkPrivilegesDynamicallyWithRequest": [Function], - "checkPrivilegesWithRequest": [Function], - "mode": Object { - "useRbacForRequest": [Function], - }, - }, - "license": Object { - "features$": Observable { - "_isScalar": false, - "operator": MapOperator { - "project": [Function], - "thisArg": undefined, - }, - "source": Observable { - "_isScalar": false, - "_subscribe": [Function], - }, - }, - "getFeatures": [Function], - "getType": [Function], - "isEnabled": [Function], - "isLicenseAvailable": [Function], - }, - } - `); + it('exposes proper contract', () => { + expect(plugin.setup(mockCoreSetup, mockSetupDependencies)).toMatchInlineSnapshot(` + Object { + "audit": Object { + "asScoped": [Function], + "getLogger": [Function], + }, + "authc": Object { + "getCurrentUser": [Function], + }, + "authz": Object { + "actions": Actions { + "alerting": AlertingActions { + "prefix": "alerting:version:", + }, + "api": ApiActions { + "prefix": "api:version:", + }, + "app": AppActions { + "prefix": "app:version:", + }, + "login": "login:", + "savedObject": SavedObjectActions { + "prefix": "saved_object:version:", + }, + "space": SpaceActions { + "prefix": "space:version:", + }, + "ui": UIActions { + "prefix": "ui:version:", + }, + "version": "version:version", + "versionNumber": "version", + }, + "checkPrivilegesDynamicallyWithRequest": [Function], + "checkPrivilegesWithRequest": [Function], + "mode": Object { + "useRbacForRequest": [Function], + }, + }, + "license": Object { + "features$": Observable { + "_isScalar": false, + "operator": MapOperator { + "project": [Function], + "thisArg": undefined, + }, + "source": Observable { + "_isScalar": false, + "_subscribe": [Function], + }, + }, + "getFeatures": [Function], + "getType": [Function], + "isEnabled": [Function], + "isLicenseAvailable": [Function], + }, + } + `); }); }); diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 450de2fc31329..1016221cb719d 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { combineLatest } from 'rxjs'; -import { first, map } from 'rxjs/operators'; +import { combineLatest, Subscription } from 'rxjs'; +import { map } from 'rxjs/operators'; import { TypeOf } from '@kbn/config-schema'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { SecurityOssPluginSetup } from 'src/plugins/security_oss/server'; @@ -30,7 +30,8 @@ import { AuthenticationServiceStart, } from './authentication'; import { AuthorizationService, AuthorizationServiceSetup } from './authorization'; -import { ConfigSchema, createConfig } from './config'; +import { AnonymousAccessService, AnonymousAccessServiceStart } from './anonymous_access'; +import { ConfigSchema, ConfigType, createConfig } from './config'; import { defineRoutes } from './routes'; import { SecurityLicenseService, SecurityLicense } from '../common/licensing'; import { setupSavedObjects } from './saved_objects'; @@ -103,9 +104,26 @@ export interface PluginStartDependencies { */ export class Plugin { private readonly logger: Logger; - private securityLicenseService?: SecurityLicenseService; private authenticationStart?: AuthenticationServiceStart; private authorizationSetup?: AuthorizationServiceSetup; + private anonymousAccessStart?: AnonymousAccessServiceStart; + private configSubscription?: Subscription; + + private config?: ConfigType; + private readonly getConfig = () => { + if (!this.config) { + throw new Error('Config is not available.'); + } + return this.config; + }; + + private kibanaIndexName?: string; + private readonly getKibanaIndexName = () => { + if (!this.kibanaIndexName) { + throw new Error('Kibana index name is not available.'); + } + return this.kibanaIndexName; + }; private readonly featureUsageService = new SecurityFeatureUsageService(); private featureUsageServiceStart?: SecurityFeatureUsageServiceStart; @@ -117,6 +135,7 @@ export class Plugin { }; private readonly auditService = new AuditService(this.initializerContext.logger.get('audit')); + private readonly securityLicenseService = new SecurityLicenseService(); private readonly authorizationService = new AuthorizationService(); private readonly elasticsearchService = new ElasticsearchService( this.initializerContext.logger.get('elasticsearch') @@ -127,12 +146,16 @@ export class Plugin { private readonly authenticationService = new AuthenticationService( this.initializerContext.logger.get('authentication') ); + private readonly anonymousAccessService = new AnonymousAccessService( + this.initializerContext.logger.get('anonymous-access'), + this.getConfig + ); constructor(private readonly initializerContext: PluginInitializerContext) { this.logger = this.initializerContext.logger.get(); } - public async setup( + public setup( core: CoreSetup, { features, @@ -143,7 +166,7 @@ export class Plugin { spaces, }: PluginSetupDependencies ) { - const [config, legacyConfig] = await combineLatest([ + this.configSubscription = combineLatest([ this.initializerContext.config.create>().pipe( map((rawConfig) => createConfig(rawConfig, this.initializerContext.logger.get('config'), { @@ -152,9 +175,13 @@ export class Plugin { ) ), this.initializerContext.config.legacy.globalConfig$, - ]) - .pipe(first()) - .toPromise(); + ]).subscribe(([config, { kibana }]) => { + this.config = config; + this.kibanaIndexName = kibana.index; + }); + + const config = this.getConfig(); + const kibanaIndexName = this.getKibanaIndexName(); // A subset of `start` services we need during `setup`. const startServicesPromise = core.getStartServices().then(([coreServices, depsServices]) => ({ @@ -162,7 +189,6 @@ export class Plugin { features: depsServices.features, })); - this.securityLicenseService = new SecurityLicenseService(); const { license } = this.securityLicenseService.setup({ license$: licensing.license$, }); @@ -172,6 +198,13 @@ export class Plugin { const showInsecureClusterWarning = !allowRbac; securityOss.showInsecureClusterWarning$.next(showInsecureClusterWarning); }); + + securityOss.setAnonymousAccessServiceProvider(() => { + if (!this.anonymousAccessStart) { + throw new Error('AnonymousAccess service is not started!'); + } + return this.anonymousAccessStart; + }); } securityFeatures.forEach((securityFeature) => @@ -192,7 +225,7 @@ export class Plugin { config, clusterClient, http: core.http, - kibanaIndexName: legacyConfig.kibana.index, + kibanaIndexName, taskManager, }); @@ -220,6 +253,8 @@ export class Plugin { session, }); + this.anonymousAccessService.setup(); + this.authorizationSetup = this.authorizationService.setup({ http: core.http, capabilities: core.capabilities, @@ -227,7 +262,7 @@ export class Plugin { startServicesPromise.then(({ elasticsearch }) => elasticsearch.client), license, loggers: this.initializerContext.logger, - kibanaIndexName: legacyConfig.kibana.index, + kibanaIndexName, packageVersion: this.initializerContext.env.packageInfo.version, buildNumber: this.initializerContext.env.packageInfo.buildNum, getSpacesService: () => spaces?.spacesService, @@ -290,7 +325,10 @@ export class Plugin { }); } - public start(core: CoreStart, { features, licensing, taskManager }: PluginStartDependencies) { + public start( + core: CoreStart, + { features, licensing, taskManager, spaces }: PluginStartDependencies + ) { this.logger.debug('Starting plugin'); this.featureUsageServiceStart = this.featureUsageService.start({ @@ -308,6 +346,13 @@ export class Plugin { this.authorizationService.start({ features, clusterClient, online$: watchOnlineStatus$() }); + this.anonymousAccessStart = this.anonymousAccessService.start({ + capabilities: core.capabilities, + clusterClient, + basePath: core.http.basePath, + spaces: spaces?.spacesService, + }); + return Object.freeze({ authc: { apiKeys: this.authenticationStart.apiKeys, @@ -326,15 +371,24 @@ export class Plugin { public stop() { this.logger.debug('Stopping plugin'); - if (this.securityLicenseService) { - this.securityLicenseService.stop(); - this.securityLicenseService = undefined; + if (this.configSubscription) { + this.configSubscription.unsubscribe(); + this.configSubscription = undefined; } if (this.featureUsageServiceStart) { this.featureUsageServiceStart = undefined; } + if (this.authenticationStart) { + this.authenticationStart = undefined; + } + + if (this.anonymousAccessStart) { + this.anonymousAccessStart = undefined; + } + + this.securityLicenseService.stop(); this.auditService.stop(); this.authorizationService.stop(); this.elasticsearchService.stop(); diff --git a/x-pack/plugins/security/tsconfig.json b/x-pack/plugins/security/tsconfig.json new file mode 100644 index 0000000000000..6c3fd1851a8cb --- /dev/null +++ b/x-pack/plugins/security/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["common/**/*", "public/**/*", "server/**/*"], + "references": [ + { "path": "../features/tsconfig.json" }, + { "path": "../licensing/tsconfig.json" }, + { "path": "../spaces/tsconfig.json" }, + { "path": "../task_manager/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/es_ui_shared/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/management/tsconfig.json" }, + { "path": "../../../src/plugins/security_oss/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" } + ] +} diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts index c409dbc7814fc..82e214398f69a 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts.spec.ts @@ -35,7 +35,7 @@ import { refreshPage } from '../tasks/security_header'; import { DETECTIONS_URL } from '../urls/navigation'; -describe.skip('Alerts', () => { +describe('Alerts', () => { context('Closing alerts', () => { beforeEach(() => { cleanKibana(); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules.spec.ts index 125848c85a84a..8becefffaea28 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules.spec.ts @@ -41,10 +41,10 @@ describe('Alerts detection rules', () => { loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); waitForAlertsPanelToBeLoaded(); waitForAlertsIndexToBeCreated(); - createCustomRule(newRule, 'rule1'); - createCustomRule(existingRule, 'rule2'); - createCustomRule(newOverrideRule, 'rule3'); - createCustomRule(newThresholdRule, 'rule4'); + createCustomRule(newRule, '1'); + createCustomRule(existingRule, '2'); + createCustomRule(newOverrideRule, '3'); + createCustomRule(newThresholdRule, '4'); }); after(() => { diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts index dff39567ecacd..9ae29efc80b51 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts @@ -86,7 +86,7 @@ import { } from '../tasks/alerts_detection_rules'; import { createCustomRuleActivated } from '../tasks/api_calls/rules'; import { createTimeline } from '../tasks/api_calls/timelines'; -import { cleanKibana } from '../tasks/common'; +import { cleanKibana, reload } from '../tasks/common'; import { createAndActivateRule, fillAboutRule, @@ -101,7 +101,6 @@ import { } from '../tasks/create_new_rule'; import { saveEditedRule, waitForKibana } from '../tasks/edit_rule'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; -import { refreshPage } from '../tasks/security_header'; import { DETECTIONS_URL } from '../urls/navigation'; @@ -216,7 +215,7 @@ describe('Custom detection rules creation', () => { }); }); -describe.skip('Custom detection rules deletion and edition', () => { +describe('Custom detection rules deletion and edition', () => { context('Deletion', () => { beforeEach(() => { cleanKibana(); @@ -226,8 +225,7 @@ describe.skip('Custom detection rules deletion and edition', () => { createCustomRuleActivated(newRule, 'rule1'); createCustomRuleActivated(newOverrideRule, 'rule2'); createCustomRuleActivated(existingRule, 'rule3'); - refreshPage(); - goToManageAlertsDetectionRules(); + reload(); }); it('Deletes one rule', () => { @@ -301,8 +299,7 @@ describe.skip('Custom detection rules deletion and edition', () => { goToManageAlertsDetectionRules(); waitForAlertsIndexToBeCreated(); createCustomRuleActivated(existingRule, 'rule1'); - refreshPage(); - goToManageAlertsDetectionRules(); + reload(); }); it('Allows a rule to be edited', () => { diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts index 0813b51cd84c3..1459a4be447a4 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts @@ -64,7 +64,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { DETECTIONS_URL } from '../urls/navigation'; -describe.skip('Detection rules, machine learning', () => { +describe('Detection rules, machine learning', () => { const expectedUrls = machineLearningRule.referenceUrls.join(''); const expectedFalsePositives = machineLearningRule.falsePositivesExamples.join(''); const expectedTags = machineLearningRule.tags.join(''); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts index 96d7c3d5d5a84..c4b925a5566f0 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts @@ -79,7 +79,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { DETECTIONS_URL } from '../urls/navigation'; -describe.skip('Detection rules, threshold', () => { +describe('Detection rules, threshold', () => { const expectedUrls = newThresholdRule.referenceUrls.join(''); const expectedFalsePositives = newThresholdRule.falsePositivesExamples.join(''); const expectedTags = newThresholdRule.tags.join(''); diff --git a/x-pack/plugins/security_solution/cypress/integration/fields_browser.spec.ts b/x-pack/plugins/security_solution/cypress/integration/fields_browser.spec.ts index e65cbf85e6e73..00ce40b10fd7c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/fields_browser.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/fields_browser.spec.ts @@ -47,7 +47,7 @@ const defaultHeaders = [ ]; describe('Fields Browser', () => { - context.skip('Fields Browser rendering', () => { + context('Fields Browser rendering', () => { before(() => { cleanKibana(); loginAndWaitForPage(HOSTS_URL); @@ -154,7 +154,7 @@ describe('Fields Browser', () => { cy.get(FIELDS_BROWSER_HOST_GEO_CITY_NAME_HEADER).should('exist'); }); - it.skip('adds a field to the timeline when the user drags and drops a field', () => { + it('adds a field to the timeline when the user drags and drops a field', () => { const filterInput = 'host.geo.c'; filterFieldsBrowser(filterInput); diff --git a/x-pack/plugins/security_solution/cypress/integration/inspect.spec.ts b/x-pack/plugins/security_solution/cypress/integration/inspect.spec.ts index 98891e65771ce..6321be1e26151 100644 --- a/x-pack/plugins/security_solution/cypress/integration/inspect.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/inspect.spec.ts @@ -18,7 +18,7 @@ import { executeTimelineKQL, openTimelineInspectButton } from '../tasks/timeline import { HOSTS_URL, NETWORK_URL } from '../urls/navigation'; -describe.skip('Inspect', () => { +describe('Inspect', () => { context('Hosts stats and tables', () => { before(() => { cleanKibana(); diff --git a/x-pack/plugins/security_solution/cypress/integration/pagination.spec.ts b/x-pack/plugins/security_solution/cypress/integration/pagination.spec.ts index 95cbf8220402f..2896b2dbc36c6 100644 --- a/x-pack/plugins/security_solution/cypress/integration/pagination.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/pagination.spec.ts @@ -17,7 +17,7 @@ import { refreshPage } from '../tasks/security_header'; import { HOSTS_PAGE_TAB_URLS } from '../urls/navigation'; -describe.skip('Pagination', () => { +describe('Pagination', () => { before(() => { cleanKibana(); loginAndWaitForPage(HOSTS_PAGE_TAB_URLS.uncommonProcesses); diff --git a/x-pack/plugins/security_solution/cypress/integration/search_bar.spec.ts b/x-pack/plugins/security_solution/cypress/integration/search_bar.spec.ts index e5e74f6eb0cac..7fcbc10f88b44 100644 --- a/x-pack/plugins/security_solution/cypress/integration/search_bar.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/search_bar.spec.ts @@ -13,7 +13,7 @@ import { HOSTS_URL } from '../urls/navigation'; import { waitForAllHostsToBeLoaded } from '../tasks/hosts/all_hosts'; import { cleanKibana } from '../tasks/common'; -describe.skip('SearchBar', () => { +describe('SearchBar', () => { before(() => { cleanKibana(); loginAndWaitForPage(HOSTS_URL); diff --git a/x-pack/plugins/security_solution/cypress/integration/sourcerer.spec.ts b/x-pack/plugins/security_solution/cypress/integration/sourcerer.spec.ts index 96007ca0326d1..91695e3f53fbb 100644 --- a/x-pack/plugins/security_solution/cypress/integration/sourcerer.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/sourcerer.spec.ts @@ -28,7 +28,7 @@ import { populateTimeline } from '../tasks/timeline'; import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline'; import { cleanKibana } from '../tasks/common'; -describe.skip('Sourcerer', () => { +describe('Sourcerer', () => { before(() => { cleanKibana(); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_attach_to_case.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_attach_to_case.spec.ts index 56b2ef00169dc..745fa9085698f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_attach_to_case.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_attach_to_case.spec.ts @@ -18,8 +18,7 @@ import { createTimeline } from '../tasks/api_calls/timelines'; import { cleanKibana } from '../tasks/common'; import { createCase } from '../tasks/api_calls/cases'; -// https://github.com/elastic/kibana/issues/86959 -describe.skip('attach timeline to case', () => { +describe('attach timeline to case', () => { context('without cases created', () => { beforeEach(() => { cleanKibana(); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts index cacf2802b6d71..0025516c75714 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts @@ -19,6 +19,7 @@ import { // TIMELINE_FILTER, TIMELINE_QUERY, TIMELINE_TITLE, + OPEN_TIMELINE_MODAL, } from '../screens/timeline'; import { TIMELINES_DESCRIPTION, @@ -79,6 +80,7 @@ describe('Timelines', () => { closeTimeline(); openTimelineFromSettings(); + cy.get(OPEN_TIMELINE_MODAL).should('be.visible'); cy.contains(timeline.title).should('exist'); cy.get(TIMELINES_DESCRIPTION).first().should('have.text', timeline.description); cy.get(TIMELINES_PINNED_EVENT_COUNT).first().should('have.text', '1'); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts index f5091dd893446..155b0b6660998 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_local_storage.spec.ts @@ -13,8 +13,7 @@ import { TABLE_COLUMN_EVENTS_MESSAGE } from '../screens/hosts/external_events'; import { waitsForEventsToBeLoaded } from '../tasks/hosts/events'; import { removeColumn } from '../tasks/timeline'; -// Failing: See https://github.com/elastic/kibana/issues/75794 -describe.skip('persistent timeline', () => { +describe('persistent timeline', () => { beforeEach(() => { cleanKibana(); loginAndWaitForPage(HOSTS_URL); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts index 705aff7b14c6c..b00739cbf17c2 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_toggle_column.spec.ts @@ -26,7 +26,7 @@ import { import { HOSTS_URL } from '../urls/navigation'; -describe.skip('toggle column in timeline', () => { +describe('toggle column in timeline', () => { before(() => { cleanKibana(); cy.intercept('POST', '/api/timeline/_export?file_name=timelines_export.ndjson').as('export'); @@ -67,7 +67,7 @@ describe.skip('toggle column in timeline', () => { cy.get(ID_HEADER_FIELD).should('exist'); }); - it.skip('adds the _id field to the timeline via drag and drop', () => { + it('adds the _id field to the timeline via drag and drop', () => { expandFirstTimelineEventDetails(); dragAndDropIdToggleFieldToTimeline(); diff --git a/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts index cf433891ac929..58ef4cd2d96ba 100644 --- a/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts @@ -19,7 +19,7 @@ const ABSOLUTE_DATE = { startTime: '2019-08-01T20:03:29.186Z', }; -describe.skip('URL compatibility', () => { +describe('URL compatibility', () => { before(() => { cleanKibana(); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts b/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts index f15096bd874bc..845c54cd4a4cc 100644 --- a/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts +++ b/x-pack/plugins/security_solution/cypress/screens/fields_browser.ts @@ -12,50 +12,42 @@ export const FIELDS_BROWSER_CHECKBOX = (id: string) => { export const FIELDS_BROWSER_CONTAINER = '[data-test-subj="fields-browser-container"]'; -export const FIELDS_BROWSER_DRAGGABLE_HOST_GEO_COUNTRY_NAME_HEADER = - '[data-test-subj="timeline"] [data-test-subj="field-name-host.geo.country_name"]'; +export const FIELDS_BROWSER_DRAGGABLE_HOST_GEO_COUNTRY_NAME_HEADER = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="field-name-host.geo.country_name"]`; -export const FIELDS_BROWSER_FIELDS_COUNT = '[data-test-subj="fields-count"]'; +export const FIELDS_BROWSER_FIELDS_COUNT = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="fields-count"]`; -export const FIELDS_BROWSER_FILTER_INPUT = '[data-test-subj="field-search"]'; +export const FIELDS_BROWSER_FILTER_INPUT = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="field-search"]`; -export const FIELDS_BROWSER_HEADER_DROP_AREA = - '[data-test-subj="timeline"] [data-test-subj="headers-group"]'; +export const FIELDS_BROWSER_HEADER_DROP_AREA = '[data-test-subj="headers-group"]'; -export const FIELDS_BROWSER_HOST_CATEGORIES_COUNT = '[data-test-subj="host-category-count"]'; +export const FIELDS_BROWSER_HOST_CATEGORIES_COUNT = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="host-category-count"]`; -export const FIELDS_BROWSER_HOST_GEO_CITY_NAME_CHECKBOX = - '[data-test-subj="field-host.geo.city_name-checkbox"]'; +export const FIELDS_BROWSER_HOST_GEO_CITY_NAME_CHECKBOX = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="field-host.geo.city_name-checkbox"]`; export const FIELDS_BROWSER_HOST_GEO_CITY_NAME_HEADER = - '[data-test-subj="header-text-host.geo.city_name"]'; + '[data-test-subj="timeline"] [data-test-subj="header-text-host.geo.city_name"]'; -export const FIELDS_BROWSER_HOST_GEO_CONTINENT_NAME_CHECKBOX = - '[data-test-subj="field-host.geo.continent_name-checkbox"]'; +export const FIELDS_BROWSER_HOST_GEO_CONTINENT_NAME_CHECKBOX = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="field-host.geo.continent_name-checkbox"]`; export const FIELDS_BROWSER_HEADER_HOST_GEO_CONTINENT_NAME_HEADER = - '[data-test-subj="header-text-host.geo.continent_name"]'; + '[data-test-subj="timeline"] [data-test-subj="header-text-host.geo.continent_name"]'; -export const FIELDS_BROWSER_HOST_GEO_COUNTRY_NAME_CHECKBOX = - '[data-test-subj="field-host.geo.country_name-checkbox"]'; +export const FIELDS_BROWSER_HOST_GEO_COUNTRY_NAME_CHECKBOX = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="field-host.geo.country_name-checkbox"]`; export const FIELDS_BROWSER_HOST_GEO_COUNTRY_NAME_HEADER = - '[data-test-subj="header-text-host.geo.country_name"]'; + '[data-test-subj="timeline"] [data-test-subj="header-text-host.geo.country_name"]'; -export const FIELDS_BROWSER_MESSAGE_CHECKBOX = - '[data-test-subj="timeline"] [data-test-subj="field-message-checkbox"]'; +export const FIELDS_BROWSER_MESSAGE_CHECKBOX = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="field-message-checkbox"]`; export const FIELDS_BROWSER_MESSAGE_HEADER = '[data-test-subj="timeline"] [data-test-subj="header-text-message"]'; -export const FIELDS_BROWSER_RESET_FIELDS = - '[data-test-subj="timeline"] [data-test-subj="reset-fields"]'; +export const FIELDS_BROWSER_RESET_FIELDS = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="reset-fields"]`; -export const FIELDS_BROWSER_TITLE = '[data-test-subj="field-browser-title"]'; +export const FIELDS_BROWSER_TITLE = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="field-browser-title"]`; -export const FIELDS_BROWSER_SELECTED_CATEGORY_COUNT = - '[data-test-subj="selected-category-count-badge"]'; +export const FIELDS_BROWSER_SELECTED_CATEGORY_COUNT = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="selected-category-count-badge"]`; -export const FIELDS_BROWSER_SELECTED_CATEGORY_TITLE = '[data-test-subj="selected-category-title"]'; +export const FIELDS_BROWSER_SELECTED_CATEGORY_TITLE = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="selected-category-title"]`; -export const FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT = '[data-test-subj="system-category-count"]'; +export const FIELDS_BROWSER_SYSTEM_CATEGORIES_COUNT = `${FIELDS_BROWSER_CONTAINER} [data-test-subj="system-category-count"]`; diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index 42d2b699fc8d5..689a65470030f 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -70,6 +70,8 @@ export const NOTES_COUNT = '[data-test-subj="timeline-notes-count"]'; export const OPEN_TIMELINE_ICON = '[data-test-subj="open-timeline-button"]'; +export const OPEN_TIMELINE_MODAL = '[data-test-subj="open-timeline-modal"]'; + export const OPEN_TIMELINE_TEMPLATE_ICON = '[data-test-subj="open-timeline-modal-body-filter-template"]'; @@ -82,7 +84,7 @@ export const PROVIDER_BADGE = '[data-test-subj="providerBadge"]'; export const REMOVE_COLUMN = '[data-test-subj="remove-column"]'; export const RESET_FIELDS = - '[data-test-subj="events-viewer-panel"] [data-test-subj="reset-fields"]'; + '[data-test-subj="fields-browser-container"] [data-test-subj="reset-fields"]'; export const SAVE_FILTER_BTN = '[data-test-subj="saveFilter"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts index 71c97654b9d67..0d7ffa2bf6485 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts @@ -41,7 +41,9 @@ export const changeToThreeHundredRowsPerPage = () => { }; export const editFirstRule = () => { + cy.get(COLLAPSED_ACTION_BTN).should('be.visible'); cy.get(COLLAPSED_ACTION_BTN).first().click({ force: true }); + cy.get(EDIT_RULE_ACTION_BTN).should('be.visible'); cy.get(EDIT_RULE_ACTION_BTN).click(); }; diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts index 26cc7c87c3055..3e9b911e0fb90 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts @@ -28,7 +28,7 @@ export const createCustomRule = (rule: CustomRule, ruleId = 'rule_testing') => failOnStatusCode: false, }); -export const createCustomRuleActivated = (rule: CustomRule, ruleId = 'rule_testing') => +export const createCustomRuleActivated = (rule: CustomRule, ruleId = '1') => cy.request({ method: 'POST', url: 'api/detection_engine/rules', @@ -51,7 +51,7 @@ export const createCustomRuleActivated = (rule: CustomRule, ruleId = 'rule_testi failOnStatusCode: false, }); -export const deleteCustomRule = (ruleId = 'rule_testing') => { +export const deleteCustomRule = (ruleId = '1') => { cy.request({ method: 'DELETE', url: `api/detection_engine/rules?rule_id=${ruleId}`, diff --git a/x-pack/plugins/security_solution/cypress/tasks/common.ts b/x-pack/plugins/security_solution/cypress/tasks/common.ts index cd8761ec3ddb2..accb4d3e2f298 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/common.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/common.ts @@ -5,6 +5,7 @@ */ import { esArchiverResetKibana } from './es_archiver'; +import { RuleEcs } from '../../common/ecs/rule'; const primaryButton = 0; @@ -64,6 +65,22 @@ export const reload = () => { export const cleanKibana = () => { const kibanaIndexUrl = `${Cypress.env('ELASTICSEARCH_URL')}/.kibana_\*`; + + cy.request('GET', '/api/detection_engine/rules/_find').then((response) => { + const rules: RuleEcs[] = response.body.data; + + if (response.body.data.length > 0) { + rules.forEach((rule) => { + const jsonRule = rule; + cy.request({ + method: 'DELETE', + url: `/api/detection_engine/rules?rule_id=${jsonRule.rule_id}`, + headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, + }); + }); + } + }); + cy.request('POST', `${kibanaIndexUrl}/_delete_by_query?conflicts=proceed`, { query: { bool: { 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 f6e8875121a4f..7836960b1a694 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 @@ -247,7 +247,8 @@ export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRule) => { const thresholdField = 0; const threshold = 1; - cy.get(CUSTOM_QUERY_INPUT).type(rule.customQuery!); + cy.get(IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK).click(); + cy.get(TIMELINE(rule.timeline.id!)).click(); cy.get(CUSTOM_QUERY_INPUT).should('have.value', rule.customQuery); cy.get(THRESHOLD_INPUT_AREA) .find(INPUT) diff --git a/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts b/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts index eb709d2dd5778..823a370ff33ce 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/fields_browser.ts @@ -32,7 +32,9 @@ export const addsHostGeoCountryNameToTimelineDraggingIt = () => { cy.get(FIELDS_BROWSER_DRAGGABLE_HOST_GEO_COUNTRY_NAME_HEADER).should('exist'); cy.get(FIELDS_BROWSER_DRAGGABLE_HOST_GEO_COUNTRY_NAME_HEADER).then((field) => drag(field)); - cy.get(FIELDS_BROWSER_HEADER_DROP_AREA).then((headersDropArea) => drop(headersDropArea)); + cy.get(FIELDS_BROWSER_HEADER_DROP_AREA) + .first() + .then((headersDropArea) => drop(headersDropArea)); }; export const clearFieldsBrowser = () => { diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index 47c1fd237432c..ee876abd8e8d3 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -183,9 +183,9 @@ export const dragAndDropIdToggleFieldToTimeline = () => { cy.get(ID_FIELD).then((field) => drag(field)); - cy.get(`[data-test-subj="timeline"] [data-test-subj="headers-group"]`).then((headersDropArea) => - drop(headersDropArea) - ); + cy.get(`[data-test-subj="timeline"] [data-test-subj="headers-group"]`) + .first() + .then((headersDropArea) => drop(headersDropArea)); }; export const removeColumn = (column: number) => { diff --git a/x-pack/plugins/security_solution/public/app/home/setup.tsx b/x-pack/plugins/security_solution/public/app/home/setup.tsx index 1ec62d63bd7f3..8ab6ce5a34bd2 100644 --- a/x-pack/plugins/security_solution/public/app/home/setup.tsx +++ b/x-pack/plugins/security_solution/public/app/home/setup.tsx @@ -14,7 +14,7 @@ export const Setup: React.FunctionComponent<{ }> = ({ fleet, notifications }) => { React.useEffect(() => { const defaultText = i18n.translate('xpack.securitySolution.endpoint.ingestToastMessage', { - defaultMessage: 'Ingest Manager failed during its setup.', + defaultMessage: 'Fleet failed during its setup.', }); const title = i18n.translate('xpack.securitySolution.endpoint.ingestToastTitle', { diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/translations.ts b/x-pack/plugins/security_solution/public/cases/components/configure_cases/translations.ts index fe0248b270f42..413e689c6f20e 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/translations.ts @@ -162,7 +162,7 @@ export const SAVE = i18n.translate('xpack.securitySolution.case.configureCases.s export const SAVE_CLOSE = i18n.translate( 'xpack.securitySolution.case.configureCases.saveAndCloseButton', { - defaultMessage: 'Save & Close', + defaultMessage: 'Save & close', } ); 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 457f538450079..50f3b722a0343 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 @@ -11,6 +11,7 @@ import { get, getOr, isEmpty, find } from 'lodash/fp'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; +import type { Filter } from '../../../../../../../src/plugins/data/common/es_query/filters'; import { TimelineId, TimelineStatus, TimelineType } from '../../../../common/types/timeline'; import { updateAlertStatus } from '../../containers/detection_engine/alerts/api'; import { SendAlertToTimelineActionProps, UpdateAlertStatusActionProps } from './types'; @@ -115,6 +116,16 @@ export const determineToAndFrom = ({ ecsData }: { ecsData: Ecs }) => { return { to, from }; }; +const getFiltersFromRule = (filters: string[]): Filter[] => + filters.reduce((acc, filterString) => { + try { + const objFilter: Filter = JSON.parse(filterString); + return [...acc, objFilter]; + } catch (e) { + return acc; + } + }, [] as Filter[]); + export const getThresholdAggregationDataProvider = ( ecsData: Ecs, nonEcsData: TimelineNonEcsData[] @@ -261,22 +272,9 @@ export const sendAlertToTimelineAction = async ({ notes: null, timeline: { ...timelineDefaults, - 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: ':', - }, - }, - ...getThresholdAggregationDataProvider(ecsData, nonEcsData), - ], + description: `_id: ${ecsData._id}`, + filters: getFiltersFromRule(ecsData.signal?.rule?.filters as string[]), + dataProviders: [...getThresholdAggregationDataProvider(ecsData, nonEcsData)], id: TimelineId.active, indexNames: [], dateRange: { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx index 0ba9764cf24af..3c1da1f1af57d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_utility_bar/index.test.tsx @@ -16,8 +16,8 @@ describe('AlertsUtilityBar', () => { test('renders correctly', () => { const wrapper = shallow( { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { const Proxy = (props: AlertsUtilityBarProps) => ( { const wrapper = mount( void; currentFilter: Status; @@ -59,8 +59,8 @@ const BuildingBlockContainer = styled(EuiFlexItem)` `; const AlertsUtilityBarComponent: React.FC = ({ - canUserCRUD, hasIndexWrite, + hasIndexMaintenance, areEventsLoading, clearSelection, totalCount, @@ -180,7 +180,7 @@ const AlertsUtilityBarComponent: React.FC = ({ - {canUserCRUD && hasIndexWrite && ( + {hasIndexWrite && hasIndexMaintenance && ( <> {i18n.SELECTED_ALERTS( diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx index 6724d3a83d617..88ec6921aa808 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx @@ -18,8 +18,8 @@ describe('AlertsTableComponent', () => { void; @@ -65,7 +65,6 @@ type AlertsTableComponentProps = OwnProps & PropsFromRedux; export const AlertsTableComponent: React.FC = ({ timelineId, - canUserCRUD, clearEventsDeleted, clearEventsLoading, clearSelected, @@ -74,6 +73,7 @@ export const AlertsTableComponent: React.FC = ({ globalFilters, globalQuery, hasIndexWrite, + hasIndexMaintenance, isSelectAllChecked, loading, loadingEventIds, @@ -259,10 +259,10 @@ export const AlertsTableComponent: React.FC = ({ (refetchQuery: inputsModel.Refetch, totalCount: number) => { return ( 0} clearSelection={clearSelectionCallback} hasIndexWrite={hasIndexWrite} + hasIndexMaintenance={hasIndexMaintenance} currentFilter={filterGroup} selectAll={selectAllOnAllPagesCallback} selectedEventIds={selectedEventIds} @@ -275,8 +275,8 @@ export const AlertsTableComponent: React.FC = ({ ); }, [ - canUserCRUD, hasIndexWrite, + hasIndexMaintenance, clearSelectionCallback, filterGroup, showBuildingBlockAlerts, 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 35f753f8cf0b1..d0a5f82a42c2a 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 @@ -99,7 +99,7 @@ const AlertContextMenuComponent: React.FC = ({ setPopover(false); }, []); const [exceptionModalType, setOpenAddExceptionModal] = useState(null); - const [{ canUserCRUD, hasIndexWrite, hasIndexUpdateDelete }] = useUserData(); + const [{ canUserCRUD, hasIndexWrite, hasIndexMaintenance, hasIndexUpdateDelete }] = useUserData(); const isEndpointAlert = useMemo((): boolean => { if (ecsRowData == null) { @@ -215,7 +215,7 @@ const AlertContextMenuComponent: React.FC = ({ data-test-subj="open-alert-status" id={FILTER_OPEN} onClick={openAlertActionOnClick} - disabled={!canUserCRUD || !hasIndexUpdateDelete} + disabled={!hasIndexUpdateDelete && !hasIndexMaintenance} > {i18n.ACTION_OPEN_ALERT} @@ -248,7 +248,7 @@ const AlertContextMenuComponent: React.FC = ({ data-test-subj="close-alert-status" id={FILTER_CLOSED} onClick={closeAlertActionClick} - disabled={!canUserCRUD || !hasIndexUpdateDelete} + disabled={!hasIndexUpdateDelete && !hasIndexMaintenance} > {i18n.ACTION_CLOSE_ALERT} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx index 1fe1b809d4f30..099145d4d9290 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/index.tsx @@ -173,15 +173,14 @@ const StepDefineRuleComponent: FC = ({ return { ...groupAcc, [groupName]: { - fields: Object.entries(groupValue.fields ?? {}).reduce>( - (fieldAcc, [fieldName, fieldValue]) => { - if (fieldValue.aggregatable === true) { - return { ...fieldAcc, [fieldName]: fieldValue }; - } - return fieldAcc; - }, - {} - ), + fields: Object.entries(groupValue.fields ?? {}).reduce< + Record> + >((fieldAcc, [fieldName, fieldValue]) => { + if (fieldValue.aggregatable === true) { + fieldAcc[fieldName] = fieldValue; + } + return fieldAcc; + }, {}), } as Partial, }; }, diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx index ffbdd74f0485c..1367924633edc 100644 --- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx @@ -37,6 +37,7 @@ describe('useUserInfo', () => { canUserCRUD: null, hasEncryptionKey: null, hasIndexManage: null, + hasIndexMaintenance: null, hasIndexWrite: null, hasIndexUpdateDelete: null, isAuthenticated: null, diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx index 8592de826a200..a3f4f7e8b85d5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx @@ -14,6 +14,7 @@ import { useKibana } from '../../../common/lib/kibana'; export interface State { canUserCRUD: boolean | null; hasIndexManage: boolean | null; + hasIndexMaintenance: boolean | null; hasIndexWrite: boolean | null; hasIndexUpdateDelete: boolean | null; isSignalIndexExists: boolean | null; @@ -27,6 +28,7 @@ export interface State { export const initialState: State = { canUserCRUD: null, hasIndexManage: null, + hasIndexMaintenance: null, hasIndexWrite: null, hasIndexUpdateDelete: null, isSignalIndexExists: null, @@ -43,6 +45,10 @@ export type Action = type: 'updateHasIndexManage'; hasIndexManage: boolean | null; } + | { + type: 'updateHasIndexMaintenance'; + hasIndexMaintenance: boolean | null; + } | { type: 'updateHasIndexWrite'; hasIndexWrite: boolean | null; @@ -90,6 +96,12 @@ export const userInfoReducer = (state: State, action: Action): State => { hasIndexManage: action.hasIndexManage, }; } + case 'updateHasIndexMaintenance': { + return { + ...state, + hasIndexMaintenance: action.hasIndexMaintenance, + }; + } case 'updateHasIndexWrite': { return { ...state, @@ -162,6 +174,7 @@ export const useUserInfo = (): State => { { canUserCRUD, hasIndexManage, + hasIndexMaintenance, hasIndexWrite, hasIndexUpdateDelete, isSignalIndexExists, @@ -178,6 +191,7 @@ export const useUserInfo = (): State => { isAuthenticated: isApiAuthenticated, hasEncryptionKey: isApiEncryptionKey, hasIndexManage: hasApiIndexManage, + hasIndexMaintenance: hasApiIndexMaintenance, hasIndexWrite: hasApiIndexWrite, hasIndexUpdateDelete: hasApiIndexUpdateDelete, } = usePrivilegeUser(); @@ -224,6 +238,16 @@ export const useUserInfo = (): State => { } }, [dispatch, loading, hasIndexUpdateDelete, hasApiIndexUpdateDelete]); + useEffect(() => { + if ( + !loading && + hasIndexMaintenance !== hasApiIndexMaintenance && + hasApiIndexMaintenance != null + ) { + dispatch({ type: 'updateHasIndexMaintenance', hasIndexMaintenance: hasApiIndexMaintenance }); + } + }, [dispatch, loading, hasIndexMaintenance, hasApiIndexMaintenance]); + useEffect(() => { if ( !loading && @@ -298,6 +322,7 @@ export const useUserInfo = (): State => { hasEncryptionKey, canUserCRUD, hasIndexManage, + hasIndexMaintenance, hasIndexWrite, hasIndexUpdateDelete, signalIndexName, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts index 21b561ec9cddb..dfe71568a1c3e 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/mock.ts @@ -1015,6 +1015,7 @@ export const mockUserPrivilege: Privilege = { index: { '.siem-signals-default': { all: true, + maintenance: true, manage_ilm: true, read: true, create_index: true, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts index dadeb1e7958b5..a26ac23c7f5bc 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/types.ts @@ -79,6 +79,7 @@ export interface Privilege { index: { [indexName: string]: { all: boolean; + maintenance: boolean; manage_ilm: boolean; read: boolean; create_index: boolean; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.test.tsx index fca4714b922f7..6fe8b279498a0 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.test.tsx @@ -20,6 +20,7 @@ describe('usePrivilegeUser', () => { expect(result.current).toEqual({ hasEncryptionKey: null, hasIndexManage: null, + hasIndexMaintenance: null, hasIndexWrite: null, hasIndexUpdateDelete: null, isAuthenticated: null, @@ -38,6 +39,7 @@ describe('usePrivilegeUser', () => { expect(result.current).toEqual({ hasEncryptionKey: true, hasIndexManage: true, + hasIndexMaintenance: true, hasIndexWrite: true, hasIndexUpdateDelete: true, isAuthenticated: true, @@ -60,6 +62,7 @@ describe('usePrivilegeUser', () => { expect(result.current).toEqual({ hasEncryptionKey: false, hasIndexManage: false, + hasIndexMaintenance: false, hasIndexWrite: false, hasIndexUpdateDelete: false, isAuthenticated: false, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.tsx index f8a61faee2914..b72dd3b2f84dd 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_privilege_user.tsx @@ -17,6 +17,7 @@ export interface ReturnPrivilegeUser { hasIndexManage: boolean | null; hasIndexWrite: boolean | null; hasIndexUpdateDelete: boolean | null; + hasIndexMaintenance: boolean | null; } /** * Hook to get user privilege from @@ -24,12 +25,23 @@ export interface ReturnPrivilegeUser { */ export const usePrivilegeUser = (): ReturnPrivilegeUser => { const [loading, setLoading] = useState(true); - const [privilegeUser, setPrivilegeUser] = useState>({ + const [privilegeUser, setPrivilegeUser] = useState< + Pick< + ReturnPrivilegeUser, + | 'isAuthenticated' + | 'hasEncryptionKey' + | 'hasIndexManage' + | 'hasIndexWrite' + | 'hasIndexUpdateDelete' + | 'hasIndexMaintenance' + > + >({ isAuthenticated: null, hasEncryptionKey: null, hasIndexManage: null, hasIndexWrite: null, hasIndexUpdateDelete: null, + hasIndexMaintenance: null, }); const [, dispatchToaster] = useStateToaster(); @@ -51,6 +63,7 @@ export const usePrivilegeUser = (): ReturnPrivilegeUser => { isAuthenticated: privilege.is_authenticated, hasEncryptionKey: privilege.has_encryption_key, hasIndexManage: privilege.index[indexName].manage, + hasIndexMaintenance: privilege.index[indexName].maintenance, hasIndexWrite: privilege.index[indexName].create || privilege.index[indexName].create_doc || @@ -68,6 +81,7 @@ export const usePrivilegeUser = (): ReturnPrivilegeUser => { hasIndexManage: false, hasIndexWrite: false, hasIndexUpdateDelete: false, + hasIndexMaintenance: false, }); errorToToaster({ title: i18n.PRIVILEGE_FETCH_FAILURE, error, dispatchToaster }); } diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index 4a8f8a586e08b..6a36c52458311 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -75,9 +75,9 @@ const DetectionEnginePageComponent = () => { isSignalIndexExists, isAuthenticated: isUserAuthenticated, hasEncryptionKey, - canUserCRUD, signalIndexName, hasIndexWrite, + hasIndexMaintenance, }, ] = useUserData(); const { @@ -232,7 +232,7 @@ const DetectionEnginePageComponent = () => { timelineId={TimelineId.detectionsPage} loading={loading} hasIndexWrite={hasIndexWrite ?? false} - canUserCRUD={(canUserCRUD ?? false) && (hasEncryptionKey ?? false)} + hasIndexMaintenance={hasIndexMaintenance ?? false} from={from} defaultFilters={alertsTableDefaultFilters} showBuildingBlockAlerts={showBuildingBlockAlerts} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index bcf28d2889a97..ef75a38d0e9e8 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -156,6 +156,7 @@ const RuleDetailsPageComponent = () => { hasEncryptionKey, canUserCRUD, hasIndexWrite, + hasIndexMaintenance, signalIndexName, }, ] = useUserData(); @@ -591,9 +592,9 @@ const RuleDetailsPageComponent = () => { {ruleId != null && ( { } diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts index 889bcc15d8df0..228e8cc1c4385 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts @@ -61,7 +61,7 @@ export interface PolicyDetailsState { /** current location of the application */ location?: Immutable; /** A summary of stats for the agents associated with a given Fleet Agent Policy */ - agentStatusSummary?: GetAgentStatusResponse['results']; + agentStatusSummary?: Omit; /** Status of an update to the policy */ updateStatus?: { success: boolean; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx index 525683cea186a..d57f90a79f5c7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.tsx @@ -47,11 +47,12 @@ const FieldsBrowserContainer = styled.div<{ width: number }>` border: ${({ theme }) => theme.eui.euiBorderWidthThin} solid ${({ theme }) => theme.eui.euiColorMediumShade}; border-radius: ${({ theme }) => theme.eui.euiBorderRadius}; - left: 8px; + left: 12px; padding: ${({ theme }) => theme.eui.paddingSizes.s} ${({ theme }) => theme.eui.paddingSizes.s} ${({ theme }) => theme.eui.paddingSizes.s}; - position: absolute; - top: calc(100% + 4px); + position: fixed; + top: 50%; + transform: translateY(-50%); width: ${({ width }) => width}px; z-index: 9990; `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx index 76fb1e986c2bb..5ed2b077c9f5e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { EuiButtonIcon, EuiPortal, EuiToolTip } from '@elastic/eui'; import { noop } from 'lodash/fp'; import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react'; import styled from 'styled-components'; @@ -138,25 +138,29 @@ export const StatefulFieldsBrowserComponent: React.FC = ({ {show && ( - + + + )} ); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts index c385f21153780..fffe3cf8c291e 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts @@ -56,7 +56,7 @@ export interface TimelineModel { deletedEventIds: string[]; /** A summary of the events and notes in this timeline */ description: string; - /** Typoe of event you want to see in this timeline */ + /** Type of event you want to see in this timeline */ eventType?: TimelineEventsType; /** A map of events in this timeline to the chronologically ordered notes (in this timeline) associated with the event */ eventIdToNoteIds: Record; diff --git a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts index 124f3005d3823..0143e34de31d3 100644 --- a/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts +++ b/x-pack/plugins/security_solution/server/endpoint/ingest_integration.ts @@ -76,7 +76,7 @@ const getManifest = async (logger: Logger, manifestManager: ManifestManager): Pr }; /** - * Callback to handle creation of PackagePolicies in Ingest Manager + * Callback to handle creation of PackagePolicies in Fleet */ export const getPackagePolicyCreateCallback = ( logger: Logger, diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index c771187bc2234..5430e46b810c5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -93,7 +93,7 @@ export const createMockPackageService = (): jest.Mocked => { }; /** - * Creates a mock IndexPatternService for use in tests that need to interact with the Ingest Manager's + * Creates a mock IndexPatternService for use in tests that need to interact with the Fleet's * ESIndexPatternService. * * @param indexPattern a string index pattern to return when called by a test diff --git a/x-pack/plugins/security_solution/server/endpoint/types.ts b/x-pack/plugins/security_solution/server/endpoint/types.ts index 2328c86f78a35..0e4f03c56b9f0 100644 --- a/x-pack/plugins/security_solution/server/endpoint/types.ts +++ b/x-pack/plugins/security_solution/server/endpoint/types.ts @@ -7,7 +7,7 @@ import { LoggerFactory } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { ConfigType } from '../config'; import { EndpointAppContextService } from './endpoint_app_context_services'; -import { JsonObject } from '../../../infra/common/typed_json'; +import { JsonObject } from '../../../../../src/plugins/kibana_utils/common'; import { HostMetadata, HostMetadataDetails, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/privileges/read_privileges.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/privileges/read_privileges.ts index 01819eb4703fb..9fa6c4b69e2ba 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/privileges/read_privileges.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/privileges/read_privileges.ts @@ -53,6 +53,7 @@ export const readPrivileges = async ( 'delete_index', 'index', 'manage', + 'maintenance', 'manage_follow_index', 'manage_ilm', 'manage_leader_index', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/reader/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/reader/detections_role.json index cc473aed405c1..de2aa18386188 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/reader/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/reader/detections_role.json @@ -14,7 +14,7 @@ "names": [ "*" ], - "privileges": ["read", "view_index_metadata"] + "privileges": ["read", "maintenance", "view_index_metadata"] } ] }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_role.json index f4950a25fdb77..da69643f3c2d3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/rule_author/detections_role.json @@ -18,7 +18,7 @@ }, { "names": [".siem-signals-*"], - "privileges": ["read", "write", "view_index_metadata"] + "privileges": ["read", "write", "maintenance", "view_index_metadata"] } ] }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_role.json index 87be597e4bdb5..10b0ffc9d9890 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t1_analyst/detections_role.json @@ -2,7 +2,7 @@ "elasticsearch": { "cluster": [], "indices": [ - { "names": [".siem-signals-*"], "privileges": ["read", "write"] }, + { "names": [".siem-signals-*"], "privileges": ["read", "write", "maintenance"] }, { "names": [ "apm-*-transaction*", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_role.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_role.json index 18ada2ef7ab21..58a069e03985c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_role.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/roles_users/t2_analyst/detections_role.json @@ -2,7 +2,7 @@ "elasticsearch": { "cluster": [], "indices": [ - { "names": [".siem-signals-*"], "privileges": ["read", "write"] }, + { "names": [".siem-signals-*"], "privileges": ["read", "write", "maintenance"] }, { "names": [ ".lists*", diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 7363f0559469e..bb71041b48448 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -34,7 +34,7 @@ import { ListPluginSetup } from '../../lists/server'; import { EncryptedSavedObjectsPluginSetup as EncryptedSavedObjectsSetup } from '../../encrypted_saved_objects/server'; import { SpacesPluginSetup as SpacesSetup } from '../../spaces/server'; import { ILicense, LicensingPluginStart } from '../../licensing/server'; -import { FleetStartContract, ExternalCallback } from '../../fleet/server'; +import { FleetStartContract } from '../../fleet/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import { initServer } from './init_server'; import { compose } from './lib/compose/kibana'; @@ -323,27 +323,41 @@ export class Plugin implements IPlugin void) | undefined; - const exceptionListsStartEnabled = () => { - return this.lists && plugins.taskManager && plugins.fleet; - }; + this.licensing$ = plugins.licensing.license$; - if (exceptionListsStartEnabled()) { - const exceptionListClient = this.lists!.getExceptionListClient(savedObjectsClient, 'kibana'); + if (this.lists && plugins.taskManager && plugins.fleet) { + // Exceptions, Artifacts and Manifests start + const exceptionListClient = this.lists.getExceptionListClient(savedObjectsClient, 'kibana'); const artifactClient = new ArtifactClient(savedObjectsClient); - registerIngestCallback = plugins.fleet!.registerExternalCallback; manifestManager = new ManifestManager({ savedObjectsClient, artifactClient, exceptionListClient, - packagePolicyService: plugins.fleet!.packagePolicyService, + packagePolicyService: plugins.fleet.packagePolicyService, logger: this.logger, cache: this.exceptionsCache, }); + + if (this.manifestTask) { + this.manifestTask.start({ + taskManager: plugins.taskManager, + }); + } else { + this.logger.debug('User artifacts task not available.'); + } + + // License related start + licenseService.start(this.licensing$); + this.policyWatcher = new PolicyWatcher( + plugins.fleet!.packagePolicyService, + core.savedObjects, + this.logger + ); + this.policyWatcher.start(licenseService); } this.endpointAppContextService.start({ @@ -362,23 +376,7 @@ export class Plugin implements IPlugin(value: T | T[] | null): string[] => { return [...acc, v.toString()]; case 'object': try { - return [...acc, JSON.stringify(value)]; + return [...acc, JSON.stringify(v)]; } catch { return [...acc, 'Invalid Object']; } diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts index b62ddc00f2e30..7e0595727f101 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.test.ts @@ -120,4 +120,276 @@ describe('#formatTimelineData', () => { }, }); }); + + it('rule signal results', () => { + const response: EventHit = { + _index: '.siem-signals-patrykkopycinski-default-000007', + _id: 'a77040f198355793c35bf22b900902371309be615381f0a2ec92c208b6132562', + _score: 0, + _source: { + signal: { + threshold_result: { + count: 10000, + value: '2a990c11-f61b-4c8e-b210-da2574e9f9db', + }, + parent: { + depth: 0, + index: + 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*', + id: '0268af90-d8da-576a-9747-2a191519416a', + type: 'event', + }, + depth: 1, + _meta: { + version: 14, + }, + rule: { + note: null, + throttle: null, + references: [], + severity_mapping: [], + description: 'asdasd', + created_at: '2021-01-09T11:25:45.046Z', + language: 'kuery', + threshold: { + field: '', + value: 200, + }, + building_block_type: null, + output_index: '.siem-signals-patrykkopycinski-default', + type: 'threshold', + rule_name_override: null, + enabled: true, + exceptions_list: [], + updated_at: '2021-01-09T13:36:39.204Z', + timestamp_override: null, + from: 'now-360s', + id: '696c24e0-526d-11eb-836c-e1620268b945', + timeline_id: null, + max_signals: 100, + severity: 'low', + risk_score: 21, + risk_score_mapping: [], + author: [], + query: '_id :*', + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + filters: [ + { + $state: { + store: 'appState', + }, + meta: { + negate: false, + alias: null, + disabled: false, + type: 'exists', + value: 'exists', + key: '_index', + }, + exists: { + field: '_index', + }, + }, + { + $state: { + store: 'appState', + }, + meta: { + negate: false, + alias: 'id_exists', + disabled: false, + type: 'exists', + value: 'exists', + key: '_id', + }, + exists: { + field: '_id', + }, + }, + ], + created_by: 'patryk_test_user', + version: 1, + saved_id: null, + tags: [], + rule_id: '2a990c11-f61b-4c8e-b210-da2574e9f9db', + license: '', + immutable: false, + timeline_title: null, + meta: { + from: '1m', + kibana_siem_app_url: 'http://localhost:5601/app/security', + }, + name: 'Threshold test', + updated_by: 'patryk_test_user', + interval: '5m', + false_positives: [], + to: 'now', + threat: [], + actions: [], + }, + original_time: '2021-01-09T13:39:32.595Z', + ancestors: [ + { + depth: 0, + index: + 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*', + id: '0268af90-d8da-576a-9747-2a191519416a', + type: 'event', + }, + ], + parents: [ + { + depth: 0, + index: + 'apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,winlogbeat-*', + id: '0268af90-d8da-576a-9747-2a191519416a', + type: 'event', + }, + ], + status: 'open', + }, + }, + fields: { + 'signal.rule.output_index': ['.siem-signals-patrykkopycinski-default'], + 'signal.rule.from': ['now-360s'], + 'signal.rule.language': ['kuery'], + '@timestamp': ['2021-01-09T13:41:40.517Z'], + 'signal.rule.query': ['_id :*'], + 'signal.rule.type': ['threshold'], + 'signal.rule.id': ['696c24e0-526d-11eb-836c-e1620268b945'], + 'signal.rule.risk_score': [21], + 'signal.status': ['open'], + 'event.kind': ['signal'], + 'signal.original_time': ['2021-01-09T13:39:32.595Z'], + 'signal.rule.severity': ['low'], + 'signal.rule.version': ['1'], + 'signal.rule.index': [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + 'signal.rule.name': ['Threshold test'], + 'signal.rule.to': ['now'], + }, + _type: '', + sort: ['1610199700517'], + aggregations: {}, + }; + + expect( + formatTimelineData( + ['@timestamp', 'host.name', 'destination.ip', 'source.ip'], + TIMELINE_EVENTS_FIELDS, + response + ) + ).toEqual({ + cursor: { + tiebreaker: null, + value: '', + }, + node: { + _id: 'a77040f198355793c35bf22b900902371309be615381f0a2ec92c208b6132562', + _index: '.siem-signals-patrykkopycinski-default-000007', + data: [ + { + field: '@timestamp', + value: ['2021-01-09T13:41:40.517Z'], + }, + ], + ecs: { + '@timestamp': ['2021-01-09T13:41:40.517Z'], + timestamp: '2021-01-09T13:41:40.517Z', + _id: 'a77040f198355793c35bf22b900902371309be615381f0a2ec92c208b6132562', + _index: '.siem-signals-patrykkopycinski-default-000007', + event: { + kind: ['signal'], + }, + signal: { + original_time: ['2021-01-09T13:39:32.595Z'], + status: ['open'], + rule: { + building_block_type: [], + exceptions_list: [], + from: ['now-360s'], + id: ['696c24e0-526d-11eb-836c-e1620268b945'], + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + language: ['kuery'], + name: ['Threshold test'], + output_index: ['.siem-signals-patrykkopycinski-default'], + risk_score: ['21'], + query: ['_id :*'], + severity: ['low'], + to: ['now'], + type: ['threshold'], + version: ['1'], + timeline_id: [], + timeline_title: [], + saved_id: [], + note: [], + threshold: [ + JSON.stringify({ + field: '', + value: 200, + }), + ], + filters: [ + JSON.stringify({ + $state: { + store: 'appState', + }, + meta: { + negate: false, + alias: null, + disabled: false, + type: 'exists', + value: 'exists', + key: '_index', + }, + exists: { + field: '_index', + }, + }), + JSON.stringify({ + $state: { + store: 'appState', + }, + meta: { + negate: false, + alias: 'id_exists', + disabled: false, + type: 'exists', + value: 'exists', + key: '_id', + }, + exists: { + field: '_id', + }, + }), + ], + }, + }, + }, + }, + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts index a9aee2175b31d..43b4e00c37201 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/helpers.ts @@ -41,12 +41,16 @@ const specialFields = ['_id', '_index', '_type', '_score']; const mergeTimelineFieldsWithHit = ( fieldName: string, flattenedFields: T, - hit: { fields: Record }, + hit: { _source: {}; fields: Record }, dataFields: readonly string[], ecsFields: readonly string[] ) => { if (fieldName != null || dataFields.includes(fieldName)) { - if (has(fieldName, hit.fields) || specialFields.includes(fieldName)) { + if ( + has(fieldName, hit._source) || + has(fieldName, hit.fields) || + specialFields.includes(fieldName) + ) { const objectWithProperty = { node: { ...get('node', flattenedFields), @@ -59,6 +63,8 @@ const mergeTimelineFieldsWithHit = ( ? toStringArray(get(fieldName, hit)) : isGeoField(fieldName) ? formatGeoLocation(hit.fields[fieldName]) + : has(fieldName, hit._source) + ? toStringArray(get(fieldName, hit._source)) : toStringArray(hit.fields[fieldName]), }, ] @@ -70,7 +76,11 @@ const mergeTimelineFieldsWithHit = ( ...fieldName.split('.').reduceRight( // @ts-expect-error (obj, next) => ({ [next]: obj }), - toStringArray(hit.fields[fieldName]) + toStringArray( + has(fieldName, hit._source) + ? get(fieldName, hit._source) + : hit.fields[fieldName] + ) ), } : get('node.ecs', flattenedFields), diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts index a5cfc86c7d5c3..4be1ddecc7078 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts @@ -73,6 +73,7 @@ export const buildTimelineEventsAllQuery = ({ track_total_hits: true, sort: getSortField(sort), fields, + _source: ['signal.*'], }, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts index 34610da7d7aa3..3c126ef7d2b0d 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.test.ts @@ -5,9 +5,9 @@ */ import { EventHit } from '../../../../../../common/search_strategy'; -import { getDataFromHits } from './helpers'; +import { getDataFromFieldsHits } from './helpers'; -describe('#getDataFromHits', () => { +describe('#getDataFromFieldsHits', () => { it('happy path', () => { const response: EventHit = { _index: 'auditbeat-7.8.0-2020.11.05-000003', @@ -47,7 +47,7 @@ describe('#getDataFromHits', () => { aggregations: {}, }; - expect(getDataFromHits(response.fields)).toEqual([ + expect(getDataFromFieldsHits(response.fields)).toEqual([ { category: 'event', field: 'event.category', diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts index 68bef2e8c656a..361f8504f9117 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/helpers.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEmpty } from 'lodash/fp'; +import { get, isEmpty, isNumber, isObject, isString } from 'lodash/fp'; -import { TimelineEventsDetailsItem } from '../../../../../../common/search_strategy/timeline'; +import { EventSource, TimelineEventsDetailsItem } from '../../../../../../common/search_strategy'; import { toStringArray } from '../../../../helpers/to_array'; export const baseCategoryFields = ['@timestamp', 'labels', 'message', 'tags']; @@ -34,7 +34,46 @@ export const formatGeoLocation = (item: unknown[]) => { export const isGeoField = (field: string) => field.includes('geo.location') || field.includes('geoip.location'); -export const getDataFromHits = (fields: Record): TimelineEventsDetailsItem[] => +export const getDataFromSourceHits = ( + sources: EventSource, + category?: string, + path?: string +): TimelineEventsDetailsItem[] => + Object.keys(sources).reduce((accumulator, source) => { + const item: EventSource = get(source, sources); + if (Array.isArray(item) || isString(item) || isNumber(item)) { + const field = path ? `${path}.${source}` : source; + const fieldCategory = getFieldCategory(field); + + return [ + ...accumulator, + { + category: fieldCategory, + field, + values: Array.isArray(item) + ? item.map((value) => { + if (isObject(value)) { + return JSON.stringify(value); + } + + return value; + }) + : [item], + originalValue: item, + } as TimelineEventsDetailsItem, + ]; + } else if (isObject(item)) { + return [ + ...accumulator, + ...getDataFromSourceHits(item, category || source, path ? `${path}.${source}` : source), + ]; + } + return accumulator; + }, []); + +export const getDataFromFieldsHits = ( + fields: Record +): TimelineEventsDetailsItem[] => Object.keys(fields).reduce((accumulator, field) => { const item: unknown[] = fields[field]; const fieldCategory = getFieldCategory(field); diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts index e5b70e22e90b9..d9fa4fdda9bef 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts @@ -4,18 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import { cloneDeep, merge } from 'lodash/fp'; +import { cloneDeep, merge, unionBy } from 'lodash/fp'; import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; import { + EventHit, TimelineEventsQueries, TimelineEventsDetailsStrategyResponse, TimelineEventsDetailsRequestOptions, -} from '../../../../../../common/search_strategy/timeline'; +} from '../../../../../../common/search_strategy'; import { inspectStringifyObject } from '../../../../../utils/build_query'; import { SecuritySolutionTimelineFactory } from '../../types'; import { buildTimelineDetailsQuery } from './query.events_details.dsl'; -import { getDataFromHits } from './helpers'; +import { getDataFromFieldsHits, getDataFromSourceHits } from './helpers'; export const timelineEventsDetails: SecuritySolutionTimelineFactory = { buildDsl: (options: TimelineEventsDetailsRequestOptions) => { @@ -24,17 +25,17 @@ export const timelineEventsDetails: SecuritySolutionTimelineFactory + response: IEsSearchResponse ): Promise => { const { indexName, eventId, docValueFields = [] } = options; - const fieldsData = cloneDeep(response.rawResponse.hits.hits[0]?.fields ?? {}); - const hitsData = cloneDeep(response.rawResponse.hits.hits[0] ?? {}); - delete hitsData._source; - delete hitsData.fields; + const { _source, fields, ...hitsData } = cloneDeep(response.rawResponse.hits.hits[0] ?? {}); const inspect = { dsl: [inspectStringifyObject(buildTimelineDetailsQuery(indexName, eventId, docValueFields))], }; - const data = getDataFromHits(merge(fieldsData, hitsData)); + const sourceData = getDataFromSourceHits(_source); + const fieldsData = getDataFromFieldsHits(merge(fields, hitsData)); + + const data = unionBy('field', fieldsData, sourceData); return { ...response, diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts index 8d70a08c214d8..dbb326275b968 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/query.events_details.dsl.ts @@ -22,6 +22,7 @@ export const buildTimelineDetailsQuery = ( }, }, fields: ['*'], + _source: ['signal.*'], }, size: 1, }); diff --git a/x-pack/plugins/spaces/public/management/components/index.ts b/x-pack/plugins/spaces/public/management/components/index.ts index 7f9f80f470d12..91f4964e1da06 100644 --- a/x-pack/plugins/spaces/public/management/components/index.ts +++ b/x-pack/plugins/spaces/public/management/components/index.ts @@ -6,4 +6,3 @@ export { ConfirmDeleteModal } from './confirm_delete_modal'; export { UnauthorizedPrompt } from './unauthorized_prompt'; -export { SecureSpaceMessage } from './secure_space_message'; diff --git a/x-pack/plugins/spaces/public/management/components/secure_space_message/secure_space_message.tsx b/x-pack/plugins/spaces/public/management/components/secure_space_message/secure_space_message.tsx deleted file mode 100644 index 9500810a395f8..0000000000000 --- a/x-pack/plugins/spaces/public/management/components/secure_space_message/secure_space_message.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiHorizontalRule, EuiLink, EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import React, { Fragment } from 'react'; -import { ApplicationStart } from 'kibana/public'; - -interface SecureSpaceMessageProps { - getUrlForApp: ApplicationStart['getUrlForApp']; -} - -export const SecureSpaceMessage = (props: SecureSpaceMessageProps) => { - const rolesLinkTextAriaLabel = i18n.translate( - 'xpack.spaces.management.secureSpaceMessage.rolesLinkTextAriaLabel', - { defaultMessage: 'Roles management page' } - ); - return ( - - - -

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

    -
    -
    - ); -}; diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap index 28aa8750beddd..c22a25ef60c31 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap +++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap @@ -59,26 +59,6 @@ exports[`EnabledFeatures renders as expected 1`] = ` values={Object {}} />

    -

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

    diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.test.tsx index aa47e439af95f..b84b1cc699d5f 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.test.tsx @@ -42,7 +42,6 @@ describe('EnabledFeatures', () => { name: 'my space', disabledFeatures: ['feature-1', 'feature-2'], }} - securityEnabled={true} onChange={jest.fn()} getUrlForApp={getUrlForApp} /> @@ -61,7 +60,6 @@ describe('EnabledFeatures', () => { name: 'my space', disabledFeatures: ['feature-1', 'feature-2'], }} - securityEnabled={true} onChange={changeHandler} getUrlForApp={getUrlForApp} /> @@ -96,7 +94,6 @@ describe('EnabledFeatures', () => { name: 'my space', disabledFeatures: [], }} - securityEnabled={true} onChange={changeHandler} getUrlForApp={getUrlForApp} /> @@ -134,7 +131,6 @@ describe('EnabledFeatures', () => { name: 'my space', disabledFeatures: [], }} - securityEnabled={true} onChange={changeHandler} getUrlForApp={getUrlForApp} /> @@ -165,7 +161,6 @@ describe('EnabledFeatures', () => { name: 'my space', disabledFeatures: ['feature-1', 'feature-2'], }} - securityEnabled={true} onChange={changeHandler} getUrlForApp={getUrlForApp} /> @@ -194,7 +189,6 @@ describe('EnabledFeatures', () => { name: 'my space', disabledFeatures: ['feature-1'], }} - securityEnabled={true} onChange={jest.fn()} getUrlForApp={getUrlForApp} /> @@ -214,7 +208,6 @@ describe('EnabledFeatures', () => { name: 'my space', disabledFeatures: [], }} - securityEnabled={true} onChange={changeHandler} getUrlForApp={getUrlForApp} /> @@ -243,7 +236,6 @@ describe('EnabledFeatures', () => { name: 'my space', disabledFeatures: [], }} - securityEnabled={true} onChange={changeHandler} getUrlForApp={getUrlForApp} /> diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx index 9429472d9e342..41cdef94fa315 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, Fragment, ReactNode } from 'react'; @@ -18,7 +18,6 @@ import { FeatureTable } from './feature_table'; interface Props { space: Partial; features: KibanaFeatureConfig[]; - securityEnabled: boolean; onChange: (space: Partial) => void; getUrlForApp: ApplicationStart['getUrlForApp']; } @@ -129,27 +128,6 @@ export class EnabledFeatures extends Component { defaultMessage="The feature is hidden in the UI, but is not disabled." />

    - {this.props.securityEnabled && ( -

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

    - )}
    ); diff --git a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx index bc26d6f132522..50e40ca75e0dc 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.test.tsx @@ -58,7 +58,6 @@ describe('ManageSpacePage', () => { spacesManager={(spacesManager as unknown) as SpacesManager} getFeatures={featuresStart.getFeatures} notifications={notificationServiceMock.createStartContract()} - securityEnabled={true} getUrlForApp={getUrlForApp} history={history} capabilities={{ @@ -120,7 +119,6 @@ describe('ManageSpacePage', () => { onLoadSpace={onLoadSpace} getFeatures={featuresStart.getFeatures} notifications={notificationServiceMock.createStartContract()} - securityEnabled={true} getUrlForApp={getUrlForApp} history={history} capabilities={{ @@ -173,7 +171,6 @@ describe('ManageSpacePage', () => { spacesManager={(spacesManager as unknown) as SpacesManager} getFeatures={() => Promise.reject(error)} notifications={notifications} - securityEnabled={true} getUrlForApp={getUrlForApp} history={history} capabilities={{ @@ -211,7 +208,6 @@ describe('ManageSpacePage', () => { spacesManager={(spacesManager as unknown) as SpacesManager} getFeatures={featuresStart.getFeatures} notifications={notificationServiceMock.createStartContract()} - securityEnabled={true} getUrlForApp={getUrlForApp} history={history} capabilities={{ @@ -273,7 +269,6 @@ describe('ManageSpacePage', () => { spacesManager={(spacesManager as unknown) as SpacesManager} getFeatures={featuresStart.getFeatures} notifications={notificationServiceMock.createStartContract()} - securityEnabled={true} getUrlForApp={getUrlForApp} history={history} capabilities={{ diff --git a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx index 285dd6a1134ec..b954b314c8854 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx @@ -23,7 +23,7 @@ import { Space } from '../../../../../../src/plugins/spaces_oss/common'; import { KibanaFeature, FeaturesPluginStart } from '../../../../features/public'; import { isReservedSpace } from '../../../common'; import { SpacesManager } from '../../spaces_manager'; -import { SecureSpaceMessage, UnauthorizedPrompt } from '../components'; +import { UnauthorizedPrompt } from '../components'; import { toSpaceIdentifier } from '../lib'; import { SpaceValidator } from '../lib/validate_space'; import { ConfirmAlterActiveSpaceModal } from './confirm_alter_active_space_modal'; @@ -39,7 +39,6 @@ interface Props { spaceId?: string; onLoadSpace?: (space: Space) => void; capabilities: Capabilities; - securityEnabled: boolean; history: ScopedHistory; getUrlForApp: ApplicationStart['getUrlForApp']; } @@ -107,7 +106,6 @@ export class ManageSpacePage extends Component { return ( {content} - {this.maybeGetSecureSpacesMessage()} ); } @@ -157,7 +155,6 @@ export class ManageSpacePage extends Component { features={this.state.features} onChange={this.onSpaceChange} getUrlForApp={this.props.getUrlForApp} - securityEnabled={this.props.securityEnabled} /> @@ -201,13 +198,6 @@ export class ManageSpacePage extends Component { ); }; - public maybeGetSecureSpacesMessage = () => { - if (this.editingExistingSpace() && this.props.securityEnabled) { - return ; - } - return null; - }; - public getFormButtons = () => { const createSpaceText = i18n.translate( 'xpack.spaces.management.manageSpacePage.createSpaceButton', diff --git a/x-pack/plugins/spaces/public/management/management_service.tsx b/x-pack/plugins/spaces/public/management/management_service.tsx index 11853e5f1abdd..aa4cc51726a4e 100644 --- a/x-pack/plugins/spaces/public/management/management_service.tsx +++ b/x-pack/plugins/spaces/public/management/management_service.tsx @@ -6,7 +6,6 @@ import { StartServicesAccessor } from 'src/core/public'; import { ManagementSetup, ManagementApp } from '../../../../../src/plugins/management/public'; -import { SecurityLicense } from '../../../security/public'; import { SpacesManager } from '../spaces_manager'; import { PluginsStart } from '../plugin'; import { spacesManagementApp } from './spaces_management_app'; @@ -15,15 +14,14 @@ interface SetupDeps { management: ManagementSetup; getStartServices: StartServicesAccessor; spacesManager: SpacesManager; - securityLicense?: SecurityLicense; } export class ManagementService { private registeredSpacesManagementApp?: ManagementApp; - public setup({ getStartServices, management, spacesManager, securityLicense }: SetupDeps) { + public setup({ getStartServices, management, spacesManager }: SetupDeps) { this.registeredSpacesManagementApp = management.sections.section.kibana.registerApp( - spacesManagementApp.create({ getStartServices, spacesManager, securityLicense }) + spacesManagementApp.create({ getStartServices, spacesManager }) ); } diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap b/x-pack/plugins/spaces/public/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap index 497b13642b81b..3b67cceb1c869 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap +++ b/x-pack/plugins/spaces/public/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap @@ -125,9 +125,6 @@ exports[`SpacesGridPage renders as expected 1`] = ` tableLayout="fixed" /> -
    `; diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx index ae93ae80c7cd2..f75bc1d084cd8 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx @@ -27,10 +27,8 @@ import { isReservedSpace } from '../../../common'; import { DEFAULT_SPACE_ID } from '../../../common/constants'; import { SpaceAvatar } from '../../space_avatar'; import { getSpacesFeatureDescription } from '../../constants'; -import { SpacesManager } from '../..//spaces_manager'; -import { ConfirmDeleteModal } from '../components/confirm_delete_modal'; -import { SecureSpaceMessage } from '../components/secure_space_message'; -import { UnauthorizedPrompt } from '../components/unauthorized_prompt'; +import { SpacesManager } from '../../spaces_manager'; +import { ConfirmDeleteModal, UnauthorizedPrompt } from '../components'; import { getEnabledFeatures } from '../lib/feature_utils'; import { reactRouterNavigate } from '../../../../../../src/plugins/kibana_react/public'; @@ -39,7 +37,6 @@ interface Props { notifications: NotificationsStart; getFeatures: FeaturesPluginStart['getFeatures']; capabilities: Capabilities; - securityEnabled: boolean; history: ScopedHistory; getUrlForApp: ApplicationStart['getUrlForApp']; } @@ -74,9 +71,6 @@ export class SpacesGridPage extends Component { return (
    {this.getPageContent()} - {this.props.securityEnabled && ( - - )} {this.getConfirmDeleteModal()}
    ); diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx index 606bb5cfbdcc9..945e3298f46a5 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_pages.test.tsx @@ -65,7 +65,6 @@ describe('SpacesGridPage', () => { spacesManager={(spacesManager as unknown) as SpacesManager} getFeatures={featuresStart.getFeatures} notifications={notificationServiceMock.createStartContract()} - securityEnabled={true} getUrlForApp={getUrlForApp} history={history} capabilities={{ @@ -88,7 +87,6 @@ describe('SpacesGridPage', () => { spacesManager={(spacesManager as unknown) as SpacesManager} getFeatures={featuresStart.getFeatures} notifications={notificationServiceMock.createStartContract()} - securityEnabled={true} getUrlForApp={getUrlForApp} history={history} capabilities={{ @@ -122,7 +120,6 @@ describe('SpacesGridPage', () => { spacesManager={(spacesManager as unknown) as SpacesManager} getFeatures={featuresStart.getFeatures} notifications={notifications} - securityEnabled={true} getUrlForApp={getUrlForApp} history={history} capabilities={{ @@ -157,7 +154,6 @@ describe('SpacesGridPage', () => { spacesManager={(spacesManager as unknown) as SpacesManager} getFeatures={() => Promise.reject(error)} notifications={notifications} - securityEnabled={true} getUrlForApp={getUrlForApp} history={history} capabilities={{ diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx index e345657a785c1..fc824bec6e2ce 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.test.tsx @@ -20,9 +20,7 @@ jest.mock('./edit_space', () => ({ import { spacesManagementApp } from './spaces_management_app'; import { coreMock, scopedHistoryMock } from '../../../../../src/core/public/mocks'; -import { securityMock } from '../../../security/public/mocks'; import { spacesManagerMock } from '../spaces_manager/mocks'; -import { SecurityLicenseFeatures } from '../../../security/public'; import { featuresPluginMock } from '../../../features/public/mocks'; import { PluginsStart } from '../plugin'; @@ -39,18 +37,12 @@ async function mountApp(basePath: string, pathname: string, spaceId?: string) { }); } - const securityLicense = securityMock.createSetup().license; - securityLicense.getFeatures.mockReturnValue({ - showLinks: true, - } as SecurityLicenseFeatures); - const [coreStart, pluginsStart] = await coreMock.createSetup().getStartServices(); (pluginsStart as PluginsStart).features = featuresPluginMock.createStart(); const unmount = await spacesManagementApp .create({ spacesManager, - securityLicense, getStartServices: async () => [coreStart, pluginsStart as PluginsStart, {}], }) .mount({ @@ -68,7 +60,6 @@ describe('spacesManagementApp', () => { expect( spacesManagementApp.create({ spacesManager: spacesManagerMock.create(), - securityLicense: securityMock.createSetup().license, getStartServices: coreMock.createSetup().getStartServices as any, }) ).toMatchInlineSnapshot(` @@ -91,7 +82,7 @@ describe('spacesManagementApp', () => {
    `); @@ -114,7 +105,7 @@ describe('spacesManagementApp', () => {
    `); @@ -139,7 +130,7 @@ describe('spacesManagementApp', () => {
    `); diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx index a328c50af4e7a..9c2584dfcee67 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx @@ -10,7 +10,6 @@ import { Router, Route, Switch, useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { StartServicesAccessor } from 'src/core/public'; import { RedirectAppLinks } from '../../../../../src/plugins/kibana_react/public'; -import { SecurityLicense } from '../../../security/public'; import { RegisterManagementAppArgs } from '../../../../../src/plugins/management/public'; import { PluginsStart } from '../plugin'; import { SpacesManager } from '../spaces_manager'; @@ -21,12 +20,11 @@ import { Space } from '..'; interface CreateParams { getStartServices: StartServicesAccessor; spacesManager: SpacesManager; - securityLicense?: SecurityLicense; } export const spacesManagementApp = Object.freeze({ id: 'spaces', - create({ getStartServices, spacesManager, securityLicense }: CreateParams) { + create({ getStartServices, spacesManager }: CreateParams) { return { id: this.id, order: 2, @@ -58,7 +56,6 @@ export const spacesManagementApp = Object.freeze({ spacesManager={spacesManager} history={history} getUrlForApp={application.getUrlForApp} - securityEnabled={securityLicense?.getFeatures().showLinks ?? false} /> ); }; @@ -81,7 +78,6 @@ export const spacesManagementApp = Object.freeze({ spacesManager={spacesManager} history={history} getUrlForApp={application.getUrlForApp} - securityEnabled={securityLicense?.getFeatures().showLinks ?? false} /> ); }; @@ -109,7 +105,6 @@ export const spacesManagementApp = Object.freeze({ onLoadSpace={onLoadSpace} history={history} getUrlForApp={application.getUrlForApp} - securityEnabled={securityLicense?.getFeatures().showLinks ?? false} /> ); }; diff --git a/x-pack/plugins/spaces/public/plugin.tsx b/x-pack/plugins/spaces/public/plugin.tsx index c479c01991e31..43888c7b5758b 100644 --- a/x-pack/plugins/spaces/public/plugin.tsx +++ b/x-pack/plugins/spaces/public/plugin.tsx @@ -11,7 +11,6 @@ import { SavedObjectsManagementPluginSetup } from 'src/plugins/saved_objects_man import { ManagementStart, ManagementSetup } from 'src/plugins/management/public'; import { AdvancedSettingsSetup } from 'src/plugins/advanced_settings/public'; import { FeaturesPluginStart } from '../../features/public'; -import { SecurityPluginStart, SecurityPluginSetup } from '../../security/public'; import { SpacesManager } from './spaces_manager'; import { initSpacesNavControl } from './nav_control'; import { createSpacesFeatureCatalogueEntry } from './create_feature_catalogue_entry'; @@ -26,14 +25,12 @@ export interface PluginsSetup { advancedSettings?: AdvancedSettingsSetup; home?: HomePublicPluginSetup; management?: ManagementSetup; - security?: SecurityPluginSetup; savedObjectsManagement?: SavedObjectsManagementPluginSetup; } export interface PluginsStart { features: FeaturesPluginStart; management?: ManagementStart; - security?: SecurityPluginStart; } export type SpacesPluginSetup = ReturnType; @@ -57,7 +54,6 @@ export class SpacesPlugin implements Plugin, spacesManager: this.spacesManager, - securityLicense: plugins.security?.license, }); } diff --git a/x-pack/plugins/spaces/tsconfig.json b/x-pack/plugins/spaces/tsconfig.json new file mode 100644 index 0000000000000..95fbecaa90936 --- /dev/null +++ b/x-pack/plugins/spaces/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["common/**/*", "public/**/*", "server/**/*"], + "references": [ + { "path": "../features/tsconfig.json" }, + { "path": "../licensing/tsconfig.json" }, + { "path": "../../../src/plugins/advanced_settings/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/management/tsconfig.json" }, + { "path": "../../../src/plugins/saved_objects_management/tsconfig.json" }, + { "path": "../../../src/plugins/spaces_oss/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" } + ] +} diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts index ad6da351b1016..08ebd7ea8f157 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.test.ts @@ -19,7 +19,7 @@ describe('alertType', () => { it('alert type creation structure is the expected value', async () => { expect(alertType.id).toBe('.index-threshold'); expect(alertType.name).toBe('Index threshold'); - expect(alertType.actionGroups).toEqual([{ id: 'threshold met', name: 'Threshold Met' }]); + expect(alertType.actionGroups).toEqual([{ id: 'threshold met', name: 'Threshold met' }]); expect(alertType.actionVariables).toMatchInlineSnapshot(` Object { diff --git a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts index 3a7e795bd5dbf..2366a872b855b 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/index_threshold/alert_type.ts @@ -50,7 +50,7 @@ export function getAlertType( const actionGroupName = i18n.translate( 'xpack.stackAlerts.indexThreshold.actionGroupThresholdMetTitle', { - defaultMessage: 'Threshold Met', + defaultMessage: 'Threshold met', } ); diff --git a/x-pack/plugins/stack_alerts/server/plugin.test.ts b/x-pack/plugins/stack_alerts/server/plugin.test.ts index 0f747e9c24eec..7226c2175a769 100644 --- a/x-pack/plugins/stack_alerts/server/plugin.test.ts +++ b/x-pack/plugins/stack_alerts/server/plugin.test.ts @@ -40,7 +40,7 @@ describe('AlertingBuiltins Plugin', () => { "actionGroups": Array [ Object { "id": "threshold met", - "name": "Threshold Met", + "name": "Threshold met", }, ], "id": ".index-threshold", diff --git a/x-pack/plugins/task_manager/README.md b/x-pack/plugins/task_manager/README.md index 6cd42cda9af6a..9be3be14ea3fc 100644 --- a/x-pack/plugins/task_manager/README.md +++ b/x-pack/plugins/task_manager/README.md @@ -45,6 +45,7 @@ The task_manager can be configured via `taskManager` config options (e.g. `taskM - `max_poll_inactivity_cycles` - How many poll intervals is work allowed to block polling for before it's timed out. This does not include task execution, as task execution does not block the polling, but rather includes work needed to manage Task Manager's state. - `index` - **deprecated** The name of the index that the task_manager will use. This is deprecated, and will be removed starting in 8.0 - `max_workers` - The maximum number of tasks a Kibana will run concurrently (defaults to 10) +- `version_conflict_threshold` - The threshold percentage for workers experiencing version conflicts for shifting the polling interval - `credentials` - Encrypted user credentials. All tasks will run in the security context of this user. See [this issue](https://github.com/elastic/dev/issues/1045) for a discussion on task scheduler security. - `override_num_workers`: An object of `taskType: number` that overrides the `num_workers` for tasks - For example: `task_manager.override_num_workers.reporting: 2` would override the number of workers occupied by tasks of type `reporting` @@ -521,4 +522,4 @@ The task manager's public API is create / delete / list. Updates aren't directly Task Manager exposes runtime statistics which enable basic observability into its inner workings and makes it possible to monitor the system from external services. -Learn More: [./MONITORING](./MONITORING.MD) \ No newline at end of file +Learn More: [./MONITORING](./MONITORING.MD) diff --git a/x-pack/plugins/task_manager/server/MONITORING.md b/x-pack/plugins/task_manager/server/MONITORING.md index 64481e81c60a4..771005f28b3f4 100644 --- a/x-pack/plugins/task_manager/server/MONITORING.md +++ b/x-pack/plugins/task_manager/server/MONITORING.md @@ -177,6 +177,8 @@ For example, if you _curl_ the `/api/task_manager/_health` endpoint, you might g "polling": { /* When was the last polling cycle? */ "last_successful_poll": "2020-10-05T17:57:55.411Z", + /* When was the last time Task Manager adjusted it's polling delay? */ + "last_polling_delay": "2020-10-05T17:57:55.411Z", /* Running average of polling duration measuring the time from the scheduled polling cycle start until all claimed tasks are marked as running */ "duration": { diff --git a/x-pack/plugins/task_manager/server/config.test.ts b/x-pack/plugins/task_manager/server/config.test.ts index d2d5ac8f22a1f..d2527d066c7b6 100644 --- a/x-pack/plugins/task_manager/server/config.test.ts +++ b/x-pack/plugins/task_manager/server/config.test.ts @@ -27,6 +27,7 @@ describe('config validation', () => { }, "poll_interval": 3000, "request_capacity": 1000, + "version_conflict_threshold": 80, } `); }); @@ -74,6 +75,7 @@ describe('config validation', () => { }, "poll_interval": 3000, "request_capacity": 1000, + "version_conflict_threshold": 80, } `); }); @@ -113,6 +115,7 @@ describe('config validation', () => { }, "poll_interval": 3000, "request_capacity": 1000, + "version_conflict_threshold": 80, } `); }); diff --git a/x-pack/plugins/task_manager/server/config.ts b/x-pack/plugins/task_manager/server/config.ts index a22c4484389ae..d5c388b08b761 100644 --- a/x-pack/plugins/task_manager/server/config.ts +++ b/x-pack/plugins/task_manager/server/config.ts @@ -10,6 +10,7 @@ export const MAX_WORKERS_LIMIT = 100; export const DEFAULT_MAX_WORKERS = 10; export const DEFAULT_POLL_INTERVAL = 3000; export const DEFAULT_MAX_POLL_INACTIVITY_CYCLES = 10; +export const DEFAULT_VERSION_CONFLICT_THRESHOLD = 80; // Monitoring Constants // =================== @@ -76,6 +77,12 @@ export const configSchema = schema.object( // disable the task manager rather than trying to specify it with 0 workers min: 1, }), + /* The threshold percenatge for workers experiencing version conflicts for shifting the polling interval. */ + version_conflict_threshold: schema.number({ + defaultValue: DEFAULT_VERSION_CONFLICT_THRESHOLD, + min: 50, + max: 100, + }), /* The rate at which we emit fresh monitored stats. By default we'll use the poll_interval (+ a slight buffer) */ monitored_stats_required_freshness: schema.number({ defaultValue: (config?: unknown) => diff --git a/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts b/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts index 01326c73bd680..fd2b8857693ae 100644 --- a/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts +++ b/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts @@ -29,6 +29,7 @@ describe('managed configuration', () => { index: 'foo', max_attempts: 9, poll_interval: 3000, + version_conflict_threshold: 80, max_poll_inactivity_cycles: 10, monitored_aggregated_stats_refresh_rate: 60000, monitored_stats_required_freshness: 4000, diff --git a/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts index 6f3dcb33d5bf5..37f97422dbe15 100644 --- a/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts @@ -17,6 +17,7 @@ describe('Configuration Statistics Aggregator', () => { index: 'foo', max_attempts: 9, poll_interval: 6000000, + version_conflict_threshold: 80, monitored_stats_required_freshness: 6000000, max_poll_inactivity_cycles: 10, request_capacity: 1000, diff --git a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts index b8502dee9a8ef..8acd32c30d65d 100644 --- a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts @@ -21,6 +21,7 @@ describe('createMonitoringStatsStream', () => { index: 'foo', max_attempts: 9, poll_interval: 6000000, + version_conflict_threshold: 80, monitored_stats_required_freshness: 6000000, max_poll_inactivity_cycles: 10, request_capacity: 1000, diff --git a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts index 3933443296c4a..b881759d9103e 100644 --- a/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts +++ b/x-pack/plugins/task_manager/server/monitoring/task_run_statistics.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { combineLatest, Observable } from 'rxjs'; +import { combineLatest, merge, Observable, of } from 'rxjs'; import { filter, startWith, map } from 'rxjs/operators'; import { JsonObject } from 'src/plugins/kibana_utils/common'; import { isNumber, mapValues } from 'lodash'; @@ -36,6 +36,7 @@ import { TaskExecutionFailureThreshold, TaskManagerConfig } from '../config'; interface FillPoolStat extends JsonObject { last_successful_poll: string; + last_polling_delay: string; duration: number[]; claim_conflicts: number[]; claim_mismatches: number[]; @@ -51,11 +52,13 @@ export interface TaskRunStat extends JsonObject { drift: number[]; load: number[]; execution: ExecutionStat; - polling: FillPoolStat | Omit; + polling: Omit & + Pick, 'last_successful_poll' | 'last_polling_delay'>; } interface FillPoolRawStat extends JsonObject { last_successful_poll: string; + last_polling_delay: string; result_frequency_percent_as_number: { [FillPoolResult.Failed]: number; [FillPoolResult.NoAvailableWorkers]: number; @@ -123,37 +126,61 @@ export function createTaskRunAggregator( const pollingDurationQueue = createRunningAveragedStat(runningAverageWindowSize); const claimConflictsQueue = createRunningAveragedStat(runningAverageWindowSize); const claimMismatchesQueue = createRunningAveragedStat(runningAverageWindowSize); - const taskPollingEvents$: Observable< - Pick - > = taskPollingLifecycle.events.pipe( - filter( - (taskEvent: TaskLifecycleEvent) => - isTaskPollingCycleEvent(taskEvent) && isOk(taskEvent.event) + const taskPollingEvents$: Observable> = combineLatest([ + // get latest polling stats + taskPollingLifecycle.events.pipe( + filter( + (taskEvent: TaskLifecycleEvent) => + isTaskPollingCycleEvent(taskEvent) && + isOk(taskEvent.event) + ), + map((taskEvent: TaskLifecycleEvent) => { + const { + result, + stats: { tasksClaimed, tasksUpdated, tasksConflicted } = {}, + } = ((taskEvent.event as unknown) as Ok).value; + const duration = (taskEvent?.timing?.stop ?? 0) - (taskEvent?.timing?.start ?? 0); + return { + polling: { + last_successful_poll: new Date().toISOString(), + // Track how long the polling cycle took from begining until all claimed tasks were marked as running + duration: duration ? pollingDurationQueue(duration) : pollingDurationQueue(), + // Track how many version conflicts occured during polling + claim_conflicts: isNumber(tasksConflicted) + ? claimConflictsQueue(tasksConflicted) + : claimConflictsQueue(), + // Track how much of a mismatch there is between claimed and updated + claim_mismatches: + isNumber(tasksClaimed) && isNumber(tasksUpdated) + ? claimMismatchesQueue(tasksUpdated - tasksClaimed) + : claimMismatchesQueue(), + result_frequency_percent_as_number: resultFrequencyQueue(result), + }, + }; + }) ), - map((taskEvent: TaskLifecycleEvent) => { - const { - result, - stats: { tasksClaimed, tasksUpdated, tasksConflicted } = {}, - } = ((taskEvent.event as unknown) as Ok).value; - const duration = (taskEvent?.timing?.stop ?? 0) - (taskEvent?.timing?.start ?? 0); - return { - polling: { - last_successful_poll: new Date().toISOString(), - // Track how long the polling cycle took from begining until all claimed tasks were marked as running - duration: duration ? pollingDurationQueue(duration) : pollingDurationQueue(), - // Track how many version conflicts occured during polling - claim_conflicts: isNumber(tasksConflicted) - ? claimConflictsQueue(tasksConflicted) - : claimConflictsQueue(), - // Track how much of a mismatch there is between claimed and updated - claim_mismatches: - isNumber(tasksClaimed) && isNumber(tasksUpdated) - ? claimMismatchesQueue(tasksUpdated - tasksClaimed) - : claimMismatchesQueue(), - result_frequency_percent_as_number: resultFrequencyQueue(result), - }, - }; - }) + // get DateTime of latest polling delay refresh + merge( + /** + * as `combineLatest` hangs until it has its first value and we're not likely to reconfigure the delay in normal deployments, we needed some initial value. + I've used _now_ (`new Date().toISOString()`) as it made the most sense (it would be the time Kibana started), but it _could_ be confusing in the future. + */ + of(new Date().toISOString()), + taskPollingLifecycle.events.pipe( + filter( + (taskEvent: TaskLifecycleEvent) => + isTaskManagerStatEvent(taskEvent) && taskEvent.id === 'pollingDelay' + ), + map(() => new Date().toISOString()) + ) + ), + ]).pipe( + map(([{ polling }, pollingDelay]) => ({ + polling: { + last_polling_delay: pollingDelay, + ...polling, + }, + })) ); return combineLatest([ @@ -234,6 +261,8 @@ export function summarizeTaskRunStat( polling: { // eslint-disable-next-line @typescript-eslint/naming-convention last_successful_poll, + // eslint-disable-next-line @typescript-eslint/naming-convention + last_polling_delay, duration: pollingDuration, result_frequency_percent_as_number: pollingResultFrequency, claim_conflicts: claimConflicts, @@ -249,6 +278,7 @@ export function summarizeTaskRunStat( value: { polling: { ...(last_successful_poll ? { last_successful_poll } : {}), + ...(last_polling_delay ? { last_polling_delay } : {}), duration: calculateRunningAverage(pollingDuration as number[]), claim_conflicts: calculateRunningAverage(claimConflicts as number[]), claim_mismatches: calculateRunningAverage(claimMismatches as number[]), diff --git a/x-pack/plugins/task_manager/server/plugin.test.ts b/x-pack/plugins/task_manager/server/plugin.test.ts index 9a1d83f6195ab..a73ba2d2958f4 100644 --- a/x-pack/plugins/task_manager/server/plugin.test.ts +++ b/x-pack/plugins/task_manager/server/plugin.test.ts @@ -20,6 +20,7 @@ describe('TaskManagerPlugin', () => { index: 'foo', max_attempts: 9, poll_interval: 3000, + version_conflict_threshold: 80, max_poll_inactivity_cycles: 10, request_capacity: 1000, monitored_aggregated_stats_refresh_rate: 5000, @@ -49,6 +50,7 @@ describe('TaskManagerPlugin', () => { index: 'foo', max_attempts: 9, poll_interval: 3000, + version_conflict_threshold: 80, max_poll_inactivity_cycles: 10, request_capacity: 1000, monitored_aggregated_stats_refresh_rate: 5000, diff --git a/x-pack/plugins/task_manager/server/polling/delay_on_claim_conflicts.test.ts b/x-pack/plugins/task_manager/server/polling/delay_on_claim_conflicts.test.ts new file mode 100644 index 0000000000000..9f0eeedf05884 --- /dev/null +++ b/x-pack/plugins/task_manager/server/polling/delay_on_claim_conflicts.test.ts @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import { Subject, of } from 'rxjs'; +import { fakeSchedulers } from 'rxjs-marbles/jest'; +import { sleep } from '../test_utils'; +import { asOk } from '../lib/result_type'; +import { delayOnClaimConflicts } from './delay_on_claim_conflicts'; +import { asTaskPollingCycleEvent } from '../task_events'; +import { bufferCount, take } from 'rxjs/operators'; +import { TaskLifecycleEvent } from '../polling_lifecycle'; +import { FillPoolResult } from '../lib/fill_pool'; + +describe('delayOnClaimConflicts', () => { + beforeEach(() => jest.useFakeTimers()); + + test( + 'initializes with a delay of 0', + fakeSchedulers(async (advance) => { + const pollInterval = 100; + const maxWorkers = 100; + const taskLifecycleEvents$ = new Subject(); + const delays = delayOnClaimConflicts( + of(maxWorkers), + of(pollInterval), + taskLifecycleEvents$, + 80, + 2 + ) + .pipe(take(1), bufferCount(1)) + .toPromise(); + + expect(await delays).toEqual([0]); + }) + ); + + test( + 'emits a random delay whenever p50 of claim clashes exceed 80% of available max_workers', + fakeSchedulers(async (advance) => { + const pollInterval = 100; + const maxWorkers = 100; + const taskLifecycleEvents$ = new Subject(); + + const delays$ = delayOnClaimConflicts( + of(maxWorkers), + of(pollInterval), + taskLifecycleEvents$, + 80, + 2 + ) + .pipe(take(2), bufferCount(2)) + .toPromise(); + + taskLifecycleEvents$.next( + asTaskPollingCycleEvent( + asOk({ + result: FillPoolResult.PoolFilled, + stats: { + tasksUpdated: 0, + tasksConflicted: 80, + tasksClaimed: 0, + }, + docs: [], + }) + ) + ); + + const [initialDelay, delayAfterClash] = await delays$; + + expect(initialDelay).toEqual(0); + // randomly delay by 25% - 75% + expect(delayAfterClash).toBeGreaterThanOrEqual(pollInterval * 0.25); + expect(delayAfterClash).toBeLessThanOrEqual(pollInterval * 0.75); + }) + ); + + test( + 'doesnt emit a new delay when conflicts have reduced', + fakeSchedulers(async (advance) => { + const pollInterval = 100; + const maxWorkers = 100; + const taskLifecycleEvents$ = new Subject(); + + const handler = jest.fn(); + + delayOnClaimConflicts( + of(maxWorkers), + of(pollInterval), + taskLifecycleEvents$, + 80, + 2 + ).subscribe(handler); + + await sleep(0); + expect(handler).toHaveBeenCalledWith(0); + + taskLifecycleEvents$.next( + asTaskPollingCycleEvent( + asOk({ + result: FillPoolResult.PoolFilled, + stats: { + tasksUpdated: 0, + tasksConflicted: 80, + tasksClaimed: 0, + }, + docs: [], + }) + ) + ); + + await sleep(0); + expect(handler.mock.calls.length).toEqual(2); + expect(handler.mock.calls[1][0]).toBeGreaterThanOrEqual(pollInterval * 0.25); + expect(handler.mock.calls[1][0]).toBeLessThanOrEqual(pollInterval * 0.75); + + // shift average below threshold + taskLifecycleEvents$.next( + asTaskPollingCycleEvent( + asOk({ + result: FillPoolResult.PoolFilled, + stats: { + tasksUpdated: 0, + tasksConflicted: 70, + tasksClaimed: 0, + }, + docs: [], + }) + ) + ); + + await sleep(0); + expect(handler.mock.calls.length).toEqual(2); + + // shift average back up to threshold (70 + 90) / 2 = 80 + taskLifecycleEvents$.next( + asTaskPollingCycleEvent( + asOk({ + result: FillPoolResult.PoolFilled, + stats: { + tasksUpdated: 0, + tasksConflicted: 90, + tasksClaimed: 0, + }, + docs: [], + }) + ) + ); + + await sleep(0); + expect(handler.mock.calls.length).toEqual(3); + expect(handler.mock.calls[2][0]).toBeGreaterThanOrEqual(pollInterval * 0.25); + expect(handler.mock.calls[2][0]).toBeLessThanOrEqual(pollInterval * 0.75); + }) + ); +}); diff --git a/x-pack/plugins/task_manager/server/polling/delay_on_claim_conflicts.ts b/x-pack/plugins/task_manager/server/polling/delay_on_claim_conflicts.ts new file mode 100644 index 0000000000000..46a4c0e6b87d6 --- /dev/null +++ b/x-pack/plugins/task_manager/server/polling/delay_on_claim_conflicts.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * This module contains the logic for polling the task manager index for new work. + */ + +import stats from 'stats-lite'; +import { isNumber, random } from 'lodash'; +import { merge, of, Observable, combineLatest } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { Option, none, some, isSome, Some } from 'fp-ts/lib/Option'; +import { isOk } from '../lib/result_type'; +import { ManagedConfiguration } from '../lib/create_managed_configuration'; +import { TaskLifecycleEvent } from '../polling_lifecycle'; +import { isTaskPollingCycleEvent } from '../task_events'; +import { ClaimAndFillPoolResult } from '../lib/fill_pool'; +import { createRunningAveragedStat } from '../monitoring/task_run_calcultors'; + +/** + * Emits a delay amount in ms to apply to polling whenever the task store exceeds a threshold of claim claimClashes + */ +export function delayOnClaimConflicts( + maxWorkersConfiguration$: ManagedConfiguration['maxWorkersConfiguration$'], + pollIntervalConfiguration$: ManagedConfiguration['pollIntervalConfiguration$'], + taskLifecycleEvents$: Observable, + claimClashesPercentageThreshold: number, + runningAverageWindowSize: number +): Observable { + const claimConflictQueue = createRunningAveragedStat(runningAverageWindowSize); + return merge( + of(0), + combineLatest([ + maxWorkersConfiguration$, + pollIntervalConfiguration$, + taskLifecycleEvents$.pipe( + map>((taskEvent: TaskLifecycleEvent) => + isTaskPollingCycleEvent(taskEvent) && + isOk(taskEvent.event) && + isNumber(taskEvent.event.value.stats?.tasksConflicted) + ? some(taskEvent.event.value.stats!.tasksConflicted) + : none + ), + filter>((claimClashes) => isSome(claimClashes)), + map((claimClashes: Option) => (claimClashes as Some).value) + ), + ]).pipe( + map(([maxWorkers, pollInterval, latestClaimConflicts]) => { + // add latest claimConflict count to queue + claimConflictQueue(latestClaimConflicts); + + const emitWhenExceeds = (claimClashesPercentageThreshold * maxWorkers) / 100; + if ( + // avoid calculating average if the new value isn't above the Threshold + latestClaimConflicts >= emitWhenExceeds && + // only calculate average and emit value if above or equal to Threshold + stats.percentile(claimConflictQueue(), 0.5) >= emitWhenExceeds + ) { + return some(pollInterval); + } + return none; + }), + filter>((pollInterval) => isSome(pollInterval)), + map, number>((maybePollInterval) => { + const pollInterval = (maybePollInterval as Some).value; + return random(pollInterval * 0.25, pollInterval * 0.75, false); + }) + ) + ); +} diff --git a/x-pack/plugins/task_manager/server/polling/index.ts b/x-pack/plugins/task_manager/server/polling/index.ts index 5c1f06eaeb256..dcd3f24291518 100644 --- a/x-pack/plugins/task_manager/server/polling/index.ts +++ b/x-pack/plugins/task_manager/server/polling/index.ts @@ -7,3 +7,4 @@ export { createObservableMonitor } from './observable_monitor'; export { createTaskPoller, PollingError, PollingErrorType } from './task_poller'; export { timeoutPromiseAfter } from './timeout_promise_after'; +export { delayOnClaimConflicts } from './delay_on_claim_conflicts'; diff --git a/x-pack/plugins/task_manager/server/polling/task_poller.test.ts b/x-pack/plugins/task_manager/server/polling/task_poller.test.ts index f5f1667312d79..3cd0ea34bf94a 100644 --- a/x-pack/plugins/task_manager/server/polling/task_poller.test.ts +++ b/x-pack/plugins/task_manager/server/polling/task_poller.test.ts @@ -27,6 +27,7 @@ describe('TaskPoller', () => { createTaskPoller({ logger: loggingSystemMock.create().get(), pollInterval$: of(pollInterval), + pollIntervalDelay$: of(0), bufferCapacity, getCapacity: () => 1, work, @@ -62,6 +63,7 @@ describe('TaskPoller', () => { createTaskPoller({ logger: loggingSystemMock.create().get(), pollInterval$, + pollIntervalDelay$: of(0), bufferCapacity, getCapacity: () => 1, work, @@ -104,6 +106,7 @@ describe('TaskPoller', () => { createTaskPoller({ logger: loggingSystemMock.create().get(), pollInterval$: of(pollInterval), + pollIntervalDelay$: of(0), bufferCapacity, work, workTimeout: pollInterval * 5, @@ -163,6 +166,7 @@ describe('TaskPoller', () => { createTaskPoller({ logger: loggingSystemMock.create().get(), pollInterval$: of(pollInterval), + pollIntervalDelay$: of(0), bufferCapacity, work, workTimeout: pollInterval * 5, @@ -209,6 +213,7 @@ describe('TaskPoller', () => { createTaskPoller({ logger: loggingSystemMock.create().get(), pollInterval$: of(pollInterval), + pollIntervalDelay$: of(0), bufferCapacity, work, workTimeout: pollInterval * 5, @@ -254,6 +259,7 @@ describe('TaskPoller', () => { createTaskPoller({ logger: loggingSystemMock.create().get(), pollInterval$: of(pollInterval), + pollIntervalDelay$: of(0), bufferCapacity, work, workTimeout: pollInterval * 5, @@ -291,6 +297,7 @@ describe('TaskPoller', () => { createTaskPoller({ logger: loggingSystemMock.create().get(), pollInterval$: of(pollInterval), + pollIntervalDelay$: of(0), bufferCapacity, work: async (...args) => { await worker; @@ -342,6 +349,7 @@ describe('TaskPoller', () => { createTaskPoller<[string, Resolvable], string[]>({ logger: loggingSystemMock.create().get(), pollInterval$: of(pollInterval), + pollIntervalDelay$: of(0), bufferCapacity, work: async (...resolvables) => { await Promise.all(resolvables.map(([, future]) => future)); @@ -402,6 +410,7 @@ describe('TaskPoller', () => { createTaskPoller({ logger: loggingSystemMock.create().get(), pollInterval$: of(pollInterval), + pollIntervalDelay$: of(0), bufferCapacity, work: async (...args) => { throw new Error('failed to work'); @@ -443,6 +452,7 @@ describe('TaskPoller', () => { createTaskPoller({ logger: loggingSystemMock.create().get(), pollInterval$: of(pollInterval), + pollIntervalDelay$: of(0), bufferCapacity, work, workTimeout: pollInterval * 5, @@ -486,6 +496,7 @@ describe('TaskPoller', () => { createTaskPoller({ logger: loggingSystemMock.create().get(), pollInterval$: of(pollInterval), + pollIntervalDelay$: of(0), bufferCapacity, work, workTimeout: pollInterval * 5, diff --git a/x-pack/plugins/task_manager/server/polling/task_poller.ts b/x-pack/plugins/task_manager/server/polling/task_poller.ts index 3d48453aa5a9a..fac0137f38ba5 100644 --- a/x-pack/plugins/task_manager/server/polling/task_poller.ts +++ b/x-pack/plugins/task_manager/server/polling/task_poller.ts @@ -10,7 +10,7 @@ import { performance } from 'perf_hooks'; import { after } from 'lodash'; -import { Subject, merge, interval, of, Observable } from 'rxjs'; +import { Subject, merge, of, Observable, combineLatest, timer } from 'rxjs'; import { mapTo, filter, scan, concatMap, tap, catchError, switchMap } from 'rxjs/operators'; import { pipe } from 'fp-ts/lib/pipeable'; @@ -33,6 +33,7 @@ type WorkFn = (...params: T[]) => Promise; interface Opts { logger: Logger; pollInterval$: Observable; + pollIntervalDelay$: Observable; bufferCapacity: number; getCapacity: () => number; pollRequests$: Observable>; @@ -56,6 +57,7 @@ interface Opts { export function createTaskPoller({ logger, pollInterval$, + pollIntervalDelay$, getCapacity, pollRequests$, bufferCapacity, @@ -70,11 +72,21 @@ export function createTaskPoller({ // emit a polling event on demand pollRequests$, // emit a polling event on a fixed interval - pollInterval$.pipe( - switchMap((period) => { - logger.debug(`Task poller now using interval of ${period}ms`); - return interval(period); - }), + combineLatest([ + pollInterval$.pipe( + tap((period) => { + logger.debug(`Task poller now using interval of ${period}ms`); + }) + ), + pollIntervalDelay$.pipe( + tap((pollDelay) => { + logger.debug(`Task poller now delaying emission by ${pollDelay}ms`); + }) + ), + ]).pipe( + // pollDelay can only shift `timer` at the scale of `period`, so we round + // the delay to modulo the interval period + switchMap(([period, pollDelay]) => timer(period + (pollDelay % period), period)), mapTo(none) ) ).pipe( diff --git a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts index bf3ff6da9fbdc..4c54033c3cb93 100644 --- a/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts +++ b/x-pack/plugins/task_manager/server/polling_lifecycle.test.ts @@ -26,6 +26,7 @@ describe('TaskPollingLifecycle', () => { index: 'foo', max_attempts: 9, poll_interval: 6000000, + version_conflict_threshold: 80, max_poll_inactivity_cycles: 10, request_capacity: 1000, monitored_aggregated_stats_refresh_rate: 5000, diff --git a/x-pack/plugins/task_manager/server/polling_lifecycle.ts b/x-pack/plugins/task_manager/server/polling_lifecycle.ts index a4522f350f745..1133d1c269ca1 100644 --- a/x-pack/plugins/task_manager/server/polling_lifecycle.ts +++ b/x-pack/plugins/task_manager/server/polling_lifecycle.ts @@ -25,6 +25,7 @@ import { TaskPollingCycle, asTaskPollingCycleEvent, TaskManagerStat, + asTaskManagerStatEvent, } from './task_events'; import { fillPool, FillPoolResult, TimedFillPoolResult } from './lib/fill_pool'; import { Middleware } from './lib/middleware'; @@ -42,6 +43,7 @@ import { TaskStore, OwnershipClaimingOpts, ClaimOwnershipResult } from './task_s import { identifyEsError } from './lib/identify_es_error'; import { BufferedTaskStore } from './buffered_task_store'; import { TaskTypeDictionary } from './task_type_dictionary'; +import { delayOnClaimConflicts } from './polling'; export type TaskPollingLifecycleOpts = { logger: Logger; @@ -121,6 +123,17 @@ export class TaskPollingLifecycle { poll_interval: pollInterval, } = config; + const pollIntervalDelay$ = delayOnClaimConflicts( + maxWorkersConfiguration$, + pollIntervalConfiguration$, + this.events$, + config.version_conflict_threshold, + config.monitored_stats_running_average_window + ); + pollIntervalDelay$.subscribe((delay) => { + emitEvent(asTaskManagerStatEvent('pollingDelay', asOk(delay))); + }); + // the task poller that polls for work on fixed intervals and on demand const poller$: Observable< Result> @@ -129,6 +142,7 @@ export class TaskPollingLifecycle { createTaskPoller({ logger, pollInterval$: pollIntervalConfiguration$, + pollIntervalDelay$, bufferCapacity: config.request_capacity, getCapacity: () => this.pool.availableWorkers, pollRequests$: this.claimRequests$, diff --git a/x-pack/plugins/task_manager/server/task_events.ts b/x-pack/plugins/task_manager/server/task_events.ts index fc09738a149a2..bd8eb56587906 100644 --- a/x-pack/plugins/task_manager/server/task_events.ts +++ b/x-pack/plugins/task_manager/server/task_events.ts @@ -53,7 +53,7 @@ export type TaskClaim = TaskEvent; export type TaskPollingCycle = TaskEvent>; -export type TaskManagerStats = 'load'; +export type TaskManagerStats = 'load' | 'pollingDelay'; export type TaskManagerStat = TaskEvent; export type OkResultOf = EventType extends TaskEvent diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/latest_function_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/latest_function_form.tsx index 3515316bb109d..b4d035940192d 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/latest_function_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/latest_function_form.tsx @@ -25,6 +25,7 @@ export const LatestFunctionForm: FC = ({ latestFunction defaultMessage="Unique keys" /> } + data-test-subj="transformLatestFunctionForm" > fullWidth diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8d0d17962ea93..999da541615c1 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1510,10 +1510,8 @@ "discover.embeddable.inspectorRequestDescription": "このリクエストはElasticsearchにクエリをかけ、検索データを取得します。", "discover.embeddable.search.displayName": "検索", "discover.fieldChooser.detailViews.emptyStringText": "空の文字列", - "discover.fieldChooser.detailViews.existsText": "存在する", "discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "{field}を除外:\"{value}\"", "discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "{field}を除外:\"{value}\"", - "discover.fieldChooser.detailViews.recordsText": "記録", "discover.fieldChooser.detailViews.visualizeLinkText": "可視化", "discover.fieldChooser.discoverField.addButtonAriaLabel": "{field}を表に追加", "discover.fieldChooser.discoverField.addFieldTooltip": "フィールドを列として追加", @@ -5147,8 +5145,6 @@ "xpack.apm.metrics.transactionChart.machineLearningLabel": "機械学習:", "xpack.apm.metrics.transactionChart.machineLearningTooltip": "平均期間の周りのストリームには予測バウンドが表示されます。異常スコアが>= 75の場合、注釈が表示されます。", "xpack.apm.metrics.transactionChart.machineLearningTooltip.withKuery": "フィルタリングで検索バーを使用しているときには、機械学習結果が表示されません", - "xpack.apm.metrics.transactionChart.requestsPerMinuteLabel": "1 分あたりのリクエスト", - "xpack.apm.metrics.transactionChart.transactionsPerMinuteLabel": "1分あたりのトランザクション数", "xpack.apm.metrics.transactionChart.viewJob": "ジョブを表示", "xpack.apm.notAvailableLabel": "N/A", "xpack.apm.propertiesTable.agentFeature.noDataAvailableLabel": "利用可能なデータがありません", @@ -5288,7 +5284,6 @@ "xpack.apm.serviceOverview.mlNudgeMessage.content": "APM の異常検知統合で、異常なトランザクションを特定し、アップストリームおよびダウンストリームサービスの正常性を確認します。わずか数分で開始できます。", "xpack.apm.serviceOverview.mlNudgeMessage.dismissButton": "閉じる", "xpack.apm.serviceOverview.mlNudgeMessage.learnMoreButton": "使ってみる", - "xpack.apm.serviceOverview.throughputChart.traffic": "トラフィック", "xpack.apm.serviceOverview.throughtputChartTitle": "トラフィック", "xpack.apm.serviceOverview.transactionsTableColumnErrorRate": "エラー率", "xpack.apm.serviceOverview.transactionsTableColumnImpact": "インパクト", @@ -5296,12 +5291,10 @@ "xpack.apm.serviceOverview.transactionsTableColumnLatency.p95": "レイテンシ(95 番目)", "xpack.apm.serviceOverview.transactionsTableColumnLatency.p99": "レイテンシ(99 番目)", "xpack.apm.serviceOverview.transactionsTableColumnName": "名前", - "xpack.apm.serviceOverview.transactionsTableColumnTroughput": "トラフィック", "xpack.apm.serviceOverview.transactionsTableLinkText": "トランザクションを表示", "xpack.apm.serviceOverview.transactionsTableTitle": "トランザクション", "xpack.apm.servicesTable.7xOldDataMessage": "また、移行が必要な古いデータがある可能性もあります。", "xpack.apm.servicesTable.7xUpgradeServerMessage": "バージョン7.xより前からのアップグレードですか?また、\n APMサーバーインスタンスを7.0以降にアップグレードしていることも確認してください。", - "xpack.apm.servicesTable.avgResponseTimeColumnLabel": "平均応答時間", "xpack.apm.servicesTable.environmentColumnLabel": "環境", "xpack.apm.servicesTable.environmentCount": "{environmentCount, plural, one {1 個の環境} other {# 個の環境}}", "xpack.apm.servicesTable.healthColumnLabel": "ヘルス", @@ -5309,7 +5302,6 @@ "xpack.apm.servicesTable.noServicesLabel": "APM サービスがインストールされていないようです。追加しましょう!", "xpack.apm.servicesTable.notFoundLabel": "サービスが見つかりません", "xpack.apm.servicesTable.transactionErrorRate": "エラー率%", - "xpack.apm.servicesTable.transactionsPerMinuteColumnLabel": "1 分あたりのトランザクション", "xpack.apm.servicesTable.UpgradeAssistantLink": "Kibana アップグレードアシスタントで詳細をご覧ください", "xpack.apm.settings.agentConfig": "エージェントの編集", "xpack.apm.settings.anomaly_detection.legacy_jobs.body": "以前の統合のレガシー機械学習ジョブが見つかりました。これは、APMアプリでは使用されていません。", @@ -5436,7 +5428,6 @@ "xpack.apm.transactionActionMenu.trace.title": "トレースの詳細", "xpack.apm.transactionActionMenu.viewInUptime": "ステータス", "xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel": "サンプルドキュメントを表示", - "xpack.apm.transactionBreakdown.chartTitle": "スパンタイプ別平均期間", "xpack.apm.transactionCardinalityWarning.body": "一意のトランザクション名の数が構成された値{bucketSize}を超えています。エージェントを再構成し、類似したトランザクションをグループ化するか、{codeBlock}の値を増やしてください。", "xpack.apm.transactionCardinalityWarning.docsLink": "詳細はドキュメントをご覧ください", "xpack.apm.transactionCardinalityWarning.title": "このビューには、報告されたトランザクションのサブセットが表示されます。", @@ -5489,8 +5480,6 @@ "xpack.apm.transactionOverview.userExperience.calloutTitle": "導入:Elasticユーザーエクスペリエンス", "xpack.apm.transactionOverview.userExperience.linkLabel": "移動", "xpack.apm.transactionRateLabel": "{value} tpm", - "xpack.apm.transactions.chart.anomalyBoundariesLabel": "異常境界", - "xpack.apm.transactions.chart.anomalyScoreLabel": "異常スコア", "xpack.apm.transactions.latency.chart.95thPercentileLabel": "95 パーセンタイル", "xpack.apm.transactions.latency.chart.99thPercentileLabel": "99 パーセンタイル", "xpack.apm.transactions.latency.chart.averageLabel": "平均", @@ -5500,7 +5489,6 @@ "xpack.apm.transactionsTable.impactColumnLabel": "インパクト", "xpack.apm.transactionsTable.nameColumnLabel": "名前", "xpack.apm.transactionsTable.notFoundLabel": "トランザクションが見つかりませんでした。", - "xpack.apm.transactionsTable.transactionsPerMinuteColumnLabel": "1 分あたりのトランザクション", "xpack.apm.tutorial.apmServer.title": "APM Server", "xpack.apm.tutorial.elasticCloud.textPre": "APM Server を有効にするには、[the Elastic Cloud console](https://cloud.elastic.co/deployments?q={cloudId}) に移動し、展開設定で APM を有効にします。有効になったら、このページを更新してください。", "xpack.apm.tutorial.elasticCloudInstructions.title": "APM エージェント", @@ -7279,20 +7267,18 @@ "xpack.enterpriseSearch.appSearch.productName": "App Search", "xpack.enterpriseSearch.appSearch.result.hideAdditionalFields": "追加フィールドを非表示", "xpack.enterpriseSearch.appSearch.roleMappings.title": "ロールマッピング", - "xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.customPolicy": "カスタム分析保持ポリシーがあります。", - "xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.ilmDisabled": "App Search は分析保持を管理していません。", + "xpack.enterpriseSearch.appSearch.logRetention.customPolicy": "カスタム {logsType} ログ保持ポリシーがあります。", + "xpack.enterpriseSearch.appSearch.logRetention.defaultPolicy": "{logsType} ログは {minAgeDays} 日以上保存されています。", + "xpack.enterpriseSearch.appSearch.logRetention.ilmDisabled": "App Search は {logsType} 保持を管理していません。", + "xpack.enterpriseSearch.appSearch.logRetention.noLogging": "すべてのエンジンの {logsType} ログが無効です。", + "xpack.enterpriseSearch.appSearch.logRetention.noLogging.collected": "前回の {logsType} ログは {disabledAtDate} に収集されました。", + "xpack.enterpriseSearch.appSearch.logRetention.noLogging.notCollected": "収集された {logsType} ログはありません。", + "xpack.enterpriseSearch.appSearch.logRetention.type.analytics.title.capitalized": "分析", + "xpack.enterpriseSearch.appSearch.logRetention.type.analytics.title.lowercase": "分析", + "xpack.enterpriseSearch.appSearch.logRetention.type.api.title.capitalized": "API", + "xpack.enterpriseSearch.appSearch.logRetention.type.api.title.lowercase": "API", "xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.label": "分析ログ", - "xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.noLogging": "すべてのエンジンの分析収集が無効です。", - "xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.noLogging.collected": "前回の分析は {disabledAt} に収集されました。", - "xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.noLogging.notCollected": "収集された分析はありません。", - "xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.stored": "分析は {minAgeDays} 日以上保存されています。", - "xpack.enterpriseSearch.appSearch.settings.logRetention.api.customPolicy": "カスタム API ログ保持ポリシーがあります。", - "xpack.enterpriseSearch.appSearch.settings.logRetention.api.ilmDisabled": "App Search は API ログ保持を管理していません。", "xpack.enterpriseSearch.appSearch.settings.logRetention.api.label": "API ログ", - "xpack.enterpriseSearch.appSearch.settings.logRetention.api.noLogging": "すべてのエンジンの API ログが無効です。", - "xpack.enterpriseSearch.appSearch.settings.logRetention.api.noLogging.collected": "前回のログは {disabledAt} に収集されました。", - "xpack.enterpriseSearch.appSearch.settings.logRetention.api.noLogging.notCollected": "収集されたログはありません。", - "xpack.enterpriseSearch.appSearch.settings.logRetention.api.stored": "ログは {minAgeDays} 日以上保存されています。", "xpack.enterpriseSearch.appSearch.settings.logRetention.description": "API ログと分析のデフォルト書き込み設定を管理します。", "xpack.enterpriseSearch.appSearch.settings.logRetention.learnMore": "保持設定の詳細をご覧ください。", "xpack.enterpriseSearch.appSearch.settings.logRetention.modal.analytics.description": "分析ログを無効にすると、すべてのエンジンがただちに分析ログのインデックス作成を停止します。既存のデータは、上記で概説したストレージタイムフレームに従って削除されます。", @@ -15986,7 +15972,6 @@ "xpack.observability.overview.apm.appLink": "アプリで表示", "xpack.observability.overview.apm.services": "サービス", "xpack.observability.overview.apm.title": "APM", - "xpack.observability.overview.apm.transactionsPerMinute": "1分あたりのトランザクション数", "xpack.observability.overview.breadcrumb": "概要", "xpack.observability.overview.loadingObservability": "オブザーバビリティを読み込んでいます", "xpack.observability.overview.logs.appLink": "アプリで表示", @@ -20785,10 +20770,8 @@ "xpack.spaces.management.enabledSpaceFeatures.allFeaturesEnabledMessage": "(表示されているすべての機能)", "xpack.spaces.management.enabledSpaceFeatures.enabledFeaturesSectionMessage": "機能", "xpack.spaces.management.enabledSpaceFeatures.enableFeaturesInSpaceMessage": "このスペースの機能の表示を設定", - "xpack.spaces.management.enabledSpaceFeatures.goToRolesLink": "機能へのアクセスを保護する場合は、{manageSecurityRoles}してください。", "xpack.spaces.management.enabledSpaceFeatures.noFeaturesEnabledMessage": "(表示されている機能がありません)", "xpack.spaces.management.enabledSpaceFeatures.notASecurityMechanismMessage": "この機能は UI で非表示になっていますが、無効ではありません。", - "xpack.spaces.management.enabledSpaceFeatures.rolesLinkText": "セキュリティロールを管理", "xpack.spaces.management.enabledSpaceFeatures.someFeaturesEnabledMessage": "({featureCount} 件中 {enabledCount} 件の機能を表示中)", "xpack.spaces.management.featureAccordionSwitchLabel": "{featureCount}件中{enabledCount}件の機能を表示中", "xpack.spaces.management.featureVisibilityTitle": "機能の表示", @@ -20815,9 +20798,6 @@ "xpack.spaces.management.manageSpacePage.spaceSuccessfullySavedNotificationMessage": "スペース {name} が保存されました。", "xpack.spaces.management.manageSpacePage.updateSpaceButton": "スペースを更新", "xpack.spaces.management.reversedSpaceBadge.reversedSpacesCanBePartiallyModifiedTooltip": "リザーブされたスペースはビルトインのため、部分的な変更しかできません。", - "xpack.spaces.management.secureSpaceMessage.howToAssignRoleToSpaceDescription": "スペースへのロールの割り当てをご希望ですか?{rolesLink} に移動してください。", - "xpack.spaces.management.secureSpaceMessage.rolesLinkText": "ロール", - "xpack.spaces.management.secureSpaceMessage.rolesLinkTextAriaLabel": "ロール管理ページ", "xpack.spaces.management.selectAllFeaturesLink": "すべて選択", "xpack.spaces.management.shareToSpace.actionDescription": "この保存されたオブジェクトを1つ以上のスペースと共有します。", "xpack.spaces.management.shareToSpace.actionTitle": "スペースと共有", @@ -22419,8 +22399,6 @@ "xpack.uptime.synthetics.screenshot.noImageMessage": "画像がありません", "xpack.uptime.synthetics.screenshotDisplay.altText": "名前「{stepName}」のステップのスクリーンショット", "xpack.uptime.synthetics.screenshotDisplay.altTextWithoutName": "スクリーンショット", - "xpack.uptime.synthetics.screenshotDisplay.fullScreenshotAltText": "名前「{stepName}」のステップの詳細スクリーンショット", - "xpack.uptime.synthetics.screenshotDisplay.fullScreenshotAltTextWithoutName": "詳細スクリーンショット", "xpack.uptime.synthetics.screenshotDisplay.thumbnailAltText": "名前「{stepName}」のステップのサムネイルスクリーンショット", "xpack.uptime.synthetics.screenshotDisplay.thumbnailAltTextWithoutName": "サムネイルスクリーンショット", "xpack.uptime.synthetics.statusBadge.failedMessage": "失敗", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 426bbb8567cca..d92a2ba94ff34 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1510,10 +1510,8 @@ "discover.embeddable.inspectorRequestDescription": "此请求将查询 Elasticsearch 以获取搜索的数据。", "discover.embeddable.search.displayName": "搜索", "discover.fieldChooser.detailViews.emptyStringText": "空字符串", - "discover.fieldChooser.detailViews.existsText": "存在于", "discover.fieldChooser.detailViews.filterOutValueButtonAriaLabel": "筛除 {field}:“{value}”", "discover.fieldChooser.detailViews.filterValueButtonAriaLabel": "筛留 {field}:“{value}”", - "discover.fieldChooser.detailViews.recordsText": "个记录", "discover.fieldChooser.detailViews.visualizeLinkText": "可视化", "discover.fieldChooser.discoverField.addButtonAriaLabel": "将 {field} 添加到表中", "discover.fieldChooser.discoverField.addFieldTooltip": "将字段添加为列", @@ -5155,8 +5153,6 @@ "xpack.apm.metrics.transactionChart.machineLearningLabel": "Machine Learning", "xpack.apm.metrics.transactionChart.machineLearningTooltip": "环绕平均持续时间的流显示预期边界。对 ≥ 75 的异常分数显示标注。", "xpack.apm.metrics.transactionChart.machineLearningTooltip.withKuery": "使用搜索栏筛选时,Machine Learning 结果处于隐藏状态", - "xpack.apm.metrics.transactionChart.requestsPerMinuteLabel": "每分钟请求数", - "xpack.apm.metrics.transactionChart.transactionsPerMinuteLabel": "每分钟事务数", "xpack.apm.metrics.transactionChart.viewJob": "查看作业", "xpack.apm.notAvailableLabel": "不可用", "xpack.apm.percentOfParent": "({parentType, select, transaction {事务} trace {追溯} }的{value})", @@ -5297,7 +5293,6 @@ "xpack.apm.serviceOverview.mlNudgeMessage.content": "通过 APM 的异常检测集成来查明异常事务,并了解上下游服务的运行状况。只需几分钟即可开始使用。", "xpack.apm.serviceOverview.mlNudgeMessage.dismissButton": "关闭", "xpack.apm.serviceOverview.mlNudgeMessage.learnMoreButton": "开始使用", - "xpack.apm.serviceOverview.throughputChart.traffic": "流量", "xpack.apm.serviceOverview.throughtputChartTitle": "流量", "xpack.apm.serviceOverview.transactionsTableColumnErrorRate": "错误率", "xpack.apm.serviceOverview.transactionsTableColumnImpact": "影响", @@ -5305,12 +5300,10 @@ "xpack.apm.serviceOverview.transactionsTableColumnLatency.p95": "延迟(第 95 个)", "xpack.apm.serviceOverview.transactionsTableColumnLatency.p99": "延迟(第 99 个)", "xpack.apm.serviceOverview.transactionsTableColumnName": "名称", - "xpack.apm.serviceOverview.transactionsTableColumnTroughput": "流量", "xpack.apm.serviceOverview.transactionsTableLinkText": "查看事务", "xpack.apm.serviceOverview.transactionsTableTitle": "事务", "xpack.apm.servicesTable.7xOldDataMessage": "可能还有需要迁移的旧数据。", "xpack.apm.servicesTable.7xUpgradeServerMessage": "从 7.x 之前的版本升级?另外,确保您已将\n APM Server 实例升级到至少 7.0。", - "xpack.apm.servicesTable.avgResponseTimeColumnLabel": "平均响应时间", "xpack.apm.servicesTable.environmentColumnLabel": "环境", "xpack.apm.servicesTable.environmentCount": "{environmentCount, plural, one {1 个环境} other {# 个环境}}", "xpack.apm.servicesTable.healthColumnLabel": "运行状况", @@ -5318,7 +5311,6 @@ "xpack.apm.servicesTable.noServicesLabel": "似乎您没有安装任何 APM 服务。让我们添加一些!", "xpack.apm.servicesTable.notFoundLabel": "未找到任何服务", "xpack.apm.servicesTable.transactionErrorRate": "错误率 %", - "xpack.apm.servicesTable.transactionsPerMinuteColumnLabel": "每分钟事务数", "xpack.apm.servicesTable.UpgradeAssistantLink": "通过访问 Kibana 升级助手来了解详情", "xpack.apm.settings.agentConfig": "代理配置", "xpack.apm.settings.anomaly_detection.legacy_jobs.body": "我们在以前的集成中发现 APM 应用中不再使用的旧版 Machine Learning 作业", @@ -5445,7 +5437,6 @@ "xpack.apm.transactionActionMenu.trace.title": "跟踪详情", "xpack.apm.transactionActionMenu.viewInUptime": "状态", "xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel": "查看样例文档", - "xpack.apm.transactionBreakdown.chartTitle": "平均持续时间(按跨度类型)", "xpack.apm.transactionCardinalityWarning.body": "唯一事务名称的数目超过 {bucketSize} 的已配置值。尝试重新配置您的代理以对类似的事务分组或增大 {codeBlock} 的值", "xpack.apm.transactionCardinalityWarning.docsLink": "在文档中了解详情", "xpack.apm.transactionCardinalityWarning.title": "此视图显示已报告事务的子集。", @@ -5499,8 +5490,6 @@ "xpack.apm.transactionOverview.userExperience.calloutTitle": "即将引入:Elastic 用户体验", "xpack.apm.transactionOverview.userExperience.linkLabel": "带我前往此处", "xpack.apm.transactionRateLabel": "{value} tpm", - "xpack.apm.transactions.chart.anomalyBoundariesLabel": "异常边界", - "xpack.apm.transactions.chart.anomalyScoreLabel": "异常分数", "xpack.apm.transactions.latency.chart.95thPercentileLabel": "第 95 个百分位", "xpack.apm.transactions.latency.chart.99thPercentileLabel": "第 99 个百分位", "xpack.apm.transactions.latency.chart.averageLabel": "平均值", @@ -5510,7 +5499,6 @@ "xpack.apm.transactionsTable.impactColumnLabel": "影响", "xpack.apm.transactionsTable.nameColumnLabel": "名称", "xpack.apm.transactionsTable.notFoundLabel": "未找到任何事务。", - "xpack.apm.transactionsTable.transactionsPerMinuteColumnLabel": "每分钟事务数", "xpack.apm.tutorial.apmServer.title": "APM Server", "xpack.apm.tutorial.elasticCloud.textPre": "要启用 APM Server,请前往 [Elastic Cloud 控制台](https://cloud.elastic.co/deployments?q={cloudId}) 并在部署设置中启用 APM。启用后,请刷新此页面。", "xpack.apm.tutorial.elasticCloudInstructions.title": "APM 代理", @@ -7298,20 +7286,18 @@ "xpack.enterpriseSearch.appSearch.productName": "App Search", "xpack.enterpriseSearch.appSearch.result.hideAdditionalFields": "隐藏其他字段", "xpack.enterpriseSearch.appSearch.roleMappings.title": "角色映射", - "xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.customPolicy": "您有定制分析保留策略。", - "xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.ilmDisabled": "App Search 不管理分析保留。", + "xpack.enterpriseSearch.appSearch.logRetention.customPolicy": "您有定制 {logsType} 日志保留策略。", + "xpack.enterpriseSearch.appSearch.logRetention.defaultPolicy": "您的 {logsType} 日志将存储至少 {minAgeDays} 天。", + "xpack.enterpriseSearch.appSearch.logRetention.ilmDisabled": "App Search 不管理 {logsType} 保留。", + "xpack.enterpriseSearch.appSearch.logRetention.noLogging": "所有引擎的 {logsType} 日志记录均已禁用。", + "xpack.enterpriseSearch.appSearch.logRetention.noLogging.collected": "{logsType} 日志的最后收集日期为 {disabledAtDate}。", + "xpack.enterpriseSearch.appSearch.logRetention.noLogging.notCollected": "未收集任何 {logsType} 日志。", + "xpack.enterpriseSearch.appSearch.logRetention.type.analytics.title.capitalized": "分析", + "xpack.enterpriseSearch.appSearch.logRetention.type.analytics.title.lowercase": "分析", + "xpack.enterpriseSearch.appSearch.logRetention.type.api.title.capitalized": "API", + "xpack.enterpriseSearch.appSearch.logRetention.type.api.title.lowercase": "API", "xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.label": "分析日志", - "xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.noLogging": "所有引擎的分析收集均已禁用。", - "xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.noLogging.collected": "分析的最后收集日期是 {disabledAt}。", - "xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.noLogging.notCollected": "未收集任何分析。", - "xpack.enterpriseSearch.appSearch.settings.logRetention.analytics.stored": "您的分析将存储至少 {minAgeDays} 天。", - "xpack.enterpriseSearch.appSearch.settings.logRetention.api.customPolicy": "您有定制 API 日志保留策略。", - "xpack.enterpriseSearch.appSearch.settings.logRetention.api.ilmDisabled": "App Search 不管理 API 日志保留。", "xpack.enterpriseSearch.appSearch.settings.logRetention.api.label": "API 日志", - "xpack.enterpriseSearch.appSearch.settings.logRetention.api.noLogging": "所有引擎的 API 日志记录均已禁用。", - "xpack.enterpriseSearch.appSearch.settings.logRetention.api.noLogging.collected": "日志的最后收集日期为 {disabledAt}。", - "xpack.enterpriseSearch.appSearch.settings.logRetention.api.noLogging.notCollected": "未收集任何日志。", - "xpack.enterpriseSearch.appSearch.settings.logRetention.api.stored": "您的日志将存储至少 {minAgeDays} 天。", "xpack.enterpriseSearch.appSearch.settings.logRetention.description": "管理 API 日志和分析的默认写入设置。", "xpack.enterpriseSearch.appSearch.settings.logRetention.learnMore": "了解保留设置的详情。", "xpack.enterpriseSearch.appSearch.settings.logRetention.modal.analytics.description": "禁用分析日志后,您的所有引擎将立即停止索引分析日志。您的现有数据将按照上述存储时间范围被删除。", @@ -16028,7 +16014,6 @@ "xpack.observability.overview.apm.appLink": "在应用中查看", "xpack.observability.overview.apm.services": "服务", "xpack.observability.overview.apm.title": "APM", - "xpack.observability.overview.apm.transactionsPerMinute": "每分钟事务数", "xpack.observability.overview.breadcrumb": "概览", "xpack.observability.overview.loadingObservability": "正在加载可观测性", "xpack.observability.overview.logs.appLink": "在应用中查看", @@ -20833,10 +20818,8 @@ "xpack.spaces.management.enabledSpaceFeatures.allFeaturesEnabledMessage": "(所有可见功能)", "xpack.spaces.management.enabledSpaceFeatures.enabledFeaturesSectionMessage": "功能", "xpack.spaces.management.enabledSpaceFeatures.enableFeaturesInSpaceMessage": "为此工作区设置功能可见性", - "xpack.spaces.management.enabledSpaceFeatures.goToRolesLink": "如果您希望保护对功能的访问,请{manageSecurityRoles}。", "xpack.spaces.management.enabledSpaceFeatures.noFeaturesEnabledMessage": "(没有可见功能)", "xpack.spaces.management.enabledSpaceFeatures.notASecurityMechanismMessage": "该功能在 UI 中已隐藏,但未禁用。", - "xpack.spaces.management.enabledSpaceFeatures.rolesLinkText": "管理安全角色", "xpack.spaces.management.enabledSpaceFeatures.someFeaturesEnabledMessage": "({enabledCount} / {featureCount} 个功能可见)", "xpack.spaces.management.featureAccordionSwitchLabel": "{enabledCount} / {featureCount} 个功能可见", "xpack.spaces.management.featureVisibilityTitle": "功能可见性", @@ -20863,9 +20846,6 @@ "xpack.spaces.management.manageSpacePage.spaceSuccessfullySavedNotificationMessage": "空间 “{name}” 已保存。", "xpack.spaces.management.manageSpacePage.updateSpaceButton": "更新工作区", "xpack.spaces.management.reversedSpaceBadge.reversedSpacesCanBePartiallyModifiedTooltip": "保留的空间是内置的,只能进行部分修改。", - "xpack.spaces.management.secureSpaceMessage.howToAssignRoleToSpaceDescription": "想要为工作区分配角色?前往{rolesLink}。", - "xpack.spaces.management.secureSpaceMessage.rolesLinkText": "角色", - "xpack.spaces.management.secureSpaceMessage.rolesLinkTextAriaLabel": "角色管理页面", "xpack.spaces.management.selectAllFeaturesLink": "全选", "xpack.spaces.management.shareToSpace.actionDescription": "将此已保存对象共享到一个或多个工作区", "xpack.spaces.management.shareToSpace.actionTitle": "共享到工作区", @@ -22470,8 +22450,6 @@ "xpack.uptime.synthetics.screenshot.noImageMessage": "没有可用图像", "xpack.uptime.synthetics.screenshotDisplay.altText": "名称为“{stepName}”的步骤的屏幕截图", "xpack.uptime.synthetics.screenshotDisplay.altTextWithoutName": "屏幕截图", - "xpack.uptime.synthetics.screenshotDisplay.fullScreenshotAltText": "名称为“{stepName}”的步骤的完整屏幕截图", - "xpack.uptime.synthetics.screenshotDisplay.fullScreenshotAltTextWithoutName": "完整屏幕截图", "xpack.uptime.synthetics.screenshotDisplay.thumbnailAltText": "名称为“{stepName}”的步骤的缩略屏幕截图", "xpack.uptime.synthetics.screenshotDisplay.thumbnailAltTextWithoutName": "缩略屏幕截图", "xpack.uptime.synthetics.statusBadge.failedMessage": "失败", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_connectors_prompt.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_connectors_prompt.tsx index 0e956ea56faa9..3468372ba81ba 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_connectors_prompt.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_connectors_prompt.tsx @@ -32,7 +32,7 @@ export const EmptyConnectorsPrompt = ({ onCTAClicked }: { onCTAClicked: () => vo

    } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_prompt.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_prompt.tsx index 303c9c5c4cdd1..4242e70d65f67 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_prompt.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_prompt.tsx @@ -24,7 +24,7 @@ export const EmptyPrompt = ({ onCTAClicked }: { onCTAClicked: () => void }) => (

    } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx index ff962b9b9d9c4..e00a183eb15ee 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx @@ -203,7 +203,7 @@ export const ActionTypeForm = ({ > } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx index cefdd9d161607..d71a36ecc9bff 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx @@ -214,7 +214,7 @@ const ConnectorAddFlyout: React.FunctionComponent = ({ > diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx index f4fec873ecc94..fa5dcc39d07f7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx @@ -385,7 +385,7 @@ export const ConnectorEditFlyout = ({ > diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.scss index 70ad1cae6c1d1..476c971cfbbff 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.scss +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.scss @@ -5,7 +5,7 @@ color: $euiColorDarkShade; } - .euiLink + .euiIcon { + .euiLink + .euiToolTipAnchor { margin-left: $euiSizeXS; } } diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx index 388f87cbf752b..417362ead76c9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/for_the_last.tsx @@ -84,6 +84,7 @@ export const ForLastExpression = ({ ownFocus display={display === 'fullWidth' ? 'block' : 'inlineBlock'} anchorPosition={popupPosition ?? 'downLeft'} + repositionOnScroll >
    setAlertDurationPopoverOpen(false)}> diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx index 785df0981ebe6..94fdd027e38f8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx @@ -115,6 +115,7 @@ export const GroupByExpression = ({ ownFocus display={display === 'fullWidth' ? 'block' : 'inlineBlock'} anchorPosition={popupPosition ?? 'downRight'} + repositionOnScroll >
    setGroupByPopoverOpen(false)}> diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx index e15b9a21570c9..c6da09ea716c1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/of.tsx @@ -104,6 +104,7 @@ export const OfExpression = ({ display={display === 'fullWidth' ? 'block' : 'inlineBlock'} anchorPosition={popupPosition ?? 'downRight'} zIndex={8000} + repositionOnScroll >
    setAggFieldPopoverOpen(false)}> diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx index 8fabf4e9aeb2d..8f186a87bb831 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx @@ -111,6 +111,7 @@ export const ThresholdExpression = ({ ownFocus display={display === 'fullWidth' ? 'block' : 'inlineBlock'} anchorPosition={popupPosition ?? 'downLeft'} + repositionOnScroll >
    setAlertThresholdPopoverOpen(false)}> diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.tsx index 5696417f241fd..1fb1e0767b29b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/when.tsx @@ -67,6 +67,7 @@ export const WhenExpression = ({ ownFocus display={display === 'fullWidth' ? 'block' : 'inlineBlock'} anchorPosition={popupPosition ?? 'downLeft'} + repositionOnScroll >
    setAggTypePopoverOpen(false)}> diff --git a/x-pack/plugins/uptime/common/constants/rest_api.ts b/x-pack/plugins/uptime/common/constants/rest_api.ts index be1f498c2e75d..6916a5ea4788c 100644 --- a/x-pack/plugins/uptime/common/constants/rest_api.ts +++ b/x-pack/plugins/uptime/common/constants/rest_api.ts @@ -29,4 +29,5 @@ export enum API_URLS { CREATE_ALERT = '/api/alerts/alert', ALERT = '/api/alerts/alert/', ALERTS_FIND = '/api/alerts/_find', + ACTION_TYPES = '/api/actions/list_action_types', } diff --git a/x-pack/plugins/uptime/common/runtime_types/alerts/status_check.ts b/x-pack/plugins/uptime/common/runtime_types/alerts/status_check.ts index 971a9f51bfae1..8996bad8d4f08 100644 --- a/x-pack/plugins/uptime/common/runtime_types/alerts/status_check.ts +++ b/x-pack/plugins/uptime/common/runtime_types/alerts/status_check.ts @@ -26,6 +26,7 @@ export const AtomicStatusCheckParamsType = t.intersection([ filters: StatusCheckFiltersType, shouldCheckStatus: t.boolean, isAutoGenerated: t.boolean, + shouldCheckAvailability: t.boolean, }), ]); diff --git a/x-pack/plugins/uptime/common/runtime_types/overview_filters/overview_filters.ts b/x-pack/plugins/uptime/common/runtime_types/overview_filters/overview_filters.ts index 9b9241494f001..854b4b6b6e9b8 100644 --- a/x-pack/plugins/uptime/common/runtime_types/overview_filters/overview_filters.ts +++ b/x-pack/plugins/uptime/common/runtime_types/overview_filters/overview_filters.ts @@ -6,7 +6,7 @@ import * as t from 'io-ts'; -export const OverviewFiltersType = t.type({ +export const OverviewFiltersType = t.partial({ locations: t.array(t.string), ports: t.array(t.number), schemes: t.array(t.string), diff --git a/x-pack/plugins/uptime/public/components/common/header/page_tabs.test.tsx b/x-pack/plugins/uptime/public/components/common/header/page_tabs.test.tsx new file mode 100644 index 0000000000000..e64b3533c0cc5 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/common/header/page_tabs.test.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render } from '../../../lib/helper/rtl_helpers'; +import { PageTabs } from './page_tabs'; +import { createMemoryHistory } from 'history'; + +describe('PageTabs', () => { + it('it renders all tabs', () => { + const { getByText } = render(); + expect(getByText('Overview')).toBeInTheDocument(); + expect(getByText('Certificates')).toBeInTheDocument(); + expect(getByText('Settings')).toBeInTheDocument(); + }); + + it('it keep params while switching', () => { + const { getByTestId } = render(, { + history: createMemoryHistory({ + initialEntries: ['/settings/?g=%22%22&dateRangeStart=now-10m&dateRangeEnd=now'], + }), + }); + expect(getByTestId('uptimeSettingsToOverviewLink')).toHaveAttribute( + 'href', + '/?dateRangeStart=now-10m' + ); + expect(getByTestId('uptimeCertificatesLink')).toHaveAttribute( + 'href', + '/certificates?dateRangeStart=now-10m' + ); + expect(getByTestId('settings-page-link')).toHaveAttribute( + 'href', + '/settings?dateRangeStart=now-10m' + ); + }); + + it('it resets params on overview if already on overview', () => { + const { getByTestId } = render(, { + history: createMemoryHistory({ + initialEntries: ['/?g=%22%22&dateRangeStart=now-10m&dateRangeEnd=now'], + }), + }); + expect(getByTestId('uptimeSettingsToOverviewLink')).toHaveAttribute('href', '/'); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/common/header/page_tabs.tsx b/x-pack/plugins/uptime/public/components/common/header/page_tabs.tsx index 56da7d5f92803..7dfc110dbb6e9 100644 --- a/x-pack/plugins/uptime/public/components/common/header/page_tabs.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/page_tabs.tsx @@ -10,6 +10,8 @@ import { EuiTabs, EuiTab } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useHistory, useRouteMatch } from 'react-router-dom'; import { CERTIFICATES_ROUTE, OVERVIEW_ROUTE, SETTINGS_ROUTE } from '../../../../common/constants'; +import { useGetUrlParams } from '../../../hooks'; +import { stringifyUrlParams } from '../../../lib/helper/stringify_url_params'; const tabs = [ { @@ -39,6 +41,8 @@ export const PageTabs = () => { const history = useHistory(); + const params = useGetUrlParams(); + const isOverView = useRouteMatch(OVERVIEW_ROUTE); const isSettings = useRouteMatch(SETTINGS_ROUTE); const isCerts = useRouteMatch(CERTIFICATES_ROUTE); @@ -58,6 +62,15 @@ export const PageTabs = () => { } }, [isCerts, isSettings, isOverView]); + const createHrefForTab = (id: string) => { + if (selectedTabId === OVERVIEW_ROUTE && id === OVERVIEW_ROUTE) { + // If we are already on overview route and user clicks again on overview tabs, + // we will reset the filters + return history.createHref({ pathname: id }); + } + return history.createHref({ pathname: id, search: stringifyUrlParams(params, true) }); + }; + const renderTabs = () => { return tabs.map(({ dataTestSubj, name, id }, index) => ( { isSelected={id === selectedTabId} key={index} data-test-subj={dataTestSubj} - href={history.createHref({ pathname: id })} + href={createHrefForTab(id)} > {name} diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/__snapshots__/ping_timestamp.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/__snapshots__/ping_timestamp.test.tsx.snap deleted file mode 100644 index ba6076151c1d7..0000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/__snapshots__/ping_timestamp.test.tsx.snap +++ /dev/null @@ -1,179 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Ping Timestamp component render without errors 1`] = ` -.c0 { - position: relative; -} - -.c0 figure.euiImage div.stepArrowsFullScreen { - display: none; -} - -.c0 figure.euiImage-isFullScreen div.stepArrowsFullScreen { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; -} - -.c0 div.stepArrows { - display: none; -} - -.c0:hover div.stepArrows { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; -} - -.c1 { - width: 120px; - text-align: center; - border: 1px solid #d3dae6; -} - -
    -
    -
    -
    - - No image available - -
    -
    -
    - -
    - - Nov 26, 2020 10:28:56 AM - -
    -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -`; - -exports[`Ping Timestamp component shallow render without errors 1`] = ` - - - - - - - -
    - - Nov 26, 2020 10:28:56 AM - - - - - - - - - - - - - -`; diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp.test.tsx index 558bdfc66a397..1baeb8a69d34c 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp.test.tsx @@ -5,16 +5,26 @@ */ import React from 'react'; -import { shallowWithIntl, renderWithIntl } from '@kbn/test/jest'; import { PingTimestamp } from './ping_timestamp'; import { mockReduxHooks } from '../../../../lib/helper/test_helpers'; +import { render } from '../../../../lib/helper/rtl_helpers'; import { Ping } from '../../../../../common/runtime_types/ping'; -import { EuiThemeProvider } from '../../../../../../observability/public'; +import * as observabilityPublic from '../../../../../../observability/public'; mockReduxHooks(); +jest.mock('../../../../../../observability/public', () => { + const originalModule = jest.requireActual('../../../../../../observability/public'); + + return { + ...originalModule, + useFetcher: jest.fn().mockReturnValue({ data: null, status: 'pending' }), + }; +}); + describe('Ping Timestamp component', () => { let response: Ping; + const { FETCH_STATUS } = observabilityPublic; beforeAll(() => { response = { @@ -49,19 +59,37 @@ describe('Ping Timestamp component', () => { }; }); - it('shallow render without errors', () => { - const component = shallowWithIntl( + it.each([[FETCH_STATUS.PENDING], [FETCH_STATUS.LOADING]])( + 'displays spinner when loading step image', + (fetchStatus) => { + jest + .spyOn(observabilityPublic, 'useFetcher') + .mockReturnValue({ status: fetchStatus, data: null, refetch: () => null }); + const { getByTestId } = render( + + ); + expect(getByTestId('pingTimestampSpinner')).toBeInTheDocument(); + } + ); + + it('displays no image available when img src is unavailable and fetch status is successful', () => { + jest + .spyOn(observabilityPublic, 'useFetcher') + .mockReturnValue({ status: FETCH_STATUS.SUCCESS, data: null, refetch: () => null }); + const { getByTestId } = render( ); - expect(component).toMatchSnapshot(); + expect(getByTestId('pingTimestampNoImageAvailable')).toBeInTheDocument(); }); - it('render without errors', () => { - const component = renderWithIntl( - - - - ); - expect(component).toMatchSnapshot(); + it('displays image when img src is available from useFetcher', () => { + const src = 'http://sample.com/sampleImageSrc.png'; + jest.spyOn(observabilityPublic, 'useFetcher').mockReturnValue({ + status: FETCH_STATUS.SUCCESS, + data: { src }, + refetch: () => null, + }); + const { container } = render(); + expect(container.querySelector('img')?.src).toBe(src); }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp.tsx index fc3b8f4d95d21..2a35587a1960f 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_list/columns/ping_timestamp.tsx @@ -12,6 +12,7 @@ import { EuiImage, EuiSpacer, EuiText, + EuiLoadingSpinner, } from '@elastic/eui'; import useIntersection from 'react-use/lib/useIntersection'; import moment from 'moment'; @@ -19,7 +20,7 @@ import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; import { Ping } from '../../../../../common/runtime_types/ping'; import { getShortTimeStamp } from '../../../overview/monitor_list/columns/monitor_status_column'; -import { euiStyled, useFetcher } from '../../../../../../observability/public'; +import { euiStyled, FETCH_STATUS, useFetcher } from '../../../../../../observability/public'; import { getJourneyScreenshot } from '../../../../state/api/journey'; import { UptimeSettingsContext } from '../../../../contexts'; @@ -81,7 +82,7 @@ export const PingTimestamp = ({ timestamp, ping }: Props) => { threshold: 1, }); - const { data } = useFetcher(() => { + const { data, status } = useFetcher(() => { if (intersection && intersection.intersectionRatio === 1 && !stepImages[stepNo - 1]) return getJourneyScreenshot(imgPath); }, [intersection?.intersectionRatio, stepNo]); @@ -94,12 +95,14 @@ export const PingTimestamp = ({ timestamp, ping }: Props) => { const imgSrc = stepImages[stepNo] || data?.src; + const isLoading = status === FETCH_STATUS.LOADING; + const isPending = status === FETCH_STATUS.PENDING; + + const captionContent = `Step:${stepNo} ${data?.stepName}`; + const ImageCaption = ( <> -
    +
    {imgSrc && ( @@ -114,9 +117,7 @@ export const PingTimestamp = ({ timestamp, ping }: Props) => { /> - - Step:{stepNo} {data?.stepName} - + {captionContent} { size="s" hasShadow caption={ImageCaption} - alt="No image available" + alt={captionContent} url={imgSrc} + data-test-subj="pingTimestampImage" /> ) : ( - + {isLoading || isPending ? ( + + ) : ( + + )} - {ImageCaption} + {ImageCaption} )} { return ( - + { +describe('ExecutedStep', () => { let step: Ping; beforeEach(() => { @@ -42,105 +40,28 @@ describe.skip('ExecutedStep', () => { }); it('renders a link to the step detail view', () => { - expect( - mountWithRouter().find( - 'StepDetailLink' - ) - ).toMatchInlineSnapshot(` - - - - - - `); + const { getByRole, getByText } = render( + + ); + expect(getByRole('link')).toHaveAttribute('href', '/journey/fake-group/step/4'); + expect(getByText('4. STEP_NAME')); }); - it('supplies status badge correct status', () => { + it.each([ + ['succeeded', 'Succeeded'], + ['failed', 'Failed'], + ['skipped', 'Skipped'], + ['somegarbage', '4.'], + ])('supplies status badge correct status', (status, expectedStatus) => { step.synthetics = { - payload: { status: 'THE_STATUS' }, + payload: { status }, }; - expect( - mountWithRouter().find( - 'StatusBadge' - ) - ).toMatchInlineSnapshot(` - - - - - - - - - - `); + const { getByText } = render(); + expect(getByText(expectedStatus)); }); - it('renders accordions for step, error message, and error stack script', () => { + it('renders accordion for step', () => { step.synthetics = { - error: { - message: 'There was an error executing the step.', - stack: 'some.stack.trace.string', - }, payload: { source: 'const someVar = "the var"', }, @@ -150,280 +71,26 @@ describe.skip('ExecutedStep', () => { }, }; - expect( - mountWithRouter().find( - 'CodeBlockAccordion' - ) - ).toMatchInlineSnapshot(` - Array [ - - -
    -
    - -
    -
    - -
    -
    - - -
    -
    -                              
    -                                const someVar = "the var"
    -                              
    -                            
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    , - - -
    -
    - -
    -
    - -
    -
    - - -
    -
    -                              
    -                                There was an error executing the step.
    -                              
    -                            
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    , - - -
    -
    - -
    -
    - -
    -
    - - -
    -
    -                              
    -                                some.stack.trace.string
    -                              
    -                            
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    , - ] - `); + const { getByText } = render(); + + expect(getByText('4. STEP_NAME')); + expect(getByText('Step script')); + expect(getByText(`const someVar = "the var"`)); + }); + + it('renders accordions for error message, and stack trace', () => { + step.synthetics = { + error: { + message: 'There was an error executing the step.', + stack: 'some.stack.trace.string', + }, + }; + + const { getByText } = render(); + + expect(getByText('4.')); + expect(getByText('Error')); + expect(getByText('There was an error executing the step.')); + expect(getByText('some.stack.trace.string')); }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.test.tsx index 5fc04ed7a718d..60e7227c64a52 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallowWithIntl, mountWithIntl } from '@kbn/test/jest'; +import { render } from '../../../lib/helper/rtl_helpers'; import React from 'react'; import { StepScreenshotDisplay } from './step_screenshot_display'; @@ -14,7 +14,7 @@ jest.mock('react-use/lib/useIntersection', () => () => ({ describe('StepScreenshotDisplayProps', () => { it('displays screenshot thumbnail when present', () => { - const wrapper = mountWithIntl( + const { getByAltText } = render( { /> ); - wrapper.update(); - - expect(wrapper.find('img')).toMatchInlineSnapshot(`null`); - - expect(wrapper.find('EuiPopover')).toMatchInlineSnapshot(` - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={false} - panelPaddingSize="m" - > - -
    -
    - -
    -
    -
    -
    - `); + expect(getByAltText('Screenshot for step with name "STEP_NAME"')).toBeInTheDocument(); }); it('uses alternative text when step name not available', () => { - const wrapper = mountWithIntl( + const { getByAltText } = render( ); - wrapper.update(); - - expect(wrapper.find('EuiPopover')).toMatchInlineSnapshot(` - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={false} - panelPaddingSize="m" - > - -
    -
    - -
    -
    -
    -
    - `); + expect(getByAltText('Screenshot')).toBeInTheDocument(); }); it('displays No Image message when screenshot does not exist', () => { - expect( - shallowWithIntl( - - ).find('EuiText') - ).toMatchInlineSnapshot(` - - - - - - `); + const { getByTestId } = render( + + ); + expect(getByTestId('stepScreenshotImageUnavailable')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.tsx index 8f84e78fa9fc1..3efcff196b55f 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_screenshot_display.tsx @@ -4,14 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiOverlayMask, - EuiPopover, - EuiText, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiImage, EuiPopover, EuiText } from '@elastic/eui'; +import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useContext, useEffect, useRef, useState, FC } from 'react'; @@ -30,6 +24,18 @@ const THUMBNAIL_HEIGHT = 180; const POPOVER_IMG_WIDTH = 640; const POPOVER_IMG_HEIGHT = 360; +const StepImage = styled(EuiImage)` + &&& { + figcaption { + display: none; + } + width: ${THUMBNAIL_WIDTH}, + height: ${THUMBNAIL_HEIGHT}, + objectFit: 'cover', + objectPosition: 'center top', + } +`; + export const StepScreenshotDisplay: FC = ({ checkGroup, screenshotExists, @@ -44,7 +50,6 @@ export const StepScreenshotDisplay: FC = ({ const { basePath } = useContext(UptimeSettingsContext); const [isImagePopoverOpen, setIsImagePopoverOpen] = useState(false); - const [isOverlayOpen, setIsOverlayOpen] = useState(false); const intersection = useIntersection(containerRef, { root: null, @@ -62,49 +67,15 @@ export const StepScreenshotDisplay: FC = ({ let content: JSX.Element | null = null; const imgSrc = basePath + `/api/uptime/journey/screenshot/${checkGroup}/${stepIndex}`; + if (hasIntersected && screenshotExists) { content = ( <> - {isOverlayOpen && ( - setIsOverlayOpen(false)}> - setIsOverlayOpen(false)} - /> - - )} = ({ defaultMessage: 'Screenshot', }) } - onClick={() => setIsOverlayOpen(true)} + caption={`Step:${stepIndex} ${stepName}`} + hasShadow + url={imgSrc} onMouseEnter={() => setIsImagePopoverOpen(true)} onMouseLeave={() => setIsImagePopoverOpen(false)} /> @@ -149,7 +122,12 @@ export const StepScreenshotDisplay: FC = ({ ); } else if (screenshotExists === false) { content = ( - + diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts index 61a8104eddd31..9032e9a4ccd2c 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts @@ -5,6 +5,7 @@ */ import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { rgba } from 'polished'; import { euiStyled } from '../../../../../../../observability/public'; import { FIXED_AXIS_HEIGHT } from './constants'; @@ -16,6 +17,19 @@ export const WaterfallChartOuterContainer = euiStyled.div (props.height ? `${props.height}` : 'auto')}; overflow-y: ${(props) => (props.height ? 'scroll' : 'visible')}; overflow-x: hidden; + &::-webkit-scrollbar { + height: ${({ theme }) => theme.eui.euiScrollBar}; + width: ${({ theme }) => theme.eui.euiScrollBar}; + } + &::-webkit-scrollbar-thumb { + background-clip: content-box; + background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)}; + border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent; + } + &::-webkit-scrollbar-corner, + &::-webkit-scrollbar-track { + background-color: transparent; + } `; export const WaterfallChartFixedTopContainer = euiStyled.div` diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx index 50b6fe2aa0ef1..1c1deb2104970 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/alerts_containers/alert_monitor_status.tsx @@ -122,7 +122,7 @@ export const AlertMonitorStatus: React.FC = ({ enabled={enabled} hasFilters={!!overviewFilters?.filters} isOldAlert={isOldAlert} - locations={locations} + locations={locations || []} numTimes={numTimes} setAlertParams={setAlertParams} shouldUpdateUrl={shouldUpdateUrl} diff --git a/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group.tsx index 45268977a543f..a5256ee1e2626 100644 --- a/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group.tsx +++ b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group.tsx @@ -24,6 +24,10 @@ const Container = styled(EuiFilterGroup)` margin-bottom: 10px; `; +function isDisabled(array?: T[]) { + return array ? array.length === 0 : true; +} + export const FilterGroupComponent: React.FC = ({ overviewFilters, loading, @@ -51,7 +55,7 @@ export const FilterGroupComponent: React.FC = ({ onFilterFieldChange, fieldName: 'observer.geo.name', id: 'location', - items: locations, + items: locations || [], selectedItems: selectedLocations, title: filterLabels.LOCATION, }, @@ -63,8 +67,8 @@ export const FilterGroupComponent: React.FC = ({ onFilterFieldChange, fieldName: 'url.port', id: 'port', - disabled: ports.length === 0, - items: ports.map((p: number) => p.toString()), + disabled: isDisabled(ports), + items: ports?.map((p: number) => p.toString()) ?? [], selectedItems: selectedPorts, title: filterLabels.PORT, }, @@ -73,8 +77,8 @@ export const FilterGroupComponent: React.FC = ({ onFilterFieldChange, fieldName: 'monitor.type', id: 'scheme', - disabled: schemes.length === 0, - items: schemes, + disabled: isDisabled(schemes), + items: schemes ?? [], selectedItems: selectedSchemes, title: filterLabels.SCHEME, }, @@ -83,8 +87,8 @@ export const FilterGroupComponent: React.FC = ({ onFilterFieldChange, fieldName: 'tags', id: 'tags', - disabled: tags.length === 0, - items: tags, + disabled: isDisabled(tags), + items: tags ?? [], selectedItems: selectedTags, title: filterLabels.TAGS, }, diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/monitor_name_col.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/monitor_name_col.tsx index 3953d35cc73db..26a8122f1357f 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/monitor_name_col.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/columns/monitor_name_col.tsx @@ -35,7 +35,7 @@ const MONITOR_TYPES: Record = { }; export const MonitorNameColumn = ({ summary }: Props) => { - const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = useGetUrlParams(); + const params = useGetUrlParams(); const linkParameters = stringifyUrlParams(params, true); diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx index 7cf24d447316c..f100659054ac2 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/most_recent_error.tsx @@ -34,7 +34,7 @@ interface MostRecentErrorProps { } export const MostRecentError = ({ error, monitorId, timestamp }: MostRecentErrorProps) => { - const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = useGetUrlParams(); + const params = useGetUrlParams(); params.statusFilter = 'down'; const linkParameters = stringifyUrlParams(params, true); diff --git a/x-pack/plugins/uptime/public/components/settings/add_connector_flyout.tsx b/x-pack/plugins/uptime/public/components/settings/add_connector_flyout.tsx index 2f0faebdf2487..ce2d0f6ace7e3 100644 --- a/x-pack/plugins/uptime/public/components/settings/add_connector_flyout.tsx +++ b/x-pack/plugins/uptime/public/components/settings/add_connector_flyout.tsx @@ -11,6 +11,10 @@ import { EuiButtonEmpty } from '@elastic/eui'; import { TriggersAndActionsUIPublicPluginStart } from '../../../../triggers_actions_ui/public'; import { getConnectorsAction } from '../../state/alerts/alerts'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { useFetcher } from '../../../../observability/public'; +import { fetchActionTypes } from '../../state/api/alerts'; + +import { ActionTypeId } from './types'; interface Props { focusInput: () => void; @@ -20,6 +24,17 @@ interface KibanaDeps { triggersActionsUi: TriggersAndActionsUIPublicPluginStart; } +export const ALLOWED_ACTION_TYPES: ActionTypeId[] = [ + '.slack', + '.pagerduty', + '.server-log', + '.index', + '.teams', + '.servicenow', + '.jira', + '.webhook', +]; + export const AddConnectorFlyout = ({ focusInput }: Props) => { const [addFlyoutVisible, setAddFlyoutVisibility] = useState(false); const { @@ -30,6 +45,8 @@ export const AddConnectorFlyout = ({ focusInput }: Props) => { const dispatch = useDispatch(); + const { data: actionTypes } = useFetcher(() => fetchActionTypes(), []); + const ConnectorAddFlyout = useMemo( () => getAddConnectorFlyout({ @@ -39,9 +56,12 @@ export const AddConnectorFlyout = ({ focusInput }: Props) => { setAddFlyoutVisibility(false); focusInput(); }, + actionTypes: (actionTypes ?? []).filter((actionType) => + ALLOWED_ACTION_TYPES.includes(actionType.id as ActionTypeId) + ), }), // eslint-disable-next-line react-hooks/exhaustive-deps - [] + [actionTypes] ); return ( diff --git a/x-pack/plugins/uptime/public/components/settings/alert_defaults_form.tsx b/x-pack/plugins/uptime/public/components/settings/alert_defaults_form.tsx index 9c2bd3e86b460..68e9a8297cf33 100644 --- a/x-pack/plugins/uptime/public/components/settings/alert_defaults_form.tsx +++ b/x-pack/plugins/uptime/public/components/settings/alert_defaults_form.tsx @@ -19,12 +19,13 @@ import { useSelector } from 'react-redux'; import styled from 'styled-components'; import { SettingsFormProps } from '../../pages/settings'; import { connectorsSelector } from '../../state/alerts/alerts'; -import { AddConnectorFlyout } from './add_connector_flyout'; +import { AddConnectorFlyout, ALLOWED_ACTION_TYPES } from './add_connector_flyout'; import { useGetUrlParams, useUrlParams } from '../../hooks'; import { alertFormI18n } from './translations'; import { useInitApp } from '../../hooks/use_init_app'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { TriggersAndActionsUIPublicPluginStart } from '../../../../triggers_actions_ui/public/'; +import { ActionTypeId } from './types'; type ConnectorOption = EuiComboBoxOptionOption; @@ -88,11 +89,13 @@ export const AlertDefaultsForm: React.FC = ({ ); }; - const options = (data ?? []).map((connectorAction) => ({ - value: connectorAction.id, - label: connectorAction.name, - 'data-test-subj': connectorAction.name, - })); + const options = (data ?? []) + .filter((action) => ALLOWED_ACTION_TYPES.includes(action.actionTypeId as ActionTypeId)) + .map((connectorAction) => ({ + value: connectorAction.id, + label: connectorAction.name, + 'data-test-subj': connectorAction.name, + })); const renderOption = (option: ConnectorOption) => { const { label, value } = option; diff --git a/x-pack/plugins/uptime/public/components/settings/types.ts b/x-pack/plugins/uptime/public/components/settings/types.ts new file mode 100644 index 0000000000000..faa1c7e72e47b --- /dev/null +++ b/x-pack/plugins/uptime/public/components/settings/types.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + IndexActionTypeId, + JiraActionTypeId, + PagerDutyActionTypeId, + ServerLogActionTypeId, + ServiceNowActionTypeId, + SlackActionTypeId, + TeamsActionTypeId, + WebhookActionTypeId, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../actions/server/builtin_action_types'; + +export type ActionTypeId = + | typeof SlackActionTypeId + | typeof PagerDutyActionTypeId + | typeof ServerLogActionTypeId + | typeof IndexActionTypeId + | typeof TeamsActionTypeId + | typeof ServiceNowActionTypeId + | typeof JiraActionTypeId + | typeof WebhookActionTypeId; diff --git a/x-pack/plugins/uptime/public/hooks/use_breadcrumbs.ts b/x-pack/plugins/uptime/public/hooks/use_breadcrumbs.ts index 2c2fc88ffa480..3806f20813bf1 100644 --- a/x-pack/plugins/uptime/public/hooks/use_breadcrumbs.ts +++ b/x-pack/plugins/uptime/public/hooks/use_breadcrumbs.ts @@ -38,10 +38,7 @@ function handleBreadcrumbClick( export const makeBaseBreadcrumb = (href: string, params?: UptimeUrlParams): EuiBreadcrumb => { if (params) { const crumbParams: Partial = { ...params }; - // We don't want to encode this values because they are often set to Date.now(), the relative - // values in dateRangeStart are better for a URL. - delete crumbParams.absoluteDateRangeStart; - delete crumbParams.absoluteDateRangeEnd; + delete crumbParams.statusFilter; const query = stringifyUrlParams(crumbParams, true); href += query === EMPTY_QUERY ? '' : query; diff --git a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.test.ts b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.test.ts index 7171aa4828637..86672efc61eb8 100644 --- a/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.test.ts +++ b/x-pack/plugins/uptime/public/lib/alert_types/monitor_status.test.ts @@ -26,9 +26,9 @@ describe('monitor status alert type', () => { "errors": Object { "typeCheckFailure": "Provided parameters do not conform to the expected type.", "typeCheckParsingMessage": Array [ - "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array }, shouldCheckStatus: boolean, isAutoGenerated: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number", - "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array }, shouldCheckStatus: boolean, isAutoGenerated: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/timerangeCount: number", - "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array }, shouldCheckStatus: boolean, isAutoGenerated: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/timerangeUnit: string", + "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array }, shouldCheckStatus: boolean, isAutoGenerated: boolean, shouldCheckAvailability: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number", + "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array }, shouldCheckStatus: boolean, isAutoGenerated: boolean, shouldCheckAvailability: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/timerangeCount: number", + "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array }, shouldCheckStatus: boolean, isAutoGenerated: boolean, shouldCheckAvailability: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/timerangeUnit: string", ], }, } @@ -151,7 +151,7 @@ describe('monitor status alert type', () => { "errors": Object { "typeCheckFailure": "Provided parameters do not conform to the expected type.", "typeCheckParsingMessage": Array [ - "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array }, shouldCheckStatus: boolean, isAutoGenerated: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number", + "Invalid value undefined supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array }, shouldCheckStatus: boolean, isAutoGenerated: boolean, shouldCheckAvailability: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number", ], }, } @@ -164,7 +164,7 @@ describe('monitor status alert type', () => { "errors": Object { "typeCheckFailure": "Provided parameters do not conform to the expected type.", "typeCheckParsingMessage": Array [ - "Invalid value \\"this isn't a number\\" supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array }, shouldCheckStatus: boolean, isAutoGenerated: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number", + "Invalid value \\"this isn't a number\\" supplied to : ({ numTimes: number, timerangeCount: number, timerangeUnit: string } & Partial<{ search: string, filters: { monitor.type: Array, observer.geo.name: Array, tags: Array, url.port: Array }, shouldCheckStatus: boolean, isAutoGenerated: boolean, shouldCheckAvailability: boolean }>)/0: { numTimes: number, timerangeCount: number, timerangeUnit: string }/numTimes: number", ], }, } diff --git a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx index 6c2c4faef63a4..97bd63614141b 100644 --- a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx +++ b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx @@ -11,7 +11,9 @@ import { createMemoryHistory, History } from 'history'; import { CoreStart } from 'kibana/public'; import { I18nProvider } from '@kbn/i18n/react'; import { coreMock } from 'src/core/public/mocks'; +import { configure } from '@testing-library/dom'; import { mockState } from '../__mocks__/uptime_store.mock'; +import { EuiThemeProvider } from '../../../../observability/public'; import { KibanaContextProvider, KibanaServices, @@ -68,7 +70,9 @@ export function MockKibanaProvider({ }; return ( - {children} + + {children} + ); } @@ -88,6 +92,7 @@ export function MockRouter({ ); } +configure({ testIdAttribute: 'data-test-subj' }); /* Custom react testing library render */ export function render( diff --git a/x-pack/plugins/uptime/public/lib/helper/stringify_url_params.test.ts b/x-pack/plugins/uptime/public/lib/helper/stringify_url_params.test.ts index 1b5a6ab7c317b..62d7ed44a2911 100644 --- a/x-pack/plugins/uptime/public/lib/helper/stringify_url_params.test.ts +++ b/x-pack/plugins/uptime/public/lib/helper/stringify_url_params.test.ts @@ -9,6 +9,8 @@ import { stringifyUrlParams } from './stringify_url_params'; describe('stringifyUrlParams', () => { it('creates expected string value', () => { const result = stringifyUrlParams({ + absoluteDateRangeStart: 1000, + absoluteDateRangeEnd: 2000, autorefreshInterval: 50000, autorefreshIsPaused: false, dateRangeStart: 'now-15m', @@ -19,13 +21,15 @@ describe('stringifyUrlParams', () => { statusFilter: 'up', }); expect(result).toMatchInlineSnapshot( - `"?autorefreshInterval=50000&autorefreshIsPaused=false&dateRangeStart=now-15m&dateRangeEnd=now&filters=monitor.id%3A%20bar&focusConnectorField=true&search=monitor.id%3A%20foo&statusFilter=up"` + `"?absoluteDateRangeStart=1000&absoluteDateRangeEnd=2000&autorefreshInterval=50000&autorefreshIsPaused=false&dateRangeStart=now-15m&dateRangeEnd=now&filters=monitor.id%3A%20bar&focusConnectorField=true&search=monitor.id%3A%20foo&statusFilter=up"` ); }); it('creates expected string value when ignore empty is true', () => { const result = stringifyUrlParams( { + absoluteDateRangeStart: 1000, + absoluteDateRangeEnd: 2000, autorefreshInterval: 50000, autorefreshIsPaused: false, dateRangeStart: 'now-15m', diff --git a/x-pack/plugins/uptime/public/lib/helper/stringify_url_params.ts b/x-pack/plugins/uptime/public/lib/helper/stringify_url_params.ts index b10af15961401..2fb970a051801 100644 --- a/x-pack/plugins/uptime/public/lib/helper/stringify_url_params.ts +++ b/x-pack/plugins/uptime/public/lib/helper/stringify_url_params.ts @@ -18,6 +18,11 @@ const { export const stringifyUrlParams = (params: Partial, ignoreEmpty = false) => { if (ignoreEmpty) { + // We don't want to encode this values because they are often set to Date.now(), the relative + // values in dateRangeStart are better for a URL. + delete params.absoluteDateRangeStart; + delete params.absoluteDateRangeEnd; + Object.keys(params).forEach((key: string) => { // @ts-ignore const val = params[key]; diff --git a/x-pack/plugins/uptime/public/state/api/alert_actions.ts b/x-pack/plugins/uptime/public/state/api/alert_actions.ts new file mode 100644 index 0000000000000..0e8781e5937dd --- /dev/null +++ b/x-pack/plugins/uptime/public/state/api/alert_actions.ts @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { NewAlertParams } from './alerts'; +import { AlertAction } from '../../../../triggers_actions_ui/public'; +import { ACTION_GROUP_DEFINITIONS } from '../../../common/constants/alerts'; +import { MonitorStatusTranslations } from '../../../common/translations'; +import { + IndexActionParams, + PagerDutyActionParams, + ServerLogActionParams, + ServiceNowActionParams, + JiraActionParams, + WebhookActionParams, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../actions/server'; +import { ActionTypeId } from '../../components/settings/types'; + +export const SLACK_ACTION_ID: ActionTypeId = '.slack'; +export const PAGER_DUTY_ACTION_ID: ActionTypeId = '.pagerduty'; +export const SERVER_LOG_ACTION_ID: ActionTypeId = '.server-log'; +export const INDEX_ACTION_ID: ActionTypeId = '.index'; +export const TEAMS_ACTION_ID: ActionTypeId = '.teams'; +export const SERVICE_NOW_ACTION_ID: ActionTypeId = '.servicenow'; +export const JIRA_ACTION_ID: ActionTypeId = '.jira'; +export const WEBHOOK_ACTION_ID: ActionTypeId = '.webhook'; + +const { MONITOR_STATUS } = ACTION_GROUP_DEFINITIONS; + +export function populateAlertActions({ defaultActions, monitorId, monitorName }: NewAlertParams) { + const actions: AlertAction[] = []; + defaultActions.forEach((aId) => { + const action: AlertAction = { + id: aId.id, + actionTypeId: aId.actionTypeId, + group: MONITOR_STATUS.id, + params: {}, + }; + switch (aId.actionTypeId) { + case PAGER_DUTY_ACTION_ID: + action.params = getPagerDutyActionParams(monitorId); + break; + case SERVER_LOG_ACTION_ID: + action.params = getServerLogActionParams(); + break; + case INDEX_ACTION_ID: + action.params = getIndexActionParams(); + break; + case SERVICE_NOW_ACTION_ID: + action.params = getServiceNowActionParams(); + break; + case JIRA_ACTION_ID: + action.params = getJiraActionParams(); + break; + case WEBHOOK_ACTION_ID: + action.params = getWebhookActionParams(); + break; + case SLACK_ACTION_ID: + case TEAMS_ACTION_ID: + default: + action.params = { + message: MonitorStatusTranslations.defaultActionMessage, + }; + } + + actions.push(action); + }); + + return actions; +} + +function getIndexActionParams(): IndexActionParams { + return { + documents: [ + { + monitorName: '{{state.monitorName}}', + monitorUrl: '{{{state.monitorUrl}}}', + statusMessage: '{{state.statusMessage}}', + latestErrorMessage: '{{{state.latestErrorMessage}}}', + observerLocation: '{{state.observerLocation}}', + }, + ], + }; +} + +function getServerLogActionParams(): ServerLogActionParams { + return { + level: 'warn', + message: MonitorStatusTranslations.defaultActionMessage, + }; +} + +function getWebhookActionParams(): WebhookActionParams { + return { + body: MonitorStatusTranslations.defaultActionMessage, + }; +} + +function getPagerDutyActionParams(monitorId: string): PagerDutyActionParams { + return { + dedupKey: monitorId + MONITOR_STATUS.id, + eventAction: 'trigger', + severity: 'error', + summary: MonitorStatusTranslations.defaultActionMessage, + }; +} + +function getServiceNowActionParams(): ServiceNowActionParams { + return { + subAction: 'pushToService', + subActionParams: { + incident: { + short_description: MonitorStatusTranslations.defaultActionMessage, + description: MonitorStatusTranslations.defaultActionMessage, + impact: '2', + severity: '2', + urgency: '2', + externalId: null, + }, + comments: [], + }, + }; +} + +function getJiraActionParams(): JiraActionParams { + return { + subAction: 'pushToService', + subActionParams: { + incident: { + summary: MonitorStatusTranslations.defaultActionMessage, + externalId: null, + description: MonitorStatusTranslations.defaultActionMessage, + issueType: null, + priority: '2', + labels: null, + parent: null, + }, + comments: [], + }, + }; +} diff --git a/x-pack/plugins/uptime/public/state/api/alerts.ts b/x-pack/plugins/uptime/public/state/api/alerts.ts index 9d4dd3a1253c3..dd78be5d08ea0 100644 --- a/x-pack/plugins/uptime/public/state/api/alerts.ts +++ b/x-pack/plugins/uptime/public/state/api/alerts.ts @@ -4,17 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ACTION_GROUP_DEFINITIONS, CLIENT_ALERT_TYPES } from '../../../common/constants/alerts'; +import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts'; import { apiService } from './utils'; import { ActionConnector } from '../alerts/alerts'; import { AlertsResult, MonitorIdParam } from '../actions/types'; -import { AlertAction } from '../../../../triggers_actions_ui/public'; +import { ActionType, AlertAction } from '../../../../triggers_actions_ui/public'; import { API_URLS } from '../../../common/constants'; -import { MonitorStatusTranslations } from '../../../common/translations'; import { Alert, AlertTypeParams } from '../../../../alerts/common'; +import { AtomicStatusCheckParams } from '../../../common/runtime_types/alerts'; -const { MONITOR_STATUS } = ACTION_GROUP_DEFINITIONS; +import { populateAlertActions } from './alert_actions'; const UPTIME_AUTO_ALERT = 'UPTIME_AUTO'; @@ -28,24 +28,28 @@ export interface NewAlertParams extends AlertTypeParams { defaultActions: ActionConnector[]; } +type NewMonitorStatusAlert = Omit< + Alert, + | 'id' + | 'createdBy' + | 'updatedBy' + | 'createdAt' + | 'updatedAt' + | 'apiKey' + | 'apiKeyOwner' + | 'muteAll' + | 'mutedInstanceIds' + | 'executionStatus' +>; + export const createAlert = async ({ defaultActions, monitorId, monitorName, }: NewAlertParams): Promise => { - const actions: AlertAction[] = []; - defaultActions.forEach((aId) => { - actions.push({ - id: aId.id, - actionTypeId: aId.actionTypeId, - group: MONITOR_STATUS.id, - params: { - message: MonitorStatusTranslations.defaultActionMessage, - }, - }); - }); + const actions: AlertAction[] = populateAlertActions({ defaultActions, monitorId, monitorName }); - const data = { + const data: NewMonitorStatusAlert = { actions, params: { numTimes: 1, @@ -60,8 +64,11 @@ export const createAlert = async ({ consumer: 'uptime', alertTypeId: CLIENT_ALERT_TYPES.MONITOR_STATUS, schedule: { interval: '1m' }, + notifyWhen: 'onActionGroupChange', tags: [UPTIME_AUTO_ALERT], name: `${monitorName} (Simple status alert)`, + enabled: true, + throttle: null, }; return await apiService.post(API_URLS.CREATE_ALERT, data); @@ -99,3 +106,7 @@ export const fetchAlertRecords = async ({ export const disableAlertById = async ({ alertId }: { alertId: string }) => { return await apiService.delete(API_URLS.ALERT + alertId); }; + +export const fetchActionTypes = async (): Promise => { + return await apiService.get(API_URLS.ACTION_TYPES); +}; diff --git a/x-pack/plugins/vis_type_timeseries_enhanced/tsconfig.json b/x-pack/plugins/vis_type_timeseries_enhanced/tsconfig.json new file mode 100644 index 0000000000000..c5ec5571917bd --- /dev/null +++ b/x-pack/plugins/vis_type_timeseries_enhanced/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": ["*.ts", "server/**/*"], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/vis_type_timeseries/tsconfig.json" } + ] +} diff --git a/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.test.js b/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.test.js index 8de025d300d55..805722db54310 100644 --- a/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.test.js +++ b/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/fetch_all_from_scroll.test.js @@ -4,84 +4,86 @@ * you may not use this file except in compliance with the Elastic License. */ +import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks'; import { fetchAllFromScroll } from './fetch_all_from_scroll'; -import { set } from '@elastic/safer-lodash-set'; describe('fetch_all_from_scroll', () => { - let mockResponse; - let stubCallWithRequest; + let mockScopedClusterClient; beforeEach(() => { - mockResponse = {}; + mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient(); - stubCallWithRequest = jest.fn(); - - // TODO: That mocking needs to be migrated to jest - // stubCallWithRequest.onCall(0).returns( - // new Promise((resolve) => { - // const mockInnerResponse = { - // hits: { - // hits: ['newhit'], - // }, - // _scroll_id: 'newScrollId', - // }; - // return resolve(mockInnerResponse); - // }) - // ); - // - // stubCallWithRequest.onCall(1).returns( - // new Promise((resolve) => { - // const mockInnerResponse = { - // hits: { - // hits: [], - // }, - // }; - // return resolve(mockInnerResponse); - // }) - // ); + elasticsearchServiceMock + .createLegacyClusterClient() + .asScoped.mockReturnValue(mockScopedClusterClient); }); describe('#fetchAllFromScroll', () => { describe('when the passed-in response has no hits', () => { - beforeEach(() => { - set(mockResponse, 'hits.hits', []); - }); + const mockSearchResults = { + hits: { + hits: [], + }, + }; it('should return an empty array of hits', () => { - return fetchAllFromScroll(mockResponse).then((hits) => { + return fetchAllFromScroll(mockSearchResults).then((hits) => { expect(hits).toEqual([]); }); }); it('should not call callWithRequest', () => { - return fetchAllFromScroll(mockResponse, stubCallWithRequest).then(() => { - expect(stubCallWithRequest).not.toHaveBeenCalled(); + return fetchAllFromScroll(mockSearchResults, mockScopedClusterClient).then(() => { + expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled(); }); }); }); - // TODO: tests were not running and are not up to date - describe.skip('when the passed-in response has some hits', () => { + describe('when the passed-in response has some hits', () => { + const mockInitialSearchResults = { + hits: { + hits: ['foo', 'bar'], + }, + _scroll_id: 'originalScrollId', + }; + beforeEach(() => { - set(mockResponse, 'hits.hits', ['foo', 'bar']); - set(mockResponse, '_scroll_id', 'originalScrollId'); + const mockResponse1 = { + hits: { + hits: ['newHit'], + }, + _scroll_id: 'newScrollId', + }; + + const mockResponse2 = { + hits: { + hits: [], + }, + }; + + mockScopedClusterClient.callAsCurrentUser + .mockReturnValueOnce(Promise.resolve(mockResponse1)) + .mockReturnValueOnce(Promise.resolve(mockResponse2)); }); it('should return the hits from the response', () => { - return fetchAllFromScroll(mockResponse, stubCallWithRequest).then((hits) => { - expect(hits).toEqual(['foo', 'bar', 'newhit']); - }); + return fetchAllFromScroll(mockInitialSearchResults, mockScopedClusterClient).then( + (hits) => { + expect(hits).toEqual(['foo', 'bar', 'newHit']); + } + ); }); it('should call callWithRequest', () => { - return fetchAllFromScroll(mockResponse, stubCallWithRequest).then(() => { - expect(stubCallWithRequest.calledTwice).toBe(true); - - const firstCallWithRequestCallArgs = stubCallWithRequest.args[0]; - expect(firstCallWithRequestCallArgs[1].body.scroll_id).toEqual('originalScrollId'); + return fetchAllFromScroll(mockInitialSearchResults, mockScopedClusterClient).then(() => { + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(2); - const secondCallWithRequestCallArgs = stubCallWithRequest.args[1]; - expect(secondCallWithRequestCallArgs[1].body.scroll_id).toEqual('newScrollId'); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenNthCalledWith(1, 'scroll', { + body: { scroll: '30s', scroll_id: 'originalScrollId' }, + }); + expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenNthCalledWith(2, 'scroll', { + body: { scroll: '30s', scroll_id: 'newScrollId' }, + }); }); }); }); diff --git a/x-pack/plugins/watcher/server/models/watch_status/watch_status.js b/x-pack/plugins/watcher/server/models/watch_status/watch_status.js index e5dd7d8ef186c..55aecc9d6f221 100644 --- a/x-pack/plugins/watcher/server/models/watch_status/watch_status.js +++ b/x-pack/plugins/watcher/server/models/watch_status/watch_status.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, map, forEach, max } from 'lodash'; +import { get, map, forEach, maxBy } from 'lodash'; import { badRequest } from '@hapi/boom'; import { getMoment } from '../../../common/lib/get_moment'; import { ActionStatus } from '../action_status'; @@ -119,7 +119,7 @@ export class WatchStatus { } get lastFired() { - const actionStatus = max(this.actionStatuses, 'lastExecution'); + const actionStatus = maxBy(this.actionStatuses, 'lastExecution'); if (actionStatus) { return actionStatus.lastExecution; } diff --git a/x-pack/plugins/watcher/server/models/watch_status/watch_status.test.js b/x-pack/plugins/watcher/server/models/watch_status/watch_status.test.js index 949d37f56d7ec..fca34085048a0 100644 --- a/x-pack/plugins/watcher/server/models/watch_status/watch_status.test.js +++ b/x-pack/plugins/watcher/server/models/watch_status/watch_status.test.js @@ -60,8 +60,7 @@ describe('watch_status', () => { }); }); - // TODO: the test was not running before and is not up to date - describe.skip('lastFired getter method', () => { + describe('lastFired getter method', () => { let upstreamJson; beforeEach(() => { upstreamJson = { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts index 992d9210b9761..2b25c82cc92e5 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts @@ -62,7 +62,7 @@ export default function alertTests({ getService }: FtrProviderContext) { }); }); - it.skip('should schedule actions on legacy alerts', async () => { + it('should schedule actions on legacy alerts', async () => { const reference = `alert:migrated-to-7.10:${user.username}`; const migratedAlertId = MIGRATED_ALERT_ID[user.username]; diff --git a/x-pack/test/api_integration/apis/management/index_lifecycle_management/lib/elasticsearch.js b/x-pack/test/api_integration/apis/management/index_lifecycle_management/lib/elasticsearch.js index 358e54d8738f6..cf43ebf01b610 100644 --- a/x-pack/test/api_integration/apis/management/index_lifecycle_management/lib/elasticsearch.js +++ b/x-pack/test/api_integration/apis/management/index_lifecycle_management/lib/elasticsearch.js @@ -13,6 +13,8 @@ import { getRandomString } from './random'; export const initElasticsearchHelpers = (es) => { let indicesCreated = []; let templatesCreated = []; + let composableTemplatesCreated = []; + let dataStreamsCreated = []; // Indices const getIndex = (index) => es.indices.get({ index }).then(({ body }) => body); @@ -30,6 +32,20 @@ export const initElasticsearchHelpers = (es) => { const deleteAllIndices = () => Promise.all(indicesCreated.map(deleteIndex)).then(() => (indicesCreated = [])); + // Data streams + const createDataStream = (dataStream = getRandomString(), document) => { + dataStreamsCreated.push(dataStream); + return es.index({ index: dataStream, body: document }); + }; + + const deleteDataStream = (dataStream) => { + dataStreamsCreated = dataStreamsCreated.filter((i) => i !== dataStream); + return es.indices.deleteDataStream({ name: dataStream }); + }; + + const deleteAllDataStreams = () => + Promise.all(dataStreamsCreated.map(deleteDataStream)).then(() => (dataStreamsCreated = [])); + // Index templates const getIndexTemplates = () => es.indices.getTemplate(); @@ -39,6 +55,11 @@ export const initElasticsearchHelpers = (es) => { return es.indices.putTemplate({ name, body: template }, { create: true }); }; + const createComposableIndexTemplate = (name, template) => { + composableTemplatesCreated.push(name); + return es.indices.putIndexTemplate({ name, body: template }, { create: true }); + }; + const deleteIndexTemplate = (name) => { templatesCreated = templatesCreated.filter((i) => i !== name); return es.indices.deleteTemplate({ name }).catch((err) => { @@ -49,22 +70,45 @@ export const initElasticsearchHelpers = (es) => { }); }; + const deleteComposableIndexTemplate = (name) => { + composableTemplatesCreated = composableTemplatesCreated.filter((i) => i !== name); + return es.indices.deleteIndexTemplate({ name }).catch((err) => { + // Silently fail if templates not found + if (err.statusCode !== 404) { + throw err; + } + }); + }; + const deleteAllTemplates = () => Promise.all(templatesCreated.map(deleteIndexTemplate)).then(() => (templatesCreated = [])); - const cleanUp = () => Promise.all([deleteAllIndices(), deleteAllTemplates()]); + const deleteAllComposableTemplates = () => + Promise.all(templatesCreated.map(deleteComposableIndexTemplate)).then( + () => (composableTemplatesCreated = []) + ); + + const cleanUp = () => + Promise.all([ + deleteAllIndices(), + deleteAllTemplates(), + deleteAllComposableTemplates(), + deleteAllDataStreams(), + ]); const getNodesStats = () => es.nodes.stats().then(({ body }) => body); return { getIndex, createIndex, + createDataStream, deleteIndex, deleteAllIndices, deleteAllTemplates, getIndexTemplates, createIndexTemplate, deleteIndexTemplate, + createComposableIndexTemplate, getNodesStats, cleanUp, }; diff --git a/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.js b/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.js index 1589baabb1ded..52979ed208bd6 100644 --- a/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.js +++ b/x-pack/test/api_integration/apis/management/index_lifecycle_management/policies.js @@ -17,7 +17,12 @@ export default function ({ getService }) { const es = getService('es'); - const { createIndex, cleanUp: cleanUpEsResources } = initElasticsearchHelpers(es); + const { + createIndex, + createComposableIndexTemplate, + createDataStream, + cleanUp: cleanUpEsResources, + } = initElasticsearchHelpers(es); const { loadPolicies, @@ -74,6 +79,34 @@ export default function ({ getService }) { const fetchedPolicy = body.find((p) => p.name === policyName); expect(fetchedPolicy.linkedIndices).to.eql([indexName]); }); + + it('should add hidden indices linked to policies', async () => { + // Create a policy + const policy = getPolicyPayload('hidden-index-link-test-policy'); + const { name: policyName } = policy; + await createPolicy(policy); + + // Create hidden data stream + await createComposableIndexTemplate('my_template', { + template: {}, + index_patterns: ['hidden*'], + data_stream: { + hidden: true, + }, + }); + + const indexName = 'hidden_index'; + await createDataStream(indexName, { + '@timestamp': '2020-01-27', + }); + + await addPolicyToIndex(policyName, indexName); + + const { body } = await loadPolicies(true); + const fetchedPolicy = body.find((p) => p.name === policyName); + // The index name is dynamically generated as .ds--XXX so we don't check for exact match + expect(fetchedPolicy.linkedIndices[0]).to.contain(indexName); + }); }); describe('create', () => { diff --git a/x-pack/test/api_integration/apis/ml/jobs/jobs_exist_spaces.ts b/x-pack/test/api_integration/apis/ml/jobs/jobs_exist_spaces.ts index 0eb8f4abebf93..ca483ffa7bc1e 100644 --- a/x-pack/test/api_integration/apis/ml/jobs/jobs_exist_spaces.ts +++ b/x-pack/test/api_integration/apis/ml/jobs/jobs_exist_spaces.ts @@ -17,6 +17,7 @@ export default ({ getService }: FtrProviderContext) => { const jobIdSpace1 = 'fq_single_space1'; const jobIdSpace2 = 'fq_single_space2'; + const groupSpace1 = 'farequote'; const idSpace1 = 'space1'; const idSpace2 = 'space2'; @@ -57,17 +58,25 @@ export default ({ getService }: FtrProviderContext) => { it('should find single job from same space', async () => { const body = await runRequest(idSpace1, 200, [jobIdSpace1]); - expect(body).to.eql({ [jobIdSpace1]: true }); + expect(body).to.eql({ [jobIdSpace1]: { exists: true, isGroup: false } }); + }); + + it('should find single job from same space', async () => { + const body = await runRequest(idSpace1, 200, [groupSpace1]); + expect(body).to.eql({ [groupSpace1]: { exists: true, isGroup: true } }); }); it('should not find single job from different space', async () => { const body = await runRequest(idSpace2, 200, [jobIdSpace1]); - expect(body).to.eql({ [jobIdSpace1]: false }); + expect(body).to.eql({ [jobIdSpace1]: { exists: false, isGroup: false } }); }); it('should only find job from same space when called with a list of jobs', async () => { const body = await runRequest(idSpace1, 200, [jobIdSpace1, jobIdSpace2]); - expect(body).to.eql({ [jobIdSpace1]: true, [jobIdSpace2]: false }); + expect(body).to.eql({ + [jobIdSpace1]: { exists: true, isGroup: false }, + [jobIdSpace2]: { exists: false, isGroup: false }, + }); }); }); }; diff --git a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts index a04c2fef92329..aec9a896afc7c 100644 --- a/x-pack/test/api_integration/apis/security_solution/timeline_details.ts +++ b/x-pack/test/api_integration/apis/security_solution/timeline_details.ts @@ -40,7 +40,7 @@ const EXPECTED_DATA = [ category: 'traefik', field: 'traefik.access.geoip.location', values: ['{"long":-122.3341,"lat":47.6103}'], - originalValue: ['[{"coordinates":[-122.3341,47.6103],"type":"Point"}]'], + originalValue: ['{"coordinates":[-122.3341,47.6103],"type":"Point"}'], }, { category: 'suricata', @@ -318,7 +318,7 @@ const EXPECTED_DATA = [ category: 'source', field: 'source.geo.location', values: ['{"long":-122.3341,"lat":47.6103}'], - originalValue: ['[{"coordinates":[-122.3341,47.6103],"type":"Point"}]'], + originalValue: ['{"coordinates":[-122.3341,47.6103],"type":"Point"}'], }, { category: 'source', diff --git a/x-pack/test/api_integration/apis/transform/transforms_create.ts b/x-pack/test/api_integration/apis/transform/transforms_create.ts index 4b2821f19e03e..2d13eb190b280 100644 --- a/x-pack/test/api_integration/apis/transform/transforms_create.ts +++ b/x-pack/test/api_integration/apis/transform/transforms_create.ts @@ -25,7 +25,7 @@ export default ({ getService }: FtrProviderContext) => { await transform.api.cleanTransformIndices(); }); - it('should not allow pivot and latest configs is same transform', async () => { + it('should not allow pivot and latest configs in same transform', async () => { const transformId = 'test_transform_id'; const { body } = await supertest diff --git a/x-pack/test/api_integration/config.ts b/x-pack/test/api_integration/config.ts index 12105dbfd74f1..546b23ab4f26c 100644 --- a/x-pack/test/api_integration/config.ts +++ b/x-pack/test/api_integration/config.ts @@ -30,7 +30,7 @@ export async function getApiIntegrationConfig({ readConfigFile }: FtrConfigProvi '--telemetry.optIn=true', '--xpack.fleet.enabled=true', '--xpack.fleet.agents.pollingRequestTimeout=5000', // 5 seconds - '--xpack.data_enhanced.search.sendToBackground.enabled=true', // enable WIP send to background UI + '--xpack.data_enhanced.search.sessions.enabled=true', // enable WIP send to background UI ], }, esTestCluster: { diff --git a/x-pack/test/apm_api_integration/basic/tests/index.ts b/x-pack/test/apm_api_integration/basic/tests/index.ts index 645d4078f9332..4e66c6e6f76c3 100644 --- a/x-pack/test/apm_api_integration/basic/tests/index.ts +++ b/x-pack/test/apm_api_integration/basic/tests/index.ts @@ -68,9 +68,5 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont describe('Metrics', function () { loadTestFile(require.resolve('./metrics_charts/metrics_charts')); }); - - describe('Correlations', function () { - loadTestFile(require.resolve('./correlations/slow_transactions')); - }); }); } diff --git a/x-pack/test/apm_api_integration/basic/tests/service_overview/instances.ts b/x-pack/test/apm_api_integration/basic/tests/service_overview/instances.ts index 084555387a690..04fe1d501c860 100644 --- a/x-pack/test/apm_api_integration/basic/tests/service_overview/instances.ts +++ b/x-pack/test/apm_api_integration/basic/tests/service_overview/instances.ts @@ -125,7 +125,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { "value": 0.941324615478516, }, "throughput": Object { - "value": 75, + "value": 2.5, }, } `); @@ -201,7 +201,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { "value": 70518.9328358209, }, "throughput": Object { - "value": 134, + "value": 4.46666666666667, }, } `); diff --git a/x-pack/test/apm_api_integration/basic/tests/transactions/transactions_groups_overview.ts b/x-pack/test/apm_api_integration/basic/tests/transactions/transactions_groups_overview.ts index 1eb9afd11c16c..a95b5143c9ed6 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transactions/transactions_groups_overview.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transactions/transactions_groups_overview.ts @@ -111,7 +111,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, "name": "DispatcherServlet#doGet", "throughput": Object { - "value": 16, + "value": 0.533333333333333, }, } `); diff --git a/x-pack/test/apm_api_integration/basic/tests/correlations/slow_transactions.ts b/x-pack/test/apm_api_integration/trial/tests/correlations/slow_transactions.ts similarity index 100% rename from x-pack/test/apm_api_integration/basic/tests/correlations/slow_transactions.ts rename to x-pack/test/apm_api_integration/trial/tests/correlations/slow_transactions.ts diff --git a/x-pack/test/apm_api_integration/trial/tests/index.ts b/x-pack/test/apm_api_integration/trial/tests/index.ts index 262b0f2b0daaf..c8ee858d9ceb0 100644 --- a/x-pack/test/apm_api_integration/trial/tests/index.ts +++ b/x-pack/test/apm_api_integration/trial/tests/index.ts @@ -42,5 +42,9 @@ export default function observabilityApiIntegrationTests({ loadTestFile }: FtrPr loadTestFile(require.resolve('./csm/has_rum_data.ts')); loadTestFile(require.resolve('./csm/page_load_dist.ts')); }); + + describe('Correlations', function () { + loadTestFile(require.resolve('./correlations/slow_transactions')); + }); }); } diff --git a/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap b/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap index 55e8407fbb8aa..e4f87e3e49ffe 100644 --- a/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap +++ b/x-pack/test/apm_api_integration/trial/tests/service_maps/__snapshots__/service_maps.snap @@ -14,6 +14,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -38,6 +39,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, @@ -113,6 +115,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -139,6 +142,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, @@ -225,6 +229,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, @@ -244,6 +249,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -271,6 +277,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -298,6 +305,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -325,6 +333,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -351,6 +360,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -377,6 +387,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, @@ -404,6 +415,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, @@ -431,6 +443,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, @@ -457,6 +470,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, @@ -471,6 +485,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -507,9 +522,11 @@ Array [ "id": "opbeans-python", "service.name": "opbeans-python", "serviceAnomalyStats": Object { + "actualValue": 24282.2352941176, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -534,6 +551,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-node", "transactionType": "request", }, }, @@ -558,6 +576,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-rum", "transactionType": "page-load", }, }, @@ -573,6 +592,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "opbeans-java", "transactionType": "request", }, }, @@ -588,6 +608,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "kibana-frontend", "transactionType": "page-load", }, }, @@ -603,6 +624,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "kibana", "transactionType": "request", }, }, @@ -627,6 +649,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-go", "transactionType": "request", }, }, @@ -637,9 +660,11 @@ Array [ "id": "opbeans-ruby", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { + "actualValue": 121348.868852459, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, @@ -712,6 +737,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "kibana-frontend", "transactionType": "page-load", }, }, @@ -739,6 +765,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "kibana-frontend", "transactionType": "page-load", }, }, @@ -753,6 +780,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "kibana", "transactionType": "request", }, }, @@ -772,6 +800,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "kibana", "transactionType": "request", }, }, @@ -799,6 +828,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-go", "transactionType": "request", }, }, @@ -827,6 +857,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-go", "transactionType": "request", }, }, @@ -841,6 +872,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-node", "transactionType": "request", }, }, @@ -860,6 +892,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-go", "transactionType": "request", }, }, @@ -869,9 +902,11 @@ Array [ "id": "opbeans-python", "service.name": "opbeans-python", "serviceAnomalyStats": Object { + "actualValue": 24282.2352941176, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -892,6 +927,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-go", "transactionType": "request", }, }, @@ -901,9 +937,11 @@ Array [ "id": "opbeans-ruby", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { + "actualValue": 121348.868852459, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, @@ -923,6 +961,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "opbeans-java", "transactionType": "request", }, }, @@ -950,6 +989,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "opbeans-java", "transactionType": "request", }, }, @@ -964,6 +1004,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-go", "transactionType": "request", }, }, @@ -984,6 +1025,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "opbeans-java", "transactionType": "request", }, }, @@ -993,9 +1035,11 @@ Array [ "id": "opbeans-python", "service.name": "opbeans-python", "serviceAnomalyStats": Object { + "actualValue": 24282.2352941176, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -1016,6 +1060,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "opbeans-java", "transactionType": "request", }, }, @@ -1025,9 +1070,11 @@ Array [ "id": "opbeans-ruby", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { + "actualValue": 121348.868852459, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, @@ -1047,6 +1094,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-node", "transactionType": "request", }, }, @@ -1074,6 +1122,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-node", "transactionType": "request", }, }, @@ -1102,6 +1151,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-node", "transactionType": "request", }, }, @@ -1116,6 +1166,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-go", "transactionType": "request", }, }, @@ -1135,6 +1186,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-node", "transactionType": "request", }, }, @@ -1149,6 +1201,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "opbeans-java", "transactionType": "request", }, }, @@ -1169,6 +1222,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-node", "transactionType": "request", }, }, @@ -1178,9 +1232,11 @@ Array [ "id": "opbeans-python", "service.name": "opbeans-python", "serviceAnomalyStats": Object { + "actualValue": 24282.2352941176, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -1201,6 +1257,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-node", "transactionType": "request", }, }, @@ -1210,9 +1267,11 @@ Array [ "id": "opbeans-ruby", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { + "actualValue": 121348.868852459, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, @@ -1227,9 +1286,11 @@ Array [ "id": "opbeans-python", "service.name": "opbeans-python", "serviceAnomalyStats": Object { + "actualValue": 24282.2352941176, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -1252,9 +1313,11 @@ Array [ "id": "opbeans-python", "service.name": "opbeans-python", "serviceAnomalyStats": Object { + "actualValue": 24282.2352941176, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -1277,9 +1340,11 @@ Array [ "id": "opbeans-python", "service.name": "opbeans-python", "serviceAnomalyStats": Object { + "actualValue": 24282.2352941176, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -1303,9 +1368,11 @@ Array [ "id": "opbeans-python", "service.name": "opbeans-python", "serviceAnomalyStats": Object { + "actualValue": 24282.2352941176, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -1320,6 +1387,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "opbeans-java", "transactionType": "request", }, }, @@ -1335,9 +1403,11 @@ Array [ "id": "opbeans-python", "service.name": "opbeans-python", "serviceAnomalyStats": Object { + "actualValue": 24282.2352941176, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -1352,6 +1422,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-node", "transactionType": "request", }, }, @@ -1366,9 +1437,11 @@ Array [ "id": "opbeans-ruby", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { + "actualValue": 121348.868852459, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, @@ -1392,9 +1465,11 @@ Array [ "id": "opbeans-ruby", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { + "actualValue": 121348.868852459, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, @@ -1409,6 +1484,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-go", "transactionType": "request", }, }, @@ -1424,9 +1500,11 @@ Array [ "id": "opbeans-ruby", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { + "actualValue": 121348.868852459, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, @@ -1441,6 +1519,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "opbeans-java", "transactionType": "request", }, }, @@ -1456,9 +1535,11 @@ Array [ "id": "opbeans-ruby", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { + "actualValue": 121348.868852459, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, @@ -1473,6 +1554,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-node", "transactionType": "request", }, }, @@ -1487,9 +1569,11 @@ Array [ "id": "opbeans-ruby", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { + "actualValue": 121348.868852459, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, @@ -1499,9 +1583,11 @@ Array [ "id": "opbeans-python", "service.name": "opbeans-python", "serviceAnomalyStats": Object { + "actualValue": 24282.2352941176, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -1521,6 +1607,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-rum", "transactionType": "page-load", }, }, @@ -1535,6 +1622,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "opbeans-java", "transactionType": "request", }, }, @@ -1554,6 +1642,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-rum", "transactionType": "page-load", }, }, @@ -1568,6 +1657,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-node", "transactionType": "request", }, }, @@ -1587,6 +1677,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-rum", "transactionType": "page-load", }, }, @@ -1596,9 +1687,11 @@ Array [ "id": "opbeans-python", "service.name": "opbeans-python", "serviceAnomalyStats": Object { + "actualValue": 24282.2352941176, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -1618,6 +1711,7 @@ Array [ "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-rum", "transactionType": "page-load", }, }, @@ -1627,9 +1721,11 @@ Array [ "id": "opbeans-ruby", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { + "actualValue": 121348.868852459, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, @@ -1654,9 +1750,11 @@ Object { "id": "opbeans-python", "service.name": "opbeans-python", "serviceAnomalyStats": Object { + "actualValue": 24282.2352941176, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -1681,6 +1779,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-node", "transactionType": "request", }, }, @@ -1705,6 +1804,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-rum", "transactionType": "page-load", }, }, @@ -1720,6 +1820,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "opbeans-java", "transactionType": "request", }, }, @@ -1735,6 +1836,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "kibana-frontend", "transactionType": "page-load", }, }, @@ -1750,6 +1852,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "kibana", "transactionType": "request", }, }, @@ -1774,6 +1877,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-go", "transactionType": "request", }, }, @@ -1784,9 +1888,11 @@ Object { "id": "opbeans-ruby", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { + "actualValue": 121348.868852459, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, @@ -1859,6 +1965,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "kibana-frontend", "transactionType": "page-load", }, }, @@ -1886,6 +1993,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "kibana-frontend", "transactionType": "page-load", }, }, @@ -1900,6 +2008,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "kibana", "transactionType": "request", }, }, @@ -1919,6 +2028,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "kibana", "transactionType": "request", }, }, @@ -1946,6 +2056,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-go", "transactionType": "request", }, }, @@ -1974,6 +2085,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-go", "transactionType": "request", }, }, @@ -1988,6 +2100,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-node", "transactionType": "request", }, }, @@ -2007,6 +2120,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-go", "transactionType": "request", }, }, @@ -2016,9 +2130,11 @@ Object { "id": "opbeans-python", "service.name": "opbeans-python", "serviceAnomalyStats": Object { + "actualValue": 24282.2352941176, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -2039,6 +2155,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-go", "transactionType": "request", }, }, @@ -2048,9 +2165,11 @@ Object { "id": "opbeans-ruby", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { + "actualValue": 121348.868852459, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, @@ -2070,6 +2189,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "opbeans-java", "transactionType": "request", }, }, @@ -2097,6 +2217,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "opbeans-java", "transactionType": "request", }, }, @@ -2111,6 +2232,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-go", "transactionType": "request", }, }, @@ -2131,6 +2253,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "opbeans-java", "transactionType": "request", }, }, @@ -2140,9 +2263,11 @@ Object { "id": "opbeans-python", "service.name": "opbeans-python", "serviceAnomalyStats": Object { + "actualValue": 24282.2352941176, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -2163,6 +2288,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "opbeans-java", "transactionType": "request", }, }, @@ -2172,9 +2298,11 @@ Object { "id": "opbeans-ruby", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { + "actualValue": 121348.868852459, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, @@ -2194,6 +2322,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-node", "transactionType": "request", }, }, @@ -2221,6 +2350,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-node", "transactionType": "request", }, }, @@ -2249,6 +2379,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-node", "transactionType": "request", }, }, @@ -2263,6 +2394,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-go", "transactionType": "request", }, }, @@ -2282,6 +2414,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-node", "transactionType": "request", }, }, @@ -2296,6 +2429,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "opbeans-java", "transactionType": "request", }, }, @@ -2316,6 +2450,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-node", "transactionType": "request", }, }, @@ -2325,9 +2460,11 @@ Object { "id": "opbeans-python", "service.name": "opbeans-python", "serviceAnomalyStats": Object { + "actualValue": 24282.2352941176, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -2348,6 +2485,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-node", "transactionType": "request", }, }, @@ -2357,9 +2495,11 @@ Object { "id": "opbeans-ruby", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { + "actualValue": 121348.868852459, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, @@ -2374,9 +2514,11 @@ Object { "id": "opbeans-python", "service.name": "opbeans-python", "serviceAnomalyStats": Object { + "actualValue": 24282.2352941176, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -2399,9 +2541,11 @@ Object { "id": "opbeans-python", "service.name": "opbeans-python", "serviceAnomalyStats": Object { + "actualValue": 24282.2352941176, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -2424,9 +2568,11 @@ Object { "id": "opbeans-python", "service.name": "opbeans-python", "serviceAnomalyStats": Object { + "actualValue": 24282.2352941176, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -2450,9 +2596,11 @@ Object { "id": "opbeans-python", "service.name": "opbeans-python", "serviceAnomalyStats": Object { + "actualValue": 24282.2352941176, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -2467,6 +2615,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "opbeans-java", "transactionType": "request", }, }, @@ -2482,9 +2631,11 @@ Object { "id": "opbeans-python", "service.name": "opbeans-python", "serviceAnomalyStats": Object { + "actualValue": 24282.2352941176, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -2499,6 +2650,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-node", "transactionType": "request", }, }, @@ -2513,9 +2665,11 @@ Object { "id": "opbeans-ruby", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { + "actualValue": 121348.868852459, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, @@ -2539,9 +2693,11 @@ Object { "id": "opbeans-ruby", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { + "actualValue": 121348.868852459, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, @@ -2556,6 +2712,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-go", "transactionType": "request", }, }, @@ -2571,9 +2728,11 @@ Object { "id": "opbeans-ruby", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { + "actualValue": 121348.868852459, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, @@ -2588,6 +2747,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "opbeans-java", "transactionType": "request", }, }, @@ -2603,9 +2763,11 @@ Object { "id": "opbeans-ruby", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { + "actualValue": 121348.868852459, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, @@ -2620,6 +2782,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-node", "transactionType": "request", }, }, @@ -2634,9 +2797,11 @@ Object { "id": "opbeans-ruby", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { + "actualValue": 121348.868852459, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, @@ -2646,9 +2811,11 @@ Object { "id": "opbeans-python", "service.name": "opbeans-python", "serviceAnomalyStats": Object { + "actualValue": 24282.2352941176, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -2668,6 +2835,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-rum", "transactionType": "page-load", }, }, @@ -2682,6 +2850,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-production-1369-high_mean_transaction_duration", + "serviceName": "opbeans-java", "transactionType": "request", }, }, @@ -2701,6 +2870,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-rum", "transactionType": "page-load", }, }, @@ -2715,6 +2885,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-node", "transactionType": "request", }, }, @@ -2734,6 +2905,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-rum", "transactionType": "page-load", }, }, @@ -2743,9 +2915,11 @@ Object { "id": "opbeans-python", "service.name": "opbeans-python", "serviceAnomalyStats": Object { + "actualValue": 24282.2352941176, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -2765,6 +2939,7 @@ Object { "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-rum", "transactionType": "page-load", }, }, @@ -2774,9 +2949,11 @@ Object { "id": "opbeans-ruby", "service.name": "opbeans-ruby", "serviceAnomalyStats": Object { + "actualValue": 121348.868852459, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-ruby", "transactionType": "request", }, }, diff --git a/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts b/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts index 37629d1492608..4dd376f8d7786 100644 --- a/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts +++ b/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts @@ -167,9 +167,11 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) "id": "opbeans-python", "service.name": "opbeans-python", "serviceAnomalyStats": Object { + "actualValue": 24282.2352941176, "anomalyScore": 0, "healthStatus": "healthy", - "jobId": "apm-production-1369-high_mean_transaction_duration", + "jobId": "apm-environment_not_defined-5626-high_mean_transaction_duration", + "serviceName": "opbeans-python", "transactionType": "request", }, }, @@ -185,6 +187,7 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-node", "transactionType": "request", }, }, @@ -200,6 +203,7 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) "anomalyScore": 0, "healthStatus": "healthy", "jobId": "apm-testing-384f-high_mean_transaction_duration", + "serviceName": "opbeans-rum", "transactionType": "page-load", }, }, diff --git a/x-pack/test/apm_api_integration/trial/tests/transactions/__snapshots__/latency.snap b/x-pack/test/apm_api_integration/trial/tests/transactions/__snapshots__/latency.snap index 1b7e2fbbc5a30..99d4026dcdb2c 100644 --- a/x-pack/test/apm_api_integration/trial/tests/transactions/__snapshots__/latency.snap +++ b/x-pack/test/apm_api_integration/trial/tests/transactions/__snapshots__/latency.snap @@ -12,11 +12,6 @@ Array [ "y": 0, "y0": 0, }, - Object { - "x": 1607437650000, - "y": 0, - "y0": 0, - }, ] `; @@ -32,11 +27,6 @@ Array [ "y": 1660982.24115757, "y0": 5732.00699123528, }, - Object { - "x": 1607437650000, - "y": 1660982.24115757, - "y0": 5732.00699123528, - }, ] `; @@ -52,10 +42,5 @@ Array [ "y": 1660982.24115757, "y0": 5732.00699123528, }, - Object { - "x": 1607437650000, - "y": 1660982.24115757, - "y0": 5732.00699123528, - }, ] `; diff --git a/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts index 4c45504f3fd0a..5b12ba4bf613f 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts @@ -35,7 +35,9 @@ export default ({ getService }: FtrProviderContext): void => { const esArchiver = getService('esArchiver'); const es = getService('es'); - describe('patch_cases', () => { + // Failing: See https://github.com/elastic/kibana/issues/88130 + // FLAKY: https://github.com/elastic/kibana/issues/87988 + describe.skip('patch_cases', () => { afterEach(async () => { await deleteCases(es); await deleteCasesUserActions(es); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts index a754966cf18a9..774bfb2863dc6 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/finalize_signals_migrations.ts @@ -47,7 +47,8 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - describe('Finalizing signals migrations', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/88302 + describe.skip('Finalizing signals migrations', () => { let legacySignalsIndexName: string; let outdatedSignalsIndexName: string; let createdMigrations: CreateResponse[]; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts index ee787f1b616e3..1497b67011fe9 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/open_close_signals.ts @@ -166,7 +166,7 @@ export default ({ getService }: FtrProviderContext) => { expect(everySignalClosed).to.eql(true); }); - it('should NOT be able to close signals with t1 analyst user', async () => { + it('should be able to close signals with t1 analyst user', async () => { const rule = getRuleForSignalTesting(['auditbeat-*']); const { id } = await createRule(supertest, rule); await waitForRuleSuccessOrStatus(supertest, id); @@ -182,7 +182,7 @@ export default ({ getService }: FtrProviderContext) => { .set('kbn-xsrf', 'true') .auth(ROLES.t1_analyst, 'changeme') .send(setSignalStatus({ signalIds, status: 'closed' })) - .expect(403); + .expect(200); // query for the signals with the superuser // to allow a check that the signals were NOT closed with t1 analyst @@ -199,7 +199,7 @@ export default ({ getService }: FtrProviderContext) => { _source: { signal: { status }, }, - }) => status === 'open' + }) => status === 'closed' ); expect(everySignalOpen).to.eql(true); }); diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts index 1d5f864c27eea..a5d347703cd92 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts @@ -388,6 +388,11 @@ const expectAssetsInstalled = ({ id: 'sample_search', }); expect(resSearch.id).equal('sample_search'); + const resLens = await kibanaServer.savedObjects.get({ + type: 'lens', + id: 'sample_lens', + }); + expect(resLens.id).equal('sample_lens'); const resIndexPattern = await kibanaServer.savedObjects.get({ type: 'index-pattern', id: 'test-*', @@ -449,6 +454,10 @@ const expectAssetsInstalled = ({ id: 'test-*', type: 'index-pattern', }, + { + id: 'sample_lens', + type: 'lens', + }, { id: 'sample_search', type: 'search', @@ -515,6 +524,7 @@ const expectAssetsInstalled = ({ { id: '60d6d054-57e4-590f-a580-52bf3f5e7cca', type: 'epm-packages-assets' }, { id: '47758dc2-979d-5fbe-a2bd-9eded68a5a43', type: 'epm-packages-assets' }, { id: '318959c9-997b-5a14-b328-9fc7355b4b74', type: 'epm-packages-assets' }, + { id: 'e21b59b5-eb76-5ab0-bef2-1c8e379e6197', type: 'epm-packages-assets' }, { id: 'e786cbd9-0f3b-5a0b-82a6-db25145ebf58', type: 'epm-packages-assets' }, { id: '53c94591-aa33-591d-8200-cd524c2a0561', type: 'epm-packages-assets' }, { id: 'b658d2d4-752e-54b8-afc2-4c76155c1466', type: 'epm-packages-assets' }, diff --git a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts index 7b264f949532e..6dd10240b247b 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts @@ -291,6 +291,10 @@ export default function (providerContext: FtrProviderContext) { id: 'sample_search2', type: 'search', }, + { + id: 'sample_lens', + type: 'lens', + }, ], installed_es: [ { @@ -338,6 +342,7 @@ export default function (providerContext: FtrProviderContext) { { id: '5c3aa147-089c-5084-beca-53c00e72ac80', type: 'epm-packages-assets' }, { id: '48e582df-b1d2-5f88-b6ea-ba1fafd3a569', type: 'epm-packages-assets' }, { id: 'bf3b0b65-9fdc-53c6-a9ca-e76140e56490', type: 'epm-packages-assets' }, + { id: '7f4c5aca-b4f5-5f0a-95af-051da37513fc', type: 'epm-packages-assets' }, { id: '2e56f08b-1d06-55ed-abee-4708e1ccf0aa', type: 'epm-packages-assets' }, { id: 'c7bf1a39-e057-58a0-afde-fb4b48751d8c', type: 'epm-packages-assets' }, { id: '8c665f28-a439-5f43-b5fd-8fda7b576735', type: 'epm-packages-assets' }, diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/lens/sample_lens.json b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/lens/sample_lens.json new file mode 100644 index 0000000000000..0eefa2790b060 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/lens/sample_lens.json @@ -0,0 +1,114 @@ +{ + "type": "lens", + "id": "sample_lens", + "attributes": { + "title": "sample-lens", + "description": "", + "visualizationType": "lnsXY", + "state": { + "datasourceStates": { + "indexpattern": { + "layers": { + "91b37ed1-ec9f-401d-8ba5-990c2fc65cd0": { + "columns": { + "2c9a6ea8-f3c1-4178-b6af-9e1a5811888a": { + "label": "Top values of HostDetails.agent.name", + "dataType": "string", + "operationType": "terms", + "scale": "ordinal", + "sourceField": "HostDetails.agent.name", + "isBucketed": true, + "params": { + "size": 5, + "orderBy": { + "type": "column", + "columnId": "64b26756-2616-4835-bf77-e0e3807cf827" + }, + "orderDirection": "desc", + "otherBucket": true, + "missingBucket": false + } + }, + "64b26756-2616-4835-bf77-e0e3807cf827": { + "label": "Count of records", + "dataType": "number", + "operationType": "count", + "isBucketed": false, + "scale": "ratio", + "sourceField": "Records" + } + }, + "columnOrder": [ + "2c9a6ea8-f3c1-4178-b6af-9e1a5811888a", + "64b26756-2616-4835-bf77-e0e3807cf827" + ], + "incompleteColumns": {} + } + } + } + }, + "visualization": { + "legend": { + "isVisible": true, + "position": "right" + }, + "valueLabels": "hide", + "fittingFunction": "None", + "axisTitlesVisibilitySettings": { + "x": true, + "yLeft": true, + "yRight": true + }, + "tickLabelsVisibilitySettings": { + "x": true, + "yLeft": true, + "yRight": true + }, + "gridlinesVisibilitySettings": { + "x": true, + "yLeft": true, + "yRight": true + }, + "preferredSeriesType": "bar_stacked", + "layers": [ + { + "layerId": "91b37ed1-ec9f-401d-8ba5-990c2fc65cd0", + "accessors": [ + "64b26756-2616-4835-bf77-e0e3807cf827" + ], + "position": "top", + "seriesType": "bar_stacked", + "showGridlines": false, + "xAccessor": "2c9a6ea8-f3c1-4178-b6af-9e1a5811888a" + } + ] + }, + "query": { + "query": "", + "language": "kuery" + }, + "filters": [] + } + }, + "references": [ + { + "type": "index-pattern", + "id": "metrics-*", + "name": "indexpattern-datasource-current-indexpattern" + }, + { + "type": "index-pattern", + "id": "metrics-*", + "name": "indexpattern-datasource-layer-91b37ed1-ec9f-401d-8ba5-990c2fc65cd0" + } + ], + "migrationVersion": { + "lens": "7.11.0" + }, + "updated_at": "2021-01-13T15:16:10.075Z", + "version": "WzI5NCwxXQ==", + "namespaces": [ + "default" + ], + "score": 0 +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/lens/sample_lens.json b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/lens/sample_lens.json new file mode 100644 index 0000000000000..0eefa2790b060 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/lens/sample_lens.json @@ -0,0 +1,114 @@ +{ + "type": "lens", + "id": "sample_lens", + "attributes": { + "title": "sample-lens", + "description": "", + "visualizationType": "lnsXY", + "state": { + "datasourceStates": { + "indexpattern": { + "layers": { + "91b37ed1-ec9f-401d-8ba5-990c2fc65cd0": { + "columns": { + "2c9a6ea8-f3c1-4178-b6af-9e1a5811888a": { + "label": "Top values of HostDetails.agent.name", + "dataType": "string", + "operationType": "terms", + "scale": "ordinal", + "sourceField": "HostDetails.agent.name", + "isBucketed": true, + "params": { + "size": 5, + "orderBy": { + "type": "column", + "columnId": "64b26756-2616-4835-bf77-e0e3807cf827" + }, + "orderDirection": "desc", + "otherBucket": true, + "missingBucket": false + } + }, + "64b26756-2616-4835-bf77-e0e3807cf827": { + "label": "Count of records", + "dataType": "number", + "operationType": "count", + "isBucketed": false, + "scale": "ratio", + "sourceField": "Records" + } + }, + "columnOrder": [ + "2c9a6ea8-f3c1-4178-b6af-9e1a5811888a", + "64b26756-2616-4835-bf77-e0e3807cf827" + ], + "incompleteColumns": {} + } + } + } + }, + "visualization": { + "legend": { + "isVisible": true, + "position": "right" + }, + "valueLabels": "hide", + "fittingFunction": "None", + "axisTitlesVisibilitySettings": { + "x": true, + "yLeft": true, + "yRight": true + }, + "tickLabelsVisibilitySettings": { + "x": true, + "yLeft": true, + "yRight": true + }, + "gridlinesVisibilitySettings": { + "x": true, + "yLeft": true, + "yRight": true + }, + "preferredSeriesType": "bar_stacked", + "layers": [ + { + "layerId": "91b37ed1-ec9f-401d-8ba5-990c2fc65cd0", + "accessors": [ + "64b26756-2616-4835-bf77-e0e3807cf827" + ], + "position": "top", + "seriesType": "bar_stacked", + "showGridlines": false, + "xAccessor": "2c9a6ea8-f3c1-4178-b6af-9e1a5811888a" + } + ] + }, + "query": { + "query": "", + "language": "kuery" + }, + "filters": [] + } + }, + "references": [ + { + "type": "index-pattern", + "id": "metrics-*", + "name": "indexpattern-datasource-current-indexpattern" + }, + { + "type": "index-pattern", + "id": "metrics-*", + "name": "indexpattern-datasource-layer-91b37ed1-ec9f-401d-8ba5-990c2fc65cd0" + } + ], + "migrationVersion": { + "lens": "7.11.0" + }, + "updated_at": "2021-01-13T15:16:10.075Z", + "version": "WzI5NCwxXQ==", + "namespaces": [ + "default" + ], + "score": 0 +} \ No newline at end of file diff --git a/x-pack/test/functional/apps/maps/embeddable/dashboard.js b/x-pack/test/functional/apps/maps/embeddable/dashboard.js index c5c02135ea976..3cca4c6562e29 100644 --- a/x-pack/test/functional/apps/maps/embeddable/dashboard.js +++ b/x-pack/test/functional/apps/maps/embeddable/dashboard.js @@ -55,6 +55,11 @@ export default function ({ getPageObjects, getService }) { return requestTimestamp; } + it('should set "data-title" attribute', async () => { + const [{ title }] = await PageObjects.dashboard.getPanelSharedItemData(); + expect(title).to.be('join example'); + }); + it('should pass index patterns to container', async () => { const indexPatterns = await filterBar.getIndexPatterns(); expect(indexPatterns).to.equal('meta_for_geo_shapes*,logstash-*'); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts index d3de41689244b..3ac85fbeff36e 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts @@ -343,6 +343,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('job deletion confirms the delete modal'); await ml.jobTable.confirmDeleteJobModal(); + await ml.api.waitForAnomalyDetectionJobNotToExist(jobIdClone, 30 * 1000); await ml.testExecution.logTestStep( 'job deletion does not display the deleted job in the job list any more' diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts index dd6310b67d844..e20b5e8b41bdf 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts @@ -326,6 +326,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('job deletion confirms the delete modal'); await ml.jobTable.confirmDeleteJobModal(); + await ml.api.waitForAnomalyDetectionJobNotToExist(jobIdClone, 30 * 1000); await ml.testExecution.logTestStep( 'job deletion does not display the deleted job in the job list any more' diff --git a/x-pack/test/functional/apps/security/doc_level_security_roles.js b/x-pack/test/functional/apps/security/doc_level_security_roles.js index 72f463be48fd5..0595322ad2d21 100644 --- a/x-pack/test/functional/apps/security/doc_level_security_roles.js +++ b/x-pack/test/functional/apps/security/doc_level_security_roles.js @@ -77,7 +77,7 @@ export default function ({ getService, getPageObjects }) { }); const rowData = await PageObjects.discover.getDocTableIndex(1); expect(rowData).to.be( - 'name:ABC Company region:EAST _id:doc1 _type: - _index:dlstest _score:0' + '_id:doc1 _type: - _index:dlstest _score:0 region.keyword:EAST name:ABC Company name.keyword:ABC Company region:EAST' ); }); after('logout', async () => { diff --git a/x-pack/test/functional/apps/security/field_level_security.js b/x-pack/test/functional/apps/security/field_level_security.js index 7b22d72885c9d..3f3984dd05a94 100644 --- a/x-pack/test/functional/apps/security/field_level_security.js +++ b/x-pack/test/functional/apps/security/field_level_security.js @@ -112,7 +112,7 @@ export default function ({ getService, getPageObjects }) { }); const rowData = await PageObjects.discover.getDocTableIndex(1); expect(rowData).to.be( - 'customer_ssn:444.555.6666 customer_name:ABC Company customer_region:WEST _id:2 _type: - _index:flstest _score:0' + '_id:2 _type: - _index:flstest _score:0 customer_name.keyword:ABC Company customer_ssn:444.555.6666 customer_region.keyword:WEST runtime_customer_ssn:444.555.6666 calculated at runtime customer_region:WEST customer_name:ABC Company customer_ssn.keyword:444.555.6666' ); }); @@ -126,7 +126,7 @@ export default function ({ getService, getPageObjects }) { }); const rowData = await PageObjects.discover.getDocTableIndex(1); expect(rowData).to.be( - 'customer_name:ABC Company customer_region:WEST _id:2 _type: - _index:flstest _score:0' + '_id:2 _type: - _index:flstest _score:0 customer_name.keyword:ABC Company customer_region.keyword:WEST customer_region:WEST customer_name:ABC Company' ); }); diff --git a/x-pack/test/functional/apps/transform/cloning.ts b/x-pack/test/functional/apps/transform/cloning.ts index 421eab656f606..aae4eae35f836 100644 --- a/x-pack/test/functional/apps/transform/cloning.ts +++ b/x-pack/test/functional/apps/transform/cloning.ts @@ -5,12 +5,26 @@ */ import { FtrProviderContext } from '../../ftr_provider_context'; -import { TransformPivotConfig } from '../../../../plugins/transform/common/types/transform'; +import { + isLatestTransform, + isPivotTransform, + TransformPivotConfig, +} from '../../../../plugins/transform/common/types/transform'; + +interface TestData { + type: 'pivot' | 'latest'; + suiteTitle: string; + originalConfig: any; + transformId: string; + transformDescription: string; + destinationIndex: string; + expected: any; +} function getTransformConfig(): TransformPivotConfig { const date = Date.now(); return { - id: `ec_cloning_${date}`, + id: `ec_cloning_1_${date}`, source: { index: ['ft_ecommerce'] }, pivot: { group_by: { category: { terms: { field: 'category.keyword' } } }, @@ -31,27 +45,39 @@ export default function ({ getService }: FtrProviderContext) { const transform = getService('transform'); describe('cloning', function () { - const transformConfig = getTransformConfig(); + const transformConfigWithPivot = getTransformConfig(); + // const transformConfigWithLatest = getLatestTransformConfig(); before(async () => { await esArchiver.loadIfNeeded('ml/ecommerce'); await transform.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); - await transform.api.createAndRunTransform(transformConfig.id, transformConfig); + await transform.api.createAndRunTransform( + transformConfigWithPivot.id, + transformConfigWithPivot + ); + // await transform.api.createAndRunTransform( + // transformConfigWithLatest.id, + // transformConfigWithLatest + // ); await transform.testResources.setKibanaTimeZoneToUTC(); await transform.securityUI.loginAsTransformPowerUser(); }); after(async () => { - await transform.testResources.deleteIndexPatternByTitle(transformConfig.dest.index); - await transform.api.deleteIndices(transformConfig.dest.index); + await transform.testResources.deleteIndexPatternByTitle(transformConfigWithPivot.dest.index); + // await transform.testResources.deleteIndexPatternByTitle(transformConfigWithLatest.dest.index); + await transform.api.deleteIndices(transformConfigWithPivot.dest.index); + // await transform.api.deleteIndices(transformConfigWithLatest.dest.index); await transform.api.cleanTransformIndices(); }); - const testDataList = [ + const testDataList: TestData[] = [ { + type: 'pivot' as const, suiteTitle: 'clone transform', - transformId: `clone_${transformConfig.id}`, + originalConfig: transformConfigWithPivot, + transformId: `clone_${transformConfigWithPivot.id}`, transformDescription: `a cloned transform`, get destinationIndex(): string { return `user-${this.transformId}`; @@ -69,7 +95,7 @@ export default function ({ getService }: FtrProviderContext) { index: 0, label: 'category', }, - pivotPreview: { + transformPreview: { column: 0, values: [ `Men's Accessories`, @@ -81,6 +107,33 @@ export default function ({ getService }: FtrProviderContext) { }, }, }, + // TODO enable tests when https://github.com/elastic/elasticsearch/issues/67148 is resolved + // { + // type: 'latest' as const, + // suiteTitle: 'clone transform with latest function', + // originalConfig: transformConfigWithLatest, + // transformId: `clone_${transformConfigWithLatest.id}`, + // transformDescription: `a cloned transform`, + // get destinationIndex(): string { + // return `user-${this.transformId}`; + // }, + // expected: { + // indexPreview: { + // columns: 10, + // rows: 5, + // }, + // transformPreview: { + // column: 0, + // values: [ + // 'July 12th 2019, 22:16:19', + // 'July 12th 2019, 22:50:53', + // 'July 12th 2019, 23:06:43', + // 'July 12th 2019, 23:15:22', + // 'July 12th 2019, 23:31:12', + // ], + // }, + // }, + // }, ]; for (const testData of testDataList) { @@ -102,13 +155,14 @@ export default function ({ getService }: FtrProviderContext) { 'should display the original transform in the transform list' ); await transform.table.refreshTransformList(); - await transform.table.filterWithSearchString(transformConfig.id, 1); + await transform.table.filterWithSearchString(testData.originalConfig.id, 1); await transform.testExecution.logTestStep('should show the actions popover'); await transform.table.assertTransformRowActions(false); await transform.testExecution.logTestStep('should display the define pivot step'); await transform.table.clickTransformRowAction('Clone'); + await transform.wizard.assertSelectedTransformFunction(testData.type); await transform.wizard.assertDefineStepActive(); }); @@ -126,27 +180,37 @@ export default function ({ getService }: FtrProviderContext) { await transform.wizard.assertQueryInputExists(); await transform.wizard.assertQueryValue(''); - await transform.testExecution.logTestStep( - 'should show the pre-filled group-by configuration' - ); - await transform.wizard.assertGroupByEntryExists( - testData.expected.groupBy.index, - testData.expected.groupBy.label - ); - - await transform.testExecution.logTestStep( - 'should show the pre-filled aggs configuration' - ); - await transform.wizard.assertAggregationEntryExists( - testData.expected.aggs.index, - testData.expected.aggs.label - ); + // assert define step form + if (isPivotTransform(testData.originalConfig)) { + await transform.testExecution.logTestStep( + 'should show the pre-filled group-by configuration' + ); + await transform.wizard.assertGroupByEntryExists( + testData.expected.groupBy.index, + testData.expected.groupBy.label + ); + + await transform.testExecution.logTestStep( + 'should show the pre-filled aggs configuration' + ); + await transform.wizard.assertAggregationEntryExists( + testData.expected.aggs.index, + testData.expected.aggs.label + ); + } else if (isLatestTransform(testData.originalConfig)) { + await transform.testExecution.logTestStep('should show pre-filler unique keys'); + await transform.wizard.assertUniqueKeysInputValue( + testData.originalConfig.latest.unique_key + ); + await transform.testExecution.logTestStep('should show pre-filler sort field'); + await transform.wizard.assertSortFieldInputValue(testData.originalConfig.latest.sort); + } await transform.testExecution.logTestStep('should show the pivot preview'); await transform.wizard.assertPivotPreviewChartHistogramButtonMissing(); await transform.wizard.assertPivotPreviewColumnValues( - testData.expected.pivotPreview.column, - testData.expected.pivotPreview.values + testData.expected.transformPreview.column, + testData.expected.transformPreview.values ); await transform.testExecution.logTestStep('should load the details step'); @@ -159,7 +223,9 @@ export default function ({ getService }: FtrProviderContext) { await transform.testExecution.logTestStep('should input the transform description'); await transform.wizard.assertTransformDescriptionInputExists(); - await transform.wizard.assertTransformDescriptionValue(transformConfig.description!); + await transform.wizard.assertTransformDescriptionValue( + testData.originalConfig.description! + ); await transform.wizard.setTransformDescription(testData.transformDescription); await transform.testExecution.logTestStep('should input the destination index'); @@ -181,9 +247,9 @@ export default function ({ getService }: FtrProviderContext) { 'should display the advanced settings and show pre-filled configuration' ); await transform.wizard.openTransformAdvancedSettingsAccordion(); - await transform.wizard.assertTransformFrequencyValue(transformConfig.frequency!); + await transform.wizard.assertTransformFrequencyValue(testData.originalConfig.frequency!); await transform.wizard.assertTransformMaxPageSearchSizeValue( - transformConfig.settings!.max_page_search_size! + testData.originalConfig.settings!.max_page_search_size! ); await transform.testExecution.logTestStep('should load the create step'); diff --git a/x-pack/test/functional/apps/transform/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation_index_pattern.ts index 13213679a6117..e3e2c75bf673a 100644 --- a/x-pack/test/functional/apps/transform/creation_index_pattern.ts +++ b/x-pack/test/functional/apps/transform/creation_index_pattern.ts @@ -7,12 +7,13 @@ import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; import { FtrProviderContext } from '../../ftr_provider_context'; - -interface GroupByEntry { - identifier: string; - label: string; - intervalLabel?: string; -} +import { + GroupByEntry, + isLatestTransformTestData, + isPivotTransformTestData, + LatestTransformTestData, + PivotTransformTestData, +} from './index'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -31,8 +32,9 @@ export default function ({ getService }: FtrProviderContext) { await transform.api.cleanTransformIndices(); }); - const testDataList = [ + const testDataList: Array = [ { + type: 'pivot', suiteTitle: 'batch transform with terms+date_histogram groups and avg agg', source: 'ft_ecommerce', groupByEntries: [ @@ -138,7 +140,7 @@ export default function ({ getService }: FtrProviderContext) { }, }, }, - pivotPreview: { + transformPreview: { column: 0, values: [`Men's Accessories`], }, @@ -168,8 +170,9 @@ export default function ({ getService }: FtrProviderContext) { { chartAvailable: true, id: 'day_of_week', legend: '7 categories' }, ], }, - }, + } as PivotTransformTestData, { + type: 'pivot', suiteTitle: 'batch transform with terms group and percentiles agg', source: 'ft_ecommerce', groupByEntries: [ @@ -236,7 +239,7 @@ export default function ({ getService }: FtrProviderContext) { }, }, }, - pivotPreview: { + transformPreview: { column: 0, values: ['AE', 'CO', 'EG', 'FR', 'GB'], }, @@ -251,7 +254,55 @@ export default function ({ getService }: FtrProviderContext) { }, histogramCharts: [], }, - }, + } as PivotTransformTestData, + { + type: 'latest', + suiteTitle: 'batch transform with the latest function', + source: 'ft_ecommerce', + uniqueKeys: [ + { + identifier: 'geoip.country_iso_code', + label: 'geoip.country_iso_code', + }, + ], + sortField: { + identifier: 'order_date', + label: 'order_date', + }, + transformId: `ec_3_${Date.now()}`, + + transformDescription: + 'ecommerce batch transform with the latest function config, sort by order_data, country code as unique key', + get destinationIndex(): string { + return `user-${this.transformId}`; + }, + expected: { + latestPreview: { + column: 0, + values: [], + }, + row: { + status: TRANSFORM_STATE.STOPPED, + mode: 'batch', + progress: '100', + }, + indexPreview: { + columns: 10, + rows: 5, + }, + histogramCharts: [], + transformPreview: { + column: 0, + values: [ + 'July 12th 2019, 22:16:19', + 'July 12th 2019, 22:50:53', + 'July 12th 2019, 23:06:43', + 'July 12th 2019, 23:15:22', + 'July 12th 2019, 23:31:12', + ], + }, + }, + } as LatestTransformTestData, ]; for (const testData of testDataList) { @@ -277,9 +328,12 @@ export default function ({ getService }: FtrProviderContext) { }); it('navigates through the wizard and sets all needed fields', async () => { - await transform.testExecution.logTestStep('displays the define pivot step'); + await transform.testExecution.logTestStep('displays the define step'); await transform.wizard.assertDefineStepActive(); + await transform.testExecution.logTestStep('has correct transform function selected'); + await transform.wizard.assertSelectedTransformFunction('pivot'); + await transform.testExecution.logTestStep('loads the index preview'); await transform.wizard.assertIndexPreviewLoaded(); @@ -289,8 +343,8 @@ export default function ({ getService }: FtrProviderContext) { testData.expected.indexPreview.rows ); - await transform.testExecution.logTestStep('displays an empty pivot preview'); - await transform.wizard.assertPivotPreviewEmpty(); + await transform.testExecution.logTestStep('displays an empty transform preview'); + await transform.wizard.assertTransformPreviewEmpty(); await transform.testExecution.logTestStep('displays the query input'); await transform.wizard.assertQueryInputExists(); @@ -308,39 +362,59 @@ export default function ({ getService }: FtrProviderContext) { testData.expected.histogramCharts ); - await transform.testExecution.logTestStep('adds the group by entries'); - for (const [index, entry] of testData.groupByEntries.entries()) { - await transform.wizard.assertGroupByInputExists(); - await transform.wizard.assertGroupByInputValue([]); - await transform.wizard.addGroupByEntry( - index, - entry.identifier, - entry.label, - entry.intervalLabel + if (isPivotTransformTestData(testData)) { + await transform.testExecution.logTestStep('adds the group by entries'); + for (const [index, entry] of testData.groupByEntries.entries()) { + await transform.wizard.assertGroupByInputExists(); + await transform.wizard.assertGroupByInputValue([]); + await transform.wizard.addGroupByEntry( + index, + entry.identifier, + entry.label, + entry.intervalLabel + ); + } + + await transform.testExecution.logTestStep('adds the aggregation entries'); + await transform.wizard.addAggregationEntries(testData.aggregationEntries); + + await transform.testExecution.logTestStep('displays the advanced pivot editor switch'); + await transform.wizard.assertAdvancedPivotEditorSwitchExists(); + await transform.wizard.assertAdvancedPivotEditorSwitchCheckState(false); + + await transform.testExecution.logTestStep('displays the advanced configuration'); + await transform.wizard.enableAdvancedPivotEditor(); + await transform.wizard.assertAdvancedPivotEditorContent( + testData.expected.pivotAdvancedEditorValueArr ); } - await transform.testExecution.logTestStep('adds the aggregation entries'); - await transform.wizard.addAggregationEntries(testData.aggregationEntries); - - await transform.testExecution.logTestStep('displays the advanced pivot editor switch'); - await transform.wizard.assertAdvancedPivotEditorSwitchExists(); - await transform.wizard.assertAdvancedPivotEditorSwitchCheckState(false); - - await transform.testExecution.logTestStep('displays the advanced configuration'); - await transform.wizard.enabledAdvancedPivotEditor(); - await transform.wizard.assertAdvancedPivotEditorContent( - testData.expected.pivotAdvancedEditorValueArr - ); + if (isLatestTransformTestData(testData)) { + await transform.testExecution.logTestStep('sets latest transform method'); + await transform.wizard.selectTransformFunction('latest'); + await transform.testExecution.logTestStep('adds unique keys'); + for (const { identifier, label } of testData.uniqueKeys) { + await transform.wizard.assertUniqueKeysInputExists(); + await transform.wizard.assertUniqueKeysInputValue([]); + await transform.wizard.addUniqueKeyEntry(identifier, label); + } + await transform.testExecution.logTestStep('sets the sort field'); + await transform.wizard.assertSortFieldInputExists(); + await transform.wizard.assertSortFieldInputValue(''); + await transform.wizard.setSortFieldValue( + testData.sortField.identifier, + testData.sortField.label + ); + } - await transform.testExecution.logTestStep('loads the pivot preview'); + await transform.testExecution.logTestStep('loads the transform preview'); await transform.wizard.assertPivotPreviewLoaded(); - await transform.testExecution.logTestStep('shows the pivot preview'); + await transform.testExecution.logTestStep('shows the transform preview'); await transform.wizard.assertPivotPreviewChartHistogramButtonMissing(); await transform.wizard.assertPivotPreviewColumnValues( - testData.expected.pivotPreview.column, - testData.expected.pivotPreview.values + testData.expected.transformPreview.column, + testData.expected.transformPreview.values ); await transform.testExecution.logTestStep('loads the details step'); diff --git a/x-pack/test/functional/apps/transform/creation_saved_search.ts b/x-pack/test/functional/apps/transform/creation_saved_search.ts index 20d276c2e017b..70a3f2a733f54 100644 --- a/x-pack/test/functional/apps/transform/creation_saved_search.ts +++ b/x-pack/test/functional/apps/transform/creation_saved_search.ts @@ -7,12 +7,13 @@ import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; import { FtrProviderContext } from '../../ftr_provider_context'; - -interface GroupByEntry { - identifier: string; - label: string; - intervalLabel?: string; -} +import { + GroupByEntry, + isLatestTransformTestData, + isPivotTransformTestData, + LatestTransformTestData, + PivotTransformTestData, +} from './index'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -32,8 +33,9 @@ export default function ({ getService }: FtrProviderContext) { await transform.api.cleanTransformIndices(); }); - const testDataList = [ + const testDataList: Array = [ { + type: 'pivot', suiteTitle: 'batch transform with terms groups and avg agg with saved search filter', source: 'ft_farequote_filter', groupByEntries: [ @@ -55,7 +57,7 @@ export default function ({ getService }: FtrProviderContext) { return `user-${this.transformId}`; }, expected: { - pivotPreview: { + transformPreview: { column: 0, values: ['ASA'], }, @@ -70,7 +72,44 @@ export default function ({ getService }: FtrProviderContext) { values: ['ASA'], }, }, - }, + } as PivotTransformTestData, + { + type: 'latest', + suiteTitle: 'batch transform with unique term and sort by time with saved search filter', + source: 'ft_farequote_filter', + uniqueKeys: [ + { + identifier: 'airline', + label: 'airline', + }, + ], + sortField: { + identifier: '@timestamp', + label: '@timestamp', + }, + transformId: `fq_2_${Date.now()}`, + transformDescription: + 'farequote batch transform with airline unique key and sort by timestamp with saved search filter', + get destinationIndex(): string { + return `user-latest-${this.transformId}`; + }, + expected: { + transformPreview: { + column: 0, + values: ['February 11th 2016, 23:59:54'], + }, + row: { + status: TRANSFORM_STATE.STOPPED, + mode: 'batch', + progress: '100', + }, + sourceIndex: 'ft_farequote', + indexPreview: { + column: 2, + values: ['ASA'], + }, + }, + } as LatestTransformTestData, ]; for (const testData of testDataList) { @@ -99,6 +138,9 @@ export default function ({ getService }: FtrProviderContext) { await transform.testExecution.logTestStep('displays the define pivot step'); await transform.wizard.assertDefineStepActive(); + await transform.testExecution.logTestStep('has correct transform function selected'); + await transform.wizard.assertSelectedTransformFunction('pivot'); + await transform.testExecution.logTestStep('loads the index preview'); await transform.wizard.assertIndexPreviewLoaded(); @@ -108,8 +150,8 @@ export default function ({ getService }: FtrProviderContext) { testData.expected.indexPreview.values ); - await transform.testExecution.logTestStep('displays an empty pivot preview'); - await transform.wizard.assertPivotPreviewEmpty(); + await transform.testExecution.logTestStep('displays an empty transform preview'); + await transform.wizard.assertTransformPreviewEmpty(); await transform.testExecution.logTestStep('hides the query input'); await transform.wizard.assertQueryInputMissing(); @@ -117,36 +159,55 @@ export default function ({ getService }: FtrProviderContext) { await transform.testExecution.logTestStep('hides the advanced query editor switch'); await transform.wizard.assertAdvancedQueryEditorSwitchMissing(); - await transform.testExecution.logTestStep('adds the group by entries'); - for (const [index, entry] of testData.groupByEntries.entries()) { - await transform.wizard.assertGroupByInputExists(); - await transform.wizard.assertGroupByInputValue([]); - await transform.wizard.addGroupByEntry( - index, - entry.identifier, - entry.label, - entry.intervalLabel - ); + if (isPivotTransformTestData(testData)) { + await transform.testExecution.logTestStep('adds the group by entries'); + for (const [index, entry] of testData.groupByEntries.entries()) { + await transform.wizard.assertGroupByInputExists(); + await transform.wizard.assertGroupByInputValue([]); + await transform.wizard.addGroupByEntry( + index, + entry.identifier, + entry.label, + entry.intervalLabel + ); + } + await transform.testExecution.logTestStep('adds the aggregation entries'); + for (const [index, agg] of testData.aggregationEntries.entries()) { + await transform.wizard.assertAggregationInputExists(); + await transform.wizard.assertAggregationInputValue([]); + await transform.wizard.addAggregationEntry(index, agg.identifier, agg.label); + } + + await transform.testExecution.logTestStep('displays the advanced pivot editor switch'); + await transform.wizard.assertAdvancedPivotEditorSwitchExists(); + await transform.wizard.assertAdvancedPivotEditorSwitchCheckState(false); } - await transform.testExecution.logTestStep('adds the aggregation entries'); - for (const [index, agg] of testData.aggregationEntries.entries()) { - await transform.wizard.assertAggregationInputExists(); - await transform.wizard.assertAggregationInputValue([]); - await transform.wizard.addAggregationEntry(index, agg.identifier, agg.label); + if (isLatestTransformTestData(testData)) { + await transform.testExecution.logTestStep('sets latest transform method'); + await transform.wizard.selectTransformFunction('latest'); + await transform.testExecution.logTestStep('adds unique keys'); + for (const { identifier, label } of testData.uniqueKeys) { + await transform.wizard.assertUniqueKeysInputExists(); + await transform.wizard.assertUniqueKeysInputValue([]); + await transform.wizard.addUniqueKeyEntry(identifier, label); + } + await transform.testExecution.logTestStep('sets the sort field'); + await transform.wizard.assertSortFieldInputExists(); + await transform.wizard.assertSortFieldInputValue(''); + await transform.wizard.setSortFieldValue( + testData.sortField.identifier, + testData.sortField.label + ); } - await transform.testExecution.logTestStep('displays the advanced pivot editor switch'); - await transform.wizard.assertAdvancedPivotEditorSwitchExists(); - await transform.wizard.assertAdvancedPivotEditorSwitchCheckState(false); - await transform.testExecution.logTestStep('loads the pivot preview'); await transform.wizard.assertPivotPreviewLoaded(); await transform.testExecution.logTestStep('shows the pivot preview'); await transform.wizard.assertPivotPreviewColumnValues( - testData.expected.pivotPreview.column, - testData.expected.pivotPreview.values + testData.expected.transformPreview.column, + testData.expected.transformPreview.values ); await transform.testExecution.logTestStep('loads the details step'); @@ -231,8 +292,8 @@ export default function ({ getService }: FtrProviderContext) { 'displays the transform preview in the expanded row' ); await transform.table.assertTransformsExpandedRowPreviewColumnValues( - testData.expected.pivotPreview.column, - testData.expected.pivotPreview.values + testData.expected.transformPreview.column, + testData.expected.transformPreview.values ); }); }); diff --git a/x-pack/test/functional/apps/transform/editing.ts b/x-pack/test/functional/apps/transform/editing.ts index 498678e7ba4b5..fa778dae1794e 100644 --- a/x-pack/test/functional/apps/transform/editing.ts +++ b/x-pack/test/functional/apps/transform/editing.ts @@ -29,122 +29,154 @@ export default function ({ getService }: FtrProviderContext) { const transform = getService('transform'); describe('editing', function () { - const transformConfig = getTransformConfig(); + const transformConfigWithPivot = getTransformConfig(); + // const transformConfigWithLatest = getLatestTransformConfig(); before(async () => { await esArchiver.loadIfNeeded('ml/ecommerce'); await transform.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); - await transform.api.createAndRunTransform(transformConfig.id, transformConfig); - await transform.testResources.setKibanaTimeZoneToUTC(); + await transform.api.createAndRunTransform( + transformConfigWithPivot.id, + transformConfigWithPivot + ); + // await transform.api.createAndRunTransform( + // transformConfigWithLatest.id, + // transformConfigWithLatest + // ); + + await transform.testResources.setKibanaTimeZoneToUTC(); await transform.securityUI.loginAsTransformPowerUser(); }); after(async () => { - await transform.testResources.deleteIndexPatternByTitle(transformConfig.dest.index); - await transform.api.deleteIndices(transformConfig.dest.index); + await transform.testResources.deleteIndexPatternByTitle(transformConfigWithPivot.dest.index); + await transform.api.deleteIndices(transformConfigWithPivot.dest.index); + // await transform.testResources.deleteIndexPatternByTitle(transformConfigWithLatest.dest.index); + // await transform.api.deleteIndices(transformConfigWithLatest.dest.index); await transform.api.cleanTransformIndices(); }); - const testData = { - suiteTitle: 'edit transform', - transformDescription: 'updated description', - transformDocsPerSecond: '1000', - transformFrequency: '10m', - expected: { - messageText: 'updated transform.', - row: { - status: TRANSFORM_STATE.STOPPED, - mode: 'batch', - progress: '100', + const testDataList = [ + { + suiteTitle: 'edit transform with pivot configuration', + originalConfig: transformConfigWithPivot, + transformDescription: 'updated description', + transformDocsPerSecond: '1000', + transformFrequency: '10m', + expected: { + messageText: 'updated transform.', + row: { + status: TRANSFORM_STATE.STOPPED, + mode: 'batch', + progress: '100', + }, }, }, - }; - - describe(`${testData.suiteTitle}`, function () { - it('opens the edit flyout for an existing transform', async () => { - await transform.testExecution.logTestStep('should load the home page'); - await transform.navigation.navigateTo(); - await transform.management.assertTransformListPageExists(); - - await transform.testExecution.logTestStep('should display the transforms table'); - await transform.management.assertTransformsTableExists(); - - await transform.testExecution.logTestStep( - 'should display the original transform in the transform list' - ); - await transform.table.refreshTransformList(); - await transform.table.filterWithSearchString(transformConfig.id, 1); - - await transform.testExecution.logTestStep('should show the actions popover'); - await transform.table.assertTransformRowActions(false); - - await transform.testExecution.logTestStep('should show the edit flyout'); - await transform.table.clickTransformRowAction('Edit'); - await transform.editFlyout.assertTransformEditFlyoutExists(); - }); - - it('navigates through the edit flyout and sets all needed fields', async () => { - await transform.testExecution.logTestStep('should update the transform description'); - await transform.editFlyout.assertTransformEditFlyoutInputExists('Description'); - await transform.editFlyout.assertTransformEditFlyoutInputValue( - 'Description', - transformConfig?.description ?? '' - ); - await transform.editFlyout.setTransformEditFlyoutInputValue( - 'Description', - testData.transformDescription - ); - - await transform.testExecution.logTestStep( - 'should update the transform documents per second' - ); - await transform.editFlyout.openTransformEditAccordionAdvancedSettings(); - await transform.editFlyout.assertTransformEditFlyoutInputExists('DocsPerSecond'); - await transform.editFlyout.assertTransformEditFlyoutInputValue('DocsPerSecond', ''); - await transform.editFlyout.setTransformEditFlyoutInputValue( - 'DocsPerSecond', - testData.transformDocsPerSecond - ); - - await transform.testExecution.logTestStep('should update the transform frequency'); - await transform.editFlyout.assertTransformEditFlyoutInputExists('Frequency'); - await transform.editFlyout.assertTransformEditFlyoutInputValue('Frequency', ''); - await transform.editFlyout.setTransformEditFlyoutInputValue( - 'Frequency', - testData.transformFrequency - ); - }); + // TODO enable tests when https://github.com/elastic/elasticsearch/issues/67148 is resolved + // { + // suiteTitle: 'edit transform with latest configuration', + // originalConfig: transformConfigWithLatest, + // transformDescription: 'updated description', + // transformDocsPerSecond: '1000', + // transformFrequency: '10m', + // expected: { + // messageText: 'updated transform.', + // row: { + // status: TRANSFORM_STATE.STOPPED, + // mode: 'batch', + // progress: '100', + // }, + // }, + // }, + ]; + + for (const testData of testDataList) { + describe(`${testData.suiteTitle}`, function () { + it('opens the edit flyout for an existing transform', async () => { + await transform.testExecution.logTestStep('should load the home page'); + await transform.navigation.navigateTo(); + await transform.management.assertTransformListPageExists(); + + await transform.testExecution.logTestStep('should display the transforms table'); + await transform.management.assertTransformsTableExists(); + + await transform.testExecution.logTestStep( + 'should display the original transform in the transform list' + ); + await transform.table.refreshTransformList(); + await transform.table.filterWithSearchString(testData.originalConfig.id, 1); + + await transform.testExecution.logTestStep('should show the actions popover'); + await transform.table.assertTransformRowActions(false); + + await transform.testExecution.logTestStep('should show the edit flyout'); + await transform.table.clickTransformRowAction('Edit'); + await transform.editFlyout.assertTransformEditFlyoutExists(); + }); - it('updates the transform and displays it correctly in the job list', async () => { - await transform.testExecution.logTestStep('should update the transform'); - await transform.editFlyout.updateTransform(); - - await transform.testExecution.logTestStep('should display the transforms table'); - await transform.management.assertTransformsTableExists(); - - await transform.testExecution.logTestStep( - 'should display the updated transform in the transform list' - ); - await transform.table.refreshTransformList(); - await transform.table.filterWithSearchString(transformConfig.id, 1); - - await transform.testExecution.logTestStep( - 'should display the updated transform in the transform list row cells' - ); - await transform.table.assertTransformRowFields(transformConfig.id, { - id: transformConfig.id, - description: testData.transformDescription, - status: testData.expected.row.status, - mode: testData.expected.row.mode, - progress: testData.expected.row.progress, + it('navigates through the edit flyout and sets all needed fields', async () => { + await transform.testExecution.logTestStep('should update the transform description'); + await transform.editFlyout.assertTransformEditFlyoutInputExists('Description'); + await transform.editFlyout.assertTransformEditFlyoutInputValue( + 'Description', + testData.originalConfig?.description ?? '' + ); + await transform.editFlyout.setTransformEditFlyoutInputValue( + 'Description', + testData.transformDescription + ); + + await transform.testExecution.logTestStep( + 'should update the transform documents per second' + ); + await transform.editFlyout.openTransformEditAccordionAdvancedSettings(); + await transform.editFlyout.assertTransformEditFlyoutInputExists('DocsPerSecond'); + await transform.editFlyout.assertTransformEditFlyoutInputValue('DocsPerSecond', ''); + await transform.editFlyout.setTransformEditFlyoutInputValue( + 'DocsPerSecond', + testData.transformDocsPerSecond + ); + + await transform.testExecution.logTestStep('should update the transform frequency'); + await transform.editFlyout.assertTransformEditFlyoutInputExists('Frequency'); + await transform.editFlyout.assertTransformEditFlyoutInputValue('Frequency', ''); + await transform.editFlyout.setTransformEditFlyoutInputValue( + 'Frequency', + testData.transformFrequency + ); }); - await transform.testExecution.logTestStep( - 'should display the messages tab and include an update message' - ); - await transform.table.assertTransformExpandedRowMessages(testData.expected.messageText); + it('updates the transform and displays it correctly in the job list', async () => { + await transform.testExecution.logTestStep('should update the transform'); + await transform.editFlyout.updateTransform(); + + await transform.testExecution.logTestStep('should display the transforms table'); + await transform.management.assertTransformsTableExists(); + + await transform.testExecution.logTestStep( + 'should display the updated transform in the transform list' + ); + await transform.table.refreshTransformList(); + await transform.table.filterWithSearchString(testData.originalConfig.id, 1); + + await transform.testExecution.logTestStep( + 'should display the updated transform in the transform list row cells' + ); + await transform.table.assertTransformRowFields(testData.originalConfig.id, { + id: testData.originalConfig.id, + description: testData.transformDescription, + status: testData.expected.row.status, + mode: testData.expected.row.mode, + progress: testData.expected.row.progress, + }); + + await transform.testExecution.logTestStep( + 'should display the messages tab and include an update message' + ); + await transform.table.assertTransformExpandedRowMessages(testData.expected.messageText); + }); }); - }); + } }); } diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index 2837ddb7333e6..1398fdfb1b202 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { FtrProviderContext } from '../../ftr_provider_context'; +import { TransformLatestConfig } from '../../../../plugins/transform/common/types/transform'; export default function ({ getService, loadTestFile }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -40,3 +41,57 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./feature_controls')); }); } +export interface ComboboxOption { + identifier: string; + label: string; +} + +export interface GroupByEntry extends ComboboxOption { + intervalLabel?: string; +} + +export interface BaseTransformTestData { + type: 'pivot' | 'latest'; + suiteTitle: string; + source: string; + transformId: string; + transformDescription: string; + expected: any; + destinationIndex: string; +} + +export interface PivotTransformTestData extends BaseTransformTestData { + groupByEntries: GroupByEntry[]; + aggregationEntries: any[]; +} + +export interface LatestTransformTestData extends BaseTransformTestData { + uniqueKeys: ComboboxOption[]; + sortField: ComboboxOption; +} + +export function isPivotTransformTestData(arg: any): arg is PivotTransformTestData { + return arg.type === 'pivot'; +} + +export function isLatestTransformTestData(arg: any): arg is LatestTransformTestData { + return arg.type === 'latest'; +} + +export function getLatestTransformConfig(): TransformLatestConfig { + const timestamp = Date.now(); + return { + id: `ec_cloning_2_${timestamp}`, + source: { index: ['ft_ecommerce'] }, + latest: { + unique_key: ['category.keyword'], + sort: 'order_date', + }, + description: 'ecommerce batch transform with category unique key and sorted by order date', + frequency: '3s', + settings: { + max_page_search_size: 250, + }, + dest: { index: `user-ec_3_${timestamp}` }, + }; +} diff --git a/x-pack/test/functional/apps/uptime/locations.ts b/x-pack/test/functional/apps/uptime/locations.ts index eb5a642c8d69d..6bfa19c6ef578 100644 --- a/x-pack/test/functional/apps/uptime/locations.ts +++ b/x-pack/test/functional/apps/uptime/locations.ts @@ -38,8 +38,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await makeChecksWithStatus(es, LessAvailMonitor, 5, 2, 10000, {}, 'down'); }; - // FLAKY: https://github.com/elastic/kibana/issues/85208 - describe.skip('Observer location', () => { + describe('Observer location', () => { const start = '~ 15 minutes ago'; const end = 'now'; diff --git a/x-pack/test/functional/es_archives/security/flstest/data/mappings.json b/x-pack/test/functional/es_archives/security/flstest/data/mappings.json index 4f419e4b6ade4..0b970d5a3c1df 100644 --- a/x-pack/test/functional/es_archives/security/flstest/data/mappings.json +++ b/x-pack/test/functional/es_archives/security/flstest/data/mappings.json @@ -7,7 +7,8 @@ "runtime_customer_ssn": { "type": "keyword", "script": { - "source": "emit(doc['customer_ssn'].value + ' calculated at runtime')" + "lang": "painless", + "source": "if (doc['customer_ssn'].size() !== 0) { return emit(doc['customer_ssn'].value + ' calculated at runtime') }" } } }, @@ -37,7 +38,8 @@ "type": "keyword" } }, - "type": "text" + "type": "text", + "fielddata": true } } }, diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index 0a0a80e52de79..e50fb1818273d 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -454,8 +454,8 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { return await esSupertest.get(`/_ml/anomaly_detectors/${jobId}`).expect(200); }, - async waitForAnomalyDetectionJobToExist(jobId: string) { - await retry.waitForWithTimeout(`'${jobId}' to exist`, 5 * 1000, async () => { + async waitForAnomalyDetectionJobToExist(jobId: string, timeout: number = 5 * 1000) { + await retry.waitForWithTimeout(`'${jobId}' to exist`, timeout, async () => { if (await this.getAnomalyDetectionJob(jobId)) { return true; } else { @@ -464,8 +464,8 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { }); }, - async waitForAnomalyDetectionJobNotToExist(jobId: string) { - await retry.waitForWithTimeout(`'${jobId}' to not exist`, 5 * 1000, async () => { + async waitForAnomalyDetectionJobNotToExist(jobId: string, timeout: number = 5 * 1000) { + await retry.waitForWithTimeout(`'${jobId}' to not exist`, timeout, async () => { if (await esSupertest.get(`/_ml/anomaly_detectors/${jobId}`).expect(404)) { return true; } else { diff --git a/x-pack/test/functional/services/transform/wizard.ts b/x-pack/test/functional/services/transform/wizard.ts index b05f1ff26199e..1bfe3284c9045 100644 --- a/x-pack/test/functional/services/transform/wizard.ts +++ b/x-pack/test/functional/services/transform/wizard.ts @@ -157,7 +157,7 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { await this.assertPivotPreviewExists('loaded'); }, - async assertPivotPreviewEmpty() { + async assertTransformPreviewEmpty() { await this.assertPivotPreviewExists('empty'); }, @@ -247,6 +247,65 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { ); }, + async assertSelectedTransformFunction(transformFunction: 'pivot' | 'latest') { + await testSubjects.existOrFail( + `transformCreation-${transformFunction}-option selectedFunction` + ); + }, + + async selectTransformFunction(transformFunction: 'pivot' | 'latest') { + await testSubjects.click(`transformCreation-${transformFunction}-option`); + await this.assertSelectedTransformFunction(transformFunction); + }, + + async assertUniqueKeysInputExists() { + await testSubjects.existOrFail('transformWizardUniqueKeysSelector > comboBoxInput'); + }, + + async getUniqueKeyEntries() { + return await comboBox.getComboBoxSelectedOptions( + 'transformWizardUniqueKeysSelector > comboBoxInput' + ); + }, + + async assertUniqueKeysInputValue(expectedIdentifier: string[]) { + await retry.tryForTime(2000, async () => { + const comboBoxSelectedOptions = await this.getUniqueKeyEntries(); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected unique keys value to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); + }); + }, + + async addUniqueKeyEntry(identified: string, label: string) { + await comboBox.set('transformWizardUniqueKeysSelector > comboBoxInput', identified); + await this.assertUniqueKeysInputValue([ + ...new Set([...(await this.getUniqueKeyEntries()), identified]), + ]); + }, + + async assertSortFieldInputExists() { + await testSubjects.existOrFail('transformWizardSortFieldSelector > comboBoxInput'); + }, + + async assertSortFieldInputValue(expectedIdentifier: string) { + await retry.tryForTime(2000, async () => { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'transformWizardSortFieldSelector > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier === '' ? [] : [expectedIdentifier], + `Expected sort field to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); + }); + }, + + async setSortFieldValue(identificator: string, label: string) { + await comboBox.set('transformWizardSortFieldSelector > comboBoxInput', identificator); + await this.assertSortFieldInputValue(identificator); + }, + async assertGroupByInputExists() { await testSubjects.existOrFail('transformGroupBySelection > comboBoxInput'); }, @@ -426,7 +485,7 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) { ); }, - async enabledAdvancedPivotEditor() { + async enableAdvancedPivotEditor() { await this.assertAdvancedPivotEditorSwitchCheckState(false); await testSubjects.click('transformAdvancedPivotEditorSwitch'); await this.assertAdvancedPivotEditorSwitchCheckState(true); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts index 36812d0cd9eef..ca753e9253bde 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts @@ -53,8 +53,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('alertsTab'); } - // Failing: See https://github.com/elastic/kibana/issues/87105 - describe.skip('alerts list', function () { + describe('alerts list', function () { before(async () => { await pageObjects.common.navigateToApp('triggersActions'); await testSubjects.click('alertsTab'); @@ -407,6 +406,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ).to.equal('Error found in 1 alert.'); }); + await refreshAlertsList(); expect(await testSubjects.getVisibleText('totalAlertsCount')).to.be( 'Showing: 2 of 2 alerts.' ); diff --git a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts index ff4ab65a310ed..04cf8bef1a16c 100644 --- a/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/uptime/alert_flyout.ts @@ -8,7 +8,8 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { - describe('uptime alerts', () => { + // FLAKY: https://github.com/elastic/kibana/issues/88177 + describe.skip('uptime alerts', () => { const pageObjects = getPageObjects(['common', 'uptime']); const supertest = getService('supertest'); const retry = getService('retry'); diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/read_list_privileges.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/read_list_privileges.ts index c1696d3205294..4734448f32df5 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/read_list_privileges.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/read_list_privileges.ts @@ -17,7 +17,8 @@ export default ({ getService }: FtrProviderContext) => { const spacesService = getService('spaces'); const supertestWithoutAuth = getService('supertestWithoutAuth'); - describe('read_list_privileges', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/88302 + describe.skip('read_list_privileges', () => { const space1Id = 'space_1'; const user1 = { diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/health_route.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/health_route.ts index 4c84ca1298e10..26ceeef59069f 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/health_route.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/health_route.ts @@ -40,6 +40,7 @@ interface MonitoringStats { }; polling: { last_successful_poll: string; + last_polling_delay: string; duration: Record; result_frequency_percent_as_number: Record; }; @@ -177,6 +178,7 @@ export default function ({ getService }: FtrProviderContext) { } = (await getHealth()).stats; expect(isNaN(Date.parse(polling.last_successful_poll as string))).to.eql(false); + expect(isNaN(Date.parse(polling.last_polling_delay as string))).to.eql(false); expect(typeof polling.result_frequency_percent_as_number.NoTasksClaimed).to.eql('number'); expect(typeof polling.result_frequency_percent_as_number.RanOutOfCapacity).to.eql('number'); expect(typeof polling.result_frequency_percent_as_number.PoolFilled).to.eql('number'); diff --git a/x-pack/test/security_api_integration/tests/anonymous/capabilities.ts b/x-pack/test/security_api_integration/tests/anonymous/capabilities.ts new file mode 100644 index 0000000000000..f2f562c0c29c0 --- /dev/null +++ b/x-pack/test/security_api_integration/tests/anonymous/capabilities.ts @@ -0,0 +1,395 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const config = getService('config'); + const security = getService('security'); + const spaces = getService('spaces'); + + const isElasticsearchAnonymousAccessEnabled = (config.get( + 'esTestCluster.serverArgs' + ) as string[]).some((setting) => setting.startsWith('xpack.security.authc.anonymous')); + + async function getAnonymousCapabilities(spaceId?: string) { + const apiResponse = await supertest + .get(`${spaceId ? `/s/${spaceId}` : ''}/internal/security_oss/anonymous_access/capabilities`) + .expect(200); + + return Object.fromEntries( + Object.entries(apiResponse.body).filter( + ([key]) => + key === 'discover' || key === 'dashboard' || key === 'visualize' || key === 'maps' + ) + ); + } + + describe('Anonymous capabilities', () => { + before(async () => { + await spaces.create({ + id: 'space-a', + name: 'space-a', + disabledFeatures: ['discover', 'visualize'], + }); + await spaces.create({ + id: 'space-b', + name: 'space-b', + disabledFeatures: ['dashboard', 'maps'], + }); + }); + + after(async () => { + await spaces.delete('space-a'); + await spaces.delete('space-b'); + }); + + describe('without anonymous service account', () => { + it('all capabilities should be disabled', async () => { + expectSnapshot(await getAnonymousCapabilities()).toMatchInline(` + Object { + "dashboard": Object { + "createNew": false, + "createShortUrl": false, + "saveQuery": false, + "show": false, + "showWriteControls": false, + "storeSearchSession": false, + }, + "discover": Object { + "createShortUrl": false, + "save": false, + "saveQuery": false, + "show": false, + "storeSearchSession": false, + }, + "maps": Object { + "save": false, + "saveQuery": false, + "show": false, + }, + "visualize": Object { + "createShortUrl": false, + "delete": false, + "save": false, + "saveQuery": false, + "show": false, + }, + } + `); + expectSnapshot(await getAnonymousCapabilities('space-a')).toMatchInline(` + Object { + "dashboard": Object { + "createNew": false, + "createShortUrl": false, + "saveQuery": false, + "show": false, + "showWriteControls": false, + "storeSearchSession": false, + }, + "discover": Object { + "createShortUrl": false, + "save": false, + "saveQuery": false, + "show": false, + "storeSearchSession": false, + }, + "maps": Object { + "save": false, + "saveQuery": false, + "show": false, + }, + "visualize": Object { + "createShortUrl": false, + "delete": false, + "save": false, + "saveQuery": false, + "show": false, + }, + } + `); + expectSnapshot(await getAnonymousCapabilities('space-b')).toMatchInline(` + Object { + "dashboard": Object { + "createNew": false, + "createShortUrl": false, + "saveQuery": false, + "show": false, + "showWriteControls": false, + "storeSearchSession": false, + }, + "discover": Object { + "createShortUrl": false, + "save": false, + "saveQuery": false, + "show": false, + "storeSearchSession": false, + }, + "maps": Object { + "save": false, + "saveQuery": false, + "show": false, + }, + "visualize": Object { + "createShortUrl": false, + "delete": false, + "save": false, + "saveQuery": false, + "show": false, + }, + } + `); + }); + }); + + describe('with anonymous service account without roles', () => { + if (!isElasticsearchAnonymousAccessEnabled) { + before(async () => { + await security.user.create('anonymous_user', { + password: 'changeme', + roles: [], + full_name: 'Guest', + }); + }); + + after(async () => { + await security.user.delete('anonymous_user'); + }); + } + + it('all capabilities should be disabled', async () => { + expectSnapshot(await getAnonymousCapabilities()).toMatchInline(` + Object { + "dashboard": Object { + "createNew": false, + "createShortUrl": false, + "saveQuery": false, + "show": false, + "showWriteControls": false, + "storeSearchSession": false, + }, + "discover": Object { + "createShortUrl": false, + "save": false, + "saveQuery": false, + "show": false, + "storeSearchSession": false, + }, + "maps": Object { + "save": false, + "saveQuery": false, + "show": false, + }, + "visualize": Object { + "createShortUrl": false, + "delete": false, + "save": false, + "saveQuery": false, + "show": false, + }, + } + `); + expectSnapshot(await getAnonymousCapabilities('space-a')).toMatchInline(` + Object { + "dashboard": Object { + "createNew": false, + "createShortUrl": false, + "saveQuery": false, + "show": false, + "showWriteControls": false, + "storeSearchSession": false, + }, + "discover": Object { + "createShortUrl": false, + "save": false, + "saveQuery": false, + "show": false, + "storeSearchSession": false, + }, + "maps": Object { + "save": false, + "saveQuery": false, + "show": false, + }, + "visualize": Object { + "createShortUrl": false, + "delete": false, + "save": false, + "saveQuery": false, + "show": false, + }, + } + `); + expectSnapshot(await getAnonymousCapabilities('space-b')).toMatchInline(` + Object { + "dashboard": Object { + "createNew": false, + "createShortUrl": false, + "saveQuery": false, + "show": false, + "showWriteControls": false, + "storeSearchSession": false, + }, + "discover": Object { + "createShortUrl": false, + "save": false, + "saveQuery": false, + "show": false, + "storeSearchSession": false, + }, + "maps": Object { + "save": false, + "saveQuery": false, + "show": false, + }, + "visualize": Object { + "createShortUrl": false, + "delete": false, + "save": false, + "saveQuery": false, + "show": false, + }, + } + `); + }); + }); + + describe('with properly configured anonymous service account', () => { + before(async () => { + await security.role.create('anonymous_role', { + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [ + { spaces: ['default'], base: ['read'], feature: {} }, + { spaces: ['space-a'], base: [], feature: { discover: ['read'], maps: ['read'] } }, + { + spaces: ['space-b'], + base: [], + feature: { dashboard: ['read'], visualize: ['read'] }, + }, + ], + }); + + if (!isElasticsearchAnonymousAccessEnabled) { + await security.user.create('anonymous_user', { + password: 'changeme', + roles: ['anonymous_role'], + full_name: 'Guest', + }); + } + }); + + after(async () => { + await security.role.delete('anonymous_role'); + + if (!isElasticsearchAnonymousAccessEnabled) { + await security.user.delete('anonymous_user'); + } + }); + + it('capabilities should be properly defined', async () => { + // Discover, dashboards, visualizations and maps should be available in read-only mode. + expectSnapshot(await getAnonymousCapabilities()).toMatchInline(` + Object { + "dashboard": Object { + "createNew": false, + "createShortUrl": false, + "saveQuery": false, + "show": true, + "showWriteControls": false, + "storeSearchSession": false, + }, + "discover": Object { + "createShortUrl": false, + "save": false, + "saveQuery": false, + "show": true, + "storeSearchSession": false, + }, + "maps": Object { + "save": false, + "saveQuery": false, + "show": true, + }, + "visualize": Object { + "createShortUrl": false, + "delete": false, + "save": false, + "saveQuery": false, + "show": true, + }, + } + `); + + // Only maps should be available in read-only mode, the rest should be disabled. + expectSnapshot(await getAnonymousCapabilities('space-a')).toMatchInline(` + Object { + "dashboard": Object { + "createNew": false, + "createShortUrl": false, + "saveQuery": false, + "show": false, + "showWriteControls": false, + "storeSearchSession": false, + }, + "discover": Object { + "createShortUrl": false, + "save": false, + "saveQuery": false, + "show": false, + "storeSearchSession": false, + }, + "maps": Object { + "save": false, + "saveQuery": false, + "show": true, + }, + "visualize": Object { + "createShortUrl": false, + "delete": false, + "save": false, + "saveQuery": false, + "show": false, + }, + } + `); + + // Only visualizations should be available in read-only mode, the rest should be disabled. + expectSnapshot(await getAnonymousCapabilities('space-b')).toMatchInline(` + Object { + "dashboard": Object { + "createNew": false, + "createShortUrl": false, + "saveQuery": false, + "show": false, + "showWriteControls": false, + "storeSearchSession": false, + }, + "discover": Object { + "createShortUrl": false, + "save": false, + "saveQuery": false, + "show": false, + "storeSearchSession": false, + }, + "maps": Object { + "save": false, + "saveQuery": false, + "show": false, + }, + "visualize": Object { + "createShortUrl": false, + "delete": false, + "save": false, + "saveQuery": false, + "show": true, + }, + } + `); + }); + }); + }); +} diff --git a/x-pack/test/security_api_integration/tests/anonymous/index.ts b/x-pack/test/security_api_integration/tests/anonymous/index.ts index 3819d26ae5efa..53fc67dad436f 100644 --- a/x-pack/test/security_api_integration/tests/anonymous/index.ts +++ b/x-pack/test/security_api_integration/tests/anonymous/index.ts @@ -10,5 +10,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Anonymous access', function () { this.tags('ciGroup6'); loadTestFile(require.resolve('./login')); + loadTestFile(require.resolve('./capabilities')); }); } diff --git a/x-pack/test/send_search_to_background_integration/config.ts b/x-pack/test/send_search_to_background_integration/config.ts index 957c183d04536..c14678febd811 100644 --- a/x-pack/test/send_search_to_background_integration/config.ts +++ b/x-pack/test/send_search_to_background_integration/config.ts @@ -29,7 +29,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...xpackFunctionalConfig.get('kbnTestServer'), serverArgs: [ ...xpackFunctionalConfig.get('kbnTestServer.serverArgs'), - '--xpack.data_enhanced.search.sendToBackground.enabled=true', // enable WIP send to background UI + '--xpack.data_enhanced.search.sessions.enabled=true', // enable WIP send to background UI ], }, services: { diff --git a/x-pack/test/stack_functional_integration/apps/maps/_maps.ts b/x-pack/test/stack_functional_integration/apps/maps/_maps.ts index cde5f950b0946..65972dd128471 100644 --- a/x-pack/test/stack_functional_integration/apps/maps/_maps.ts +++ b/x-pack/test/stack_functional_integration/apps/maps/_maps.ts @@ -14,11 +14,13 @@ export default function ({ }: FtrProviderContext & { updateBaselines: boolean }) { const screenshot = getService('screenshots'); const browser = getService('browser'); + const find = getService('find'); const PageObjects = getPageObjects(['maps']); describe('check Elastic Maps Server', function () { before(async function () { await PageObjects.maps.loadSavedMap('EMS Test'); + await find.clickByButtonText('Dismiss'); await browser.setScreenshotSize(1000, 1000); }); diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 5487f1e41aab2..7e7cc9d58f331 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -9,14 +9,17 @@ "exclude": ["../typings/jest.d.ts"], "references": [ { "path": "../../src/core/tsconfig.json" }, + { "path": "../../src/plugins/telemetry_management_section/tsconfig.json" }, { "path": "../../src/plugins/management/tsconfig.json" }, { "path": "../../src/plugins/bfetch/tsconfig.json" }, { "path": "../../src/plugins/charts/tsconfig.json" }, + { "path": "../../src/plugins/console/tsconfig.json" }, { "path": "../../src/plugins/dashboard/tsconfig.json" }, { "path": "../../src/plugins/discover/tsconfig.json" }, { "path": "../../src/plugins/data/tsconfig.json" }, { "path": "../../src/plugins/embeddable/tsconfig.json" }, { "path": "../../src/plugins/enterprise_search/tsconfig.json" }, + { "path": "../../src/plugins/es_ui_shared/tsconfig.json" }, { "path": "../../src/plugins/expressions/tsconfig.json" }, { "path": "../../src/plugins/home/tsconfig.json" }, { "path": "../../src/plugins/kibana_react/tsconfig.json" }, @@ -25,6 +28,7 @@ { "path": "../../src/plugins/navigation/tsconfig.json" }, { "path": "../../src/plugins/newsfeed/tsconfig.json" }, { "path": "../../src/plugins/saved_objects/tsconfig.json" }, + { "path": "../../src/plugins/saved_objects_management/tsconfig.json" }, { "path": "../../src/plugins/saved_objects_tagging_oss/tsconfig.json" }, { "path": "../../src/plugins/share/tsconfig.json" }, { "path": "../../src/plugins/telemetry_collection_manager/tsconfig.json" }, @@ -33,13 +37,18 @@ { "path": "../../src/plugins/ui_actions/tsconfig.json" }, { "path": "../../src/plugins/url_forwarding/tsconfig.json" }, + { "path": "../plugins/console_extensions/tsconfig.json" }, { "path": "../plugins/data_enhanced/tsconfig.json" }, { "path": "../plugins/global_search/tsconfig.json" }, + { "path": "../plugins/global_search_providers/tsconfig.json" }, { "path": "../plugins/features/tsconfig.json" }, { "path": "../plugins/embeddable_enhanced/tsconfig.json" }, { "path": "../plugins/licensing/tsconfig.json" }, { "path": "../plugins/task_manager/tsconfig.json" }, { "path": "../plugins/telemetry_collection_xpack/tsconfig.json" }, { "path": "../plugins/ui_actions_enhanced/tsconfig.json" }, + { "path": "../plugins/spaces/tsconfig.json" }, + { "path": "../plugins/security/tsconfig.json" }, + { "path": "../plugins/encrypted_saved_objects/tsconfig.json" } ] } diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 32adcac19086c..c8c963121d81f 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -4,19 +4,27 @@ "exclude": [ "plugins/apm/e2e/cypress/**/*", "plugins/apm/scripts/**/*", + "plugins/console_extensions/**/*", "plugins/data_enhanced/**/*", + "plugins/discover_enhanced/**/*", "plugins/dashboard_enhanced/**/*", "plugins/global_search/**/*", + "plugins/global_search_providers/**/*", "plugins/graph/**/*", "plugins/features/**/*", "plugins/embeddable_enhanced/**/*", "plugins/enterprise_search/**/*", "plugins/licensing/**/*", + "plugins/searchprofiler/**/*", "plugins/security_solution/cypress/**/*", "plugins/task_manager/**/*", "plugins/telemetry_collection_xpack/**/*", "plugins/translations/**/*", "plugins/ui_actions_enhanced/**/*", + "plugins/vis_type_timeseries_enhanced/**/*", + "plugins/spaces/**/*", + "plugins/security/**/*", + "plugins/encrypted_saved_objects/**/*", "test/**/*" ], "compilerOptions": { @@ -25,14 +33,17 @@ }, "references": [ { "path": "../src/core/tsconfig.json" }, + { "path": "../src/plugins/telemetry_management_section/tsconfig.json" }, { "path": "../src/plugins/management/tsconfig.json" }, { "path": "../src/plugins/bfetch/tsconfig.json" }, { "path": "../src/plugins/charts/tsconfig.json" }, + { "path": "../src/plugins/console/tsconfig.json" }, { "path": "../src/plugins/dashboard/tsconfig.json" }, { "path": "../src/plugins/discover/tsconfig.json" }, { "path": "../src/plugins/data/tsconfig.json" }, { "path": "../src/plugins/dev_tools/tsconfig.json" }, { "path": "../src/plugins/embeddable/tsconfig.json" }, + { "path": "../src/plugins/es_ui_shared/tsconfig.json" }, { "path": "../src/plugins/expressions/tsconfig.json" }, { "path": "../src/plugins/home/tsconfig.json" }, { "path": "../src/plugins/inspector/tsconfig.json" }, @@ -43,6 +54,7 @@ { "path": "../src/plugins/navigation/tsconfig.json" }, { "path": "../src/plugins/newsfeed/tsconfig.json" }, { "path": "../src/plugins/saved_objects/tsconfig.json" }, + { "path": "../src/plugins/saved_objects_management/tsconfig.json" }, { "path": "../src/plugins/saved_objects_tagging_oss/tsconfig.json" }, { "path": "../src/plugins/presentation_util/tsconfig.json" }, { "path": "../src/plugins/security_oss/tsconfig.json" }, @@ -54,15 +66,23 @@ { "path": "../src/plugins/url_forwarding/tsconfig.json" }, { "path": "../src/plugins/usage_collection/tsconfig.json" }, + { "path": "./plugins/console_extensions/tsconfig.json" }, { "path": "./plugins/data_enhanced/tsconfig.json" }, + { "path": "./plugins/discover_enhanced/tsconfig.json" }, { "path": "./plugins/global_search/tsconfig.json" }, + { "path": "./plugins/global_search_providers/tsconfig.json" }, { "path": "./plugins/features/tsconfig.json" }, { "path": "./plugins/graph/tsconfig.json" }, { "path": "./plugins/embeddable_enhanced/tsconfig.json" }, { "path": "./plugins/licensing/tsconfig.json" }, + { "path": "./plugins/searchprofiler/tsconfig.json" }, { "path": "./plugins/task_manager/tsconfig.json" }, { "path": "./plugins/telemetry_collection_xpack/tsconfig.json" }, + { "path": "./plugins/ui_actions_enhanced/tsconfig.json" }, + { "path": "./plugins/vis_type_timeseries_enhanced/tsconfig.json" }, { "path": "./plugins/translations/tsconfig.json" }, - { "path": "./plugins/ui_actions_enhanced/tsconfig.json" } + { "path": "./plugins/spaces/tsconfig.json" }, + { "path": "./plugins/security/tsconfig.json" }, + { "path": "./plugins/encrypted_saved_objects/tsconfig.json" } ] } diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json index 3699ea84b147c..a5b1e2bef7812 100644 --- a/x-pack/tsconfig.refs.json +++ b/x-pack/tsconfig.refs.json @@ -3,15 +3,23 @@ "references": [ { "path": "./plugins/dashboard_enhanced/tsconfig.json" }, { "path": "./plugins/licensing/tsconfig.json" }, + { "path": "./plugins/console_extensions/tsconfig.json" }, + { "path": "./plugins/discover_enhanced/tsconfig.json" }, { "path": "./plugins/data_enhanced/tsconfig.json" }, { "path": "./plugins/global_search/tsconfig.json" }, + { "path": "./plugins/global_search_providers/tsconfig.json" }, { "path": "./plugins/features/tsconfig.json" }, { "path": "./plugins/graph/tsconfig.json" }, { "path": "./plugins/embeddable_enhanced/tsconfig.json" }, { "path": "./plugins/enterprise_search/tsconfig.json" }, + { "path": "./plugins/searchprofiler/tsconfig.json" }, { "path": "./plugins/task_manager/tsconfig.json" }, { "path": "./plugins/telemetry_collection_xpack/tsconfig.json" }, + { "path": "./plugins/ui_actions_enhanced/tsconfig.json" }, + { "path": "./plugins/vis_type_timeseries_enhanced/tsconfig.json" }, { "path": "./plugins/translations/tsconfig.json" }, - { "path": "./plugins/ui_actions_enhanced/tsconfig.json" } + { "path": "./plugins/spaces/tsconfig.json" }, + { "path": "./plugins/security/tsconfig.json" }, + { "path": "./plugins/encrypted_saved_objects/tsconfig.json" } ] } diff --git a/x-pack/typings/elasticsearch/aggregations.d.ts b/x-pack/typings/elasticsearch/aggregations.d.ts index 9f25920353b2f..0328877aae8fe 100644 --- a/x-pack/typings/elasticsearch/aggregations.d.ts +++ b/x-pack/typings/elasticsearch/aggregations.d.ts @@ -387,22 +387,24 @@ interface AggregationResponsePart - : TAggregationOptionsMap extends { - top_metrics: { metrics: MaybeReadonlyArray<{ field: infer TFieldName }> }; - } - ? TopMetricsMap - : TopMetricsMap - >; - } - ]; + top_metrics: { + top: [ + { + sort: [string | number]; + metrics: UnionToIntersection< + TAggregationOptionsMap extends { + top_metrics: { metrics: { field: infer TFieldName } }; + } + ? TopMetricsMap + : TAggregationOptionsMap extends { + top_metrics: { metrics: MaybeReadonlyArray<{ field: infer TFieldName }> }; + } + ? TopMetricsMap + : TopMetricsMap + >; + } + ]; + }; avg_bucket: { value: number | null; }; diff --git a/yarn.lock b/yarn.lock index 02bd107ea0e65..8595ed43bf6d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1773,13 +1773,6 @@ "@hapi/boom" "9.x.x" "@hapi/hoek" "9.x.x" -"@hapi/address@^4.1.0": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@hapi/address/-/address-4.1.0.tgz#d60c5c0d930e77456fdcde2598e77302e2955e1d" - integrity sha512-SkszZf13HVgGmChdHo/PxchnSaCJ6cetVqLzyciudzZRT0jcOouIF/Q93mgjw8cce+D+4F4C1Z/WrfFN+O3VHQ== - dependencies: - "@hapi/hoek" "^9.0.0" - "@hapi/ammo@5.x.x", "@hapi/ammo@^5.0.1": version "5.0.1" resolved "https://registry.yarnpkg.com/@hapi/ammo/-/ammo-5.0.1.tgz#9d34560f5c214eda563d838c01297387efaab490" @@ -1869,11 +1862,6 @@ resolved "https://registry.yarnpkg.com/@hapi/file/-/file-2.0.0.tgz#2ecda37d1ae9d3078a67c13b7da86e8c3237dfb9" integrity sha512-WSrlgpvEqgPWkI18kkGELEZfXr0bYLtr16iIN4Krh9sRnzBZN6nnWxHFxtsnP684wueEySBbXPDg/WfA9xJdBQ== -"@hapi/formula@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@hapi/formula/-/formula-2.0.0.tgz#edade0619ed58c8e4f164f233cda70211e787128" - integrity sha512-V87P8fv7PI0LH7LiVi8Lkf3x+KCO7pQozXRssAHNXXL9L1K+uyu4XypLXwxqVDKgyQai6qj3/KteNlrqDx4W5A== - "@hapi/good-squeeze@6.0.0": version "6.0.0" resolved "https://registry.yarnpkg.com/@hapi/good-squeeze/-/good-squeeze-6.0.0.tgz#bb72d6869cd7398b615a6b7270f630dc4f76aebf" @@ -1987,11 +1975,6 @@ "@hapi/hoek" "9.x.x" "@hapi/nigel" "4.x.x" -"@hapi/pinpoint@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@hapi/pinpoint/-/pinpoint-2.0.0.tgz#805b40d4dbec04fc116a73089494e00f073de8df" - integrity sha512-vzXR5MY7n4XeIvLpfl3HtE3coZYO4raKXW766R6DZw/6aLqR26iuZ109K7a0NtF2Db0jxqh7xz2AxkUwpUFybw== - "@hapi/podium@4.x.x", "@hapi/podium@^4.1.1": version "4.1.1" resolved "https://registry.yarnpkg.com/@hapi/podium/-/podium-4.1.1.tgz#106e5849f2cb19b8767cc16007e0107f27c3c791" @@ -3205,22 +3188,22 @@ jsonwebtoken "^8.3.0" lru-cache "^5.1.1" -"@octokit/auth-token@^2.4.0": - version "2.4.2" - resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.2.tgz#10d0ae979b100fa6b72fa0e8e63e27e6d0dbff8a" - integrity sha512-jE/lE/IKIz2v1+/P0u4fJqv0kYwXOTujKemJMFr6FeopsxlIK3+wKDCJGnysg81XID5TgZQbIfuJ5J0lnTiuyQ== +"@octokit/auth-token@^2.4.0", "@octokit/auth-token@^2.4.4": + version "2.4.4" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.4.tgz#ee31c69b01d0378c12fd3ffe406030f3d94d3b56" + integrity sha512-LNfGu3Ro9uFAYh10MUZVaT7X2CnNm2C8IDQmabx+3DygYIQjs9FwzFAHN/0t6mu5HEPhxcb1XOuxdpY82vCg2Q== dependencies: - "@octokit/types" "^5.0.0" + "@octokit/types" "^6.0.0" -"@octokit/core@^3.0.0": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.1.2.tgz#c937d5f9621b764573068fcd2e5defcc872fd9cc" - integrity sha512-AInOFULmwOa7+NFi9F8DlDkm5qtZVmDQayi7TUgChE3yeIGPq0Y+6cAEXPexQ3Ea+uZy66hKEazR7DJyU+4wfw== +"@octokit/core@^3.2.3": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.2.4.tgz#5791256057a962eca972e31818f02454897fd106" + integrity sha512-d9dTsqdePBqOn7aGkyRFe7pQpCXdibSJ5SFnrTr0axevObZrpz3qkWm7t/NjYv5a66z6vhfteriaq4FRz3e0Qg== dependencies: - "@octokit/auth-token" "^2.4.0" - "@octokit/graphql" "^4.3.1" - "@octokit/request" "^5.4.0" - "@octokit/types" "^5.0.0" + "@octokit/auth-token" "^2.4.4" + "@octokit/graphql" "^4.5.8" + "@octokit/request" "^5.4.12" + "@octokit/types" "^6.0.3" before-after-hook "^2.1.0" universal-user-agent "^6.0.0" @@ -3243,15 +3226,20 @@ is-plain-object "^5.0.0" universal-user-agent "^6.0.0" -"@octokit/graphql@^4.3.1": - version "4.5.6" - resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.5.6.tgz#708143ba15cf7c1879ed6188266e7f270be805d4" - integrity sha512-Rry+unqKTa3svswT2ZAuqenpLrzJd+JTv89LTeVa5UM/5OX8o4KTkPL7/1ABq4f/ZkELb0XEK/2IEoYwykcLXg== +"@octokit/graphql@^4.5.8": + version "4.5.8" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.5.8.tgz#d42373633c3015d0eafce64a8ce196be167fdd9b" + integrity sha512-WnCtNXWOrupfPJgXe+vSmprZJUr0VIu14G58PMlkWGj3cH+KLZEfKMmbUQ6C3Wwx6fdhzVW1CD5RTnBdUHxhhA== dependencies: "@octokit/request" "^5.3.0" - "@octokit/types" "^5.0.0" + "@octokit/types" "^6.0.0" universal-user-agent "^6.0.0" +"@octokit/openapi-types@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-2.2.0.tgz#123e0438a0bc718ccdac3b5a2e69b3dd00daa85b" + integrity sha512-274lNUDonw10kT8wHg8fCcUc1ZjZHbWv0/TbAwb0ojhBQqZYc1cQ/4yqTVTtPMDeZ//g7xVEYe/s3vURkRghPg== + "@octokit/plugin-paginate-rest@^1.1.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-1.1.2.tgz#004170acf8c2be535aba26727867d692f7b488fc" @@ -3259,17 +3247,17 @@ dependencies: "@octokit/types" "^2.0.1" -"@octokit/plugin-paginate-rest@^2.2.0": - version "2.4.0" - resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.4.0.tgz#92f951ddc8a1cd505353fa07650752ca25ed7e93" - integrity sha512-YT6Klz3LLH6/nNgi0pheJnUmTFW4kVnxGft+v8Itc41IIcjl7y1C8TatmKQBbCSuTSNFXO5pCENnqg6sjwpJhg== +"@octokit/plugin-paginate-rest@^2.6.2": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.7.0.tgz#6bb7b043c246e0654119a6ec4e72a172c9e2c7f3" + integrity sha512-+zARyncLjt9b0FjqPAbJo4ss7HOlBi1nprq+cPlw5vu2+qjy7WvlXhtXFdRHQbSL1Pt+bfAKaLADEkkvg8sP8w== dependencies: - "@octokit/types" "^5.5.0" + "@octokit/types" "^6.0.1" -"@octokit/plugin-request-log@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.0.tgz#eef87a431300f6148c39a7f75f8cfeb218b2547e" - integrity sha512-ywoxP68aOT3zHCLgWZgwUJatiENeHE7xJzYjfz8WI0goynp96wETBF+d95b8g/uL4QmS6owPVlaxiz3wyMAzcw== +"@octokit/plugin-request-log@^1.0.0", "@octokit/plugin-request-log@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.2.tgz#394d59ec734cd2f122431fbaf05099861ece3c44" + integrity sha512-oTJSNAmBqyDR41uSMunLQKMX0jmEXbwD1fpz8FG27lScV3RhtGfBa1/BBLym+PxcC16IBlF7KH9vP1BUYxA+Eg== "@octokit/plugin-rest-endpoint-methods@2.4.0": version "2.4.0" @@ -3279,12 +3267,12 @@ "@octokit/types" "^2.0.1" deprecation "^2.3.1" -"@octokit/plugin-rest-endpoint-methods@4.2.0": - version "4.2.0" - resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.2.0.tgz#c5a0691b3aba5d8b4ef5dffd6af3649608f167ba" - integrity sha512-1/qn1q1C1hGz6W/iEDm9DoyNoG/xdFDt78E3eZ5hHeUfJTLJgyAMdj9chL/cNBHjcjd+FH5aO1x0VCqR2RE0mw== +"@octokit/plugin-rest-endpoint-methods@4.4.1": + version "4.4.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.4.1.tgz#105cf93255432155de078c9efc33bd4e14d1cd63" + integrity sha512-+v5PcvrUcDeFXf8hv1gnNvNLdm4C0+2EiuWt9EatjjUmfriM1pTMM+r4j1lLHxeBQ9bVDmbywb11e3KjuavieA== dependencies: - "@octokit/types" "^5.5.0" + "@octokit/types" "^6.1.0" deprecation "^2.3.1" "@octokit/plugin-retry@^2.2.0": @@ -3324,14 +3312,14 @@ once "^1.4.0" universal-user-agent "^2.0.1" -"@octokit/request@^5.2.0", "@octokit/request@^5.3.0", "@octokit/request@^5.4.0": - version "5.4.9" - resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.4.9.tgz#0a46f11b82351b3416d3157261ad9b1558c43365" - integrity sha512-CzwVvRyimIM1h2n9pLVYfTDmX9m+KHSgCpqPsY8F1NdEK8IaWqXhSBXsdjOBFZSpEcxNEeg4p0UO9cQ8EnOCLA== +"@octokit/request@^5.2.0", "@octokit/request@^5.3.0", "@octokit/request@^5.4.12": + version "5.4.12" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.4.12.tgz#b04826fa934670c56b135a81447be2c1723a2ffc" + integrity sha512-MvWYdxengUWTGFpfpefBBpVmmEYfkwMoxonIB3sUGp5rhdgwjXL1ejo6JbgzG/QD9B/NYt/9cJX1pxXeSIUCkg== dependencies: "@octokit/endpoint" "^6.0.1" "@octokit/request-error" "^2.0.0" - "@octokit/types" "^5.0.0" + "@octokit/types" "^6.0.3" deprecation "^2.0.0" is-plain-object "^5.0.0" node-fetch "^2.6.1" @@ -3378,15 +3366,15 @@ once "^1.4.0" universal-user-agent "^4.0.0" -"@octokit/rest@^18.0.6": - version "18.0.6" - resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.0.6.tgz#76c274f1a68f40741a131768ef483f041e7b98b6" - integrity sha512-ES4lZBKPJMX/yUoQjAZiyFjei9pJ4lTTfb9k7OtYoUzKPDLl/M8jiHqt6qeSauyU4eZGLw0sgP1WiQl9FYeM5w== +"@octokit/rest@^18.0.12": + version "18.0.12" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.0.12.tgz#278bd41358c56d87c201e787e8adc0cac132503a" + integrity sha512-hNRCZfKPpeaIjOVuNJzkEL6zacfZlBPV8vw8ReNeyUkVvbuCvvrrx8K8Gw2eyHHsmd4dPlAxIXIZ9oHhJfkJpw== dependencies: - "@octokit/core" "^3.0.0" - "@octokit/plugin-paginate-rest" "^2.2.0" - "@octokit/plugin-request-log" "^1.0.0" - "@octokit/plugin-rest-endpoint-methods" "4.2.0" + "@octokit/core" "^3.2.3" + "@octokit/plugin-paginate-rest" "^2.6.2" + "@octokit/plugin-request-log" "^1.0.2" + "@octokit/plugin-rest-endpoint-methods" "4.4.1" "@octokit/types@^2.0.0", "@octokit/types@^2.0.1": version "2.16.2" @@ -3395,37 +3383,45 @@ dependencies: "@types/node" ">= 8" -"@octokit/types@^5.0.0", "@octokit/types@^5.0.1", "@octokit/types@^5.5.0": +"@octokit/types@^5.0.0", "@octokit/types@^5.0.1": version "5.5.0" resolved "https://registry.yarnpkg.com/@octokit/types/-/types-5.5.0.tgz#e5f06e8db21246ca102aa28444cdb13ae17a139b" integrity sha512-UZ1pErDue6bZNjYOotCNveTXArOMZQFG6hKJfOnGnulVCMcVVi7YIIuuR4WfBhjo7zgpmzn/BkPDnUXtNx+PcQ== dependencies: "@types/node" ">= 8" -"@percy/agent@^0.26.0": - version "0.26.0" - resolved "https://registry.yarnpkg.com/@percy/agent/-/agent-0.26.0.tgz#9f06849d752df7368198835d0b5edc16c2d69a0c" - integrity sha512-PKah93vdcmHWlvDd2/QTir4iboqpLAcbAxhUDJYfp8rTzVy5kBstKyPrQk+8IifnGRWNSjsXOO6+qZr+cYHdjA== +"@octokit/types@^6.0.0", "@octokit/types@^6.0.1", "@octokit/types@^6.0.3", "@octokit/types@^6.1.0": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.2.1.tgz#7f881fe44475ab1825776a4a59ca1ae082ed1043" + integrity sha512-jHs9OECOiZxuEzxMZcXmqrEO8GYraHF+UzNVH2ACYh8e/Y7YoT+hUf9ldvVd6zIvWv4p3NdxbQ0xx3ku5BnSiA== + dependencies: + "@octokit/openapi-types" "^2.2.0" + "@types/node" ">= 8" + +"@percy/agent@^0.28.6": + version "0.28.6" + resolved "https://registry.yarnpkg.com/@percy/agent/-/agent-0.28.6.tgz#b220fab6ddcf63ae4e6c343108ba6955a772ce1c" + integrity sha512-SDAyBiUmfQMVTayjvEjQ0IJIA7Y3AoeyWn0jmUxNOMRRIJWo4lQJghfhFCgzCkhXDCm67NMN2nAQAsvXrlIdkQ== dependencies: "@oclif/command" "1.5.19" "@oclif/config" "^1" "@oclif/plugin-help" "^2" "@oclif/plugin-not-found" "^1.2" - axios "^0.19.0" + axios "^0.21.1" body-parser "^1.18.3" colors "^1.3.2" cors "^2.8.4" cosmiconfig "^5.2.1" - cross-spawn "^6.0.5" + cross-spawn "^7.0.2" deepmerge "^4.0.0" express "^4.16.3" - follow-redirects "1.9.0" + follow-redirects "1.12.1" generic-pool "^3.7.1" globby "^10.0.1" image-size "^0.8.2" js-yaml "^3.13.1" percy-client "^3.2.0" - puppeteer "^2.0.0" + puppeteer "^5.3.1" retry-axios "^1.0.1" which "^2.0.1" winston "^3.0.0" @@ -3510,6 +3506,23 @@ dependencies: url-pattern "^1.0.3" +"@sideway/address@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.0.tgz#0b301ada10ac4e0e3fa525c90615e0b61a72b78d" + integrity sha512-wAH/JYRXeIFQRsxerIuLjgUu2Xszam+O5xKeatJ4oudShOOirfmsQ1D6LL54XOU2tizpCYku+s1wmU0SYdpoSA== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@sideway/formula@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c" + integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg== + +"@sideway/pinpoint@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" + integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" @@ -3556,19 +3569,19 @@ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== -"@slack/types@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@slack/types/-/types-1.0.0.tgz#1dc7a63b293c4911e474197585c3feda012df17a" - integrity sha512-IktC4uD/CHfLQcSitKSmjmRu4a6+Nf/KzfS6dTgUlDzENhh26l8aESKAuIpvYD5VOOE6NxDDIAdPJOXBvUGxlg== +"@slack/types@^1.2.1": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@slack/types/-/types-1.9.0.tgz#aa8f90b2f66ac54a77e42606644366f93cff4871" + integrity sha512-RmwgMWqOtzd2JPXdiaD/tyrDD0vtjjRDFdxN1I3tAxwBbg4aryzDUVqFc8na16A+3Xik/UN8X1hvVTw8J4EB9w== -"@slack/webhook@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@slack/webhook/-/webhook-5.0.0.tgz#0044a3940afc16cbc607c71acdffddb9e9d4f161" - integrity sha512-cDj3kz3x9z9271xPNzlwb90DpKTYybG2OWPJHigJL8FegR80rzQyD0v4bGuStGGkHbAYDKE2BMpJambR55hnSg== +"@slack/webhook@^5.0.4": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@slack/webhook/-/webhook-5.0.4.tgz#5d3e947387c1d0ccb176a153cec68c594edb7060" + integrity sha512-IC1dpVSc2F/pmwCxOb0QzH2xnGKmyT7MofPGhNkeaoiMrLMU+Oc7xV/AxGnz40mURtCtaDchZSM3tDo9c9x6BA== dependencies: - "@slack/types" "^1.0.0" + "@slack/types" "^1.2.1" "@types/node" ">=8.9.0" - axios "^0.18.0" + axios "^0.21.1" "@storybook/addon-a11y@^6.0.26": version "6.0.26" @@ -7642,20 +7655,12 @@ axe-core@^4.0.2: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.0.2.tgz#c7cf7378378a51fcd272d3c09668002a4990b1cb" integrity sha512-arU1h31OGFu+LPrOLGZ7nB45v940NMDMEJeNmbutu57P+UFDVnkZg3e+J1I2HJRZ9hT7gO8J91dn/PMrAiKakA== -axios@^0.18.0: - version "0.18.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.1.tgz#ff3f0de2e7b5d180e757ad98000f1081b87bcea3" - integrity sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g== +axios@^0.21.0, axios@^0.21.1: + version "0.21.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" + integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA== dependencies: - follow-redirects "1.5.10" - is-buffer "^2.0.2" - -axios@^0.19.0, axios@^0.19.2: - version "0.19.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27" - integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA== - dependencies: - follow-redirects "1.5.10" + follow-redirects "^1.10.0" axobject-query@^2.0.2: version "2.0.2" @@ -8189,17 +8194,17 @@ bach@^1.0.0: async-settle "^1.0.0" now-and-later "^2.0.0" -backport@5.6.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/backport/-/backport-5.6.0.tgz#6dcc0485e5eecf66bb6f950983fd0b018217ec20" - integrity sha512-wz7Ve3uslhGUMtHuctqIEtZFItTGKRRMiNANYso0iw1ar81ILsczDGgxeOlzmmnIQFi1ZvEs6lX3cgypGfef9A== +backport@^5.6.4: + version "5.6.4" + resolved "https://registry.yarnpkg.com/backport/-/backport-5.6.4.tgz#8cf4bc750b26d27161306858ee9069218ad7cdfd" + integrity sha512-ZhuZcGxOBHBXFBCwweVf02b+KhWe0tdgg71jPSl583YYxlru+JBRH+TFM8S0J6/6YUuTWO81M9funjehJ18jqg== dependencies: - "@octokit/rest" "^18.0.6" + "@octokit/rest" "^18.0.12" "@types/lodash.difference" "^4.5.6" "@types/lodash.intersection" "^4.4.6" - axios "^0.19.0" + axios "^0.21.1" dedent "^0.7.0" - del "^5.1.0" + del "^6.0.0" find-up "^5.0.0" inquirer "^7.3.3" lodash.difference "^4.5.0" @@ -8209,11 +8214,11 @@ backport@5.6.0: lodash.isstring "^4.0.1" lodash.uniq "^4.5.0" make-dir "^3.1.0" - ora "^5.1.0" + ora "^5.2.0" safe-json-stringify "^1.2.0" strip-json-comments "^3.1.1" winston "^3.3.3" - yargs "^16.0.3" + yargs "^16.2.0" bail@^1.0.0: version "1.0.2" @@ -9409,17 +9414,18 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -chromedriver@^87.0.0: - version "87.0.0" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-87.0.0.tgz#e8390deed8ada791719a67ad6bf1116614f1ba2d" - integrity sha512-PY7FnHOQKfH0oPfSdhpLx5nEy5g4dGYySf2C/WZGkAaCaldYH8/3lPPucZ8MlOCi4bCSGoKoCUTeG6+wYWavvw== +chromedriver@^87.0.3: + version "87.0.5" + resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-87.0.5.tgz#5a56bae6e23fc5eaa0c5ac3b76f936e4dd0989a1" + integrity sha512-bWAKdZANrt3LXMUOKFP+DgW7DjVKfihCbjej6URkUcKsvbQBDYpf5YY5d/dXE3SOSzIFZ7fmLxogusxpsupCJg== dependencies: "@testim/chrome-version" "^1.0.7" - axios "^0.19.2" - del "^5.1.0" + axios "^0.21.0" + del "^6.0.0" extract-zip "^2.0.1" https-proxy-agent "^5.0.0" mkdirp "^1.0.4" + proxy-from-env "^1.1.0" tcp-port-used "^1.0.1" ci-info@^2.0.0: @@ -9512,10 +9518,10 @@ cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" -cli-spinners@^2.2.0, cli-spinners@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.4.0.tgz#c6256db216b878cfba4720e719cec7cf72685d7f" - integrity sha512-sJAofoarcm76ZGpuooaO0eDy8saEy+YoZBLjC4h8srt4jeBnkYeOgqxgsJQTpyt2LjI5PTfLJHSL+41Yu4fEJA== +cli-spinners@^2.2.0, cli-spinners@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.5.0.tgz#12763e47251bf951cb75c201dfa58ff1bcb2d047" + integrity sha512-PC+AmIuK04E6aeSs/pUccSujsTzBhu4HzC2dL+CfJB/Jcc2qTRbEwZQDfIUpt2Xl8BodYBEq8w4fc0kU2I9DjQ== cli-table3@0.6.0, cli-table3@~0.6.0: version "0.6.0" @@ -9624,10 +9630,10 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" -cliui@^7.0.0: - version "7.0.1" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.1.tgz#a4cb67aad45cd83d8d05128fc9f4d8fbb887e6b3" - integrity sha512-rcvHOWyGyid6I1WjT/3NatKj2kDt9OdSHSXpyLXaMWFbKpGACNW8pRhhdPUq9MWUOdwn8Rz9AVETjF4105rZZQ== +cliui@^7.0.0, cliui@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.2.tgz#e3a412e1d5ec0ccbe50d1b4120fc8164e97881f4" + integrity sha512-lhpKkuUj67j5JgZIPZxLe7nSa4MQoojzRVWQyzMqBp2hBg6gwRjUDAwC1YDeBaC3APDBKNnjWbv2mlDF4XgOSA== dependencies: string-width "^4.2.0" strip-ansi "^6.0.0" @@ -10441,7 +10447,7 @@ cross-fetch@2.2.2: node-fetch "2.1.2" whatwg-fetch "2.0.4" -cross-spawn@7.0.1, cross-spawn@^7.0.0: +cross-spawn@7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.1.tgz#0ab56286e0f7c24e153d04cc2aa027e43a9a5d14" integrity sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg== @@ -10469,6 +10475,15 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" +cross-spawn@^7.0.0, cross-spawn@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + crypt@~0.0.1: version "0.0.2" resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" @@ -11226,7 +11241,7 @@ debug@2.6.9, debug@^2.1.1, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6. dependencies: ms "2.0.0" -debug@3.1.0, debug@=3.1.0: +debug@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== @@ -11503,6 +11518,20 @@ del@^5.1.0: rimraf "^3.0.0" slash "^3.0.0" +del@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/del/-/del-6.0.0.tgz#0b40d0332cea743f1614f818be4feb717714c952" + integrity sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ== + dependencies: + globby "^11.0.1" + graceful-fs "^4.2.4" + is-glob "^4.0.1" + is-path-cwd "^2.2.0" + is-path-inside "^3.0.2" + p-map "^4.0.0" + rimraf "^3.0.2" + slash "^3.0.0" + delaunator@4: version "4.0.1" resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-4.0.1.tgz#3d779687f57919a7a418f8ab947d3bddb6846957" @@ -11659,6 +11688,11 @@ devtools-protocol@0.0.809251: resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.809251.tgz#300b3366be107d5c46114ecb85274173e3999518" integrity sha512-pf+2OY6ghMDPjKkzSWxHMq+McD+9Ojmq5XVRYpv/kPd9sTMQxzEt21592a31API8qRjro0iYYOc3ag46qF/1FA== +devtools-protocol@0.0.818844: + version "0.0.818844" + resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.818844.tgz#d1947278ec85b53e4c8ca598f607a28fa785ba9e" + integrity sha512-AD1hi7iVJ8OD0aMLQU5VK0XH9LDlA1+BcPIgrAxPfaibx2DbWucuyOhc4oyQCbnvDDO68nN6/LcKfqTP343Jjg== + dezalgo@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" @@ -12620,10 +12654,10 @@ es6-weak-map@^2.0.1, es6-weak-map@^2.0.2: es6-iterator "^2.0.1" es6-symbol "^3.1.1" -escalade@^3.0.2, escalade@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.0.tgz#e8e2d7c7a8b76f6ee64c2181d6b8151441602d4e" - integrity sha512-mAk+hPSO8fLDkhV7V0dXazH5pDc6MrjBTPyD3VeKzxnVFjH1MIxbCdqGZB9O8+EwWakZs3ZCbDS4IpRt79V1ig== +escalade@^3.0.2, escalade@^3.1.0, escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== escape-goat@^2.0.0: version "2.1.1" @@ -13348,7 +13382,7 @@ extract-stack@^1.0.0: resolved "https://registry.yarnpkg.com/extract-stack/-/extract-stack-1.0.0.tgz#b97acaf9441eea2332529624b732fc5a1c8165fa" integrity sha1-uXrK+UQe6iMyUpYktzL8WhyBZfo= -extract-zip@1.7.0, extract-zip@^1.6.6, extract-zip@^1.7.0: +extract-zip@1.7.0, extract-zip@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927" integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA== @@ -13872,19 +13906,15 @@ folktale@2.3.2: resolved "https://registry.yarnpkg.com/folktale/-/folktale-2.3.2.tgz#38231b039e5ef36989920cbf805bf6b227bf4fd4" integrity sha512-+8GbtQBwEqutP0v3uajDDoN64K2ehmHd0cjlghhxh0WpcfPzAIjPA03e1VvHlxL02FVGR0A6lwXsNQKn3H1RNQ== -follow-redirects@1.5.10: - version "1.5.10" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" - integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== - dependencies: - debug "=3.1.0" +follow-redirects@1.12.1: + version "1.12.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.12.1.tgz#de54a6205311b93d60398ebc01cf7015682312b6" + integrity sha512-tmRv0AVuR7ZyouUHLeNSiO6pqulF7dYa3s19c6t+wz9LD69/uSzdMxJ2S91nTI9U3rt/IldxpzMOFejp6f0hjg== -follow-redirects@1.9.0, follow-redirects@^1.0.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.9.0.tgz#8d5bcdc65b7108fe1508649c79c12d732dcedb4f" - integrity sha512-CRcPzsSIbXyVDl0QI01muNDu69S8trU4jArW9LpOt2WtC6LyUJetcIrmfHsRBx7/Jb6GHJUiuqyYxPooFfNt6A== - dependencies: - debug "^3.0.0" +follow-redirects@^1.0.0, follow-redirects@^1.10.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db" + integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA== font-awesome@4.7.0: version "4.7.0" @@ -16557,7 +16587,7 @@ is-buffer@^1.0.2, is-buffer@^1.1.0, is-buffer@^1.1.4, is-buffer@^1.1.5, is-buffe resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-buffer@^2.0.0, is-buffer@^2.0.2, is-buffer@~2.0.3: +is-buffer@^2.0.0, is-buffer@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725" integrity sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw== @@ -17946,16 +17976,16 @@ joi@13.x.x, joi@^13.5.2: isemail "3.x.x" topo "3.x.x" -joi@^17.1.1: - version "17.2.1" - resolved "https://registry.yarnpkg.com/joi/-/joi-17.2.1.tgz#e5140fdf07e8fecf9bc977c2832d1bdb1e3f2a0a" - integrity sha512-YT3/4Ln+5YRpacdmfEfrrKh50/kkgX3LgBltjqnlMPIYiZ4hxXZuVJcxmsvxsdeHg9soZfE3qXxHC2tMpCCBOA== +joi@^17.3.0: + version "17.3.0" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.3.0.tgz#f1be4a6ce29bc1716665819ac361dfa139fff5d2" + integrity sha512-Qh5gdU6niuYbUIUV5ejbsMiiFmBdw8Kcp8Buj2JntszCkCfxJ9Cz76OtHxOZMPXrt5810iDIXs+n1nNVoquHgg== dependencies: - "@hapi/address" "^4.1.0" - "@hapi/formula" "^2.0.0" "@hapi/hoek" "^9.0.0" - "@hapi/pinpoint" "^2.0.0" "@hapi/topo" "^5.0.0" + "@sideway/address" "^4.1.0" + "@sideway/formula" "^3.0.0" + "@sideway/pinpoint" "^2.0.0" jpeg-js@0.0.4: version "0.0.4" @@ -19827,7 +19857,7 @@ mime-db@1.44.0, mime-db@1.x.x, "mime-db@>= 1.40.0 < 2": resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== -mime-types@^2.0.1, mime-types@^2.1.12, mime-types@^2.1.25, mime-types@^2.1.26, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: +mime-types@^2.0.1, mime-types@^2.1.12, mime-types@^2.1.26, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.27" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== @@ -19839,7 +19869,7 @@ mime@1.6.0, mime@^1.2.11, mime@^1.3.4, mime@^1.4.1: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.0.3, mime@^2.4.4: +mime@^2.4.4: version "2.4.6" resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== @@ -21293,17 +21323,17 @@ ora@^4.0.3, ora@^4.0.4: strip-ansi "^6.0.0" wcwidth "^1.0.1" -ora@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/ora/-/ora-5.1.0.tgz#b188cf8cd2d4d9b13fd25383bc3e5cba352c94f8" - integrity sha512-9tXIMPvjZ7hPTbk8DFq1f7Kow/HU/pQYB60JbNq+QnGwcyhWVZaQ4hM9zQDEsPxw/muLpgiHSaumUZxCAmod/w== +ora@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.2.0.tgz#de10bfd2d15514384af45f3fa9d9b1aaf344fda1" + integrity sha512-+wG2v8TUU8EgzPHun1k/n45pXquQ9fHnbXVetl9rRgO6kjZszGGbraF3XPTIdgeA+s1lbRjSEftAnyT0w8ZMvQ== dependencies: + bl "^4.0.3" chalk "^4.1.0" cli-cursor "^3.1.0" - cli-spinners "^2.4.0" + cli-spinners "^2.5.0" is-interactive "^1.0.0" log-symbols "^4.0.0" - mute-stream "0.0.8" strip-ansi "^6.0.0" wcwidth "^1.0.1" @@ -22568,11 +22598,16 @@ proxy-addr@~2.0.5: forwarded "~0.1.2" ipaddr.js "1.9.0" -proxy-from-env@1.0.0, proxy-from-env@^1.0.0: +proxy-from-env@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" integrity sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4= +proxy-from-env@^1.0.0, proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + proxyquire@1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/proxyquire/-/proxyquire-1.8.0.tgz#02d514a5bed986f04cbb2093af16741535f79edc" @@ -22665,21 +22700,23 @@ pupa@^2.0.1: dependencies: escape-goat "^2.0.0" -puppeteer@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-2.1.1.tgz#ccde47c2a688f131883b50f2d697bd25189da27e" - integrity sha512-LWzaDVQkk1EPiuYeTOj+CZRIjda4k2s5w4MK4xoH2+kgWV/SDlkYHmxatDdtYrciHUKSXTsGgPgPP8ILVdBsxg== +puppeteer@^5.3.1: + version "5.5.0" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-5.5.0.tgz#331a7edd212ca06b4a556156435f58cbae08af00" + integrity sha512-OM8ZvTXAhfgFA7wBIIGlPQzvyEETzDjeRa4mZRCRHxYL+GNH5WAuYUQdja3rpWZvkX/JKqmuVgbsxDNsDFjMEg== dependencies: - "@types/mime-types" "^2.1.0" debug "^4.1.0" - extract-zip "^1.6.6" + devtools-protocol "0.0.818844" + extract-zip "^2.0.0" https-proxy-agent "^4.0.0" - mime "^2.0.3" - mime-types "^2.1.25" + node-fetch "^2.6.1" + pkg-dir "^4.2.0" progress "^2.0.1" proxy-from-env "^1.0.0" - rimraf "^2.6.1" - ws "^6.1.0" + rimraf "^3.0.2" + tar-fs "^2.0.0" + unbzip2-stream "^1.3.3" + ws "^7.2.3" "puppeteer@npm:@elastic/puppeteer@5.4.1-patch.1": version "5.4.1-patch.1" @@ -24656,7 +24693,7 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@2, rimraf@2.6.3, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3: +rimraf@2, rimraf@2.6.3, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== @@ -24816,10 +24853,10 @@ rxjs-marbles@^5.0.6: dependencies: fast-equals "^2.0.0" -rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.5.1, rxjs@^6.5.3, rxjs@^6.5.5, rxjs@^6.6.0: - version "6.6.2" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.2.tgz#8096a7ac03f2cc4fe5860ef6e572810d9e01c0d2" - integrity sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg== +rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.5.1, rxjs@^6.5.3, rxjs@^6.5.5, rxjs@^6.6.0, rxjs@^6.6.3: + version "6.6.3" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552" + integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ== dependencies: tslib "^1.9.0" @@ -29037,16 +29074,16 @@ w3c-xmlserializer@^2.0.0: dependencies: xml-name-validator "^3.0.0" -wait-on@^5.0.1: - version "5.2.0" - resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-5.2.0.tgz#6711e74422523279714a36d52cf49fb47c9d9597" - integrity sha512-U1D9PBgGw2XFc6iZqn45VBubw02VsLwnZWteQ1au4hUVHasTZuFSKRzlTB2dqgLhji16YVI8fgpEpwUdCr8B6g== +wait-on@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-5.2.1.tgz#05b66fcb4d7f5da01537f03e7cf96e8836422996" + integrity sha512-H2F986kNWMU9hKlI9l/ppO6tN8ZSJd35yBljMLa1/vjzWP++Qh6aXyt77/u7ySJFZQqBtQxnvm/xgG48AObXcw== dependencies: - axios "^0.19.2" - joi "^17.1.1" - lodash "^4.17.19" + axios "^0.21.1" + joi "^17.3.0" + lodash "^4.17.20" minimist "^1.2.5" - rxjs "^6.5.5" + rxjs "^6.6.3" walk@^2.3.14: version "2.3.14" @@ -29646,7 +29683,7 @@ write@^0.2.1: dependencies: mkdirp "^0.5.1" -ws@^6.1.0, ws@^6.1.2, ws@^6.2.1: +ws@^6.1.2, ws@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== @@ -29769,7 +29806,7 @@ y18n@^4.0.0: resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== -y18n@^5.0.1: +y18n@^5.0.1, y18n@^5.0.5: version "5.0.5" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.5.tgz#8769ec08d03b1ea2df2500acef561743bbb9ab18" integrity sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg== @@ -29818,10 +29855,10 @@ yargs-parser@^18.1.2, yargs-parser@^18.1.3: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^20.0.0: - version "20.2.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.0.tgz#944791ca2be2e08ddadd3d87e9de4c6484338605" - integrity sha512-2agPoRFPoIcFzOIp6656gcvsg2ohtscpw2OINr/q46+Sq41xz2OYLqx5HRHabmFU1OARIPAYH5uteICE7mn/5A== +yargs-parser@^20.0.0, yargs-parser@^20.2.2: + version "20.2.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.2.tgz#84562c6b1c41ccec2f13d346c7dd83f8d1a0dc70" + integrity sha512-XmrpXaTl6noDsf1dKpBuUNCOHqjs0g3jRMXf/ztRxdOmb+er8kE5z5b55Lz3p5u2T8KJ59ENBnASS8/iapVJ5g== yargs-unparser@1.6.0: version "1.6.0" @@ -29865,18 +29902,18 @@ yargs@^15.0.2, yargs@^15.3.1, yargs@^15.4.1: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^16.0.3, yargs@~16.0.3: - version "16.0.3" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.0.3.tgz#7a919b9e43c90f80d4a142a89795e85399a7e54c" - integrity sha512-6+nLw8xa9uK1BOEOykaiYAJVh6/CjxWXK/q9b5FpRgNslt8s22F2xMBqVIKgCRjNgGvGPBy8Vog7WN7yh4amtA== +yargs@^16.0.3, yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== dependencies: - cliui "^7.0.0" - escalade "^3.0.2" + cliui "^7.0.2" + escalade "^3.1.1" get-caller-file "^2.0.5" require-directory "^2.1.1" string-width "^4.2.0" - y18n "^5.0.1" - yargs-parser "^20.0.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" yargs@^3.15.0: version "3.32.0" @@ -29910,6 +29947,19 @@ yargs@^7.1.0: y18n "^3.2.1" yargs-parser "5.0.0-security.0" +yargs@~16.0.3: + version "16.0.3" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.0.3.tgz#7a919b9e43c90f80d4a142a89795e85399a7e54c" + integrity sha512-6+nLw8xa9uK1BOEOykaiYAJVh6/CjxWXK/q9b5FpRgNslt8s22F2xMBqVIKgCRjNgGvGPBy8Vog7WN7yh4amtA== + dependencies: + cliui "^7.0.0" + escalade "^3.0.2" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.1" + yargs-parser "^20.0.0" + yargs@~3.10.0: version "3.10.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"